La premessa
In AngularJS, quando definiamo un componente (o una direttiva), possiamo creare variabili inner scope dagli attributi. L’API per farlo è piuttosto contorta:
bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}
Mi sono stancato di pagare il prezzo di avvolgermi il cervello ogni volta che lo uso, quindi in questo post dissezioneremo la differenza tra i quattro simboli una volta per tutte.
In particolare, noi…
- impareremo a passare stringhe (
@
) - impareremo a passare espressioni dinamiche (
<
) - impareremo a catturare l’output (
&
) - impareremo a configurare binding di dati a due vie ().way data bindings (
=
) - impara come fare tutto quanto sopra senza usare nessuno dei quattro
- impara perché
<
fa il culo agli altri tre
Lettura di un attributo come testo
Cominciamo con @
, il più semplice dei quattro in quanto legge semplicemente l’attributo come testo. In altre parole passiamo una stringa al componente.
Diciamo che abbiamo questo componente:
app.component("readingstring", {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
E lo renderizziamo così:
<readingstring text="hello"></readingstring>
Ecco cosa otteniamo:
L’uso di @
crea una variabile interna popolata con il contenuto della stringa dell’attributo nominato. Si potrebbe dire che serve come configurazione iniziale del componente.
Valutare un attributo come espressione
Più interessante è la necessità di valutare un attributo come espressione, e farlo rivalutare ogni volta che l’espressione cambia. Input dinamico!
Vogliamo essere in grado di fare questo…
<dynamicinput in="outervariable"></dynamicinput>
…e passare la valutazione di outervariable
in dynamicinput
.
Prima di AngularJS 1.5, l’unica sintassi che avevamo per questo era =
:
app.component("dynamicinput",{
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});
Lo svantaggio di =
era che creava un legame di dati a due vie, anche se avevamo bisogno solo di una via. Questo significa anche che l’espressione che passiamo deve essere una variabile.
Ma con AngularJS 1.5 abbiamo <
, che significa un data binding a senso unico. Questo ci permette di usare qualsiasi espressione come input, come una chiamata di funzione:
<dynamicinput in="calculateSomething()"></dynamicinput>
L’implementazione del componente sarebbe esattamente la stessa, eccetto cambiare =
con <
.
Catturare l’output
È ora di cambiare le cose – come possiamo catturare l’output da un componente? Vedi la piccola applicazione qui sotto – i pulsanti sono resi in un figlio, e quando vengono cliccati vogliamo aggiornare il valore esterno di conseguenza.
Ecco dove entra in gioco &
. Interpreta l’attributo come una dichiarazione e lo avvolge in una funzione. Il componente può quindi chiamare quella funzione a piacimento, e popolare le variabili nella dichiarazione. Output al genitore!
Se il nostro html esterno assomiglia a questo…
Outer value: {{count}}
<output out="count = count + amount"></output>
..allora un’implementazione di output
usando &
potrebbe assomigliare a questa:
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 come passiamo un oggetto con le variabili da popolare. Questa sintassi contorta significa che per usare un componente con un output dobbiamo sapere due cose:
- il nome dell’attributo o degli attributi da usare
- i nomi delle variabili che saranno magicamente create.
Perché &
è così contorto, molti usano =
per fare output. Passando la variabile da manipolare…
Outer value: {{count}}
<output out="count"></output>
…poi cambiamo semplicemente la variabile all’interno 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>`
});
Questo non è molto carino però:
- siamo di nuovo facendo un data binding a due vie anche se abbiamo bisogno solo di una via
- potremmo non voler salvare l’output, ma semplicemente agire su di esso
Una soluzione più carina di tutte quelle precedenti è usare <
per creare output passando un callback!
Creiamo il callback nel controller esterno…
$scope.callback = function(amount){
$scope.count += amount;
}
…e lo passiamo al componente:
<output out="callback"></output>
Il componente ora lo chiama semplicemente di conseguenza:
app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`
});
Molto simile a &
, ma senza la magia contorta!
Come interessante inciso, questo schema è esattamente il modo in cui l’output da un componente funziona in React.
Data binding a due vie
Questo è dove =
di solito può brillare come un ragazzo poster di AngularJS. Prendiamo questa applicazione:
Se la renderizziamo così…
Outer: <input ng-model="value">
<twoway connection="value"></twoway>
…allora possiamo implementare twoway
usando =
in questo modo:
app.component("twowayeq",{
bindings: { connection: '=' },
template: `inner: <input ng-model="$ctrl.connection">`
});
Si può dire che è facile, ma si noti che è piuttosto raro aver bisogno di un data binding bidirezionale. Spesso ciò che si vuole effettivamente è un input e un output.
Che ci porta a come possiamo implementare il binding bidirezionale usando solo <
! Se creiamo ancora una volta una funzione di callback nel controller esterno…
$scope.callback = function(newval){
$scope.value = newval;
}
…e passiamo sia il valore che il callback…
<twoway value="value" callback="callback"></twoway>
…allora il componente può essere creato così:
app.component("twowayin",{
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});
Abbiamo ottenuto un data binding bidirezionale, ma stiamo ancora aderendo a un flusso dati unidirezionale. Meglio il karma!
Lasciare del tutto i simboli
Il fatto è che i quattro simboli sono solo scorciatoie. Possiamo fare tutto quello che fanno senza di loro.
L’applicazione di passaggio di stringhe…
…che abbiamo reso così…
<readingstring text="hello"></readingstring>
…potrebbe essere implementato accedendo al servizio $element
:
app.component("readingstring", {
controller: function($element){
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
O con una direttiva, utilizzando i attrs
che vengono passati 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>'
};
});
L’app di input dinamico…
…reso così…
<dynamicinput in="outervariable"></dynamicinput>
…potrebbe essere implementato usando una chiamata .$watch
nell’ambito 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>'
});
L’app di output…
…reso così…
<output out="count = count + amount"></output>
…potrebbe essere implementato chiamando $scope.$apply
nell’ambito genitore:
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>`
});
Questa non è esattamente la stessa cosa di &
poiché ora abbiamo anche inquinato l’ambito genitore con una variabile amount
, ma comunque, mostra il concetto abbastanza bene.
Finalmente l’applicazione bidirezionale…
…resa come con =
…
<twoway connection="value"></twoway>
…potrebbe essere implementato impostando un $watch
in entrambi gli scopi padre e figlio:
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">`
});
Questo è un leggero imbroglio poiché ora stiamo assumendo che il valore legato sia sempre una stringa, ma, la sostanza è ancora lì!
Concludendo
Speriamo che questo viaggio sia stato istruttivo, e che @
, <
, =
e &
ora sembrino meno intimidatori.
E che abbiate notato come <
faccia il culo agli altri! Può fare tutto, cosa che anche =
può fare, ma <
sembra molto meglio farlo.
Entrambi sono un po’ goffi per leggere le stringhe (<
richiede una stringa in una stringa, e =
ha bisogno di una variabile proxy), ma questo è abbastanza facile da fare vaniglia così @
non dovrebbe diventare troppo presuntuoso.
Inoltre, &
può girare su un bastone.