Introducing React ReExt – Sencha Ext JS Components in React! LEARN MORE

Understanding Widgets in Ext JS 5

November 4, 2014 151 Views
Show

Understanding Widgets in Ext JS 5Ext JS 5 introduced support for components in grid cells using the new “widgetcolumn”. At the same time, Ext JS 5 introduced a new type of lightweight component called a “Widget”. There are several widgets included in Ext JS 5, and this article will show you how easy it is to build your own.

To illustrate the key concepts, we’re going to build a simple “ratings” widget. Something like this:

Getting Started

Unlike normal components that derive from Ext.Component, widgets derive from the new base class Ext.Widget. While an Ext.Widget derived class is almost entirely defined by the Config System (more on that later), Ext.Widget also defines how DOM elements are produced and how DOM events are wired to methods.

Rendering

The first consideration for a widget is to define its DOM tree. This is typically done by specifying the “element” property in the class declaration like so:

Ext.define(‘Ext.ux.rating.Picker’, {
extend: ‘Ext.Widget’,

//…

element: {
cls: ‘ux-rating-picker’,
reference: ‘element’,

children: [{
reference: ‘innerEl’,
cls: ‘ux-rating-picker-inner’,

listeners: {
click: ‘onClick’,
mousemove: ‘onMouseMove’,
mouseenter: ‘onMouseEnter’,
mouseleave: ‘onMouseLeave’
},

children: [{
reference: ‘valueEl’,
cls: ‘ux-rating-picker-value’
},{
reference: ‘trackerEl’,
cls: ‘ux-rating-picker-tracker’
}] }] },

//…
});

The “element” object is basically a Ext.dom.Helper specification for creating DOM elements. The primary additions being “reference” and “listeners” properties. These names will be familiar to those who have looked at ViewControllers, and they perform a similar function with widgets. In the case of Ext.Widget, all elements with a “reference” property will be cached on the widget instance using the name from the value of that property (for example, “element”, “innerEl”, etc.).

Events

In the “element” descriptor above, there are is also a “listeners” object defined on the “innerEl” object. These listeners are attached to the element produced from that piece of the specification. The methods are found by name lookup on the widget’s class. For example:

Ext.define(‘Ext.ux.rating.Picker’, {
extend: ‘Ext.Widget’,

//…

onClick: function (event) {
var value = this.valueFromEvent(event);
this.setValue(value);
},

onMouseEnter: function () {
this.element.addCls(this.overCls);
},

onMouseLeave: function () {
this.element.removeCls(this.overCls);
},

onMouseMove: function (event) {
var value = this.valueFromEvent(event);
this.setTrackingValue(value);
},

While this may look similar to writing traditional Component classes, the lack of initialization and cleanup code is probably rather striking. The Ext.Widget constructor handles creating elements, tracking their references and setting up their listeners. Beyond these activities (and the corresponding destroy method), Ext.Widget has no additional lifecycle or associated overhead.

Instead, the derived class defines its behavior by providing “config” properties using the Config System. For those who haven’t had a chance to get to know the config system, we’ll take a brief detour there now.

Config System 101

One of the core philosophies of Ext JS is the concept of “config” properties. They have been part of Ext JS since the beginning, but with Ext JS 5 (and Sencha Touch 2.x), the framework has formalized the mechanics of these properties. These formal “configs” are declared like this:

Ext.define(‘Ext.ux.rating.Picker’, {
//…
config: {
family: ‘monospace’
}
//…
});

The above declaration is basically equivalent to this handwritten code:

Ext.define(‘Ext.ux.rating.Picker’, {
//…

getFamily: function () {
return this._family;
},

setFamily: function (newValue) {
var oldValue = this._family;

if (this.applyTitle) {
newValue = this.applyFamily(newValue, oldValue); // #1
if (newValue === undefined) {
return this;
}
}

if (newValue !== oldValue) {
this._family = newValue;

if (this.updateFamily) {
this.updateFamily(newValue, oldValue); // #2
}
}

return this;
},

//…
});

Some obvious advantages of this automation are:

  • Clarity – Less code makes the class more readable.
  • Consistency – All configs behave the same way.
  • Flexibility – When done correctly, config properties can be changed at any time as opposed to set only at creation time (a common limitation for many older config properties in Ext JS).

There are two critical, yet optional, methods that developers can provide for any property such as “family”: applyFamily and updateFamily (#1 and #2 above). It is these methods that are almost always overridden rather than the get or set method.

applier

The apply method allows developers to convert the received value to the actual value for storage. For many apply methods, this could mean creating an instance of some class based on the received config object, or perhaps the apply method just standardizes the internal representation in one place to avoid checking in all the places the property is used.

updater

When a config property changes value, the update method is called. It is the responsibility of the update method to transition from the old value to the new value.

initConfig – Bringing it Together

Finally, for a class to participate in the config system, it must call the initConfig method at some point. In the case of Ext.Widget, this is done in the constructor. The initConfig method accepts the config object and processes each of its properties as well as those declared on the class by calling the appropriate set, apply and/or update methods.

This method also provides a “just in time” setup mechanism to resolve ordering issues between config properties. For example, if the update method of one config property needs the value of another config, it just calls the get method for the other config. Under the covers, initConfig ensures that the proper set/apply/update sequence is invoked for that requested property prior to returning the result.

Optimizing With cachedConfig

For widgets, many configs manipulate the DOM in some way. Because any given instance of a widget is unlikely to override all the default config values, it would be ideal if the result of processing those defaults could be cached. For these configs, we can make a simple change to the class:

Ext.define(‘Ext.panel.Panel’, {
//…
cachedConfig: {
family: ‘monospace’
}
//…
});

In most ways, these configs are the same as normal configs. When there are cached configs, however, the config system performs some extra processing when the first instance of the class is created.

The First Instance

Prior to processing the first instance’s config object, the config system initializes that first instance using only the default values from the class. This process will invoke the various apply and update methods which will in turn update the DOM elements initially produced from the “element” specification.

Consider the “family” config, which has this updater:

updateFamily: function (family) {
this.element.setStyle(‘fontFamily’, “‘” + family + “‘”);
},

All of the updaters contribute to the default state of the DOM for the widget. Once the configs are set to their default values, the afterCachedConfig method is called. It is in this method, for that first instance only, that Ext.Widget deeply clones the resulting DOM tree (using the cloneNode(true) DOM API).

The Second Instance (and Beyond)

When it’s time to create another instance of the same widget class, Ext.Widget will use the cached clone of the DOM tree and deeply clone it to produce the new widget’s DOM tree. This avoids the cost of reprocessing the “element” spec and running the updaters for the default values. If the config updaters are written properly, this process will be largely transparent.

Of course, Ext.Widget has some work to do once it has the duplicated DOM tree. Things such as retrieving references to the elements, wiring up listeners and setting any non-default value configs passed to the instance. The cost of this is now, however, directly related to the number of config values given to that instance instead of all configs for the class.

Reuse, Recycle

Now that we’ve seen how a single widget is created and initialized, there are some important concepts to mention related to using a widget in a widgetcolumn.

Because it’s always important to limit the number of instances we create, buffered rendering is key. Using this approach, the grid renders far fewer widget instances than there are records, and recycles them when rows are removed “behind” the scrolling area and new rows are rendered “in front”.

When these transitions take place, the widgetcolumn will move the widget in the DOM to the new row, read the field indicated by its dataIndex from the corresponding record and call setConfig on the widget to set its defaultBindProperty. This will invoke the apply and update methods, so as long as these are coded properly, the widget will now be reconfigured to represent the new field value.

In the case of our widget, since it represents a value that should be editable, we need to check in the updateValue method to see if the widget is being used in a grid cell:

column = me.getWidgetColumn && me.getWidgetColumn();
record = column && me.getWidgetRecord && me.getWidgetRecord();
if (record && column.dataIndex) {
record.set(column.dataIndex, value);
}

The getWidgetColumn and getWidgetRecord methods are placed on the widget by the widgetcolumn, so it knows its context in the grid.

Conclusion

While much of the discussion has been related to grids, widgets can be used anywhere a traditional Component can be used. This is true of our new rating widget as well as the sparkline widgets introduced in Ext JS 5.

Here is screenshot of the main panel of the example app showing four instances from its “items” array:

If any of the above sounds familiar, it may be that you recognize the pattern from Sencha Touch. While there are some extensions in Ext JS 5, Ext.Widget is essentially the latest version of Ext.AbstractComponent that was originally in Sencha Touch.

So when should you create a widget instead of a Component? In many ways, writing widgets is simpler than writing a Component. This is especially true if the layout requirements can be purely handled in CSS. Also, widgets will have crossover potential in the future, as we continue to combine the Sencha mobile and desktop frameworks.

Check out the complete code for our new widget here and the demo here. Enjoy and let us know what you think!