In the article, Using ViewControllers in Ext JS 5, we touched briefly on a feature improved in Ext JS 5. That is the “Declarative Event Listeners.” In this piece, we’ll dive in deeper and explore how you can use declarative listeners to;
- Simplify your application’s Views, and
- Reduce boilerplate code in your custom Components.
Note: This article assumes you are using the Ext JS 5.0.1 version or higher.
Table of Contents
What Are Declarative Listeners?
When we say “declarative listeners,” we refer to listeners registered on the body of a class or the configuration object of an instance using the listeners’ config. This feature is not new to the Ext JS network. In Ext JS 4, you could declare listeners on a class, but only if the handler function or scope had already been defined. For example:
Ext.define('MyApp.view.User', { extend: 'Ext.panel.Panel', listeners: { // function must inline or previously defined: collapse: function() { // respond to panel collapse here } }, // This method cannot be declared as the collapse handler: onCollapse: function() { } });
Declarative listeners were limited in Ext JS 4 because the requested handler function was not always accessible at class definition time. Developers typically added listeners by overriding initComponent and using the on method like this:
Ext.define('MyApp.view.User', { extend: 'Ext.panel.Panel', initComponent: function() { this.callParent(); this.on({ collapse: this.onCollapse, scope: this }); }, onCollapse: function() { console.log(this); // the panel instance } });
What Is The Scope Resolution Solution?
This was our solution. We improved the listener’s config in Ext JS 5 by allowing event handlers to be specified as strings that correspond to method names. The framework resolves the method names to actual function references at run time (any time an event is fired). We call this process the “Listener Sope Resolution.”
In Ext JS 4, you could only resolve string handlers if an explicit “scope” was given. In Ext JS 5, we added special rules for default scope resolution when a “string” listener is declared without an explicit scope.
Scope resolution has two possible outcomes:
- A component or
- A ViewController.
Whichever the result, the search begins with the component. It could be that the component or its ViewController is the scope. And if not, the framework will “climb” the component hierarchy until it finds a suitable component or ViewController. Here’s how it works;
Part 1: Resolving Scope to Components
The first way that the framework resolves scope is to look for a component with the defaultListenerScope config set to true. For listeners declared on the class, the search begins with the component itself. Look at this;
Ext.define('MyApp.view.user.User', { extend: 'Ext.panel.Panel', xtype: 'user', defaultListenerScope: true, listeners: { save: 'onUserSave' }, onUserSave: function() { console.log('user saved'); } });
This listener is then declared on the “class body” of the User view. This means that the framework will check the User view itself for defaultListenerScope before ascending the hierarchy. In this case, because the User view has defaultListenerScope set to true, the scope for this listener will resolve to the User view.
For listeners declared on an instance config, the component itself is skipped, and the framework searches upward, starting with the parent container. Like shown in the example below:
Ext.define('MyApp.view.main.Main', { extend: 'Ext.container.Container', defaultListenerScope: true, items: [{ xtype: 'user', listeners: { remove: 'onUserRemove' } }], onUserRemove: function() { console.log('user removed'); } });
This type of listener is declared on the “instance config” for the User view. Meaning that the framework will skip the User view (even though it was declared with defaultListenerScope:true) and resolve upward to the Main view.
Part 2: Resolving Scope to ViewControllers
In Ext JS 5, we introduced a new type of Controller called, Ext.app.ViewController. We covered ViewControllers in detail in Using ViewControllers in Ext JS 5, so we’ll focus only on event listeners as they relate to ViewControllers in this discussion.
In contrast to Ext.app.Controller, which can manage many views; each ViewController instance is bound to a single View instance. Because View and ViewController have a one-to-one relationship, the ViewController can serve as the default scope for listeners declared on its View or View’s items.
The same rules apply to ViewControllers as to defaultListenerScope. Class-level listeners always look for a ViewController on the component itself before searching upward in the component hierarchy. For Instance;
Ext.define('MyApp.view.user.User', { extend: 'Ext.panel.Panel', controller: 'user', xtype: 'user', listeners: { save: 'onUserSave' } }); Ext.define('MyApp.view.user.UserController', { extend: 'Ext.app.ViewController', alias: 'controller.user', onUserSave: function() { console.log('user saved'); } });
The above listener is declared on the “class body” of the User view. Because the User view has its own controller, the framework will resolve scope to the UserController. If the User view did not have its own controller, then the scope would resolve upwards in the hierarchy.
On the other hand, instance-level listeners skip the component and resolve to a ViewController upward in the hierarchy, starting with the parent Container. For example:
Ext.define('MyApp.view.main.Main', { extend: 'Ext.container.Container', controller: 'main', items: [{ xtype: 'user', listeners: { remove: 'onUserRemove' } }] }); Ext.define('MyApp.view.main.MainController', { extend: 'Ext.app.ViewController', alias: 'controller.main', onUserRemove: function() { console.log('user removed'); } });
How Is Listener Config Merged?
In Ext JS 4, listeners indicated on a base class could be completely overwritten by a listeners’ config, declared on a subclass or instance. In Ext JS 5, we improved on the listeners API by properly merging declared listeners between base classes, subclasses, and instances. To see this in action, let’s look at a simple example:
Ext.define('BaseClass', { extend: 'Ext.Component', listeners: { foo: function() { console.log('foo fired'); } } }); Ext.define('SubClass', { extend: 'BaseClass', listeners: { bar: function() { console.log('bar fired'); } } }); var instance = new SubClass({ listeners: { baz: function() { console.log('baz fired'); } } }); instance.fireEvent('foo'); instance.fireEvent('bar'); instance.fireEvent('baz');
In Ext JS 4, the above example would output “baz,” but in Ext JS 5, the listeners’ configs are merged correctly, and the output is “foo bar baz.” This allows classes to declare only the listeners they need without concern for what listeners their superclass might already have.
How Can You Get Started?
Looking for how to get started? Click here to download the latest version of Ext JS today; https://www.sencha.com/products/extjs/evaluate/
Pro tip: When you combine ViewControllers for handling application logic with ViewModels for two-way data binding, you should have a much-improved application development experience. Give it a try and let us know what you think.
Awesome, Ext JS 5 shows Sencha is listening to developers. Every improvement I see is a feast of recognition like the handler scoping and listener confit merging! Great work guys
Two questions:
1. If I have nested triples consisting of View+ViewController+ViewModel, how can I delegate method resolution to a ViewController somewhere up the hierarchy. Seems like this doesn’t work out of the box.
2. Is there a (preferably declarative) way to augment the method resolution with some custom parameters (which would be filled via declarative binding)?
Stephan, A Parent ViewController cannot handle a grandchild’s events if the Child has a ViewController of its own. That is a line of encapsulation that simply cannot be crossed, however, the Child ViewController could handle the grandchild’s event and relay it upward using fireViewEvent (http://docs.sencha.com/extjs/5.0/apidocs/#!/api/Ext.app.ViewController-method-fireViewEvent). Then the Parent can listen for the relayed event on the child.
I made a small fiddle to demonstrate: https://fiddle.sencha.com/#fiddle/a6c
I would be cautious of nesting ViewControllers too deeply though. If you find yourself trying to break out of the encapsulation boundaries too often it may be a sign that you have too many ViewControllers, and you should just combine them into one larger ViewController at a higher level in the hierarchy.
@Phil: Yes, I was aware of fireViewEvent but I did not know, that method resolution indeed bubbles up to a parent ViewController if the child doesn’t have one. Thank you! Maybe this should be made more explicit in the docs (or maybe I just didn’t see it).
Any idea for my second question?
Hello,
I am currently trying to migrate a ExtJs4 application to ExtJs6 and have some issue to reproduce old parent listener overriding. What should i do to refactor my child listeners to override the parent ones please ?
Ext.define(‘BaseClass’, {
extend: ‘Ext.Component’,
listeners: {
foo: function() {
console.log(‘foo fired’);
}
}
});
var instance = new BaseClass({
listeners: {
// here i want only execute this code and not the BaseClass one.
foo: function() {
console.log(‘baz fired’);
}
}
});
I think other customers could be interesting to. Only thing i could find in docs was : ” Users who were dependent upon the past behavior of declarative listeners that overrode their parent classes’ listeners will need to change their code to override the handler method instead.”
But i am not able to do that.