W tej części tutorialu chciałbym przybliżyć czym są dyrektywy. Zakładam że zapoznałeś się z wcześniejszym wpisem o postawach AngularJS. Jeśli nie to zapraszam do lektury. Jednak zanim zaczniemy chciałbym opowiedzieć nieco więcej o scope.

DIY #13 – robimy prostą TODO listę w AngularJS!

Scope

Jest to obiekt który można określić jako model dla widoku. Wszystkie przypisane do niego funkcje i property będą bezpośrednio dostępne w widoku. Dodatkowo scope posiada możliwość obserwowania zmian które w nim zachodzą i wykonywania operacji gdy to nastąpi (służy do tego funkcja $watch). Przykładowym zastosowaniem $watch może być obliczenie ceny produktu w zależności od wartości na input range. Jeśli logika ceny jest bardziej zaawansowana niż przykładowo wartość * 10 warto ją dodać do scope i wykonywać kod po zmianie wartości inputa. Jak to wygląda w praktyce? Bardzo prosto:

[js]

$scope.$watch(‚propertyName’, function(){

// tutaj kod do kalkulacji ceny

// po obliczeniu zapisujemy ją w innym property w scope np. $scope.price = calculatedPrice;

});

[/js]

I tyle :) Wracając do scope, posiada on jeszcze funkcję $apply ale o niej opowiem innym razem. Dla żądnych wiedzy odsyłam do dokumentacji (https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply)

Scope’s (scopy? nie wiem jak to po polsku odmienić ;)) posiadają jeszcze jedną właściwość. Mianowicie mogą być zagnieżdżane. Jeśli jeden scope znajduje się w drugim może mieć dostęp do wszystkich property parenta. Jest też druga opcja (isolated scope) która to uniemożliwia. Z tych drugiej przeważnie korzysta się w dyrektywach.

Dyrektywy

Czym więc są dyrektywy? Według dokumentacji to znaczniki (takie jak atrybut, nazwa elementu lub klasa css) na elementach DOM które mówią angularowi żeby dołączył pewne zachowanie do tego elementu. Kilka dyrektyw już poznaliśmy (ng-model, ng-repeat, ng-click, ng-app, ng-controller). Zauważ że wszystkie są atrybutami oraz wszystkie rozpoczynają się od „ng-” jest to nomenklatura przyjęta przez angular i twoje dyrektywy nie powinny rozpoczynać się od „ng-” jest to dobra praktyka które poprawia czytelność kodu.

Jak tworzymy dyrektywę

[js]

.directive(’myCustomer’, function() {

return {

restrict: 'E’

template: 'Name: {{customer.name}} Address: {{customer.address}}’

};

});

[/js]

służy do tego funkcja directive. Tak samo jak w kontrolerze wywołujemy ją z 2 argumentami, pierwszy jest nazwą a drugi funkcją która musi zwrócić obiekt. W obiekcie tym możemy zdefiniować kilka rzeczy

  1. restrict – opisuje w jaki sposób będzie można osadzić dyrektywę w naszym widoku
    1. A – jako atrybut (my-customer)
    2. E – jako element (<my-customer />)
    3. C – jako klasę css (class=”my-dir”>)
    4. M – jako komentarz (<!— directive: my-customer –>)

dobrą praktyką jest używanie 2 pierwszych opcji (które są ustawione jako domyślne jeśli nie zdefiniujemy restrict), sprawia to że kod widoku jest czytelniejszy. Dodatkowo restrict można definiować jako łączoną wartość opcji przykładowo ‚AEM’ – dyrektywa będzie dostępna wtedy jako atrybut, element lub komentarz. Warto zaznaczyć że nazwy dyrektyw są normalizowane i camelCase zamieniany jest na nazwę dash-delimited. W skrócie wielka litera zamieniana jest na małą i poprzedzana myślnikiem. Dlatego z nazwy myCustomer otrzymaliśmy my-customer. Nazwy zaczynamy od małej litery.

2. template / templateUrl – widok naszej dyrektywy. Możemy tutaj podać bezpośrednio kod widoku (jako template) lub adres do pliku html (jako templateUrl)

3. scope – definiując scope oznaczmy scope dyrektywy jako isolated. Nie będzie on miał dostępu to scope parenta. Dlaczego jest to przydatne? A dlatego że możemy dokładnie oznaczyć jakich danych wymaga nasza dyrektywa do działania i przypisać je w momencie osadzania dyrektywy w widoku. Definiujemy to tak

[js]

scope: {

customerInfo: '=info’

},

[/js]

oznacza to że nasza dyrektywa będzie oczekiwała że w atrybucie info dostanie dane które w jej scope będą dostępne jako customerInfo

będziemy mogli ją osadzić tak:

[html]

<my-customer info="customer1"></my-customer>

<my-customer info="customer2"></my-customer>

<my-customer info="customer3"></my-customer>

[/html]

jak widać dzięki isolated scope możemy przekazywać dowolne dane do dyrektywy (oczywiście muszą być zgodne z tym czego dyrektywa oczekuje).

można również zdefiniować scope w taki sposób

[js]

scope: {

customerInfo: '=’

},

[/js]

jest to równoznaczne z customerInfo: '=customerInfo’

Istnieje jeszcze klika sposobów na definiowanie scope (odsyłam do dokumentacji https://docs.angularjs.org/api/ng/service/$compile#directive-definition-object)

Nasza pierwsza dyrektywa

Wiemy już wystarczająco dużo żeby stworzyć swoją pierwszą dyrektywę. Jako ćwiczenie proponuję dyrektywę do naszej todo listy z poprzedniego tutoriala. Zmienimy item na liście na dyrektywę.

kod dyrektywy:

[js]

.directive(’todoListItem’, function(){

return {

scope: {

toDoItem: "=item",

index: "="

},

template: '<input type="checkbox" ng-model="toDoItem.done" id="todo-item{{index}}"/><label for="todo-item{{index}}">{{toDoItem.title}}</label>’

};

});

[/js]

jak widzimy ustawiamy isolated scope a nasza dyrektywa oczekuje danych w atrybutach item oraz index (wprawdzie nie musimy tego tego robić, bo sam ng-repeat tworzy isolated scope ale na potrzeby tutorialu definiujemy ;)).

jak widać w template odnosimy się do toDoItem (bo tak został przekazany item z parent scope do naszej dyrektywy) oraz do index. ng-repeat oferuje specjalne property $index która zawiera informacje o indexie tablicy na którym aktualnie się znajdujemy. Jest więcej takich property (https://docs.angularjs.org/api/ng/directive/ngRepeat). Dodatkowo dodałem lekką zmianę zasugerowaną wcześniej w kometarzach. Span zmienił się na label, dlatego potrzebowaliśmy index elementu żeby stworzyć unikalny id dla checkboxa oraz for dla labela.

Natomiast w widoku todo listy zmieniamy stary element <li> na:

[html]

<li ng-repeat="item in list">

<todo-list-item item="item" index="$index"></todo-list-item>

</li>

[/html]

przekazujemy item oraz index zgodnie z wymaganiami które określiliśmy w scope dyrektywy. Nazwa elementu jest znormalizowana o czym pisałem wcześniej.

Myślę że pomimo tego że dyrektywa jest bardzo prosta, da pogląd na możliwości tego mechanizmu. Można ich używać wszędzie od prostych elementów UI jak awatar użytkownika, po bardziej skomplikowane jak system komentarzy pod wpisami na blogu. Pamiętajcie że nic nie stoi na przeszkodzie żeby jedna dyrektywa znajdowała się w drugiej a ta w kolejnej. Przykładowy kod wyrenderowanego widoku systemu komentarzy może więc wyglądać tak (pozwolę sobie na lekki pseudo-kod):

[html]

<comments>

<comment>

<user-info>

<user-avatar/>

<span>DailyWeb</span>

</user-info>

<p>Lorem ipsum…</p>

<comment-date> </comment-date>

</comment>

….

</comments>

[/html]

Warto jeszcze wspomnieć o tym że od wersji 1.5.* pojawiły się komponenty, które ułatwiają pisanie aplikacji s tylu bazującym na web components i powinny ułatwić upgrade do wersji 2. Więcej informacji oraz porównanie możliwości dyrektyw i komponentów możecie znaleść tutaj (https://code.angularjs.org/snapshot/docs/guide/component)

Efekt?

Autorem wpisu jest Paweł Płoneczka.