La prémisse

En AngularJS, lorsque nous définissons un composant (ou une directive), nous pouvons créer des variables inner scope à partir d’attributs. L’API pour le faire est plutôt alambiquée :

bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}

J’en ai eu assez de payer le prix d’envelopper mon cerveau autour d’elle chaque fois que j’ai utilisé, donc dans ce post, nous allons disséquer la différence entre les quatre symboles une fois pour toutes.

Spécifiquement, nous…

  • Apprendre comment passer des chaînes de caractères (@)
  • Apprendre comment passer des expressions dynamiques (<)
  • Apprendre comment attraper la sortie (&)
  • Apprendre comment mettre en place des liaisons de données à deux voies ().way data bindings (=)
  • apprendre comment faire tout ce qui précède sans utiliser aucun des quatre
  • apprendre pourquoi < botte le cul des trois autres

Lecture d’un attribut en tant que texte

Commençons par @, la plus simple des quatre car elle lit simplement l’attribut en tant que texte. En d’autres termes, nous passons une chaîne de caractères au composant.

Disons que nous avons ce composant:

app.component("readingstring", {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});

Et nous le rendons comme ceci:

<readingstring text="hello"></readingstring>

Alors voici ce que nous obtenons:

L’utilisation de @ crée une variable interne peuplée avec le contenu de la chaîne de caractères de l’attribut nommé. On pourrait dire qu’elle sert de configuration initiale du composant.

Evaluer un attribut comme une expression

Plus intéressant est le besoin d’évaluer un attribut comme une expression, et de le faire réévaluer chaque fois que l’expression change. Entrée dynamique!

Nous voulons pouvoir faire cela…

<dynamicinput in="outervariable"></dynamicinput>

…et passer l’évaluation de outervariable dans dynamicinput.

Avant AngularJS 1.5, la seule syntaxe dont nous disposions pour cela était =:

app.component("dynamicinput",{
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});

L’inconvénient de = était qu’il créait une liaison de données à double sens, même si nous n’avions besoin que d’un sens unique. Cela signifiait également que l’expression que nous transmettons doit être une variable.

Mais avec AngularJS 1.5, nous avons obtenu <, ce qui signifie une liaison de données à sens unique. Cela nous permet d’utiliser n’importe quelle expression comme entrée, comme un appel de fonction :

<dynamicinput in="calculateSomething()"></dynamicinput>

L’implémentation du composant serait exactement la même, sauf en changeant = pour <.

Catching output

Il est temps de retourner les choses – comment attraper la sortie d’un composant ? Voir la minuscule application ci-dessous – les boutons sont rendus dans un enfant, et quand ils sont cliqués, nous voulons mettre à jour la valeur extérieure en conséquence.

C’est là que & intervient. Il interprète l’attribut comme une déclaration et l’enveloppe dans une fonction. Le composant peut alors appeler cette fonction à volonté, et peupler les variables de l’énoncé. Sortie vers le parent!

Si notre html externe ressemble à ceci…

Outer value: {{count}}
<output out="count = count + amount"></output>

..alors une implémentation de output utilisant & pourrait ressembler à ceci:

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> `
});

Notez comment nous passons dans un objet avec les variables à remplir. Cette syntaxe alambiquée signifie que pour utiliser un composant avec une sortie, nous devons savoir deux choses:

  • le ou les noms d’attributs à utiliser
  • les noms des variables qui seront magiquement créées.

Parce que & est si alambiqué, beaucoup utilisent = pour faire la sortie. En passant la variable à manipuler…

Outer value: {{count}}
<output out="count"></output>

…nous changeons alors simplement cette variable à l’intérieur du composant:

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>`
});

Ce n’est pas vraiment très joli cependant:

  • nous faisons à nouveau du data binding bidirectionnel même si nous n’avons besoin que d’une seule voie
  • nous pourrions ne pas vouloir sauvegarder la sortie, mais simplement agir dessus

Une solution plus agréable que tout ce qui précède est d’utiliser < pour créer la sortie en passant dans un callback !

Nous créons le callback dans le contrôleur externe…

$scope.callback = function(amount){
$scope.count += amount;
}

…et le passons au composant:

<output out="callback"></output>

Le composant l’appelle maintenant simplement en conséquence:

app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`
});

Très similaire à &, mais sans la magie alambiquée !

En guise d’aparté intéressant, ce modèle est exactement la façon dont la sortie d’un composant fonctionne dans React.

Liaison de données à deux voies

C’est là que = est généralement autorisé à briller comme un poster boy AngularJS. Prenez cette application:

Si nous la rendons comme ceci…

Outer: <input ng-model="value">
<twoway connection="value"></twoway>

…alors nous pouvons implémenter twoway en utilisant = comme ceci:

app.component("twowayeq",{
bindings: { connection: '=' },
template: `inner: <input ng-model="$ctrl.connection">`
});

Admirablement facile, mais notez – il est plutôt rare d’avoir besoin d’une liaison de données bidirectionnelle. Souvent, ce que vous voulez réellement, c’est une entrée et une sortie.

Ce qui nous amène à la façon dont nous pouvons mettre en œuvre la liaison bidirectionnelle en utilisant seulement < ! Si nous créons à nouveau une fonction de rappel dans le contrôleur externe…

$scope.callback = function(newval){
$scope.value = newval;
}

…et passons à la fois la valeur et le rappel…

<twoway value="value" callback="callback"></twoway>

…alors le composant peut être créé ainsi:

app.component("twowayin",{
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});

Nous avons réalisé une liaison de données bidirectionnelle, mais nous adhérons toujours à un flux de données unidirectionnel. Meilleur karma !

Laissons les symboles derrière nous tout à fait

En réalité, les quatre symboles ne sont que des raccourcis. Nous pouvons faire tout ce qu’ils font sans eux.

L’application de passage de chaîne…

…que nous avons rendu comme ceci…

<readingstring text="hello"></readingstring>

…pourrait être implémenté en accédant au service $element:

app.component("readingstring", {
controller: function($element){
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});

Ou avec une directive, en utilisant les attrs qui sont passés à 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’entrée dynamique app…

…rendue comme ceci…

<dynamicinput in="outervariable"></dynamicinput>

…pourrait être implémentée en utilisant un appel .$watch dans la portée parent:

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’application de sortie…

…rendue comme ceci…

<output out="count = count + amount"></output>

…pourrait être implémentée en appelant $scope.$apply dans la portée parent:

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>`
});

Ce n’est pas exactement la même chose que & puisque nous avons maintenant aussi pollué la portée parent avec une variable amount, mais quand même, cela montre assez bien le concept.

Enfin, l’application bidirectionnelle…

…rendue comme avec =

<twoway connection="value"></twoway>

….pourrait être implémenté en définissant un $watch dans les portées parent et enfant:

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">`
});

C’est une légère tricherie puisque nous supposons maintenant que la valeur liée est toujours une chaîne de caractères, mais l’essentiel est toujours là !

Wrapping up

Nous espérons que ce voyage a été éducatif, et que @, <, = et & semblent maintenant moins intimidants.

Et que vous avez remarqué comment < botte le cul du reste ! Il peut tout faire, ce que = peut aussi faire, mais < a l’air beaucoup mieux en le faisant.

Les deux sont un peu maladroits pour lire les chaînes de caractères (< nécessite une chaîne dans une chaîne, et = a besoin d’une variable proxy), mais c’est assez facile à faire vanille donc @ ne devrait pas être trop arrogant.

Alors, & peut aller tourner sur un bâton.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.