Förutsättningen
I AngularJS kan vi, när vi definierar en komponent (eller ett direktiv), skapa inre variabler från attribut. API:et för att göra detta är ganska invecklat:
bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}
Jag blev trött på att betala priset för att slå in min hjärna runt det varje gång jag använde, så i det här inlägget kommer vi att dissekera skillnaden mellan de fyra symbolerna en gång för alla.
Specifikt kommer vi att…
- lär dig att skicka strängar (
@
) - lär dig att skicka dynamiska uttryck (
<
) - lär dig att fånga upp utdata (
&
) - lär dig att ställa in två-way data bindings (
=
) - lär dig att göra allt ovanstående utan att använda någon av de fyra
- lär dig varför
<
är bättre än de andra tre
Läsning av ett attribut som text
Låt oss börja med @
, den enklaste av de fyra eftersom den helt enkelt läser attributet som text. Med andra ord skickar vi en sträng till komponenten.
Säg att vi har den här komponenten:
app.component("readingstring", {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
Och vi renderar den så här:
<readingstring text="hello"></readingstring>
Det här är vad vi får:
Användning av @
skapar en inre variabel som fylls med stränginnehållet i det namngivna attributet. Man kan säga att den fungerar som en initial konfigurering av komponenten.
Utvärdering av ett attribut som ett uttryck
Mer intressant är behovet av att utvärdera ett attribut som ett uttryck, och få det omvärderat när uttrycket ändras. Dynamisk inmatning!
Vi vill kunna göra detta…
<dynamicinput in="outervariable"></dynamicinput>
…och överföra utvärderingen av outervariable
till dynamicinput
.
Förut i AngularJS 1.5 var den enda syntax vi hade för detta =
:
app.component("dynamicinput",{
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});
Nackdelen med =
var att det skapade en tvåvägs databindning, trots att vi bara behövde en envägsbindning. Detta innebar också att uttrycket vi skickar in måste vara en variabel.
Men med AngularJS 1.5 fick vi <
, vilket innebär envägs databindning. Detta gör att vi kan använda vilket uttryck som helst som indata, till exempel ett funktionsanrop:
<dynamicinput in="calculateSomething()"></dynamicinput>
Komponentimplementationen skulle vara exakt densamma, förutom att vi byter ut =
mot <
.
Fånga upp utdata
Det är dags att vända på saker och ting – hur fångar vi upp utdata från en komponent? Se den lilla appen nedan – knapparna återges i ett barn och när de klickas vill vi uppdatera det yttre värdet i enlighet med detta.
Det är här som &
kommer in. Den tolkar attributet som ett uttalande och sveper in det i en funktion. Komponenten kan sedan anropa den funktionen när den vill och fylla variablerna i uttalandet. Output till överordnad!
Om vår yttre html ser ut så här:
Outer value: {{count}}
<output out="count = count + amount"></output>
…så kan en implementering av output
med hjälp av &
se ut så här:
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> `
});
Notera hur vi skickar in ett objekt med variablerna som ska fyllas på. Den här invecklade syntaxen innebär att för att använda en komponent med en utgång måste vi veta två saker:
- attributnamnet/attributnamnen som ska användas
- namnen på variablerna som kommer att skapas på ett magiskt sätt.
Eftersom &
är så invecklad är det många som använder =
för att göra utdata. Genom att skicka in variabeln som ska manipuleras…
Outer value: {{count}}
<output out="count"></output>
…ändrar vi sedan helt enkelt variabeln inne i komponenten:
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>`
});
Detta är dock inte särskilt vackert:
- Vi gör återigen tvåvägsdatabindning trots att vi bara behöver ett sätt
- Vi kanske inte vill spara utmatningen, utan helt enkelt agera på den
En snyggare lösning än allt det ovan nämnda är att använda <
för att skapa utmatning genom att lämna in en callback!
Vi skapar callbacken i den yttre controllern…
$scope.callback = function(amount){
$scope.count += amount;
}
…och överlämnar den till komponenten:
<output out="callback"></output>
Komponenten anropar den nu helt enkelt i enlighet med detta:
app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`
});
Som liknar &
, men utan den invecklade magin!
Som ett intressant sidospår är det här mönstret exakt hur utdata från en komponent fungerar i React.
Tvåvägs databindning
Det är här =
brukar få lov att glänsa som en AngularJS poster boy. Ta den här appen:
Om vi renderar den så här…
Outer: <input ng-model="value">
<twoway connection="value"></twoway>
…så kan vi implementera twoway
med hjälp av =
så här:
app.component("twowayeq",{
bindings: { connection: '=' },
template: `inner: <input ng-model="$ctrl.connection">`
});
Det är visserligen enkelt, men observera – det är ganska sällsynt att man behöver tvåvägs databindning. Ofta är det du faktiskt vill ha en input och en output.
Det för oss till hur vi kan implementera tvåvägsbindning med hjälp av endast <
! Om vi återigen skapar en callback-funktion i den yttre kontrollern…
$scope.callback = function(newval){
$scope.value = newval;
}
…och skickar in både värdet och callbacken…
<twoway value="value" callback="callback"></twoway>
…så kan komponenten skapas på följande sätt:
app.component("twowayin",{
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});
Vi har uppnått tvåvägsdatabindning, men vi håller oss fortfarande till ett enkelriktat dataflöde. Bättre karma!
Lämna symbolerna bakom sig helt och hållet
Det är faktiskt så att de fyra symbolerna bara är genvägar. Vi kan göra allt som de gör utan dem.
Strängpassningsappen…
…som vi gjorde så här…
<readingstring text="hello"></readingstring>
…skulle kunna genomföras genom att få tillgång till tjänsten $element
:
app.component("readingstring", {
controller: function($element){
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
Och med ett direktiv, genom att använda attrs
som skickas till 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>'
};
});
Den dynamiska inmatningsappen…
…som återges så här…
<dynamicinput in="outervariable"></dynamicinput>
…kan implementeras genom att använda ett .$watch
-anrop i det överordnade scope:
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>'
});
Utdataprogrammet…
…som återges så här…
<output out="count = count + amount"></output>
…kan implementeras genom att anropa $scope.$apply
i det överordnade scope:
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>`
});
Detta är inte exakt samma sak som &
eftersom vi nu också har förorenat det överordnade scope med en amount
-variabel, men det visar ändå konceptet tillräckligt bra.
Äntligen är tvåvägsappen…
…renderad som med =
…
<twoway connection="value"></twoway>
…kan implementeras genom att ställa in en $watch
i både över- och underordnad räckvidd:
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">`
});
Detta är lite fusk eftersom vi nu antar att det bundna värdet alltid är en sträng, men kärnan är fortfarande där!
Avslutning
Vi hoppas att den här resan har varit lärorik och att @
, <
, =
och &
nu känns mindre skrämmande.
Och att du märkt hur <
spöar upp resten! Den kan göra allt, vilket även =
kan, men <
ser mycket bättre ut när den gör det.
Båda är något klumpiga när det gäller att läsa strängar (<
kräver en sträng i en sträng, och =
behöver en proxy-variabel), men det är lätt nog att göra vanilla så @
bör inte bli alltför kaxig.
Alltså, &
kan gå och rotera på en pinne.