How to create custom ExtJS form field component?

后端 未结 9 523
无人共我
无人共我 2020-12-02 11:34

I want to create custom ExtJS form field components using other ExtJS components in it (e.g. TreePanel). How can I do it most easily?

I\'ve read doc

相关标签:
9条回答
  • 2020-12-02 12:03

    Now that's cool. The other day, I created a fiddle to answer another question before realizing I was off-topic. And here your are, finally bringing to my attention the question to my answer. Thanks!

    So, here are the steps required in implementing a custom field from another component:

    1. Creating the child component
    2. Render the child component
    3. Ensuring the child component is sized and resized correctly
    4. Getting and setting value
    5. Relaying events

    Creating the child component

    The first part, creating the component, is easy. There's nothing particular compared to creating a component for any other usage.

    However, you must create the child in the parent field's initComponent method (and not at rendering time). This is because external code can legitimately expect that all dependent objects of a component are instantiated after initComponent (e.g. to add listeners to them).

    Furthermore, you can be kind to yourself and create the child before calling the super method. If you create the child after the super method, you may get a call to your field's setValue method (see bellow) at a time when the child is not yet instantiated.

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

    As you see, I am creating a single component, which is what you'll want in most case. But you can also want to go fancy and compose multiple child components. In this case, I think it would be clever to back to well known territories as quickly as possible: that is, create one container as the child component, and compose in it.

    Rendering

    Then comes the question of rendering. At first I considered using fieldSubTpl to render a container div, and have the child component render itself in it. However, we don't need the template features in that case, so we can as well bypass it completely using the getSubTplMarkup method.

    I explored other components in Ext to see how they manage the rendering of child components. I found a good example in BoundList and its paging toolbar (see the code). So, in order to obtain the child component's markup, we can use Ext.DomHelper.generateMarkup in combination with the child's getRenderTree method.

    So, here's the implementation of getSubTplMarkup for our field:

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

    Now, that's not enough. The code of BoundList learns us that there's another important part in component rendering: calling the finishRender() method of the child component. Fortunately, our custom field will have its own finishRenderChildren method called just when that needs to be done.

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

    Resizing

    Now our child will be rendered in the right place, but it will not respect its parent field size. That is especially annoying in the case of a form field, because that means it won't honor the anchor layout.

    That's very straightforward to fix, we just need to resize the child when the parent field is resized. From my experience, this is something that was greatly improved since Ext3. Here, we just need to not forget the extra space for the label:

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

    Handling value

    This part will, of course, depend on your child component(s), and the field you're creating. Moreover, from now on, it's just a matter of using your child components in a regular way, so I won't detail this part too much.

    A minima, you also need to implement the getValue and setValue methods of your field. That will make the getFieldValues method of the form work, and that will be enough to load/update records from the form.

    To handle validation, you must implement getErrors. To polish this aspect, you may want to add a handful of CSS rules to visually represent the invalid state of your field.

    Then, if you want your field to be usable in a form that will be submitted as an actual form (as opposed to with an AJAX request), you'll need getSubmitValue to return a value that can be casted to a string without damage.

    Apart from that, as far as I know, you don't have to worry about the concept or raw value introduced by Ext.form.field.Base since that's only used to handle the representation of the value in an actual input element. With our Ext component as input, we're way off that road!

    Events

    Your last job will be to implement the events for your fields. You will probably want to fire the three events of Ext.form.field.Field, that is change, dirtychange and validitychange.

    Again, the implementation will be very specific to the child component you use and, to be honest, I haven't explored this aspect too much. So I'll let you wire this for yourself.

    My preliminary conclusion though, is that Ext.form.field.Field offers to do all the heavy lifting for you, provided that (1) you call checkChange when needed, and (2) isEqual implementation is working with your field's value format.

    Example: TODO list field

    Finally, here's a complete code example, using a grid to represent a TODO list field.

    You can see it live on jsFiddle, where I tries to show that the field behaves in an orderly manner.

    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(',');
        }
    });
    
    0 讨论(0)
  • 2020-12-02 12:10

    Could you describe the UI requirements that you have a bit more? Are you sure that you even need to do build an entire field to support the TreePanel? Why not set the value of a hidden field (see the "hidden" xtype in the API) from a click handler on a normal tree panel?

    To answer your question more fully, you can find many tutorials on how to extend ExtJS components. You do this by leveraging the Ext.override() or Ext.Extend() methods.

    But my feeling is that you may be over-complicating your design. You can achieve what you need to do by setting a value to this hidden field. If you have complex data, you can set the value as some XML or JSON string.

    EDIT Here's a few tutorials. I highly recommend going with the KISS rule when it comes to your UI design. Keep It Simple Stupid! Extending components using panels

    0 讨论(0)
  • 2020-12-02 12:15

    Heh. After posting the bounty I found out that Ext.form.FieldContainer isn't just a field container, but a fully fledged component container, so there is a simple solution.

    All you need to do is extend FieldContainer, overriding initComponent to add the child components, and implement setValue, getValue and the validation methods as appropriate for your value data type.

    Here's an example with a grid whose value is a list of name/value pair objects:

    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
      }
    });
    
    0 讨论(0)
提交回复
热议问题