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.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.