📜 ⬆️ ⬇️

ExtJS - learning how to write components

I want to open a small series of articles devoted to the problem of creating custom components in ExtJS. In them I want to share with Habr's readers my experience in this field, I will describe in detail all the subtleties of this process, to which one should always pay attention, what errors lie in the way of beginner programmers and how they can be avoided.

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 make 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 already been discussed and described many times, I will not paint them, but I will sketch schematically:

- –-> Ext.extend –-> xtype Ext.reg

But behind the apparent simplicity hides a lot of nuances. First of all - how to choose a suitable grandparent? Novice developers use the following approach - they select the inherited component so that they write as little code as possible in the end, and only within the framework of familiar constructions. They are intimidated by onRender, creating elements, hanging event handlers. I will not conceal that in certain cases this approach is certainly correct and justifies its simplicity. There is a field with a button next to it - inherit Ext.form.TriggerField, it is necessary a field with a drop-down list - inherit Ext.from.Combobox, you need to embed a visual editor - inherit Ext.form.TextArea. But there are also quite “abnormal” situations in which the choice of the inherited component should be carried out carefully and deliberately.
')
Consider the following practical example. For the admin panel of a single site with a video gallery, I needed to create a control for entering the duration of the video. It had to contain three input fields (hours, minutes, seconds) in one line and contain 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:

Yes, the component would work, give / set values. True, his code would be horribly ugly, and the getValue () method would constantly refer to the fields and recalculate the total duration (even if the values ​​in the fields did not change). But this is still half the trouble. In the future, when it would be necessary to validate the form or use the methods of serialization / loading of forms (getValues ​​/ setValues, loadRecord / updateRecord), I would inevitably run into problems. The form would simply “forget” about the existence of the component as such, would not persistently recognize it as its field. As a result, I would have to write a bunch of “crutches”, copy-paste 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 should work as a form field and participate in the serialization and validation processes should be inherited exclusively from Ext.form.Field or any of its descendants.

To begin with, let's create a new component, inheriting Ext.form.Field:

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


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

Since we have a component that contains three fields inside at once, we will create a div element in which our three fields and their labels will be wrapped as the default element. To change the tag and properties of the element by default, predetermine the property defaultAutoCreate:

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


Now you can create an internal structure ("frame") of our input field. Let's display 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. The entire custom render is placed in the onRender inherited method, after calling the parent method:

  1. Ext.Admin.TimeField = Ext.extend (Ext.form.Field, {
  2. timeFieldTpl: new Ext.XTemplate (
  3. '<div class = "hours-ct"> </ div> <div class = "timeunittext-ct"> h </ div>' ,
  4. '<div class = "minutes-ct"> </ div> <div class = "timeunittext-ct"> m </ div>' ,
  5. '<div class = "seconds-ct"> </ div> <div class = "timeunittext-ct"> with </ div>'
  6. ),
  7. .................................................. ...............
  8. onRender: function (ct, position) {
  9. Ext.Admin.TimeField.superclass.onRender.call ( this , ct, position);
  10. this .el.update ( this .timeFieldTpl.apply ( this ));
  11. .................................................. ...............
* This source code was highlighted with Source Code Highlighter .


In order to “frame” the component is located as we need - in one line, we 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. div.hours-ct,
  9. div.minutes-ct,
  10. div.seconds-ct {
  11. width: 50px;
  12. }
* This source code was highlighted with Source Code Highlighter .


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

The "frame" component is ready. To complete the rendering procedure, it remains only to create and display field components. First, we find using Ext.query the DOM elements of their containers, and then we create instances of the components, telling them to render the corresponding 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 fuzzy constructions described by the pair of paragraphs above).

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

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



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 the onTimeFieldsChanged handler for 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. .................................................. ...............
* This source code was highlighted with Source Code Highlighter .


As you can see, when updating the value, we also retranslate 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 bug: an error when trying to call setValue if the component had 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, as well as we will 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. setValue: function (v) {
  7. var setFn = function (v) {
  8. var h = Math.floor (v / 3600),
  9. m = Math.floor ((v% 3600) / 60),
  10. s = v% 60;
  11. this .hoursField.setValue (h);
  12. this .minutesField.setValue (m);
  13. this .secondsField.setValue (s);
  14. };
  15. this .value = v;
  16. if ( this .rendered) {
  17. setFn.call ( this , v);
  18. } else {
  19. this .on ( 'afterrender' , setFn.createDelegate ( this , [v]), {single: true });
  20. }
  21. },
  22. .................................................. ............... <
* This source code was highlighted with Source Code Highlighter .


As you can see, when you try to set the value of the component before the render, 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 final component render (by installing a one-time event handler afterrender)

And to make the component “marketable” it remains only to take care of validation and serialization.
To implement the validation, we will go the standard way Ext.from.Field, namely:



  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. .time-field-wrap.x-form-invalid .x-form-text {
  6. background-color: #FFFFFF;
  7. background-image: url (../../ resources / images / default /grid/invalid_line.gif);
  8. background-position: left bottom;
  9. border-color: # CC3300;
  10. }
* This source code was highlighted with Source Code Highlighter .


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

To make the component serialize normally, you have to go for tricks. At the moment, loading of values ​​into it will be performed normally, and get / setValue methods will work without problems. But during serialization, it will give three values ​​instead of a single value with a count of seconds. This is because, based on compatibility with standard submit, forms are not serialized by calling the getValue methods, but by selecting the form (<input, <textarea, etc.) from the rendered HTML code of the elements and reading to their value properties. Therefore, we will have to create and update the hidden field for any changes in the component value. The same approach, by the way, is used in the implementation of Ext.form.Combobox

  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. onRender: function (ct, position) {
  12. Ext.ux.TimeField.superclass.onRender.call ( this , ct, position);
  13. .................................................. .................................................. ........
  14. this .hiddenField = this .el.insertSibling ({
  15. tag: 'input' ,
  16. type: 'hidden' ,
  17. name: this .name || this .id
  18. id: ( this .id + 'hidden' )
  19. }, 'before' , true );
  20. if ( this .value) this .setValue ( this. value);
  21. }
* 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 to you at first glance. The component we created fit in just 99 lines of code.

You can download the archive with an example from the link ( an alternative link without the ExtJS distribution), and see the demo here .

Source: https://habr.com/ru/post/88261/


All Articles