javascript - directivas - AngularJS: ¿Cuál es la mejor manera de vincularse a un evento global en una directiva?
directive angularjs (5)
Imagine la situación en AngularJS donde desea crear una directiva que necesita responder a un evento global. En este caso, digamos, el evento de cambio de tamaño de la ventana.
¿Cuál es el mejor enfoque para esto? De la forma en que lo veo, tenemos dos opciones: 1. Deje que cada directiva se una al evento y haga magia en el elemento actual 2. Cree un detector de eventos global que haga un selector de DOM para obtener cada elemento sobre el cual debería estar la lógica aplicado.
La opción 1 tiene la ventaja de que ya tiene acceso al elemento en el que desea realizar algunas operaciones. Pero ... las opciones 2 tienen la ventaja de que no es necesario vincular varias veces (para cada directiva) en el mismo evento, lo que puede ser un beneficio de rendimiento.
Vamos a ilustrar ambas opciones:
Opción 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);
});
}
};
});
Opcion 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);
});
Ambos enfoques funcionan bien, pero creo que la opción dos no es realmente ''Angular-ish''.
¿Algunas ideas?
Al desarrollarme en la parte superior de un marco, a menudo me resulta útil pensar de forma independiente sobre un problema antes de diseñar un lenguaje idiomático. Contestar el "qué" y el "por qué" elimina el "cómo".
La respuesta aquí realmente depende de la complejidad de doSomethingFancy()
. ¿Hay datos, un conjunto de funciones u objetos de dominio asociados con las instancias de esta directiva? ¿Es una preocupación puramente de presentación, como ajustar las propiedades de width
o height
de ciertos elementos a proporciones apropiadas del tamaño de la ventana? Asegúrese de estar usando la herramienta correcta para el trabajo; no traigas toda la navaja suiza cuando el trabajo requiera pinzas y tienes acceso a un par independiente. Para continuar en esta línea, voy a operar con la suposición de que doSomethingFancy()
es una función puramente de presentación.
La preocupación de ajustar un evento de navegador global en un evento angular podría manejarse mediante una configuración simple de fase de ejecución:
angular.module(''myApp'')
.run(function ($rootScope) {
angular.element(window).on(''resize'', function () {
$rootScope.$broadcast(''global:resize'');
})
})
;
Ahora Angular no tiene que hacer todo el trabajo asociado con una directiva en cada $digest
, pero obtienes la misma funcionalidad.
La segunda preocupación es operar en n
cantidad de elementos cuando se dispara este evento. Nuevamente, si no necesita todos los detalles de una directiva, existen otras formas de hacerlo. Puede expandir o adaptar el enfoque en el bloque de ejecución anterior:
angular.module(''myApp'')
.run(function () {
angular.element(window).on(''resize'', function () {
var elements = document.querySelectorAll(''.reacts-to-resize'');
})
})
;
Si tiene una lógica más compleja que debe suceder en el evento de cambio de tamaño, todavía no significa necesariamente que una o más directivas son la mejor manera de manejarlo. Puede usar un servicio de mediación simple que obtiene una instancia en lugar de la configuración de fase de ejecución anónima antes mencionada:
/**
* 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)
;
Ahora tenemos un servicio encapsulado que puede probarse en unidades, pero no ejecuta fases de ejecución no utilizadas.
Un par de preocupaciones que también serían factores en la decisión:
- Escuchas muertos : con la Opción 1, está creando al menos un oyente de eventos para cada instancia de la directiva. Si estos elementos se agregan o eliminan dinámicamente del DOM, y no se invoca
$on(''$destroy'')
, se corre el riesgo de que los manejadores de eventos se apliquen cuando sus elementos ya no existan. - Rendimiento de los operadores de ancho / alto : asumo que aquí hay una lógica de modelo de caja, dado que el evento global es el tamaño del navegador. Si no, ignora este; de ser así, deberá tener cuidado con las propiedades a las que está accediendo y con qué frecuencia, ya que los reflujos de los navegadores pueden ser un gran culpable en la degradación del rendimiento .
Es probable que esta respuesta no sea tan "angular" como esperabas, pero es la forma en que resolvería el problema, ya que lo entiendo con la suposición añadida de la lógica de solo modelo de caja.
Aquí hay una manera de hacerlo, simplemente almacene sus elementos en una matriz, luego en el "evento global" puede recorrer los elementos y hacer lo que necesita hacer.
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);
}
};
});
El segundo enfoque parece más frágil, ya que Angular ofrece muchas maneras de referirse a la directiva en la plantilla ( my-directive
, my_directive
, my:directive
, x-my-directive
, data-my-directive
, etc.) por lo que un selector de CSS cubrirlos a todos puede ser realmente complejo.
Probablemente esto no sea un gran problema si solo usa las directivas internamente o si constan de una sola palabra. Pero si otros desarrolladores (con diferentes convenciones de codificación) pudieran estar usando sus directivas, es posible que desee evitar el segundo enfoque.
Pero sería pragmático. Si está lidiando con un puñado de instancias, vaya con # 1. Si tienes cientos de ellos, yo iría con el n. ° 2.
En mi opinión, usaría el método n. ° 1 y una pequeña modificación en el uso del servicio $ 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 referencia a este enfoque y un ligero cambio en el razonamiento aquí, puede poner este oyente de eventos en un lugar más alto en decir app.run, y aquí cuando ocurre el evento puede transmitir otro evento que la directiva recoge y hace algo elegante cuando ese evento tiene lugar
EDITAR : Cuanto más pienso en este método, más me gusta el primero ... Una gran forma de escuchar el evento de cambio de tamaño de la ventana. Tal vez en el futuro, otra cosa también necesite "conocer" esta información. y a menos que haga algo como esto, se ve obligado a configurar, una vez más , otro oyente de eventos para el evento window.resize.
app.run
app.run(function($window, $rootScope) {
angular.element($window).bind(''resize'', function(){
$rootScope.$broadcast(''window-resize'');
});
}
Directiva angular.module (''app''). Directiva (''myDirective'', función ($ 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);
});
}
};
});
Finalmente, una gran fuente de cómo hacer las cosas es seguir a los chicos del angular-ui, por ejemplo, el ui-bootstrap . He aprendido un montón de cómo cosas de estos chicos, por ejemplo, las alegrías de aprender a la prueba de unidad en angular. Proporcionan una gran base de código limpia para realizar el pago.
He elegido otro método para localizar de manera efectiva los eventos globales, como el cambio de tamaño de la ventana. Convierte eventos de Javascript a eventos de alcance angular, a través de otra directiva.
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);
}
}
});
Que se puede usar, en el caso básico, en el elemento raíz de la aplicación
<body ng-app="myApp" resize>...
Y luego escucha el evento en otras directivas
<div my-directive>....
codificado como:
app.directive(''myDirective'', function() {
return {
link: function(scope, element) {
scope.$on(''resize::resize'', function() {
doSomethingFancy(element);
});
});
}
});
Esto tiene una serie de beneficios sobre otros enfoques:
No es frágil con la forma exacta de cómo se usan las directivas. Su Opción 2 requiere
my-directive
cuando angular trata lo siguiente como equivalente:my:directive
,data-my-directive
,x-my-directive
,my_directive
como se puede ver en la guía de directivasTiene un único lugar para afectar exactamente cómo se convierte el evento Javascript al evento angular, que luego afecta a todos los oyentes. Digamos que más tarde quiere eliminar el rebote del evento de cambio de
resize
JavaScript, utilizando la función de eliminación de rebotes de Lodash . Puede modificar la directiva de cambio deresize
a:angular.element($window).on(''resize'', $window._.debounce(function() { scope.$broadcast(''resize::resize''); },500));
Debido a que no necesariamente
$rootScope
los eventos en$rootScope
, puede restringir los eventos a solo una parte de su aplicación con solo mover a donde coloque la directiva de cambio deresize
<body ng-app="myApp"> <div> <!-- No ''resize'' events here --> </div> <div resize> <!-- ''resize'' events are $broadcast here --> </div>
Puede extender la directiva con opciones y usarla de manera diferente en diferentes partes de su aplicación. Digamos que quiere diferentes versiones antirrebote en diferentes partes:
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)); }
Usado como:
<div resize> <!-- Undebounced ''resize'' Angular events here --> </div> <div resize="500"> <!-- ''resize'' is debounced by 500 milliseconds --> </div>
Luego puede ampliar la directiva con otros eventos que podrían ser útiles. Tal vez cosas como
resize::heightIncrease
.resize::heightDecrease
,resize::heightDecrease
resize::widthIncrease
,resize::widthDecrease
. Luego tiene un lugar en su aplicación que trata de recordar y procesar las dimensiones exactas de la ventana.Puede pasar datos junto con los eventos. Digamos, por ejemplo, el alto / ancho de la ventana gráfica, donde es posible que tenga que lidiar con problemas entre navegadores (dependiendo de cuán atrás necesita el soporte de IE, y si incluye otra biblioteca para ayudarlo).
angular.element($window).on(''resize'', function() { // From http://.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 }); });
que le da un lugar único para agregar a los datos más adelante. Por ejemplo, supongamos que quieres enviar la diferencia de dimensiones desde el último evento con rebote. Probablemente podría agregar un poco de código para recordar el tamaño antiguo y enviar la diferencia.
Esencialmente, este diseño proporciona una forma de convertir, de manera configurable, eventos globales de Javascript a eventos angulares locales, y locales no solo a una aplicación, sino local a diferentes partes de una aplicación, dependiendo de la ubicación de la directiva.