In a previous article, Using ViewControllers in Ext JS 5, we touched briefly on a feature that has been greatly improved in Ext JS 5 — declarative event listeners. In this article, 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 Ext JS 5.0.1 or later.
Table of Contents
What Are Declarative Listeners?
When we say “declarative listeners,” we are referring to listeners that are declared on the body of a class or the configuration object of an instance using the listeners config. The ability to declare listeners in this manner is not new to Ext JS 5. 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() { } });
Because the desired handler function is not usually available at class definition time, declarative listeners had limited usefulness in Ext JS 4. Developers typically added listeners by overriding initComponent and using the on method:
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 } });
Scope Resolution
We improved the listeners config in Ext JS 5 by allowing event handlers to be specified as strings that correspond to method names. The framework resolves these method names to actual function references at run time (any time an event is fired). We refer to this process as listener scope resolution.
In Ext JS 4, you could only resolve string handlers if an explicit “scope” was given. In Ext JS 5, we’ve added some 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, but if not, the framework will “climb” the component hierarchy until it finds a suitable component or ViewController.
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.
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 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. Consider the following example:
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 listener is declared on the “instance config” for the User view. This means that the framework will skip the User view (even though it was declared with defaultListenerScope:true) and resolve upward to the Main view.
Resolving Scope to ViewControllers
In Ext JS 5, we introduced a new type of Controller, 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.
Unlike Ext.app.Controller, which can manage many views, each ViewController instance is bound to a single View instance. This one-to-one relationship between View and ViewController allows the ViewController to serve as the default scope for listeners declared on its View or its View’s items.
The same rules applies to ViewControllers as to defaultListenerScope. Class-level listeners always look for a ViewController on the component itself before searching upward in the component hierarchy.
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 the UserController. If the User view did not have its own controller, then 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'); } });
Listener Config Merging
In Ext JS 4, listeners that were declared on a base class would be completely overwritten by a listeners config declared on a subclass or instance. In Ext JS 5, we improved upon the listeners API by enabling proper merging of 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 just output “baz,” but in Ext JS 5 the listeners configs are correctly merged 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.
Conclusion
We think declarative listeners are a great step forward in simplifying the way you configure event listeners in your applications. Combine them with ViewControllers for handling application logic and ViewModels for two-way data binding, and 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.