Как создать пользовательский компонент поля формы ExtJS? - PullRequest
31 голосов
/ 27 мая 2011

Я хочу создать пользовательское ExtJS поле формы компонентов, используя другие компоненты ExtJS (например, TreePanel).Как я могу сделать это проще всего?

Я прочитал документы Ext.form.field.Base , но я не хочу определять тело поля с помощью fieldSubTpl.Я просто хочу написать код, который создает компоненты ExtJS и, возможно, какой-то другой код, который получает и устанавливает значения.

Обновление: Обобщенными целями являются следующие:

  • Этот новый компонент должен соответствовать форме графического интерфейса в качестве поля.Он должен иметь метку и такое же выравнивание (метка, якорь) других полей без необходимости дальнейшего взлома.

  • Возможно, мне нужно написать некоторую логику getValue, setValue.Я бы предпочел встроить его в этот компонент, чем создавать отдельный код, который копирует вещи в дополнительные скрытые поля формы, которыми я также должен управлять.

Ответы [ 9 ]

27 голосов
/ 12 июня 2013

Чтобы расширить ответ @RobAgar, следуйте по-настоящему простому полю Date Time, которое я написал для ExtJS 3, и его быстрому порту, который я сделал для ExtJS 4. Важным моментом является использование Ext.form.field.Field* 1003. * миксин. Этот миксин предоставляет общий интерфейс для логического поведения и состояния полей формы, включая:

Методы получения и установки для значений полей События и методы для отслеживания изменений стоимости и достоверности Методы запуска проверки

Это можно использовать для объединения нескольких полей, и пусть они действуют как одно. Для общего пользовательского типа поля я рекомендую расширить Ext.form.field.Base

Вот пример, который я упоминал выше. Это должно показать, насколько легко это можно сделать даже для чего-то вроде объекта даты, где нам нужно отформатировать данные в методах получения и установки.

Ext.define('QWA.form.field.DateTime', {
    extend: 'Ext.form.FieldContainer',
    mixins: {
        field: 'Ext.form.field.Field'
    },
    alias: 'widget.datetimefield',
    layout: 'hbox',
    width: 200,
    height: 22,
    combineErrors: true,
    msgTarget: 'side',
    submitFormat: 'c',

    dateCfg: null,
    timeCfg: null,

    initComponent: function () {
        var me = this;
        if (!me.dateCfg) me.dateCfg = {};
        if (!me.timeCfg) me.timeCfg = {};
        me.buildField();
        me.callParent();
        me.dateField = me.down('datefield')
        me.timeField = me.down('timefield')

        me.initField();
    },

    //@private
    buildField: function () {
        var me = this;
        me.items = [
        Ext.apply({
            xtype: 'datefield',
            submitValue: false,
            format: 'd.m.Y',
            width: 100,
            flex: 2
        }, me.dateCfg),
        Ext.apply({
            xtype: 'timefield',
            submitValue: false,
            format: 'H:i',
            width: 80,
            flex: 1
        }, me.timeCfg)]
    },

    getValue: function () {
        var me = this,
            value,
            date = me.dateField.getSubmitValue(),
            dateFormat = me.dateField.format,
            time = me.timeField.getSubmitValue(),
            timeFormat = me.timeField.format;
        if (date) {
            if (time) {
                value = Ext.Date.parse(date + ' ' + time, me.getFormat());
            } else {
                value = me.dateField.getValue();
            }
        }
        return value;
    },

    setValue: function (value) {
        var me = this;
        me.dateField.setValue(value);
        me.timeField.setValue(value);
    },

    getSubmitData: function () {
        var me = this,
            data = null;
        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
            data = {},
            value = me.getValue(),
            data[me.getName()] = '' + value ? Ext.Date.format(value, me.submitFormat) : null;
        }
        return data;
    },

    getFormat: function () {
        var me = this;
        return (me.dateField.submitFormat || me.dateField.format) + " " + (me.timeField.submitFormat || me.timeField.format)
    }
});
24 голосов
/ 06 июня 2013

Теперь это круто.На днях я создал скрипку, чтобы ответить на другой вопрос, прежде чем понял, что я не по теме.И вот, наконец, вы обратили мое внимание на вопрос к моему ответу.Спасибо!

Итак, вот шаги, необходимые для реализации настраиваемого поля из другого компонента:

  1. Создание дочернего компонента
  2. Визуализация дочернего компонента
  3. Обеспечение правильного размера и размера дочернего компонента
  4. Получение и установка значения
  5. Передача событий

Создание дочернего компонента

Первая часть, создание компонента, проста.Нет ничего особенного по сравнению с созданием компонента для любого другого использования.

Однако вы должны создать дочерний элемент в методе initComponent родительского поля (а не во время рендеринга).Это связано с тем, что внешний код может на законных основаниях ожидать, что все зависимые объекты компонента будут созданы после initComponent (например, для добавления к ним слушателей).

Более того, вы можете быть добрыми к себе и создать дочернего элемента перед вызовомсупер метод.Если вы создаете дочерний элемент после использования супер-метода, вы можете получить вызов метода setValue вашего поля (см. Ниже) в тот момент, когда дочерний элемент еще не создан.

initComponent: function() {
    this.childComponent = Ext.create(...);
    this.callParent(arguments);
}

Как видите, я создаю отдельный компонент, который вам в большинстве случаев нужен.Но вы также можете захотеть создать несколько дочерних компонентов.В этом случае, я думаю, было бы разумно как можно быстрее вернуться к хорошо известным территориям: создать один контейнер как дочерний компонент и создать в нем.

Rendering

Затем возникает вопрос рендеринга.Сначала я подумал об использовании fieldSubTpl для рендеринга контейнера div, и чтобы дочерний компонент отображал себя в нем.Однако в этом случае нам не нужны функции шаблона, поэтому мы также можем полностью обойти его, используя метод getSubTplMarkup.

Я исследовал другие компоненты в Ext, чтобы увидеть, какони управляют рендерингом дочерних компонентов.Я нашел хороший пример в BoundList и его панели инструментов подкачки (см. code ).Итак, чтобы получить разметку дочернего компонента, мы можем использовать Ext.DomHelper.generateMarkup в сочетании с дочерним getRenderTree методом.

Итак, вот реализацияgetSubTplMarkup для нашего поля:

getSubTplMarkup: function() {
    // generateMarkup will append to the passed empty array and return it
    var buffer = Ext.DomHelper.generateMarkup(this.childComponent.getRenderTree(), []);
    // but we want to return a single string
    return buffer.join('');
}

Теперь этого недостаточно.Код BoundList узнает, что в рендеринге компонента есть еще одна важная часть: вызов метода finishRender() дочернего компонента.К счастью, наше настраиваемое поле будет иметь собственный метод finishRenderChildren, вызываемый именно тогда, когда это необходимо сделать.

finishRenderChildren: function() {
    this.callParent(arguments);
    this.childComponent.finishRender();
}

Изменение размера

Теперь наш дочерний элемент будет обработанв нужном месте, но он не будет учитывать размер родительского поля.Это особенно раздражает в случае поля формы, потому что это означает, что оно не будет соответствовать макету привязки.

Это очень просто исправить, нам просто нужно изменить размер дочернего элемента при изменении размера родительского поля.По моему опыту, это то, что было значительно улучшено с Ext3.Здесь нам просто нужно не забыть дополнительное пространство для метки:

onResize: function(w, h) {
    this.callParent(arguments);
    this.childComponent.setSize(w - this.getLabelWidth(), h);
}

Значение обработки

Эта часть, конечно, будет зависеть от вашего дочернего компонента (компонентов), иполе, которое вы создаете.Более того, с этого момента, это просто вопрос регулярного использования ваших дочерних компонентов, поэтому я не буду подробно описывать эту часть.

Минимумы, вам также нужно реализовать getValue и setValue методы вашего поля.Это заставит работать метод формы getFieldValues, и этого будет достаточно для загрузки / обновления записей из формы.

Для обработки проверки необходимо реализовать getErrors.Чтобы улучшить этот аспект, вы можете добавить несколько CSS-правил, чтобы визуально представить недопустимое состояние вашего поля.

Затем, если вы хотите, чтобы ваше поле можно было использовать в форме, которая будет отправлена ​​как фактическая форма (в отличие от запроса AJAX), вам потребуется getSubmitValue для возврата значение, которое можно привести к строке без ущерба.

Кроме того, насколько я знаю, вам не нужно беспокоиться о концепции или необработанном значении , введенном Ext.form.field.Base, поскольку оно используется только для обработки представления значения в фактический элемент ввода. С нашим компонентом Ext в качестве входных данных мы далеко от этой дороги!

События

Ваша последняя работа будет заключаться в реализации событий для ваших полей. Возможно, вы захотите запустить три события Ext.form.field.Field, то есть change, dirtychange и validitychange.

Опять же, реализация будет очень специфичной для дочернего компонента, который вы используете, и, если честно, я не слишком подробно исследовал этот аспект. Так что я позволю вам сделать это самостоятельно.

Мой предварительный вывод состоит в том, что Ext.form.field.Field предлагает выполнить всю тяжелую работу за вас, при условии, что (1) вы позвоните checkChange, когда это необходимо, и (2) isEqual реализация работает с форматом значения вашего поля.

Пример: поле списка TODO

Наконец, вот полный пример кода с использованием сетки для представления поля списка TODO.

Вы можете видеть это в прямом эфире на jsFiddle , где я пытаюсь показать, что поле ведет себя упорядоченно.

Ext.define('My.form.field.TodoList', {
    // Extend from Ext.form.field.Base for all the label related business
    extend: 'Ext.form.field.Base'

    ,alias: 'widget.todolist'

    // --- Child component creation ---

    ,initComponent: function() {

        // Create the component

        // This is better to do it here in initComponent, because it is a legitimate 
        // expectationfor external code that all dependant objects are created after 
        // initComponent (to add listeners, etc.)

        // I will use this.grid for semantical access (value), and this.childComponent
        // for generic issues (rendering)
        this.grid = this.childComponent = Ext.create('Ext.grid.Panel', {
            hideHeaders: true
            ,columns: [{dataIndex: 'value', flex: 1}]
            ,store: {
                fields: ['value']
                ,data: []
            }
            ,height: this.height || 150
            ,width: this.width || 150

            ,tbar: [{
                text: 'Add'
                ,scope: this
                ,handler: function() {
                    var value = prompt("Value?");
                    if (value !== null) {
                        this.grid.getStore().add({value: value});
                    }
                }
            },{
                text: "Remove"
                ,itemId: 'removeButton'
                ,disabled: true // initial state
                ,scope: this
                ,handler: function() {
                    var grid = this.grid,
                        selModel = grid.getSelectionModel(),
                        store = grid.getStore();
                    store.remove(selModel.getSelection());
                }
            }]

            ,listeners: {
                scope: this
                ,selectionchange: function(selModel, selection) {
                    var removeButton = this.grid.down('#removeButton');
                    removeButton.setDisabled(Ext.isEmpty(selection));
                }
            }
        });

        // field events
        this.grid.store.on({
            scope: this
            ,datachanged: this.checkChange
        });

        this.callParent(arguments);
    }

    // --- Rendering ---

    // Generates the child component markup and let Ext.form.field.Base handle the rest
    ,getSubTplMarkup: function() {
        // generateMarkup will append to the passed empty array and return it
        var buffer = Ext.DomHelper.generateMarkup(this.childComponent.getRenderTree(), []);
        // but we want to return a single string
        return buffer.join('');
    }

    // Regular containers implements this method to call finishRender for each of their
    // child, and we need to do the same for the component to display smoothly
    ,finishRenderChildren: function() {
        this.callParent(arguments);
        this.childComponent.finishRender();
    }

    // --- Resizing ---

    // This is important for layout notably
    ,onResize: function(w, h) {
        this.callParent(arguments);
        this.childComponent.setSize(w - this.getLabelWidth(), h);
    }

    // --- Value handling ---

    // This part will be specific to your component of course

    ,setValue: function(values) {
        var data = [];
        if (values) {
            Ext.each(values, function(value) {
                data.push({value: value});
            });
        }
        this.grid.getStore().loadData(data);
    }

    ,getValue: function() {
        var data = [];
        this.grid.getStore().each(function(record) {
            data.push(record.get('value'));
        });
        return data;        
    }

    ,getSubmitValue: function() {
        return this.getValue().join(',');
    }
});
4 голосов
/ 11 июня 2013

Хех.После публикации награды я узнал, что Ext.form.FieldContainer - это не просто контейнер field , а полноценный контейнер component , поэтому существует простое решение.

Все, что вам нужно сделать, это расширить FieldContainer, переопределив initComponent, чтобы добавить дочерние компоненты, и реализовать setValue, getValue и методы проверки в соответствии с вашим типом данных значения.

Вот пример с сеткой, значением которой является список объектов пары имя / значение:

Ext.define('MyApp.widget.MyGridField', {
  extend: 'Ext.form.FieldContainer',
  alias: 'widget.mygridfield',

  layout: 'fit',

  initComponent: function()
  {
    this.callParent(arguments);

    this.valueGrid = Ext.widget({
      xtype: 'grid',
      store: Ext.create('Ext.data.JsonStore', {
        fields: ['name', 'value'],
        data: this.value
      }),
      columns: [
        {
          text: 'Name',
          dataIndex: 'name',
          flex: 3
        },
        {
          text: 'Value',
          dataIndex: 'value',
          flex: 1
        }
      ]
    });

    this.add(this.valueGrid);
  },

  setValue: function(value)
  {
    this.valueGrid.getStore().loadData(value);
  },

  getValue: function()
  {
    // left as an exercise for the reader :P
  }
});
3 голосов
/ 12 июня 2013

Поскольку вопрос был задан довольно расплывчато - я могу предоставить только базовый шаблон для ExtJS v4.

Даже если он не слишком конкретен, он имеет преимущество в том, что он довольно универсален, например:

Ext.define('app.view.form.field.CustomField', {
    extend: 'Ext.form.field.Base',
    requires: [
        /* require further components */
    ],

    /* custom configs & callbacks */

    getValue: function(v){
        /* override function getValue() */
    },

    setValue: function(v){
        /* override function setValue() */
    },

    getSubTplData: [
       /* most likely needs to be overridden */
    ],

    initComponent: function(){

        /* further code on event initComponent */

        this.callParent(arguments);
    }
});

Файл /ext/src/form/field/Base.js содержит имена всех конфигов и функций, которые могут быть переопределены.

3 голосов
/ 02 июня 2011

Я делал это несколько раз. Вот общий процесс / псевдокод, который я использую:

  • Создание расширения поля, которое обеспечивает наиболее полезное повторное использование (обычно Ext.form.TextField, если вы просто хотите получить / установить строковое значение)
  • В afterrender поля скройте текстовое поле и создайте элемент обтекания this.el с помощью this.wrap = this.resizeEl = this.positionEl = this.el.wrap()
  • Отобразить любые компоненты в this.wrap (например, используя renderTo: this.wrap в конфигурации)
  • Переопределите getValue и setValue, чтобы поговорить с компонентами, которые вы визуализировали вручную
  • Возможно, вам потребуется выполнить некоторые настройки вручную в resize слушателе, если макет вашей формы изменится
  • Не забудьте очистить все компоненты, которые вы создаете методом beforeDestroy!

Я не могу дождаться, чтобы переключить нашу кодовую базу на ExtJS 4, где такие вещи просты.

Удачи!

2 голосов
/ 11 августа 2011

Следуя документации на http://docs.sencha.com/ext-js/4-0/#/api/Ext.form.field.Base

Этот код создаст повторно используемое поле стиля TypeAhead / Autocomplete для выбора языка.

var langs = Ext.create( 'Ext.data.store', {
    fields: [ 'label', 'code' ],
    data: [
        { code: 'eng', label: 'English' },
        { code: 'ger', label: 'German' },
        { code: 'chi', label: 'Chinese' },
        { code: 'ukr', label: 'Ukranian' },
        { code: 'rus', label: 'Russian' }
    ]
} );

Ext.define( 'Ext.form.LangSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.LangSelector',
    allowBlank: false,
    hideTrigger: true,
    width: 225,
    displayField: 'label',
    valueField: 'code',
    forceSelection: true,
    minChars: 1,
    store: langs
} );

Вы можете использовать поле в форме, просто установив для xtype имя виджета:

{
    xtype: 'LangSelector'
    fieldLabel: 'Language',
    name: 'lang'
}
1 голос
/ 22 августа 2013

Многие ответы либо используют Mixin Ext.form.field.Field, либо просто распространяются на некоторые уже созданные классы, которые соответствуют их потребностям - и это хорошо.

Но я не рекомендую полностью перезаписывать метод setValue, то есть IMO действительно плохая форма!

Происходит гораздо больше, чем просто установка и получение значения, и если вы полностью перезапишите его - хорошо, например, вы испортите грязное состояние, обработаете rawValue и т. Д.

Здесь, я полагаю, есть два варианта, один из них - вызывать callParent (аргументы) внутри метода, который вы объявляете, чтобы упростить процесс, или в конце, когда вы закончите, применять унаследованный метод от того места, где вы его получили (mixin или extension) .

Но не перезаписывайте его безотносительно к тому, что этот уже созданный метод делает за кадром.

Также помните, что если вы используете другие типы полей в своем новом классе - тогда установите для свойства isFormField значение false - в противном случае ваш метод getValues ​​в форме примет эти значения и запустится с em!

0 голосов
/ 21 августа 2012

Вот пример пользовательской панели, которая расширяет Ext Panel. Вы можете расширить любой компонент, проверить документы на предмет полей, методов и событий, с которыми вы можете играть.

Ext.ns('yournamespace');

yournamespace.MyPanel = function(config) {
    yournamespace.MyPanel.superclass.constructor.call(this, config);
} 

Ext.extend(yournamespace.MyPanel, Ext.Panel, {

    myGlobalVariable : undefined,

    constructor : function(config) {
        yournamespace.MyPanel.superclass.constructor.apply(this, config);
    },

    initComponent : function() {
        this.comboBox = new Ext.form.ComboBox({
            fieldLabel: "MyCombo",
            store: someStore,
            displayField:'My Label',
            typeAhead: true,
            mode: 'local',
            forceSelection: true,
            triggerAction: 'all',
            emptyText:'',
            selectOnFocus:true,
            tabIndex: 1,
            width: 200
        });

        // configure the grid
        Ext.apply(this, {
            listeners: {
                'activate': function(p) {
                    p.doLayout();
                 },
                 single:true
            },

            xtype:"form",
            border: false,
            layout:"absolute",
            labelAlign:"top",
            bodyStyle:"padding: 15px",
            width: 350,
            height: 75,

            items:[{
                xtype:"panel",
                layout:"form",
                x:"10",
                y:"10",
                labelAlign:"top",
                border:false,
                items:[this.comboBox]
            },
            {
                xtype:"panel",
                layout:"form",
                x:"230",
                y:"26",
                labelAlign:"top",
                border:false,
                items:[{
                    xtype:'button',
                    handler: this.someAction.createDelegate(this),
                    text: 'Some Action'
                }]
            }]
        }); // eo apply

        yournamespace.MyPanel.superclass.initComponent.apply(this, arguments);

        this.comboBox.on('select', function(combo, record, index) {
            this.myGlobalVariable = record.get("something");
        }, this);

    }, // eo function initComponent

    someAction : function() {
        //do something
    },

    getMyGlobalVariable : function() {
        return this.myGlobalVariable;
    }

}); // eo extend

Ext.reg('mypanel', yournamespace.MyPanel);
0 голосов
/ 27 мая 2011

Не могли бы вы описать требования к пользовательскому интерфейсу, которые у вас есть немного больше?Вы уверены, что вам даже нужно создать целое поле для поддержки TreePanel?Почему бы не установить значение скрытого поля (см. «Скрытый» xtype в API) из обработчика щелчков на панели нормального дерева?

Чтобы ответить на свой вопрос более полно, вы можете найти множество учебных пособий о том, какрасширить компоненты ExtJS.Вы делаете это, используя методы Ext.override () или Ext.Extend ().

Но я чувствую, что вы, возможно, слишком усложняете свой дизайн.Вы можете достичь того, что вам нужно сделать, установив значение в этом скрытом поле.Если у вас есть сложные данные, вы можете установить значение в виде какой-либо строки XML или JSON.

EDIT Вот несколько уроков.Я настоятельно рекомендую придерживаться правила KISS, когда дело доходит до вашего дизайна пользовательского интерфейса.Держать его просто глупо! Расширение компонентов с помощью панелей

...