Założenie
W AngularJS, kiedy definiujemy komponent (lub dyrektywę), możemy tworzyć zmienne zakresu wewnętrznego z atrybutów. API do tego jest dość zawiłe:
bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}
Zmęczyło mnie płacenie ceny za owijanie mojego mózgu wokół niego za każdym razem, gdy go używałem, więc w tym poście rozbierzemy różnicę między czterema symbolami raz na zawsze.
Specyficznie, będziemy…
- nauczymy się jak przekazywać łańcuchy (
@
) - nauczymy się jak przekazywać wyrażenia dynamiczne (
<
) - nauczymy się jak przechwytywać dane wyjściowe (
&
) - nauczymy się jak ustawiać dwukierunkowedwukierunkowe wiązania danych (
=
) - nauczyć się, jak zrobić wszystko powyższe bez użycia żadnego z czterech
- nauczyć się, dlaczego
<
kopie tyłek pozostałym trzem
Odczytywanie atrybutu jako tekstu
Zacznijmy od @
, najprostszego z czterech, ponieważ po prostu odczytuje atrybut jako tekst. Innymi słowy, przekazujemy ciąg znaków do komponentu.
Mówimy, że mamy ten komponent:
app.component("readingstring", {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
I renderujemy go w ten sposób:
<readingstring text="hello"></readingstring>
A oto co otrzymamy:
Użycie @
tworzy zmienną wewnętrzną wypełnioną zawartością łańcucha znaków nazwanego atrybutu. Można powiedzieć, że służy ona jako początkowa konfiguracja komponentu.
Ocenianie atrybutu jako wyrażenia
Najciekawsza jest potrzeba oceniania atrybutu jako wyrażenia i ponownego oceniania go za każdym razem, gdy wyrażenie się zmienia. Dynamiczne dane wejściowe!
Chcemy być w stanie zrobić to…
<dynamicinput in="outervariable"></dynamicinput>
…i przekazać ocenę outervariable
do dynamicinput
.
Przed AngularJS 1.5, jedyną składnią jaką mieliśmy do tego celu była =
:
app.component("dynamicinput",{
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});
Wadą =
było to, że tworzyło dwukierunkowe wiązanie danych, mimo że potrzebowaliśmy tylko jednokierunkowego. Oznaczało to również, że wyrażenie, które przekazujemy, musi być zmienną.
Ale z AngularJS 1.5 dostaliśmy <
, co oznacza jednokierunkowe wiązanie danych. Dzięki temu możemy używać dowolnych wyrażeń jako danych wejściowych, takich jak wywołanie funkcji:
<dynamicinput in="calculateSomething()"></dynamicinput>
Wdrożenie komponentu byłoby dokładnie takie samo, z wyjątkiem zmiany =
na <
.
Łapanie danych wyjściowych
Czas odwrócić sytuację – jak złapać dane wyjściowe z komponentu? Zobacz poniższą aplikację – przyciski są renderowane w elemencie child, a kiedy zostaną kliknięte, chcemy odpowiednio zaktualizować wartość zewnętrzną.
W tym miejscu pojawia się &
. Interpretuje on atrybut jako deklarację i opakowuje go w funkcję. Komponent może następnie dowolnie wywoływać tę funkcję i wypełniać zmienne w deklaracji. Wyjście do rodzica!
Jeśli nasz zewnętrzny html wygląda tak…
Outer value: {{count}}
<output out="count = count + amount"></output>
…to implementacja output
przy użyciu &
mogłaby wyglądać tak:
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> `
});
Zauważ, jak przekazujemy obiekt ze zmiennymi do wypełnienia. Ta zawiła składnia oznacza, że aby użyć komponentu z wyjściem, musimy wiedzieć dwie rzeczy:
- nazwy atrybutu(ów) do użycia
- nazwy zmiennych, które w magiczny sposób zostaną utworzone.
Ponieważ &
jest tak zawiła, wiele osób używa =
do robienia wyjścia. Przekazując zmienną, która ma być manipulowana…
Outer value: {{count}}
<output out="count"></output>
…następnie po prostu zmieniamy tę zmienną wewnątrz komponentu:
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>`
});
To naprawdę nie jest zbyt ładne:
- znowu robimy dwukierunkowe wiązanie danych, mimo że potrzebujemy tylko jednego sposobu
- możemy nie chcieć zapisywać danych wyjściowych, ale po prostu działać na nich
Przyjemniejszym rozwiązaniem niż wszystkie powyższe jest użycie <
do tworzenia danych wyjściowych przez przekazanie wywołania zwrotnego!
Tworzymy callback w zewnętrznym kontrolerze…
$scope.callback = function(amount){
$scope.count += amount;
}
…i przekazujemy go do komponentu:
<output out="callback"></output>
Komponent teraz po prostu wywołuje go odpowiednio:
app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`
});
Bardzo podobne do &
, ale bez zagmatwanej magii!
Jako interesujący dodatek, ten wzorzec jest dokładnie tym, jak wyjście z komponentu działa w React.
Dwukierunkowe wiązanie danych
To jest miejsce, w którym =
jest zwykle dozwolone błyszczeć jako chłopiec z plakatu AngularJS. Weźmy taką aplikację:
Jeśli wyrenderujemy ją w ten sposób…
Outer: <input ng-model="value">
<twoway connection="value"></twoway>
…to możemy zaimplementować twoway
używając =
w ten sposób:
app.component("twowayeq",{
bindings: { connection: '=' },
template: `inner: <input ng-model="$ctrl.connection">`
});
Przyznać trzeba, że jest to łatwe, ale uwaga – raczej rzadko potrzebujemy dwukierunkowego wiązania danych. Często to, czego tak naprawdę chcesz, to wejście i wyjście.
To prowadzi nas do tego, jak możemy zaimplementować dwukierunkowe wiązanie używając tylko <
! Jeśli ponownie utworzymy funkcję wywołania zwrotnego w zewnętrznym kontrolerze…
$scope.callback = function(newval){
$scope.value = newval;
}
…i przekażemy zarówno wartość jak i wywołanie zwrotne…
<twoway value="value" callback="callback"></twoway>
…wtedy komponent może być utworzony w ten sposób:
app.component("twowayin",{
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});
Osiągnęliśmy dwukierunkowe wiązanie danych, ale nadal trzymamy się jednokierunkowego przepływu danych. Lepsza karma!
Zostawienie symboli za sobą
Faktem jest, że te cztery symbole są tylko skrótami. Możemy zrobić wszystko to, co one robią bez nich.
Aplikacja do przekazywania łańcuchów…
…którą wyrenderowaliśmy w ten sposób…
<readingstring text="hello"></readingstring>
….można zaimplementować poprzez dostęp do usługi $element
:
app.component("readingstring", {
controller: function($element){
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
lub za pomocą dyrektywy, wykorzystując attrs
, które są przekazywane do 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>'
};
});
Dynamiczna aplikacja wejściowa…
…wyrenderowana w ten sposób…
<dynamicinput in="outervariable"></dynamicinput>
…mogłaby być zaimplementowana przez użycie wywołania .$watch
w zakresie nadrzędnym:
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>'
});
Aplikacja wyjściowa…
…wyrenderowana w ten sposób…
<output out="count = count + amount"></output>
….może być zaimplementowana przez wywołanie $scope.$apply
w zakresie nadrzędnym:
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>`
});
To nie jest dokładnie to samo, co &
, ponieważ teraz również zanieczyściliśmy zakres nadrzędny zmienną amount
, ale mimo to, pokazuje koncepcję wystarczająco dobrze.
Wreszcie, dwukierunkowa aplikacja…
…renderowana jak w przypadku =
…
<twoway connection="value"></twoway>
…można zaimplementować poprzez ustawienie $watch
zarówno w zakresie rodzica, jak i dziecka:
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">`
});
Jest to lekkie oszustwo, ponieważ teraz zakładamy, że powiązana wartość zawsze będzie ciągiem znaków, ale sedno jest nadal tam!
Wrapping up
Mamy nadzieję, że ta podróż była edukacyjna, i że @
, <
, =
i &
teraz czują się mniej onieśmielające.
I że zauważyłeś jak <
kopie tyłek reszcie! Może zrobić wszystko, co również =
może, ale <
wygląda o wiele lepiej robiąc to.
Oba są nieco niezdarne do czytania ciągów (<
wymaga łańcucha w łańcuchu, a =
potrzebuje zmiennej proxy), ale to jest wystarczająco łatwe do zrobienia wanilii, więc @
nie powinien być zbyt zarozumiały.
A także, &
może iść obracać się na patyku.
.