如何创建自定义ExtJS表单字段组件?

时间:2011-05-27 14:04:54

标签: forms extjs extjs4

我想使用其中的其他ExtJS组件(例如TreePanel)创建自定义ExtJS 表单字段组件。我怎样才能最轻松地完成这项工作?

我已阅读Ext.form.field.Base的文档,但我不想按fieldSubTpl定义字段正文。我只想编写创建ExtJS组件的代码,也可以编写一些获取和设置值的代码。

更新:汇总目的如下:

  • 这个新组件应该适合 将GUI表示为字段。应该有 标签和相同的对齐方式(标签, 其他领域没有必要 进一步黑客攻击。

  • 可能,我有 写一些getValue,setValue 逻辑。我宁愿将它嵌入到这个组件中,而不是将分离的代码复制到我必须管理的更隐藏的表单字段中。

9 个答案:

答案 0 :(得分:27)

为了扩展@RobAgar的答案,遵循我为ExtJS 3编写的非常简单的Date Time字段,它是我为ExtJS 4创建的quickport。重要的是使用Ext.form.field.Field mixin。这个mixin为逻辑行为和表单字段状态提供了一个通用接口,包括:

字段值的getter和setter方法 跟踪价值和有效性变化的事件和方法 触发验证的方法

这可用于组合多个字段,并将它们作为一个字段进行操作。对于总自定义字段类型,我建议扩展Ext.form.field.Base

这是我上面提到的例子。即使是像日期对象那样我们需要在getter和setter中格式化数据,这应该是多么容易。

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)
    }
});

答案 1 :(得分:24)

现在很酷。前几天,我创造了一个小提琴来回答另一个问题,然后才意识到我是偏离主题的。在这里,你终于把我的问题提到了我的答案。谢谢!

因此,以下是从另一个组件实现自定义字段所需的步骤:

  1. 创建子组件
  2. 渲染子组件
  3. 确保子组件的大小和大小正确
  4. 获取和设置值
  5. 转发事件
  6. 创建子组件

    创建组件的第一部分很简单。与为任何其他用途创建组件相比,没有什么特别的。

    但是,您必须在父字段的initComponent方法中创建子项(而不是在呈现时)。这是因为外部代码可以合法地期望在initComponent之后实例化组件的所有依赖对象(例如,向它们添加侦听器)。

    此外,您可以善待自己并在调用super方法之前创建子项。如果您在super方法之后创建子项,则可能会在子项尚未实例化时调用您的字段的setValue方法(请参阅下面的内容)。

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

    如您所见,我正在创建一个单独的组件,这在大多数情况下都是您想要的。但是你也可以想要花哨并组合多个子组件。在这种情况下,我认为尽快回到众所周知的领域是很聪明的:也就是说,创建一个容器作为子组件,然后组成它。

    渲染

    然后是渲染问题。起初我考虑使用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('');
    }
    

    现在,这还不够。 code of 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);
    }
    

    处理价值

    这部分当然取决于您的子组件以及您正在创建的字段。此外,从现在开始,这只是一个以常规方式使用您的子组件的问题,所以我不会过多地详述这部分。

    最小值,您还需要实现字段的getValuesetValue方法。这将使表单的getFieldValues方法起作用,这足以从表单加载/更新记录。

    要处理验证,您必须实施getErrors。为了完善这一方面,您可能需要添加一些CSS规则来直观地表示您的字段的无效状态。

    然后,如果您希望您的字段可以在将作为实际表单提交的表单中使用(而不是使用AJAX请求),则需要getSubmitValue来返回可以是的值在没有损坏的情况下铸成一根绳子。

    除此之外,据我所知,您不必担心Ext.form.field.Base引入的概念或raw value,因为它仅用于处理实际值的表示输入元素。以我们的Ext组件作为输入,我们离开了这条路!

    活动

    您的上一份工作是为您的领域实施活动。您可能希望触发Ext.form.field.Field的三个事件,即changedirtychangevaliditychange

    同样,实现将非常特定于您使用的子组件,说实话,我没有太多地探讨这个方面。所以我会让你自己联系。

    我的初步结论是Ext.form.field.Field提供了为你做所有繁重的工作,前提是(1)你需要时拨打checkChange,以及(2)isEqual实施正在使用您的字段的值格式。

    示例:TODO列表字段

    最后,这是一个完整的代码示例,使用网格来表示TODO列表字段。

    你可以看到它live on 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(',');
        }
    });
    

答案 2 :(得分:4)

嘿。在发布赏金后,我发现Ext.form.FieldContainer不仅仅是字段容器,而是一个完全成熟的组件容器,因此有一个简单的解决方案。

您需要做的就是扩展FieldContainer,覆盖initComponent以添加子组件,并实施setValuegetValue以及适合您的值的验证方法数据类型。

以下是一个网格示例,其值为名称/值对对象列表:

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 :(得分:3)

我已经这样做了几次。这是我使用的一般过程/伪代码:

  • 创建一个字段扩展,提供最有用的重用(如果您只想获取/设置字符串值,通常为Ext.form.TextField)
  • 在该字段的afterrender中,隐藏文本字段,并使用this.wrap = this.resizeEl = this.positionEl = this.el.wrap()
  • 在this.el周围创建一个包装元素
  • 将所有组件呈现为this.wrap(例如,在配置中使用renderTo: this.wrap
  • 覆盖getValuesetValue以与您手动呈现的组件对话
  • 如果表单的布局发生变化,您可能需要在resize侦听器中手动调整大小
  • 不要忘记清除您在beforeDestroy方法中创建的任何组件!

我迫不及待地将我们的代码库切换到ExtJS 4,在这种情况下这些事情很容易。

祝你好运!

答案 4 :(得分:3)

由于问题相当模糊 - 我只能提供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提供了可以覆盖的所有配置和功能的名称。

答案 5 :(得分:2)

遵循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'
}

答案 6 :(得分:1)

许多答案要么使用Mixin Ext.form.field.Field,要么扩展到一些已经满足其需求的已经制作的类 - 这很好。

但是我不建议完全覆盖setValue方法,那就是IMO非常糟糕的形式!

除了设置和获取值之外还发生了很多其他事情,并且如果你完全覆盖它 - 那么你就会搞砸脏状态,处理rawValue等等。

我猜这里有两个选项,一个是在你声明的方法中使用callParent(arguments)以保持简化,或者在你完成时最后应用继承的方法(mixin或extend)

但不要只是覆盖它而不考虑已经制作的方法在幕后做什么。

还要记住,如果在新类中使用其他字段类型 - 那么请将isFormField属性设置为false - 否则表单上的getValues方法将获取这些值并与em一起运行!

答案 7 :(得分:-1)

以下是扩展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);

答案 8 :(得分:-2)

您能描述一下您需要更多的UI要求吗?您确定甚至需要构建整个字段来支持TreePanel吗?为什么不从普通树面板上的单击处理程序设置隐藏字段的值(请参阅API中的“隐藏”xtype)?

为了更全面地回答您的问题,您可以找到许多有关如何扩展ExtJS组件的教程。您可以通过利用Ext.override()或Ext.Extend()方法来实现此目的。

但我的感觉是你的设计可能过于复杂。您可以通过为此隐藏字段设置值来实现您需要执行的操作。如果您有复杂的数据,则可以将该值设置为某些XML或JSON字符串。

编辑以下是一些教程。在您的UI设计方面,我强烈建议您使用KISS规则。保持简单愚蠢! Extending components using panels

相关问题