La premisa
En AngularJS, cuando definimos un componente (o una directiva), podemos crear variables de ámbito interno a partir de atributos. La API para hacerlo es bastante enrevesada:
bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}
Me he cansado de pagar el precio de envolver mi cerebro cada vez que lo utilizaba, así que en este post vamos a diseccionar la diferencia entre los cuatro símbolos de una vez por todas.
En concreto, vamos a…
- aprender a pasar cadenas (
@
) - aprender a pasar expresiones dinámicas (
<
) - aprender a capturar la salida (
&
) - aprender a configurar bindings de datos de dosde dos vías (
=
) - aprender a hacer todo lo anterior sin usar ninguno de los cuatro
- aprender por qué
<
le da una patada en el culo a los otros tres
Lectura de un atributo como texto
Empecemos con @
, el más sencillo de los cuatro, ya que simplemente lee el atributo como texto. En otras palabras, pasamos una cadena al componente.
Digamos que tenemos este componente:
app.component("readingstring", {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
Y lo renderizamos así:
<readingstring text="hello"></readingstring>
Entonces esto es lo que obtenemos:
Usando @
se crea una variable interna poblada con el contenido de la cadena del atributo nombrado. Se podría decir que sirve como una configuración inicial del componente.
Evaluar un atributo como una expresión
Más interesante es la necesidad de evaluar un atributo como una expresión, y que se reevalúe cada vez que la expresión cambia. Entrada dinámica
Queremos ser capaces de hacer esto…
<dynamicinput in="outervariable"></dynamicinput>
…y pasar la evaluación de outervariable
a dynamicinput
.
Antes de AngularJS 1.5, la única sintaxis que teníamos para esto era =
:
app.component("dynamicinput",{
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});
El inconveniente de =
era que creaba un enlace de datos de dos vías, aunque sólo necesitábamos una vía. Esto también significa que la expresión que pasamos debe ser una variable.
Pero con AngularJS 1.5 obtuvimos <
, que significa un enlace de datos unidireccional. Esto nos permite utilizar cualquier expresión como entrada, como una llamada a una función:
<dynamicinput in="calculateSomething()"></dynamicinput>
La implementación del componente sería exactamente la misma, excepto cambiando =
por <
.
Capturando la salida
Es hora de darle la vuelta a las cosas – ¿cómo capturamos la salida de un componente? Ver la pequeña aplicación de abajo – los botones se representan en un niño, y cuando se hace clic queremos actualizar el valor exterior en consecuencia.
Aquí es donde &
entra en juego. Interpreta el atributo como una declaración y lo envuelve en una función. El componente puede entonces llamar a esa función a voluntad, y poblar las variables en la declaración. Si nuestro html externo se ve así…
Outer value: {{count}}
<output out="count = count + amount"></output>
…entonces una implementación de output
usando &
podría verse así:
app.component("output",{
bindings: { out: '&' },
template: `
<button ng-click="$ctrl.out({amount: 1})">buy one</button>
<button ng-click="$ctrl.out({amount: 5})">buy many</button> `
});
Nota cómo pasamos un objeto con las variables a rellenar. Esta enrevesada sintaxis significa que para utilizar un componente con una salida debemos saber dos cosas:
- el nombre del atributo o atributos a utilizar
- los nombres de las variables que se crearán mágicamente.
Dado que &
es tan enrevesado, muchos utilizan =
para hacer la salida. Pasando la variable a manipular…
Outer value: {{count}}
<output out="count"></output>
…entonces simplemente cambiamos esa variable dentro del componente:
app.component("output",{
bindings: { out: '=' },
template: `<div>
<button ng-click="$ctrl.out = $ctrl.out + 1;">buy one</button>
<button ng-click="$ctrl.out = $ctrl.out + 5;">buy many</button>
</div>`
});
Sin embargo, esto no es muy bonito:
- estamos de nuevo haciendo un enlace de datos de dos vías aunque sólo necesitemos una vía
- puede que no queramos guardar la salida, sino simplemente actuar sobre ella
¡Una solución más agradable que todo lo anterior es usar <
para crear la salida pasando un callback!
Creamos el callback en el controlador externo…
$scope.callback = function(amount){
$scope.count += amount;
}
…y se lo pasamos al componente:
<output out="callback"></output>
El componente ahora simplemente lo llama en consecuencia:
app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`
});
¡Muy parecido a &
, pero sin la magia enrevesada!
Como un interesante aparte, este patrón es exactamente cómo la salida de un componente funciona en React.
Enlace de datos de dos vías
Aquí es donde =
se suele permitir brillar como un chico del cartel de AngularJS. Tomemos esta aplicación:
Si la renderizamos así…
Outer: <input ng-model="value">
<twoway connection="value"></twoway>
…entonces podemos implementar twoway
usando =
así:
app.component("twowayeq",{
bindings: { connection: '=' },
template: `inner: <input ng-model="$ctrl.connection">`
});
Es cierto que es fácil, pero ten en cuenta que es bastante raro necesitar un enlace de datos bidireccional. A menudo lo que quieres es una entrada y una salida.
¡Lo que nos lleva a cómo podemos implementar la vinculación bidireccional utilizando sólo <
! Si volvemos a crear una función de devolución de llamada en el controlador externo…
$scope.callback = function(newval){
$scope.value = newval;
}
…y pasamos tanto el valor como la devolución de llamada…
<twoway value="value" callback="callback"></twoway>
…entonces el componente puede crearse así:
app.component("twowayin",{
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});
Hemos conseguido una vinculación de datos bidireccional, pero seguimos ciñéndonos a un flujo de datos unidireccional. Mejor karma!
Dejando atrás los símbolos
El hecho es que los cuatro símbolos son sólo atajos. Podemos hacer todo lo que hacen sin ellos.
La aplicación de pasar cadenas…
…que renderizamos así…
<readingstring text="hello"></readingstring>
…podría implementarse accediendo al servicio $element
:
app.component("readingstring", {
controller: function($element){
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
O con una directiva, utilizando los attrs
que se pasan a link
:
app.directive("readingstring", function(){
return {
restrict: 'E',
scope: {},
link: function(scope,elem,attrs){
scope.text = attrs.text;
},
template: '<p>text: <strong>{{text}}</strong></p>'
};
});
La app de entrada dinámica…
…renderizada así…
<dynamicinput in="outervariable"></dynamicinput>
…podría implementarse utilizando una llamada .$watch
en el ámbito padre:
app.component("dynamicinput",{
controller: ($scope,$element) => {
let expression = $element.attr("in");
$scope.$parent.$watch(expression, newVal => $scope.in = newVal);
},
template: '<p>dynamic input: <strong>{{in}}</strong></p>'
});
La app de salida…
…renderizada así…
<output out="count = count + amount"></output>
…podría implementarse llamando a $scope.$apply
en el ámbito padre:
app.component("output",{
controller: ($scope,$element,$timeout) => {
let statement = $element.attr("out");
$scope.increaseBy = by => {
$timeout(function(){
$scope.$parent.$apply(`amount = ${by}; ${statement}`);
});
}
},
template: `
<button ng-click="increaseBy(1)">buy one</button>
<button ng-click="increaseBy(5)">buy many</button>`
});
Esto no es exactamente lo mismo que &
ya que ahora también hemos contaminado el ámbito padre con una variable amount
, pero aun así, muestra el concepto lo suficientemente bien.
Por último, la aplicación bidireccional…
…renderizada como con =
…
<twoway connection="value"></twoway>
…podría implementarse estableciendo un $watch
tanto en el ámbito padre como en el hijo:
app.component("twoway",{
controller: ($scope,$element,$timeout) => {
let variable = $element.attr("connection");
$scope.$parent.$watch(variable, newVal => $scope.inner = newVal;
$scope.$watch('inner', (newVal='') => $timeout( () => {
$scope.$parent.$apply(`${variable} = "${newVal}";`);
}));
},
template: `inner: <input ng-model="inner">`
});
¡Esto es una pequeña trampa ya que ahora estamos asumiendo que el valor vinculado es siempre una cadena, pero, la esencia sigue ahí!
Concluyendo
Esperamos que este viaje haya sido educativo, y que @
, <
, =
y &
se sientan ahora menos intimidantes.
¡Y que te hayas dado cuenta de cómo <
le da una patada en el culo al resto! Puede hacer todo, lo que también puede =
, pero <
se ve mucho mejor haciéndolo.
Ambos son algo torpes para leer cadenas (<
requiere una cadena en una cadena, y =
necesita una variable proxy), pero eso es bastante fácil de hacer vainilla, así que @
no debería ponerse demasiado gallito.
Además, &
puede ir girando en un palo.