ExtJS - learn to write components correctly

    I want to open a short series of articles devoted to the problem of creating custom components in ExtJS. In them I want to share my experience in this field with Habr readers, I will describe in detail all the subtleties of this process, what you should always pay attention to, what mistakes await novice programmers and how to avoid them.

    Sooner or later, the time comes when the standard ExtJS components cannot meet the needs of the developer. Or, in the process of refactoring an application, it becomes necessary to move a part of the interface (several components, form, table, tab) into a separate component. In both cases, you have to resort to creating custom components.

    The basics of this process have been discussed and described many times, I will not paint them, but I will depict them schematically:

    ищем подходящий компонент-прародитель –-> наследуем его при помощи Ext.extend –-> регистрируем xtype при помощи Ext.reg

    But behind the apparent simplicity hides many nuances. First of all, how to choose a suitable ancestor? Novice developers use the following approach - they select the inherited component so that as a result they write as little code as possible, and only within the framework of the constructions they know. They are frightened by onRender, creating elements, hanging event handlers. I will not admit, in certain cases, this approach is certainly correct and justifies its simplicity. You need a field with a button next to it - inherit Ext.form.TriggerField, you need a field with a drop-down list - inherit Ext.from.Combobox, you need to build in a visual editor - inherit Ext.form.TextArea. But there are also quite “contingency” situations in which the choice of the inherited component must be performed carefully and deliberately.

    Consider the following practical example. For the admin panel of one site with a video gallery, I needed to create a control to enter the duration of the clip. It should contain three input fields (hours, minutes, seconds) in one line and contain a single input / output (setValue / getValue methods), which would operate with a duration in seconds.

    A year and a half ago, when I was still a beginner ExtJS developer, I would solve this problem like this:
    • would inherit a component from Ext.Panel
    • using ColumnLayout would display in it three fields in three columns
    • would write getValue / setValue methods, accessing fields through furious constructions like this.items.items [0] .items.items [0] .getValue () ...


    Yes, the component would work, give / set values. True, his code would be terribly ugly, and the getValue () method would constantly access the fields and recount the total duration (even if the values ​​in the fields did not change). But this is not so bad. In the future, when it would be necessary to validate the form or use the methods of serializing / loading forms (getValues ​​/ setValues, loadRecord / updateRecord), I would inevitably run into problems. The form would simply “forget” about the existence of the component as such, persistently would not recognize it as its field. As a result, I would have to write a bunch of “crutches”, copy-paste the code from Ext.form.Field to make the component work as a form field.

    Therefore, at present I adhere to the following principle: any component that will have to work as a form field and participate in serialization and validation processes must be inherited exclusively from Ext.form.Field or any of its descendants.

    First, create a new component, inheriting Ext.form.Field:

    1. Ext.ux.TimeField = Ext.extend(Ext.form.Field, {
    2.  
    3.  
    4. });
    5.  
    6. Ext.reg('admintimefield', Ext.Admin.TimeField);
    * This source code was highlighted with Source Code Highlighter.


    Each form field renders its own element by default. In standard form component fields, this is either an input field or a checkbox. The input field element is stored in the el property after rendering. It also automatically resizes when the component container is resized.

    Since our component contains three fields inside, we will create as a default element a div into which our three fields and their labels will be “wrapped”. To change the tag and properties of an element by default, we define the defaultAutoCreate property:

    1. Ext.ux.TimeField = Ext.extend(Ext.form.Field, {
    2.  
    3.   defaultAutoCreate : {tag: 'div', 'class' : 'time-field-wrap'},
    4.  
    5. .................................................................
    * This source code was highlighted with Source Code Highlighter.


    Now you can create the internal structure (“frame”) of our input field. Put 6 divs in a row. Three of them will be containers for spinner controls (for entering hours of minutes and seconds), and the other three will contain corresponding labels. For clarity, we will create them not using Ext.DomHelper, but using the Ext.XTemplate template engine. All user renderer is placed in the inherited onRender method, after calling the parent method:

    1. Ext.Admin.TimeField = Ext.extend(Ext.form.Field, {
    2. timeFieldTpl : new Ext.XTemplate(
    3.    '
      ч
      ',
    4.    '
      м
      ',
    5.    '
      с
      '  
    6.   ),
    7. .................................................................
    8.  
    9. onRender : function(ct, position){
    10.   Ext.Admin.TimeField.superclass.onRender.call(this, ct, position);
    11.   this.el.update(this.timeFieldTpl.apply(this));
    12.  
    13. .................................................................
    * This source code was highlighted with Source Code Highlighter.


    So that the "frame" of the component is located as we need it - in one line, write and connect the following css table:

    1. div.hours-ct,
    2. div.minutes-ct,
    3. div.seconds-ct,
    4. div.timeunittext-ct {
    5.        display: inline-block;
    6.        width: 10px;
    7. }
    8.  
    9. div.hours-ct,
    10. div.minutes-ct,
    11. div.seconds-ct {
    12.        width: 50px;
    13. }
    * This source code was highlighted with Source Code Highlighter.


    For ease of implementation, I took the size of the fields fixed - 50 pixels.

    The wireframe of the component is ready. To complete the rendering procedure, it remains only to create and display the field components. First, using Ext.query, we find the DOM elements of their containers, and then create instances of the components, telling them to render to the appropriate containers:

    1. onRender : function(ct, position){
    2.   Ext.Admin.TimeField.superclass.onRender.call(this, ct, position);
    3.   this.el.update(this.timeFieldTpl.apply(this));
    4.   Ext.each(['hours', 'minutes', 'seconds'], function (i) {
    5.            this[i+'Ct'] = Ext.query('.' + i + '-ct', this.el.dom)[0];
    6.            this[i+'Field'] = Ext.create({
    7.              xtype: 'spinnerfield',
    8.              minValue: 0,
    9.              maxValue: i=='hours' ? 23 : 59,
    10.              renderTo : this[i+'Ct'],
    11.              width: 45,
    12.              value: 0
    13.            });         
    14.     }, this);
    15. .................................................................
    * This source code was highlighted with Source Code Highlighter.


    Note that the components themselves, after rendering, are stored in the properties of this.xxxField, which allows us to easily and conveniently access them (instead of the furious constructions described in a couple of paragraphs above).

    The visual part of the component is ready, it remains to complete the functional - getValue / setValue methods and support for validation / serialization.

    So that the getValue method does not recount the number of seconds each time, proceed as follows:

    • seconds will be stored in the value property
    • this property will be recalculated and updated if and only if we change the values ​​in the input fields
    • the getValue method will simply return the value of the value property


    Add methods to the component

    1. .................................................................
    2. getValue : function(){
    3.    return this.value;
    4. },
    5. getRawValue : function () {
    6.               return this.value;
    7. },
    8. onTimeFieldsChanged : function () {
    9.           this.value = this.hoursField.getValue() * 3600 + this.minutesField.getValue() * 60 + this.secondsField.getValue();
    10.           this.fireEvent('change', this, this.value);
    11. },
    12. .................................................................
    * This source code was highlighted with Source Code Highlighter.


    and when creating input fields, set onTimeFieldsChanged by the handler of all possible change events:
    1. .................................................................
    2. this[i+'Field'] = Ext.create({
    3.   xtype: 'spinnerfield',
    4.   minValue: 0,
    5.   maxValue: i=='hours' ? 23 : 59,
    6.   renderTo : this[i+'Ct'],
    7.   width: 45,
    8.   value: 0,
    9.   enableKeyEvents: true,
    10.   listeners : {
    11.          keyup: this.onTimeFieldsChanged,
    12.          spinup: this.onTimeFieldsChanged,
    13.          spindown: this.onTimeFieldsChanged,
    14.         scope: this
    15. }
    16.  
    17. .................................................................
    * This source code was highlighted with Source Code Highlighter.


    As you can see, when updating the value, we also relay the change event received from the input fields. This is still useful for us to support validation.

    To set the value, write the setValue method. I used to work with many custom components from third-party developers and in the implementation of most of them I had to fix the same glitch: an error while trying to call setValue if the component has not yet rendered. The developers simply forgot to check this and immediately turned to the this.el property (which has not yet been created). In our component, we will take this into account, and we will also additionally initialize the value to zero if it was not specified during creation:
    1. .................................................................
    2. initComponent: function () {
    3.           if (!Ext.isDefined(this.value)) this.value = 0;
    4.           Ext.Admin.TimeField.superclass.initComponent.call(this);
    5. }, 
    6.  
    7. setValue : function (v) {
    8.            var setFn = function (v) {
    9.                   var h = Math.floor(v / 3600),
    10.                     m = Math.floor((v % 3600) / 60),
    11.                     s = v % 60;
    12.                   this.hoursField.setValue(h);
    13.                   this.minutesField.setValue(m);
    14.                   this.secondsField.setValue(s);
    15.            };
    16.     this.value = v;
    17.     if (this.rendered) {
    18.       setFn.call(this, v);
    19.     } else {
    20.            this.on('afterrender', setFn.createDelegate(this, [v]), {single:true});
    21.     }
    22.   },
    23. .................................................................<
    * This source code was highlighted with Source Code Highlighter.


    As you can see, when you try to set the value of the component before rendering, it will only be saved in the this.value property, and the actual substitution of the necessary values ​​in the input fields will be delayed until the component is finally rendered (by installing a one-time afterrender event handler)

    And to give the component a "presentation" all that remains is to take care of validation and serialization.
    To implement validation, we will go the standard way Ext.from.Field, namely:

    • indicate the event at which the field will be revalidated (change)
    • set up validation monitoring for the desired event in initEvents
    • override validateValue method
    • make changes to CSS


    1. ................................................
    2. validationEvent : 'change',
    3. ................................................
    4.   initEvents : function () {
    5.      Ext.ux.TimeField.superclass.initEvents.call(this);
    6.      if (this.validationEvent !== false && this.validationEvent != 'blur'){
    7.      this.mon(this, this.validationEvent, this.validate, this, {buffer: this.validationDelay});
    8.    }
    9.   }, 
    10. ................................................
    11.   validateValue : function(value) {
    12.     if (this.allowBlank !== false) {
    13.        return true;
    14.     } else {
    15.      if (Ext.isDefined(value) && value != '' && value != '0' && value > 0) {
    16.         this.clearInvalid();
    17.         return true;     
    18.      } else {
    19.         this.markInvalid(this.blankText);
    20.         return false;
    21.      }
    22.     }
    23.   },
    * This source code was highlighted with Source Code Highlighter.


    1. .time-field-wrap.x-form-invalid {
    2.   background: none;
    3.   border: 0px none;
    4. }
    5.  
    6. .time-field-wrap.x-form-invalid .x-form-text {
    7.   background-color:#FFFFFF;
    8.   background-image:url(../../resources/images/default/grid/invalid_line.gif);
    9.   background-position: left bottom;
    10.   border-color:#CC3300;
    11. }
    * This source code was highlighted with Source Code Highlighter.


    When monitoring an event, buffering is applied. If we change the value faster than this.validationDelay (default is 250) ms, then only one call to the handler (for the last event in the series) will occur. This is a standard approach to monitoring validation events; it is used in all components.

    To make the component normally serialize, you have to go to tricks. At the moment, loading values ​​into it will be normal, the get / setValue methods will work without problems. But during serialization, it will give instead of a single value with the number of seconds three values ​​at once. This is because, in order to be compatible with the standard submit, forms are not serialized by accessing getValue methods, but by selecting form elements from the rendered HTML code (
    1. var setFn = function (v) {
    2. ................................................................
    3.         this.hiddenField.value = v;
    4.       };
    5. ....................................................
    6.   onTimeFieldsChanged : function () {
    7. ..............................................................................
    8.      this.hiddenField.value = this.value;
    9.      this.fireEvent('change', this, this.value);
    10.   },
    11.  
    12.   onRender : function(ct, position){
    13.     Ext.ux.TimeField.superclass.onRender.call(this, ct, position);
    14. ............................................................................................................
    15.     this.hiddenField = this.el.insertSibling({
    16.       tag:'input',
    17.       type:'hidden',
    18.       name: this.name || this.id,
    19.       id: (this.id+'hidden')
    20.     }, 'before', true);
    21.     if (this.value) this.setValue(this.value);
    22.   }
    * This source code was highlighted with Source Code Highlighter.


    That's all. As you can see, creating even non-standard components by inheriting Ext.form.Field is not as difficult as it might seem at first glance. The component we created fit in just 99 lines of code.

    You can download the example archive using the link ( alternative link without ExtJS distribution kit), and see the demo here .

    Also popular now: