Forudsætningen
I AngularJS kan vi, når vi definerer en komponent (eller et direktiv), oprette indre scope-variabler fra attributter. API’et til at gøre dette er temmelig indviklet:
bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}
Jeg blev træt af at betale prisen for at pakke min hjerne ind i det hver gang jeg brugte, så i dette indlæg vil vi dissekere forskellen mellem de fire symboler en gang for alle.
Specifikt vil vi…
- lære at videregive strenge (
@
) - lære at videregive dynamiske udtryk (
<
) - lære at fange output (
&
) - lære at opsætte to-way data bindings (
=
) - lær hvordan du kan gøre alt ovenstående uden at bruge nogen af de fire
- lær hvorfor
<
sparker de andre tre i røven
Læsning af en attribut som tekst
Lad os starte med @
, den mest ligetil af de fire, da den blot læser attributten som tekst. Med andre ord overfører vi en streng til komponenten.
Sæt, vi har denne komponent:
app.component("readingstring", {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
Og vi renderer den således:
<readingstring text="hello"></readingstring>
Så er her, hvad vi får:
Ved hjælp af @
oprettes en indre variabel, der er udfyldt med strengindholdet af den navngivne attribut. Man kan sige, at den tjener som en indledende konfiguration af komponenten.
Evaluering af en attribut som et udtryk
Mere interessant er behovet for at evaluere en attribut som et udtryk og få den reevalueret, når udtrykket ændres. Dynamisk input!
Vi ønsker at kunne gøre dette…
<dynamicinput in="outervariable"></dynamicinput>
…og videregive evalueringen af outervariable
til dynamicinput
.
Forud for AngularJS 1.5 var den eneste syntaks, vi havde til dette, =
:
app.component("dynamicinput",{
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});
Den ulempe ved =
var, at det skabte en tovejs databinding, selv om vi kun havde brug for en envejsbinding. Det betød også, at det udtryk, vi sender ind, skal være en variabel.
Men med AngularJS 1.5 fik vi <
, hvilket betyder envejs databinding. Dette giver os mulighed for at bruge et hvilket som helst udtryk som input, f.eks. et funktionskald:
<dynamicinput in="calculateSomething()"></dynamicinput>
Komponentimplementeringen ville være nøjagtig den samme, bortset fra at ændre =
til <
.
Fange output
Tid til at vende tingene om – hvordan fanger vi output fra en komponent? Se den lille app nedenfor – knapperne gengives i et barn, og når der klikkes på dem, ønsker vi at opdatere den ydre værdi i overensstemmelse hermed.
Det er her, &
kommer ind i billedet. Den fortolker attributten som et statement og indpakker den i en funktion. Komponenten kan derefter kalde denne funktion efter behag og udfylde variabler i erklæringen. Output til overordnet!
Hvis vores ydre html ser således ud:
Outer value: {{count}}
<output out="count = count + amount"></output>
…så kunne en implementering af output
ved hjælp af &
se således ud:
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> `
});
Bemærk, hvordan vi indsender et objekt med de variabler, der skal udfyldes. Denne indviklede syntaks betyder, at vi for at bruge en komponent med et output skal kende to ting:
- det eller de attributnavne, der skal bruges
- navnene på de variabler, der på magisk vis vil blive oprettet.
Da &
er så indviklet, bruger mange =
til at lave output. Ved at indsætte den variabel, der skal manipuleres…
Outer value: {{count}}
<output out="count"></output>
…ændrer vi så blot den pågældende variabel inde 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>`
});
Dette er dog ikke særlig kønt:
- vi laver igen tovejs databinding, selv om vi kun har brug for én måde
- vi ønsker måske ikke at gemme output, men blot at handle på det
En pænere løsning end alt det ovenstående er at bruge <
til at skabe output ved at overgive et callback!
Vi opretter callback’en i den ydre controller…
$scope.callback = function(amount){
$scope.count += amount;
}
…og videregiver den til komponenten:
<output out="callback"></output>
Komponenten kalder den nu blot i overensstemmelse hermed:
app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`
});
Meget lig &
, men uden den indviklede magi!
Som en interessant sidebemærkning er dette mønster præcis, hvordan output fra en komponent fungerer i React.
Tovejs databinding
Det er her, hvor =
normalt får lov til at skinne som AngularJS-plakatdreng. Tag denne app:
Hvis vi renderer den sådan her…
Outer: <input ng-model="value">
<twoway connection="value"></twoway>
…så kan vi implementere twoway
ved hjælp af =
sådan her:
app.component("twowayeq",{
bindings: { connection: '=' },
template: `inner: <input ng-model="$ctrl.connection">`
});
Ganske vist er det nemt, men bemærk – det er ret sjældent, at man har brug for tovejs databinding. Ofte er det, man faktisk ønsker, et input og et output.
Det bringer os til, hvordan vi kan implementere tovejsbinding ved hjælp af kun <
! Hvis vi igen opretter en callback-funktion i den ydre controller…
$scope.callback = function(newval){
$scope.value = newval;
}
…og indsender både værdien og callbacken…
<twoway value="value" callback="callback"></twoway>
…så kan komponenten oprettes således:
app.component("twowayin",{
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});
Vi har opnået tovejs databinding, men vi holder os stadig til et ensrettet datastrøm. Bedre karma!
Lad symbolerne helt udeblive
Faktisk er det sådan, at de fire symboler blot er genveje. Vi kan gøre alt det, de gør, uden dem.
Den string passing app…
…som vi renderede på denne måde…
<readingstring text="hello"></readingstring>
…kunne implementeres ved at få adgang til $element
-tjenesten:
app.component("readingstring", {
controller: function($element){
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
Og med et direktiv, ved at bruge de attrs
, der overføres til 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 dynamiske input-app…
…gengives på denne måde…
<dynamicinput in="outervariable"></dynamicinput>
…kunne implementeres ved at bruge et .$watch
-opkald i det overordnede 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>'
});
Udgangsapp…
…gengives på denne måde…
<output out="count = count + amount"></output>
…kunne implementeres ved at kalde $scope.$apply
i det overordnede 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>`
});
Det er ikke helt det samme som &
, da vi nu også har forurenet det overordnede scope med en amount
-variabel, men det viser alligevel konceptet godt nok.
Endeligt er den tovejs-app…
…gengivet som med =
…
<twoway connection="value"></twoway>
…kunne implementeres ved at indstille en $watch
i både overordnet og underordnet rækkevidde:
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">`
});
Dette er lidt snyd, da vi nu antager, at den bundne værdi altid er en streng, men essensen er der stadig!
Fuldstændig op
Vi håber, at denne rejse har været lærerig, og at @
, <
, =
og &
nu føles mindre skræmmende.
Og at du har bemærket, hvordan <
sparker resten i røven! Den kan alt, hvilket =
også kan, men <
ser meget bedre ud til at gøre det.
Både er lidt klodsede til at læse strenge (<
kræver en streng i en streng, og =
har brug for en proxy-variabel), men det er nemt nok at gøre vanilla, så @
skal ikke blive for kæphøj.
Og &
kan også gå og rotere på en pind.