C'est quoi, des declarative listeners ?
Quand nous disons “declarative listeners
,” nous faisons référence à des listeners
qui sont déclarés dans le corps d'une classe ou dans l'objet de configuration d'une instance utilisant la config
listeners
. La capacité de déclarer des listeners
de cette manière n'est pas une nouveauté de Ext JS 5. Avec Ext JS 4, vous pouviez déclarer des listeners
dans une classe mais seulement si la fonction de gestion (handler
) ou la portée (scope
) avait déjà été définie. Par exemple :
Ext.define('MyApp.view.User', { extend: 'Ext.panel.Panel', listeners: { // la fonction doit être « inline » ou avoir été définie au préalable : collapse: function() { // réagir au masquage du panel ici } }, // Cette méthode ne peut pas être déclarée comme handler pour collapse // (définie après) : onCollapse: function() { } });
Du fait que la fonction handler
désirée n'est habituellement pas disponible au moment de la définition de la classe, les declarative listeners
avaient une utilité limitée avec Ext JS 4. Typiquement, les développeurs ajoutaient des listeners
en surchargeant initComponent
et en utilisant la méthode on
:
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); // l'instance du panel } });
Résolution de portée
Nous (NdT : eux, moi j'ai rien fait) avons amélioré la config
listeners
dans Ext JS 5 en permettant d'indiquer les handlers
d'événement par leurs noms (sous forme d'une chaîne de caractères) correspondant aux noms des méthodes. Le framework transforme ces noms de méthodes en références réelles de fonctions à l'exécution (à chaque fois qu'un événement est lancé). Nous faisons référence à ce processus en tant que « listener scope resolution
» (résolution de portée des listeners
).
Avec Ext JS 4, vous ne pouviez résoudre les handlers
via leur nom que si un scope
était explicitement donné. Avec Ext JS 5, nous avons ajouté certaines règles spéciales permettant la résolution du scope
par défaut quand un listener
est déclaré via son nom sans que le scope
soit explicitement indiqué.
La résolution de portée a deux résultats possibles : un composant (component
) ou un ViewController
. Quel que soit le résultat, la recherche commence avec le composant. Il se peut que le composant ou son ViewController
soit la portée cherchée, mais si ce n'est pas le cas, le framework va « remonter » la hiérarchie du composant jusqu'à ce qu'il trouve un composant ou un ViewController
qui corresponde.
Résolution de la portée en composants
La première façon dont le frameworlk résout la portée est de chercher un composant dont la config
defaultListenerScope
est positionnée à true
. Pour les listeners
déclarés sur la classe, la recherche commence par le composant lui-même.
Ext.define('MyApp.view.user.User', { extend: 'Ext.panel.Panel', xtype: 'user', defaultListenerScope: true, listeners: { save: 'onUserSave' }, onUserSave: function() { console.log('utilisateur enregistré'); } });
Le listener
est déclaré dans le corps de la classe de la vue User. Ça signifie que le framework va vérifier le defaultListenerScope
de la vue User
elle-même avant de remonter dans la hiérarchie. Dans le cas présent, comme la vue User
possède la config
defaultListenerScope
mise à true
, la portée pour le ce listener
se résoudra avec la vue User
.
Pour les listeners
déclarés sur une config
d'instance, le composant lui-même est ignoré et le framework cherche plus haut en commençant par le container parent. Examinez l'exemple suivant :
Ext.define('MyApp.view.main.Main', { extend: 'Ext.container.Container', defaultListenerScope: true, items: [{ xtype: 'user', listeners: { remove: 'onUserRemove' } }], onUserRemove: function() { console.log('utilisateur supprimé'); } });
Le listener est déclaré sur la config
de l'instance pour la vue User
. Ça signifie que le framework va ignorer la vue User
(même si elle a été déclarée avec defaultListenerScope:true
) et résoudre la portée en remontant vers la vue Main
.
Résolution de la portée en ViewControllers
Avec Ext JS 5, nous avons ajouté un nouveau type de Controller
, Ext.app.ViewController
. Nous avons décrit ces ViewControllers
en détails dans Utiliser les ViewControllers avec Ext JS 5, si bien que nous allons seulement nous concentrer ici sur les event listeners
et la façon dont ils sont liés aux ViewControllers
.
Contrairement aux Ext.app.Controller
qui peuvent gérer plusieurs vues, chaque instance de ViewController
n'est liée qu'à une unique instance de vue. Cette relation un-un entre la vue et le ViewController
permet au ViewController
de servir de portée par défaut pour les listeners
déclarés dans sa vue ou ses éléments de vue.
La même règle de defaultListenerScope
s'applique aux ViewControllers
. Les listeners
du niveau de la classe cherchent toujours un ViewController
sur le composant lui-même avant de remonter dans la hiérarchie des composants.
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('utilisateur enregistré'); } });
Le listener
précédent est déclaré dans le corps de la classe de la vue User. Puisque la vue User possède son propre contrôleur, le framework va résoudre la portée à UserController
. Si la vue User
n'avait pas eu son propre contrôleur, la portée aurait été résolue en remontant dans la hiérarchie.
D'un autre côté, les listeners
du niveau d'instance ignorent le composant et se résolvent en tant que ViewController
en remontant dans la hiérarchie en commençant par le container parent. Par exemple :
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('utilisateur supprimé'); } });
Fusion des config listener
Avec Ext JS 4, les listeners
qui étaient déclarés dans une classe auraient été complètement écrasés par une config
listeners
déclarée dans une sous-classe ou une instance. Avec Ext JS 5, nous avons amélioré l'API listeners
en autorisant une fusion adaptée des listeners
déclarés dans les classes, les sous-classes et les instances. Pour voir ceci en action, jetez un œil à cet exemple simple :
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');
Avec Ext JS 4, l'exemple précédent aurait simplement retourné “baz
,” dans la console, mais avec Ext JS 5, les configs
listeners
sont correctement fusionnés et la sortie console est “foo bar baz
.” Ceci permet aux classes de ne déclarer que les listeners
qui les concernent sans s'occuper des listeners
que leurs super-classes pourraient déjà posséder.
Conclusion
Nous pensons que les declarative listeners
sont un formidable pas en avant pour simplifier la façon dont vous configurez les listeners
d'événements dans vos applications. Associez-les avec des ViewControllers
pour gérer la logique de votre application et des ViewModels
pour le data binding
bidirectionnel et vous devriez fortement améliorer votre façon de développer. Essayez et dites-nous ce que vous en pensez.
Écrit par Phil Guerrant
Phil est un ingénieur développement de Sencha qui travaille sur Ext JS. Il a plus de 10 ans d'expérience en tant que développeur et est spécialisé dans le développement HTML5 et web, les IHM et les méthodes agiles.