AngularJS: Quel est le meilleur moyen de se lier à un événement global dans une directive
Imaginez la situation dans AngularJS où vous souhaitez créer une directive devant répondre à un événement global. Dans ce cas, disons, l'événement de redimensionnement de la fenêtre.
Quelle est la meilleure approche pour cela? D'après ce que je vois, nous avons deux options: 1. Laisser chaque directive se lier à l'événement et faire sa magie sur l'élément actuel 2. Créer un écouteur d'événement global qui utilise un sélecteur DOM pour obtenir chaque élément sur lequel la logique devrait être appliqué.
L'option 1 présente l'avantage que vous avez déjà accès à l'élément sur lequel vous souhaitez effectuer certaines opérations. Mais ... options 2 présente l’avantage de ne pas avoir à lier plusieurs fois (pour chaque directive) le même événement, ce qui peut être un avantage en termes de performances.
Illustrons les deux options:
Option 1:
angular.module('app').directive('myDirective', function(){
function doSomethingFancy(el){
// In here we have our operations on the element
}
return {
link: function(scope, element){
// Bind to the window resize event for each directive instance.
angular.element(window).on('resize', function(){
doSomethingFancy(element);
});
}
};
});
Option 2:
angular.module('app').directive('myDirective', function(){
function doSomethingFancy(){
var elements = document.querySelectorAll('[my-directive]');
angular.forEach(elements, function(el){
// In here we have our operations on the element
});
}
return {
link: function(scope, element){
// Maybe we have to do something in here, maybe not.
}
};
// Bind to the window resize event only once.
angular.element(window).on('resize', doSomethingFancy);
});
Les deux approches fonctionnent bien, mais j'estime que la deuxième option n'est pas vraiment "angulaire".
Des idées?
J'ai choisi une autre méthode pour localiser efficacement les événements globaux, tels que le redimensionnement de fenêtre. Il convertit les événements Javascript en événements Angular scope, via une autre directive.
app.directive('resize', function($window) {
return {
link: function(scope) {
function onResize(e) {
// Namespacing events with name of directive + event to avoid collisions
scope.$broadcast('resize::resize');
}
function cleanUp() {
angular.element($window).off('resize', onResize);
}
angular.element($window).on('resize', onResize);
scope.$on('$destroy', cleanUp);
}
}
});
Qui peut être utilisé, dans le cas de base, sur l'élément racine de l'application
<body ng-app="myApp" resize>...
Et puis écoutez l'événement dans d'autres directives
<div my-directive>....
codé comme:
app.directive('myDirective', function() {
return {
link: function(scope, element) {
scope.$on('resize::resize', function() {
doSomethingFancy(element);
});
});
}
});
Cela présente un certain nombre d'avantages par rapport aux autres approches:
Non fragile à la forme exacte sur la façon dont les directives sont utilisées. Votre option 2 nécessite
my-directive
when angular traite les éléments suivants comme équivalents:my:directive
,data-my-directive
,x-my-directive
,my_directive
comme on peut le voir dans le guide des directivesVous avez un emplacement unique pour affecter exactement la conversion de l'événement Javascript en événement Angular, qui affecte alors tous les écouteurs. Supposons que vous souhaitiez ultérieurement annuler l'effet javascript
resize
, en utilisant fonction Lodash debounce . Vous pouvez modifier la directiveresize
en:angular.element($window).on('resize', $window._.debounce(function() { scope.$broadcast('resize::resize'); },500));
Parce qu'il ne déclenche pas nécessairement les événements sur
$rootScope
, vous pouvez limiter les événements à une partie seulement de votre application en déplaçant simplement la directiveresize
<body ng-app="myApp"> <div> <!-- No 'resize' events here --> </div> <div resize> <!-- 'resize' events are $broadcast here --> </div>
Vous pouvez étendre la directive avec des options et l'utiliser différemment dans différentes parties de votre application. Supposons que vous vouliez des versions différentes dans différentes parties:
link: function(scope, element, attrs) { var wait = 0; attrs.$observe('resize', function(newWait) { wait = $window.parseInt(newWait || 0); }); angular.element($window).on('resize', $window._.debounce(function() { scope.$broadcast('resize::resize'); }, wait)); }
Utilisé comme:
<div resize> <!-- Undebounced 'resize' Angular events here --> </div> <div resize="500"> <!-- 'resize' is debounced by 500 milliseconds --> </div>
Vous pouvez ensuite étendre la directive à d’autres événements pouvant être utiles. Peut-être que des choses comme
resize::heightIncrease
.resize::heightDecrease
,resize::widthIncrease
,resize::widthDecrease
. Vous avez alors un endroit dans votre application qui traite de la mémorisation et du traitement des dimensions exactes de la fenêtre.Vous pouvez transmettre des données avec les événements. Dites, par exemple, la hauteur/largeur de la fenêtre d'affichage où vous pourriez avoir besoin de traiter des problèmes inter-navigateurs (en fonction de la date à laquelle vous avez besoin de IE, et si vous incluez une autre bibliothèque pour vous aider).
angular.element($window).on('resize', function() { // From http://stackoverflow.com/a/11744120/1319998 var w = $window, d = $document[0], e = d.documentElement, g = d.getElementsByTagName('body')[0], x = w.innerWidth || e.clientWidth || g.clientWidth, y = w.innerHeight|| e.clientHeight|| g.clientHeight; scope.$broadcast('resize::resize', { innerWidth: x, innerHeight: y }); });
qui vous donne un seul endroit pour ajouter aux données plus tard. Par exemple. vous voulez envoyer la différence de dimensions depuis le dernier événement annoncé? Vous pourriez probablement ajouter un peu de code pour vous rappeler l'ancienne taille et envoyer la différence.
Cette conception fournit essentiellement un moyen de convertir, de manière configurable, des événements Javascript globaux en événements locaux Angular), et locaux non seulement pour une application, mais locaux pour différentes parties d'une application, en fonction des besoins. l'emplacement de la directive.
Lorsque je développe sur un framework, je trouve souvent utile de penser de manière agnologique à un problème avant de concevoir un idiomatique. Répondre au "quoi" et au "pourquoi" chasse le "comment".
La réponse ici dépend vraiment de la complexité de doSomethingFancy()
. Existe-t-il des données, un ensemble de fonctionnalités ou des objets de domaine associés aux instances de cette directive? S'agit-il d'une simple question de présentation, comme de rajuster les propriétés width
ou height
de certains éléments aux proportions appropriées de la taille de la fenêtre? Assurez-vous que vous utilisez le bon outil pour le travail; n'apportez pas tout le couteau suisse lorsque le travail exige des pincettes et que vous avez accès à une paire autonome. Afin de continuer dans la même veine, je vais opérer en supposant que doSomethingFancy()
est une fonction purement présentation.
Le souci d'encapsuler un événement de navigateur global dans un événement Angular pourrait être traité par une simple configuration de phase d'exécution:
angular.module('myApp')
.run(function ($rootScope) {
angular.element(window).on('resize', function () {
$rootScope.$broadcast('global:resize');
})
})
;
Maintenant Angular ne doit pas effectuer tout le travail associé à une directive sur chaque $digest
, Mais vous obtenez les mêmes fonctionnalités.
La deuxième préoccupation concerne le n
nombre d'éléments lorsque cet événement est déclenché. Encore une fois, si vous n'avez pas besoin de tous les avertissements d'une directive, il existe d'autres moyens pour y arriver. Vous pouvez développer ou adapter l'approche dans le bloc d'exécution ci-dessus:
angular.module('myApp')
.run(function () {
angular.element(window).on('resize', function () {
var elements = document.querySelectorAll('.reacts-to-resize');
})
})
;
Si vous avez une logique plus complexe qui doit se produire lors de l'événement de redimensionnement, cela ne signifie toujours pas qu'une ou plusieurs directives sont la meilleure façon de procéder. le gérer. Vous pouvez utiliser un service médiateur simple qui est instancié à la place de la configuration de phase d'exécution anonyme susmentionnée:
/**
* you can inject any services you want: $rootScope if you still want to $broadcast (in)
* which case, you'd have a "Publisher" instead of a "Mediator"), one or more services
* that maintain some domain objects that you want to manipulate, etc.
*/
function ResizeMediator($window) {
function doSomethingFancy() {
// whatever fancy stuff you want to do
}
angular.element($window).bind('resize', function () {
// call doSomethingFancy() or maybe some other stuff
});
}
angular.module('myApp')
.service('resizeMediator', ResizeMediator)
.run(resizeMediator)
;
Nous avons maintenant un service encapsulé qui peut être testé par unité, mais n'exécute pas de phases d'exécution inutilisées.
Quelques préoccupations qui pourraient également être prises en compte dans la décision:
- Écouteurs morts - avec l'option 1, vous créez au moins un écouteur d'événement pour chaque instance de la directive. Si ces éléments sont ajoutés ou supprimés dynamiquement dans le DOM et que vous n'appelez pas
$on('$destroy')
, vous courez le risque que les gestionnaires d'événements s'appliquent eux-mêmes lorsque leurs éléments n'existent plus. - Performances des opérateurs width/height - Je suppose qu'il existe une logique de modèle de boîte ici, étant donné que l'événement global est le redimensionnement du navigateur. Sinon, ignorez celui-ci; Si tel est le cas, vous devrez faire attention aux propriétés auxquelles vous accédez et à quelle fréquence, car les renvois du navigateur peuvent être un énorme coupable de la dégradation des performances .
Il est probable que cette réponse ne soit pas aussi "angulaire" que vous l'espériez, mais c'est la façon dont je résoudrais le problème tel que je le comprends avec l'hypothèse supplémentaire d'une logique basée sur un modèle de boîte uniquement.
À mon avis, j'irais avec la méthode n ° 1 et un petit Tweak d'utiliser le service $ window.
angular.module('app').directive('myDirective', function($window){
function doSomethingFancy(el){
// In here we have our operations on the element
}
return {
link: function(scope, element){
// Bind to the window resize event for each directive instance.
anguar.element($window).bind('resize', function(){
doSomethingFancy(element);
});
}
};
});
# 2 En référence à cette approche et à un léger changement de mentalité ici - vous pouvez placer cet auditeur d'événement quelque part plus haut dans dis app.run - et ici lorsque l'événement se produit, vous pouvez diffuser un autre événement pris en compte par la directive et faire quelque chose d'extraordinaire lorsque cet événement a lieu.
[~ # ~] edit [~ # ~] : Plus je pense à cette méthode, plus je commence à l’aimer par-dessus la première. .. Excellente manière robuste d’écouter l’événement de redimensionnement de la fenêtre - peut-être qu’à l’avenir, quelque chose d’autre doit également "connaître" cette information et, à moins que vous ne fassiez quelque chose comme cela, vous êtes obligé de configurer - encore une fois - un autre écouteur d'événement à l'événement window.resize.
app.run
app.run(function($window, $rootScope) {
angular.element($window).bind('resize', function(){
$rootScope.$broadcast('window-resize');
});
}
Directive angular.module ('app'). Directive ('myDirective', fonction ($ rootScope) {
function doSomethingFancy(el){
// In here we have our operations on the element
}
return {
link: function(scope, element){
// Bind to the window resize event for each directive instance.
$rootScope.$on('window-resize', function(){
doSomethingFancy(element);
});
}
};
});
Enfin Une excellente source d'informations sur la procédure est de suivre les types angulaires, par exemple i-bootstrap . J'ai appris un tas de trucs de la part de ces gars-là, par exemple les joies d'apprendre à tester unitairement en angulaire. Ils fournissent une excellente base de code propre à la caisse.
Voici une façon de le faire: il suffit de stocker vos éléments dans un tableau, puis dans le "événement global", vous pouvez parcourir les éléments en boucle et faire ce que vous devez faire.
angular.module('app').directive('myDirective', function($window){
var elements = [];
$window.on('resize', function(){
elements.forEach(function(element){
// In here we have our operations on the element
});
});
return {
link: function(scope, element){
elements.Push(element);
}
};
});
La seconde approche est plus fragile, car Angular offre de nombreuses façons de faire référence à la directive dans le modèle (my-directive
, my_directive
, my:directive
, x-my-directive
, data-my-directive
, etc.) de sorte qu'un sélecteur CSS les couvrant tous peut devenir vraiment complexe.
Ce n'est probablement pas un problème si vous utilisez uniquement les directives en interne ou si elles consistent en un seul mot. Mais si d’autres développeurs (avec des conventions de codage différentes) peuvent utiliser vos directives, vous voudrez peut-être éviter la deuxième approche.
Mais je serais pragmatique. Si vous avez affaire à une poignée d'instances, optez pour # 1. Si vous en avez des centaines, j'irais avec # 2.