【问题标题】:How to create custom ExtJS form field component?如何创建自定义 ExtJS 表单域组件?
【发布时间】:2011-09-03 10:42:34
【问题描述】:

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

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

更新:总结的目的如下:

  • 这个新组件应该适合 将 GUI 形成为一个字段。它应该有 标签和相同的对齐方式(标签, 锚)的其他领域不需要 进一步的黑客攻击。

  • 可能,我有 写一些getValue,setValue 逻辑。我宁愿将它嵌入到这个组件中,也不愿制作单独的代码,将内容复制到我还必须管理的更多隐藏表单字段中。

【问题讨论】:

  • 你可能想看看 ItemSelector 扩展正是这样做的。
  • 类似问题在这里得到解答:stackoverflow.com/questions/6092112/…
  • 除了作为表单域之外,你还想让它做什么?

标签: forms extjs extjs4


【解决方案1】:

为了扩展 @RobAgar 的答案,遵循我为 ExtJS 3 编写的一个非常简单的日期时间字段,它是我为 ExtJS 4 制作的快速端口。重要的是使用 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)
    }
});

【讨论】:

    【解决方案2】:

    现在这很酷。前几天,我创建了一个小提琴来回答另一个问题,然后才意识到我跑题了。你在这里,终于让我注意到我的回答问题。谢谢!

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

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

    创建子组件

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

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

    此外,你可以善待自己,在调用 super 方法之前创建孩子。如果您在 super 方法之后创建子项,您可能会在子项尚未实例化时调用您的字段的 setValue 方法(见下文)。

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

    如您所见,我正在创建一个组件,这在大多数情况下都是您想要的。但是您也可以想花哨并组合多个子组件。在这种情况下,我认为最好尽快回到众所周知的领域:即创建一个容器作为子组件,并在其中进行组合。

    渲染

    然后是渲染的问题。起初我考虑使用fieldSubTpl 来渲染一个容器div,并让子组件在其中渲染自己。但是,在这种情况下,我们不需要模板功能,因此我们也可以使用 getSubTplMarkup 方法完全绕过它。

    我探索了 Ext 中的其他组件,以了解它们如何管理子组件的呈现。我在 BoundList 及其分页工具栏中找到了一个很好的示例(请参阅code)。所以,为了获取子组件的标记,我们可以结合子组件的getRenderTree方法使用Ext.DomHelper.generateMarkup

    所以,这是我们领域的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(',');
        }
    });
    

    【讨论】:

      【解决方案3】:

      呵呵。发布赏金后,我发现Ext.form.FieldContainer 不仅仅是一个 field 容器,而是一个完全成熟的 component 容器,所以有一个简单的解决方案。

      您需要做的就是扩展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
        }
      });
      

      【讨论】:

      • 嗨,Rob,我想你忘记了一件重要的事情; Ext.form.field.Field 混音。我添加了一个answer,并附上了如何使用它的示例。与您的非常相似,但发布它然后将所有内容添加到您的答案的 cmets 更容易。
      【解决方案4】:

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

      • 创建一个可提供最有用重用的字段扩展(如果您只想获取/设置字符串值,通常是 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,在这些地方这些事情很容易。

      祝你好运!

      【讨论】:

      • 他们在 Ext 4 中并不容易 - 这就是他要求 EXT 4 示例的原因:D
      • 不确定这个问题后来是否用 Ext4 标记,但在那个版本中,它应该像使用 Ext.form.field.Field mixin 并覆盖你的 getValuesetValue 一样简单。虽然我还没有这样做,但我知道这是我的想法。
      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

        按照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'
        }
        

        【讨论】:

        • 这并不真正符合自定义表单字段的条件 - 它只是一个组合框,应用了一组定义的默认值。
        【解决方案7】:

        许多答案要么使用 Mixin Ext.form.field.Field,要么只是扩展了一些适合他们需要的已经制作的类 - 这很好。

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

        除了设置和获取值之外,还会发生很多事情,如果你完全覆盖它 - 例如,你会弄乱脏状态、处理 rawValue 等。

        我猜这里有两个选项,一个是在您声明的方法中调用Parent(arguments) 以保持精简,或者在您完成后应用继承的方法从您得到它的地方(混合或扩展) .

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

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

        【讨论】:

          【解决方案8】:

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

          【讨论】:

          • 当 OP 要求使用 Ext 4 时,又是一个 Ext 3 示例。
          【解决方案9】:

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

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

          但我的感觉是,您的设计可能过于复杂。您可以通过为此隐藏字段设置值来实现您需要做的事情。如果您有复杂的数据,您可以将值设置为一些 XML 或 JSON 字符串。

          编辑这里有一些教程。我强烈建议您在 UI 设计中遵循 KISS 规则。保持简单愚蠢! Extending components using panels

          【讨论】:

          • 我为这个问题写了一个新部分。
          • 我已经表达了对自定义字段的要求,所以我不能忽略它。也许这很复杂,但我想做的是通过制作一个新组件将复杂的部分推入应用程序的较低级别,并通过使用这个新组件作为这样的 xtype 或 sg 来保持顶层简单。有道理吗?我正在寻找一个好的教程,但我还没有找到终极教程。
          • 再次重申,这是一个 Ext 3 示例,而不是 Ext 4。两个版本的框架的代码语法存在巨大差异。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-09-05
          • 2014-08-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-09-01
          • 2019-10-13
          相关资源
          最近更新 更多