One of the biggest features that draws developers to Ext JS is the vast array of UI widgets available out of the box. The ease with which they can be integrated with each other and the attractive and consistent visuals each of them offers is also a big attraction. No other framework can compete on this front, and this is a huge reason Ext JS leads the field of large-scale web applications.
In this article by Stuart Ashworth and Andrew Duncan by authors of the book, Ext JS Essentials, we will look at how UI widgets fit into the framework's structure, how they interact with each other, and how we can retrieve and reference them. We will then delve under the surface and investigate the lifecycle of a component and the stages it will go through during the lifetime of an application.
(For more resources related to this topic, see here.)
Every UI element in Ext JS extends from the base component class Ext.Component. This class is responsible for rendering UI elements to the HTML document. They are generally sized and positioned by layouts used by their parent components and participate in the automatic component lifecycle process.
You can imagine an instance of Ext.Component as a single section of the user interface in a similar way that you might think of a DOM element when building traditional web interfaces.
Each subclass of Ext.Component builds upon this simple fact and is responsible for generating more complex HTML structures or combining multiple Ext.Components to create a more complex interface.
Ext.Component classes, however, can't contain other Ext.Components. To combine components, one must use the Ext.container.Container class, which itself extends from Ext.Component. This class allows multiple components to be rendered inside it and have their size and positioning managed by the framework's layout classes.
Creating and manipulating UIs using components requires a slightly different way of thinking than you may be used to when creating interactive websites with libraries such as jQuery.
The Ext.Component class provides a layer of abstraction from the underlying HTML and allows us to encapsulate additional logic to build and manipulate this HTML. This concept is different from the way other libraries allow you to manipulate UI elements and provides a hurdle for new developers to get over.
The Ext.Component class generates HTML for us, which we rarely need to interact with directly; instead, we manipulate the configuration and properties of the component. The following code and screenshot show the HTML generated by a simple Ext.Component instance:
var simpleComponent = Ext.create('Ext.Component', { html : 'Ext JS Essentials!', renderTo: Ext.getBody() });
As you can see, a simple <DIV> tag is created, which is given some CSS classes and an autogenerated ID, and has the HTML config displayed inside it.
This generated HTML is created and managed by the Ext.dom.Element class, which wraps a DOM element and its children, offering us numerous helper methods to interrogate and manipulate it. After it is rendered, each Ext.Component instance has the element instance stored in its el property. You can then use this property to manipulate the underlying HTML that represents the component.
As mentioned earlier, the el property won't be populated until the component has been rendered to the DOM. You should put logic dependent on altering the raw HTML of the component in an afterrender event listener or override the afterRender method.
The following example shows how you can manipulate the underlying HTML once the component has been rendered. It will set the background color of the element to red:
Ext.create('Ext.Component', { html : 'Ext JS Essentials!', renderTo : Ext.getBody(), listeners: { afterrender: function(comp) { comp.el.setStyle('background-color', 'red'); } } });
It is important to understand that digging into and updating the HTML and CSS that Ext JS creates for you is a dangerous game to play and can result in unexpected results when the framework tries to update things itself. There is usually a framework way to achieve the manipulations you want to include, which we recommend you use first.
We always advise new developers to try not to fight the framework too much when starting out. Instead, we encourage them to follow its conventions and patterns, rather than having to wrestle it to do things in the way they may have previously done when developing traditional websites and web apps.
When a component is created, it follows a lifecycle process that is important to understand, so as to have an awareness of the order in which things happen. By understanding this sequence of events, you will have a much better idea of where your logic will fit and ensure you have control over your components at the right points.
The following process is followed when a new component is instantiated and rendered to the document by adding it to an existing container. When a component is shown explicitly (for example, without adding to a parent, such as a floating component) some additional steps are included. These have been denoted with a * in the following process.
First, the class' constructor function is executed, which triggers all of the other steps in turn. By overriding this function, we can add any setup code required for the component.
The next thing to be handled is the config options that are present in the class. This involves each option's apply and update methods being called, if they exist, meaning the values are available via the getter from now onwards.
The initComponent method is now called and is generally used to apply configurations to the class and perform any initialization logic.
Once added to a container, or when the show method is called, the component is rendered to the document.
At this stage, the component is rendered and has been laid out by its parent's layout class, and is ready at its initial size. This event will only happen once on the component's first layout.
If the component is a floating item, then the activate event will fire, showing that the component is the active one on the screen. This will also fire when the component is brought back to focus, for example, in a Tab panel when a tab is selected.
Similar to the previous step, the show event will fire when the component is finally visible on screen.
When we are removing a component from the Viewport and want to destroy it, it will follow a destruction sequence that we can use to ensure things are cleaned up sufficiently, so as to avoid memory leaks and so on. The framework takes care of the majority of this cleanup for us, but it is important that we tidy up any additional things we instantiate.
When a component is manually hidden (using the hide method), this event will fire and any additional hide logic can be included here.
Similar to the activate step, this is fired when the component becomes inactive. As with the activate step, this will happen when floating and nested components are hidden and are no longer the items under focus.
This is the final step in the teardown process and is implemented when the component and its internal properties and objects are cleaned up. At this stage, it is best to remove event handlers, destroy subclasses, and ensure any other references are released.
Ext JS boasts a powerful system to retrieve references to components called Component Queries. This is a CSS/XPath style query syntax that lets us target broad sets or specific components within our application. For example, within our controller, we may want to find a button with the text "Save" within a component of type MyForm.
In this section, we will demonstrate the Component Query syntax and how it can be used to select components. We will also go into details about how it can be used within Ext.container.Container classes to scope selections.
Before we dive in, it is important to understand the concept of xtypes in Ext JS. An xtype is a shorthand name for an Ext.Component that allows us to identify its declarative component configuration objects. For example, we can create a new Ext.Component as a child of an Ext.container.Container using an xtype with the following code:
Ext.create('Ext.Container', { items: [ { xtype: 'component', html : 'My Component!' } ] });
Using xtypes allows you to lazily instantiate components when required, rather than having them all created upfront.
Common component xtypes include:
Classes |
xtypes |
Ext.tab.Panel |
tabpanel |
Ext.container.Container |
container |
Ext.grid.Panel |
gridpanel |
Ext.Button |
button |
xtypes form the basis of our Component Query syntax in the same way that element types (for example, div, p, span, and so on) do for CSS selectors. We will use these heavily in the following examples.
We will use the following sample component structure—a panel with a child tab panel, form, and buttons—to perform our example queries on:
var panel = Ext.create('Ext.panel.Panel', { height : 500, width : 500, renderTo: Ext.getBody(), layout: { type : 'vbox', align: 'stretch' }, items : [ { xtype : 'tabpanel', itemId: 'mainTabPanel', flex : 1, items : [ { xtype : 'panel', title : 'Users', itemId: 'usersPanel', layout: { type : 'vbox', align: 'stretch' }, tbar : [ { xtype : 'button', text : 'Edit', itemId: 'editButton' } ], items : [ { xtype : 'form', border : 0, items : [ { xtype : 'textfield', fieldLabel: 'Name', allowBlank: false }, { xtype : 'textfield', fieldLabel: 'Email', allowBlank: false } ], buttons: [ { xtype : 'button', text : 'Save', action: 'saveUser' } ] }, { xtype : 'grid', flex : 1, border : 0, columns: [ { header : 'Name', dataIndex: 'Name', flex : 1 }, { header : 'Email', dataIndex: 'Email' } ], store : Ext.create('Ext.data.Store', { fields: [ 'Name', 'Email' ], data : [ { Name : 'Joe Bloggs', Email: 'joe@example.com' }, { Name : 'Jane Doe', Email: 'jane@example.com' } ] }) } ] } ] }, { xtype : 'component', itemId : 'footerComponent', html : 'Footer Information', extraOptions: { option1: 'test', option2: 'test' }, height : 40 } ] });
The Ext.ComponentQuery class is used to perform Component Queries, with the query method primarily used. This method accepts two parameters: a query string and an optional Ext.container.Container instance to use as the root of the selection (that is, only components below this one in the hierarchy will be returned). The method will return an array of components or an empty array if none are found.
We will work through a number of scenarios and use Component Queries to find a specific set of components.
As we have seen, we use xtypes like element types in CSS selectors. We can select all the Ext.panel.Panel instances using its xtype—panel:
var panels = Ext.ComponentQuery.query('panel');
We can also add the concept of hierarchy by including a second xtype separated by a space. The following code will select all Ext.Button instances that are descendants (at any level) of an Ext.panel.Panel class:
var buttons = Ext.ComponentQuery.query('panel buttons');
We could also use the > character to limit it to buttons that are direct descendants of a panel.
var directDescendantButtons = Ext.ComponentQuery.query('panel > button');
It is simple to select a component based on the value of a property. We use the XPath syntax to specify the attribute and the value. The following code will select buttons with an action attribute of saveUser:
var saveButtons = Ext.ComponentQuery.query('button[action="saveUser"]);
ItemIds are commonly used to retrieve components, and they are specially optimized for performance within the ComponentQuery class. They should be unique only within their parent container and not globally unique like the id config. To select a component based on itemId, we prefix the itemId with a # symbol:
var usersPanel = Ext.ComponentQuery.query('#usersPanel');
It is also possible to identify matching components based on the result of a function of that component. For example, we can select all text fields whose values are valid (that is, when a call to the isValid method returns true):
var validFields = Ext.ComponentQuery.query('form > textfield{isValid()}');
All of our previous examples will search the entire component tree to find matches, but often we may want to keep our searches local to a specific container and its descendants. This can help reduce the complexity of the query and improve the performance, as fewer components have to be processed.
Ext.Containers have three handy methods to do this: up, down, and query. We will take each of these in turn and explain their features.
This method accepts a selector and will traverse up the hierarchy to find a single matching parent component. This can be useful to find the grid panel that a button belongs to, so an action can be taken on it:
var grid = button.up('gridpanel');
This returns the first descendant component that matches the given selector:
var firstButton = grid.down('button');
The query method performs much like Ext.ComponentQuery.query but is automatically scoped to the current container. This means that it will search all descendant components of the current container and return all matching ones as an array.
var allButtons = grid.query('button');
Now that we know and understand components, their lifecycle, and how to retrieve references to them, we will move on to more specific UI widgets.
The tree panel component allows us to display hierarchical data in a way that reflects the data's structure and relationships.
In our application, we are going to use a tree panel to represent our navigation structure to allow users to see how the different areas of the app are linked and structured.
Like all other data-bound components, tree panels must be bound to a data store—in this particular case it must be an Ext.data.TreeStore instance or subclass, as it takes advantage of the extra features added to this specialist store class.
We will make use of the BizDash.store.Navigation TreeStore to bind to our tree panel.
The tree panel is defined in the Ext.tree.Panel class (which has an xtype of treepanel), which we will extend to create a custom class called BizDash.view.navigation.NavigationTree:
Ext.define('BizDash.view.navigation.NavigationTree', { extend: 'Ext.tree.Panel', alias: 'widget.navigation-NavigationTree', store : 'Navigation', columns: [ { xtype : 'treecolumn', text : 'Navigation', dataIndex: 'Label', flex : 1 } ], rootVisible: false, useArrows : true });
We configure the tree to be bound to our TreeStore by using its storeId, in this case, Navigation.
A tree panel is a subclass of the Ext.panel.Table class (similar to the Ext.grid.Panel class), which means it must have a columns configuration present. This tells the component what values to display as part of the tree. In a simple, traditional tree, we might only have one column showing the item and its children; however, we can define multiple columns and display additional fields in each row. This would be useful if we were displaying, for example, files and folders and wanted to have additional columns to display the file type and file size of each item.
In our example, we are only going to have one column, displaying the Label field. We do this by using the treecolumn xtype, which is responsible for rendering the tree's navigation elements. Without defining treecolumn, the component won't display correctly.
The treecolumn xtype's configuration allows us to define which of the attached data model's fields to use (dataIndex), the column's header text (text), and the fact that the column should fill the horizontal space.
Additionally, we set the rootVisible to false, so the data's root is hidden, as it has no real meaning other than holding the rest of the data together. Finally, we set useArrows to true, so the items with children use an arrow instead of the +/- icon.
In this article, we have learnt how Ext JS' components fit together and the lifecycle that they follow when created and destroyed. We covered the component lifecycle and Component Queries.
Further resources on this subject: