| Summary: This tutorial will describe how to create custom user interface controls by extending the capabilities of existing classes |
| Author: Nige, aka Animal |
| Published: January 7, 2009 |
| Ext Version: 2.0 |
Languages: English Chinese
|
Contents |
This document describes how to create custom user interface controls by extending the capabilities of existing classes in the ExtJs library version 2.0 and above. If you wish to discuss this tutorial, please use this forum thread.
When creating a new class, the decision must be made whether to own an instance of a utility class which is to perform a major role, or to extend that class.
When using ExtJs, it is recommended that you extend the nearest base class to the functionality required. This is because of the automated lifecycle management Ext provides which includes automated rendering when needed, automatic sizing and positioning of UI Components when managed by an appropriate layout manager, and automated destruction on removal from a Container.
It is easier to write a new class which is an ExtJs class and can take its place in the Container→Component hierarchy rather than a new class which has an ExtJs class, and then has to render and manage it from outside.
ExtJs’s Component class hierarchy uses the Template Method pattern to delegate to subclasses, behaviour which is specific only to that subclass.
The meaning of this is that each class in the inheritance chain may “contribute” an extra piece of logic to certain phases in the Component’s lifecycle. Each class implements its own special behaviour while allowing the other classes in the inheritance chain to continue to contribute their own logic.
An example is the render function. The render function must not be overridden, but it calls onRender during processing to allow the subclass implementor to add an onRender method to perform class-specific processing. Every onRender method must call its superclass’s onRender method before “contributing” its extra logic.
The diagram below illustrates the functioning of the onRender template method.
The render method is called (This is done by a Container’s layout manager). This method may not be overridden and is implemented by the Ext base class. It calls this.onRender which is the implementation within the current subclass (if implemented). This calls the superclass version which calls its superclass version etc. Eventually, each class has contributed its functionality, and control returns to the render function.
There are several Template Methods in the ExtJs Component lifecycle which provide useful points to implement class-specific logic.
Important: When subclassing, it is essential to use Template Methods to perform class logic at important phases in the lifecycle and not events. Events may be programmatically suspended, or may be stopped by a handler.
The Template Methods available to all Component subclasses are
Further Ext classes down the hierarchy add their own Template methods which are appropriate to their own capabilities.
Hint: When calling the superclass template method, the easiest way to ensure that all the correct arguments are passed is to invoke it using the Function.apply method:
Ext.ux.Subclass.superclass.onRender.apply(this, arguments);
Choosing the best class to extend is mainly a matter of efficiency, and which capabilities the base class must provide. There has been a tendency to always extend Ext.Panel whenever any set of UI controls needs to be rendered and managed.
The Panel class has many capabilities:
If these are not needed, then using a Panel is wasteful of resources.
If the required UI control does not need to contain any other controls, that is, if it just to encapsulate some form of HTML which performs the requirements, then extending either Ext.BoxComponent or Ext.Component is appropriate. If the required UI control does not have its size managed by the layout manager of its Container, then an extension of Ext.Component is the best option.
Important: Prior to Ext 3.0, the Component class did not automatically know what kind of element to create. In order for it to create the required Element, use the autoEl config option. Ext 3.0 assumes that the Component will be encapsulated by a <DIV>
For example, to encapsulate and add value to an image, a class might be defined thus:
Ext.ux.Image = Ext.extend(Ext.Component, { autoEl: { tag: 'img', src: Ext.BLANK_IMAGE_URL, cls: 'my-managed-image' }, // Add our custom processing to the onRender phase. // We add a ‘load’ listener to our element. onRender: function() { this.autoEl = Ext.apply({}, this.initialConfig, this.autoEl); Ext.ux.Image.superclass.onRender.apply(this, arguments); this.el.on('load', this.onLoad, this); }, onLoad: function() { this.fireEvent('load', this); }, setSrc: function(src) { if (this.rendered) { this.el.dom.src = src; } else { this.src = src; } } }); Ext.reg('image', Ext.ux.Image);
This class is an Ext Component which can participate in a non box-sizing layout which encapsulates the abilities of an image.
Usage:
{ xtype: 'image', isFormField: true, // Inform the FormLayout to render as a Field fieldLabel: 'Image', // The label enabled by the above setting src: 'http://extjs.com/deploy/dev/examples/shared/screens/desktop.gif', qtip: 'Image Tooltip' }
If the required UI control does not need to contain any other controls, that is, if it just to encapsulate some form of HTML which is to have its size and/or position managed by a layout manager, then Ext.BoxComponent is the appropriate class to extend.
For example, imagine a Logger class that is to simply add logged messages. It must participate in layouts, for example being inserted into a layout:’fit’ Window. This might be defined:
Ext.ux.Logger = Ext.extend(Ext.BoxComponent, { tpl: new Ext.Template("<li class='x-log-entry x-log-{0:lowercase}-entry'>", "<div class='x-log-level'>", "{0:capitalize}", "</div>", "<span class='x-log-time'>", "{2:date('H:i:s.u')}", "</span>", "<span class='x-log-message'>", "{1}", "</span>", "</li>"), autoEl: { tag: 'ul', cls: 'x-logger' }, onRender: function() { Ext.ux.Logger.superclass.onRender.apply(this, arguments); this.contextMenu = new Ext.menu.Menu({ items: [new Ext.menu.CheckItem({ id: 'debug', text: 'Debug', checkHandler: Ext.ux.Logger.prototype.onMenuCheck, scope: this }), new Ext.menu.CheckItem({ id: 'info', text: 'Info', checkHandler: Ext.ux.Logger.prototype.onMenuCheck, scope: this }), new Ext.menu.CheckItem({ id: 'warning', text: 'Warning', checkHandler: Ext.ux.Logger.prototype.onMenuCheck, scope: this }), new Ext.menu.CheckItem({ id: 'error', text: 'Error', checkHandler: Ext.ux.Logger.prototype.onMenuCheck, scope: this })] }); this.el.on('contextmenu', this.onContextMenu, this, {stopEvent: true}); }, onContextMenu: function(e) { this.contextMenu.logger = this; this.contextMenu.showAt(e.getXY()); }, onMenuCheck: function(checkItem, state) { var logger = checkItem.parentMenu.logger; var cls = 'x-log-show-' + checkItem.id; if (state) { logger.el.addClass(cls); } else { logger.el.removeClass(cls); } }, debug: function(msg) { this.tpl.insertFirst(this.el, ['debug', msg, new Date()]); this.el.scrollTo("top", 0, true); }, info: function(msg) { this.tpl.insertFirst(this.el, ['info', msg, new Date()]); this.el.scrollTo("top", 0, true); }, warning: function(msg) { this.tpl.insertFirst(this.el, ['warning', msg, new Date()]); this.el.scrollTo("top", 0, true); }, error: function(msg) { this.tpl.insertFirst(this.el, ['error', msg, new Date()]); this.el.scrollTo("top", 0, true); } });
Then using the following CSS:
.x-logger { overflow: auto; } .x-log-entry .x-log-level { float: left; width: 4em; text-align: center; margin-right: 3px; } .x-log-entry .x-log-time { margin-right: 3px; } .x-log-entry .x-log-message { margin-right: 3px; } .x-log-debug-entry, .x-log-info-entry, .x-log-warning-entry, .x-log-error-entry { display: none; } .x-log-show-debug .x-log-debug-entry { display: block } .x-log-show-info .x-log-info-entry { display: block } .x-log-show-warning .x-log-warning-entry { display: block } .x-log-show-error .x-log-error-entry { display: block } .x-log-debug-entry .x-log-level { background-color: #46c } .x-log-info-entry .x-log-level { background-color: green } .x-log-warning-entry .x-log-level { background-color: yellow } .x-log-error-entry .x-log-level { background-color: red }
We have an HTML list of log messages which participates in a layout. We added processing at the onRender phase to create a context menu which manipulates the visibility of logged items via CSS class names. Template methods added at this level which provide the opportunity to add extra functionality are
If the required UI control is to contain other UI elements, but does not need any of the previously mentioned additional capabilities of an Ext.Panel, then Ext.Container is the appropriate class to extend. Again, versions prior to Ext 3.0 must be provided with an autoEl configuration in order to be able to render an Element.
It must also be styled (either using the style config option, or CSS rules which apply to it via a class name on its Element) to acquire visual attributes, and scrolling behaviour.
Important: At the Container level, it is important to remember which layout class is to be used to render and manage child Components.
An example would be a class which encapsulated a query condition line which allowed the user to filter a Store based upon tests on fields in the Store. This would encapsulate the functionality and the layout tasks into one manageable class which could itself be added and removed to a Container allowing flexible creation of query conditions:
Ext.ux.FilterCondition = Ext.extend(Ext.Container, { layout: 'table', layoutConfig: { columns: 7 }, autoEl: { cls: 'x-filter-condition' }, Field: Ext.data.Record.create(['name', 'type']), initComponent: function() { this.fields = this.store.reader.recordType.prototype.fields; this.fieldStore = new Ext.data.Store(); // Create a Store containing the field names and types // in the passed Store. this.fields.each(function(f) { this.fieldStore.add(new this.Field(f)) }, this); // Create a Combo which allows selection of a field this.fieldCombo = new Ext.form.ComboBox({ triggerAction: 'all', store: this.fieldStore, valueField: 'name', displayField: 'name', editable: false, forceSelection: true, mode: 'local', listeners: { select: this.onFieldSelect, scope: this } }); // Create a Combo which allows selection of a test this.testCombo = new Ext.form.ComboBox({ triggerAction: 'all', store: ['<', '<=', '=', '!=', '>=', '>'] }); // Inputs for each type of field. Hidden and shown as necessary this.booleanInput = new Ext.form.Checkbox({ hideParent: true, hidden: true }); this.intInput = new Ext.form.NumberField({ allowDecimals: false, hideParent: true, hidden: true }); this.floatInput = new Ext.form.NumberField({ hideParent: true, hidden: true }); this.textInput = new Ext.form.TextField({ hideParent: true, hidden: true }); this.dateInput = new Ext.form.DateField({ hideParent: true, hidden: true }); this.items = [ this.fieldCombo, this.testCombo, this.booleanInput, this.intInput, this.floatInput, this.textInput, this.dateInput]; Ext.ux.FilterCondition.superclass.initComponent.apply(this, arguments); }, onFieldSelect: function(combo, rec, index) { this.booleanInput.hide(); this.intInput.hide(); this.floatInput.hide(); this.textInput.hide(); this.dateInput.hide(); var t = rec.get('type'); if (t == 'boolean') { this.booleanInput.show(); this.valueInput = this.booleanInput; } else if (t == 'int') { this.intInput.show(); this.valueInput = this.intInput; } else if (t == 'float') { this.floatInput.show(); this.valueInput = this.floatInput; } else if (t == 'date') { this.dateInput.show(); this.valueInput = this.dateInput; } else { this.textInput.show(); this.valueInput = this.textInput; } }, getValue: function() { return { field: this.fieldCombo.getValue(), test: this.testCombo.getValue(), value: this.valueInput.getValue() }; } });
This class manages the input fields it contains, and allows styling of the precise layout - size, padding etc – through the CSS class assigned to its Element. Template methods added at this level which provide the opportunity to add extra functionality are
If the required UI control must have a header, footer, or toolbars, then Ext.Panel is the appropriate class to extend.
Important: A Panel is a Container. It is important to remember which layout class is to be used to render and manage child Components.
Classes which extend Ext.Panel are usually highly application-specific and are generally used to aggregate other UI Components (Usually Containers, or form Fields) in a configured layout, and provide means to operate on the contained Components by means of controls in the tbar, the bbar.
Template methods added at this level which provide the opportunity to add extra functionality are
If the required UI control provides for user interaction, and is to be able to display application information and be modified so that the information may be sent back to the server, then the class to extend would be Ext.form.TextField, or Ext.Form.NumberField, or, if a Trigger button was needed, perhaps to trigger a key lookup, Ext.form.TriggerField.
Template methods added at this level which provide the opportunity to add extra functionality are
In some cases, creating a subclass may be “a hammer to crack a nut”. If just a few extra capabilities must be added to a class, there are three options fo modify the behaviour of a class.
When in one particular situation, one piece of functionality of an existing class needs to be overridden, or added to, an instance-specific implementation of a template method may be injected into a single instance of the class at construction time. This template method must not call its superclass method to perform the default processing, it must call its own class’s template method. It does this through the constructor’s prototype.
For example to create a one-off special text field with a button next to it to test the functioning of the entered web address:
new Ext.form.TextField({ name: 'associatedUrl', fieldLabel: 'Associated page', // Wrap the input, and render the test button onRender: function() { // Call this class's onRender to perform initial rendering this.constructor.prototype.onRender.apply(this, arguments); // Postprocess the rendered Field to add functionality this.wrap = this.el.wrap({ cls: 'x-form-field-wrap' }); this.testButton = new Ext.Button({ renderTo: this.wrap, buttonSelector: 'em', cls: 'x-trigger-button', style: { position: 'absolute', right: 0, top: 0 }, text: 'Test', handler: this.testUrl, scope: this }); if(!this.width){ this.wrap.setWidth(this.el.getWidth() + this.testButton.getEl().getWidth() + 5); } }, // Keep the wrap size correct onResize : function(w, h){ // Call this class's onResize to perform sizing calculations this.constructor.prototype.onResize.call(this, w, h); // Adjust the constituent element widths if(typeof w == 'number'){ this.el.setWidth(this.adjustWidth('input', w - (this.testButton.getEl().getWidth() + this.buttonSpacing))); } this.wrap.setWidth(this.el.getWidth() + this.testButton.getEl().getWidth() + this.buttonSpacing); }, // Perform the test by opening the fields value as a web page testUrl: function(button, event) { if (this.isValid()) { var e = this.testButton.getEl(); var w = window.open(this.getValue(), this.id.replace('-',''), 'status=true'); } } })
A Plugin is a class which implements an init(Component) method which is attached to a Component through the plugins config option. The Component passes itself when it calls the Plugin's init method at initialization time.
An example could be adding the ability to show local messages to a Component:
Ext.ux.PopupMessage = Ext.extend(Object, { init: function(c) { this.client = c; c.showMessage = this.showMessage.createDelegate(this); if (c.rendered) { this.onRender(c); } else { c.on('render', this.onRender, this); } }, onRender: function(c) { this.el = c.el.createChild( '<div class="x-hidden x-popup-el">' + '<div class="x-popup-body">' + '<span class="x-popup-message-text"></span>' + '</div>' + '</div>' ); this.el.syncFx(); this.msgEl = this.el.child('span.x-popup-message-text'); }, showMessage: function(m, cls) { if (this.fading) { clearTimeout(this.fading); this.fading = 0; } this.msgEl.dom.innerHTML = m; if (cls) { this.msgEl.extraCls = cls; this.msgEl.addClass(cls); } this.el.stopFx(); this.el.alignTo(this.client.el, "bl-bl", [0, -1]); this.el.slideIn('b').fadeIn({ // 3.0 chaining. 2.* must call separately callback: this.hide, scope: this }); }, hide: function() { this.fading = this.el.fadeOut.defer(5000, this.el, [{ callback: function() { this.msgEl.removeClass(this.msgEl.extraCls); }, scope: this }]); } });
Together with approptiate CSS rules:
.x-popup-el { position: absolute; background: transparent url(../../resources/images/default/qtip/tip-sprite.gif) no-repeat right 0; border-left:1px solid #99BBE8; padding-right:6px; overflow:hidden; zoom:1; } .x-popup-body { background: transparent url(../../resources/images/default/qtip/tip-sprite.gif) no-repeat 0 -62px; padding-top:3px; overflow:hidden; zoom:1; } .x-popup-message-text { padding:0 20px 0 10px; font-family:helvetica,tahoma,verdana,sans-serif; }
This adds the method showMessage(text, [extraCls]) to any Component which pops up a temporary message.
If all that is required is a specific configuration of an existing class, and no class methods are to be overridden, then creating a subclass is not necessary.
Instead, create a factory function which is called to create instances of the class configured according to need.
The factory method may use either or both of the two techniques illustrated above to create customized instances of existing classes:
function createMyPanel(config) { return new Ext.Panel(Ext.apply({//Pre-configured config options go here width: 300, height: 300, plugins: [ new Ext.ux.MyPluginClass() ] }, config)); }; var myfirstpanel = createMyPanel({ title: 'My First Panel' }); var mysecondpanel = createMyPanel({ title: 'My Second Panel' });