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

33

我想使用其他的ExtJS组件(如TreePanel)创建自定义的ExtJS表单字段组件。我应该如何最轻松地实现它?

我已经阅读了Ext.form.field.Base 的文档,但我不想通过 fieldSubTpl 定义字段主体。我只想编写代码来创建ExtJS组件,可能还有一些其他代码来获取和设置值。

更新: 总结目的如下:

  • 这个新组件应该适合作为表单GUI中的一个字段。它应该有标签并且与其他字段相同的对齐方式(标签,锚点),而无需进一步的修改。

  • 可能需要编写一些获取和设置值的逻辑。我宁愿将其嵌入到这个组件中,而不是编写单独的代码将内容复制到其他隐藏的表单字段中,我还需要管理这些字段。


你可能想要查看ItemSelector扩展,它正好可以做到这一点。 - Erich Kitzmueller
类似的问题在这里得到了解答:https://dev59.com/p2025IYBdhLWcg3wSkAq - lava
除了作为表单字段外,您还希望它做什么? - Neil McGuigan
10个回答

29

在@RobAgar的回答基础上,我提供了一个非常简单的日期时间字段,适用于ExtJS 3,以及我为ExtJS 4制作的快速端口。重要的是使用Ext.form.field.Field mixin。该mixin为表单字段的逻辑行为和状态提供了一个通用接口,包括:

获取器和设置器方法用于字段值 跟踪值和有效性更改的事件和方法 触发验证的方法

这可用于组合多个字段并让它们作为一个整体操作。对于完全自定义的字段类型,建议扩展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)
    }
});

25

现在这很酷。前几天,我创建了一个小工具来回答另一个问题,后来意识到我离题了。而你,最终让我注意到了我的回答的问题。谢谢!

那么,在实现来自其他组件的自定义字段时需要执行以下步骤:

  1. 创建子组件
  2. 渲染子组件
  3. 确保子组件的大小调整正确
  4. 获取和设置值
  5. 中继事件

创建子组件

首先,创建组件很容易。与为任何其他用途创建组件相比,没有什么特别之处。

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

此外,您可以在调用超级方法之前创建子项。如果在超级方法之后创建子项,则可能会在尚未实例化子项时调用字段的 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('');
}

现在,这还不够。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的三个事件,即change, dirtychangevaliditychange

再次强调,实现方式非常具体化,取决于您使用的子组件,说实话,我还没有深入探索这个方面。所以我会让您自己进行连接。

然而,我的初步结论是,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

嘿,发布了悬赏后,我发现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
  }
});

1
嗨,Rob,我认为你忘记了一个重要的东西; Ext.form.field.Field mixin。我添加了一个答案,其中包含如何使用它的示例。与您的方式非常相似,但是将其发布比将所有内容添加到您的答案评论中更容易。 - sra

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 提供了所有可重写的配置和函数的名称。

3
我已经做了几次了。这是我通常使用的一般流程/伪代码:
  • 创建一个字段扩展,提供最有用的重复使用功能(如果只想获取/设置字符串值,通常使用Ext.form.TextField)
  • 在字段的afterrender中,隐藏textfield,并使用this.wrap = this.resizeEl = this.positionEl = this.el.wrap()在此.el周围创建包装元素
  • 将任何组件渲染到this.wrap中(例如,在配置中使用renderTo: this.wrap
  • 覆盖getValuesetValue以与手动呈现的组件进行通信
  • 如果表单的布局更改,您可能需要在resize侦听器中进行一些手动调整大小
  • 不要忘记在beforeDestroy方法中清理任何创建的组件!
我迫不及待地想把我们的代码库切换到ExtJS 4,因为这些事情都很容易。
祝你好运!

2
他们在Ext 4中并不容易 - 这就是为什么他要求一个EXT 4示例的原因:D - slashwhatever
1
不确定问题是否后来被标记为Ext4,但在那个版本中,只需使用Ext.form.field.Field mixin并覆盖您的getValuesetValue即可轻松实现。虽然我还没有必须这样做的经验,但我知道这就是想法。 - Sean Adkinson

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设置为widget名称来简单地在表单中使用字段:

{
    xtype: 'LangSelector'
    fieldLabel: 'Language',
    name: 'lang'
}

3
这并不算是一个自定义表单字段,它只是一个带有一组预定义默认值的组合框。 - slashwhatever

1
许多答案要么使用Mixin Ext.form.field.Field,要么只是扩展了一些已经适合他们需求的类 - 这很好。
但我不建议完全重写setValue方法,我认为这样做真的很糟糕!
除了设置和获取值之外,还会发生很多事情,如果您完全重写它,那么您将会打乱脏状态、rawValue等的处理。
在这里有两个选项,一个是在您声明的方法内调用callParent(arguments)以保持流畅,或者在完成后应用从任何地方继承的方法(mixin或extend)。
但不要只是无视已经存在的方法在背后所做的事情而将其覆盖。
还要记住,如果您在新类中使用其他字段类型,则需要将isFormField属性设置为false - 否则表单上的getValues方法将获取这些值并运行它们!

0

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

又是一个 Ext 3 的例子,而 OP 却要求 Ext 4。 - slashwhatever

-2

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

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

但我觉得您可能过于复杂化了设计。通过将值设置为此隐藏字段,您可以实现所需的操作。如果您有复杂的数据,可以将该值设置为某些XML或JSON字符串。

编辑 这里有一些教程。当涉及到UI设计时,我强烈建议遵循KISS规则。保持简单傻瓜! 使用面板扩展组件


我为这个问题写了一个新的部分。 - pcjuzer
我已经表达了对自定义字段的需求,所以不能忽略它。也许这很复杂,但我想做的是通过创建一个新组件并将复杂部分推入应用程序的较低级别来保持顶层简单,通过使用此新组件作为xtype或sg等。这有意义吗?我正在寻找一个好的教程,但我还没有找到最终的教程。 - pcjuzer
再次强调,这是一个Ext 3的示例,而不是Ext 4。这两个版本的框架在代码语法上有很大的区别。 - slashwhatever

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接