Die Prämisse

In AngularJS können wir, wenn wir eine Komponente (oder eine Direktive) definieren, Variablen im inneren Bereich aus Attributen erstellen. Die API dafür ist ziemlich verworren:

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

Ich hatte es satt, mir jedes Mal den Kopf zu zerbrechen, wenn ich sie benutzte, also werden wir in diesem Beitrag den Unterschied zwischen den vier Symbolen ein für alle Mal aufschlüsseln.

Speziell werden wir…

  • lernen, wie man Strings übergibt (@)
  • lernen, wie man dynamische Ausdrücke übergibt (<)
  • lernen, wie man Ausgaben abfängt (&)
  • lernen, wie man Zwei-Wege(=)
  • Lernen Sie, wie Sie all das oben genannte tun können, ohne eine der vier
  • Lernen Sie, warum < den anderen drei in den Arsch tritt

Lesen eines Attributs als Text

Fangen wir mit @ an, dem einfachsten der vier, da es das Attribut einfach als Text liest. Mit anderen Worten, wir übergeben der Komponente eine Zeichenkette.

Angenommen, wir haben diese Komponente:

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

Und wir rendern sie so:

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

Dann erhalten wir folgendes Ergebnis:

Die Verwendung von @ erzeugt eine innere Variable, die mit dem Zeichenketteninhalt des genannten Attributs gefüllt wird. Man könnte sagen, sie dient als Anfangskonfiguration der Komponente.

Auswertung eines Attributs als Ausdruck

Interessanter ist die Notwendigkeit, ein Attribut als Ausdruck auszuwerten und es neu auszuwerten, wenn sich der Ausdruck ändert. Dynamische Eingabe!

Wir wollen in der Lage sein, dies zu tun…

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

…und die Auswertung von outervariable in dynamicinput zu übergeben.

Vor AngularJS 1.5 war die einzige Syntax, die wir dafür hatten, =:

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

Der Nachteil von = war, dass es eine zweiseitige Datenbindung erzeugte, obwohl wir nur eine einseitige benötigten. Das bedeutete auch, dass der Ausdruck, den wir übergeben, eine Variable sein muss.

Aber mit AngularJS 1.5 haben wir <, was eine einseitige Datenbindung bedeutet. Damit können wir jeden beliebigen Ausdruck als Eingabe verwenden, z. B. einen Funktionsaufruf:

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

Die Komponentenimplementierung wäre genau dieselbe, außer dass wir = gegen < austauschen.

Abfangen von Ausgaben

Zeit, die Dinge umzudrehen – wie fangen wir Ausgaben von einer Komponente ab? Sehen Sie sich die kleine Anwendung unten an – die Schaltflächen werden in einem Child gerendert, und wenn sie angeklickt werden, wollen wir den äußeren Wert entsprechend aktualisieren.

Hier kommt & ins Spiel. Es interpretiert das Attribut als Anweisung und verpackt es in eine Funktion. Die Komponente kann dann diese Funktion nach Belieben aufrufen und die Variablen in der Anweisung füllen. Ausgabe an das Elternteil!

Wenn unser äußeres Html so aussieht…

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

…dann könnte eine Implementierung von output unter Verwendung von & wie folgt aussehen:

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

Beachten Sie, wie wir ein Objekt mit den zu füllenden Variablen übergeben. Diese verworrene Syntax bedeutet, dass wir, um eine Komponente mit einer Ausgabe zu verwenden, zwei Dinge wissen müssen:

  • die zu verwendenden Attributnamen
  • die Namen der Variablen, die auf magische Weise erstellt werden.

Weil & so verworren ist, verwenden viele = für die Ausgabe. Indem wir die zu manipulierende Variable übergeben…

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

…ändern wir dann einfach die Variable innerhalb der Komponente:

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

Das ist allerdings nicht sehr hübsch:

  • Wir machen wieder eine Zwei-Wege-Datenbindung, obwohl wir nur einen Weg brauchen
  • Wir wollen die Ausgabe vielleicht nicht speichern, sondern einfach nur darauf reagieren

Eine schönere Lösung als alle oben genannten ist die Verwendung von <, um die Ausgabe zu erzeugen, indem wir einen Callback übergeben!

Wir erstellen den Callback im äußeren Controller…

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

…und übergeben ihn an die Komponente:

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

Die Komponente ruft ihn nun einfach entsprechend auf:

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

Sehr ähnlich wie &, aber ohne den komplizierten Zauber!

Interessanterweise ist dieses Muster genau die Art und Weise, wie die Ausgabe einer Komponente in React funktioniert:

Zwei-Wege-Datenbindung

Hier darf = normalerweise als AngularJS-Posterboy glänzen. Nehmen wir diese App:

Wenn wir sie so rendern…

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

…dann können wir twoway mit = wie folgt implementieren:

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

Zugegebenermaßen einfach, aber Achtung – es ist eher selten, dass man eine Zwei-Wege-Datenbindung braucht. Oft will man eigentlich nur eine Eingabe und eine Ausgabe.

Was uns zu der Frage bringt, wie wir eine Zwei-Wege-Bindung mit nur < implementieren können! Wenn wir wieder eine Callback-Funktion im äußeren Controller erstellen…

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

…und sowohl den Wert als auch den Callback übergeben…

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

…dann kann die Komponente folgendermaßen erstellt werden:

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

Wir haben eine bidirektionale Datenbindung erreicht, aber wir halten uns immer noch an einen unidirektionalen Datenfluss. Besseres Karma!

Die Symbole ganz weglassen

Fakt ist, dass die vier Symbole nur Abkürzungen sind. Wir können alles, was sie tun, auch ohne sie tun.

Die Stringübergabe-App…

…die wir so gerendert haben…

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

…könnte durch den Zugriff auf den $element-Dienst implementiert werden:

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

oder mit einer Richtlinie, indem die attrs verwendet werden, die an link übergeben werden:

app.directive("readingstring", function(){
return {
restrict: 'E',
scope: {},
link: function(scope,elem,attrs){
scope.text = attrs.text;
},
template: '<p>text: <strong>{{text}}</strong></p>'
};
});

Die dynamische Eingabe app.

…wird so dargestellt…

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

…könnte durch einen .$watch-Aufruf im übergeordneten Bereich implementiert werden:

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

Die Ausgabe-App…

…wird so dargestellt…

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

…könnte durch den Aufruf von $scope.$apply im übergeordneten Bereich implementiert werden:

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

Das ist nicht genau dasselbe wie &, da wir jetzt auch den übergeordneten Bereich mit einer amount-Variable verschmutzt haben, aber es zeigt das Konzept dennoch gut genug.

Schließlich kann die Zwei-Wege-Anwendung…

…gerendert wie bei =

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

…könnte implementiert werden, indem ein $watch sowohl im übergeordneten als auch im untergeordneten Bereich gesetzt wird:

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

Das ist zwar ein wenig geschummelt, da wir jetzt davon ausgehen, dass der gebundene Wert immer ein String ist, aber das Wesentliche ist immer noch da!

Zusammenfassung

Wir hoffen, dass diese Reise lehrreich war und dass @, <, = und & sich jetzt weniger einschüchternd anfühlen.

Und dass du bemerkt hast, wie < den anderen in den Arsch tritt! Es kann alles, was auch = kann, aber < sieht dabei viel besser aus.

Beide sind etwas ungeschickt beim Lesen von Strings (< benötigt einen String in einem String, und = braucht eine Proxy-Variable), aber das ist einfach genug, um Vanille zu machen, also sollte @ nicht zu übermütig werden.

Auch & kann sich am Stock drehen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.