Nierzadko podczas prac deweloperskich zauważamy, iż część kodu się powtarza. Jeśli stosujemy się do zasady DRY, często próbujemy jakoś ten powtarzający się kod odseparować, napisać go w taki sposób by jego kolejne użycie było szybkie i proste bez konieczności jego powtarzania.
Wyobraźmy sobie kod, który realizuje pewną jedną, określoną czynność. Niech tą czynnością będzie wyświetlenie wizytówki strony, czyli np. jej loga, tytułu, adresu i krótkiego opisu. W kodzie HTML taka wizytówka może opierać się na standardowej warstwie, w której umieścimy kolejne znaczniki. Całość będzie więc jednolitą strukturą, która będzie miała za zadanie wyświetlenie kodu HTMLa. Idąc dalej, wyobraźmy sobie, iż na stronie chcemy umieścić kilkadziesiąt tego typu wizytówek. Nagle okazuje się, że stworzymy bardzo dużo kodu, różniącego się od siebie tylko i wyłącznie 4 elementami – logiem, tytułem, adresem i opisem.
W tym miejscu z pomocą przychodzi nam Angular i jego dyrektywy. W dzisiejszym odcinku poradnika spróbujemy napisać własne dyrektywy. Powyżej opisany przykład jest zdecydowanie trywialny, gdyż Angular oprócz tak prostych rzeczy jak wyświetlenie kodu HTML w postaci szablonu, umożliwia nam również:
- dodawanie osobnych kontrolerów, działających tylko w zakresie danej dyrektywy
- dodawanie własnych zakresów dla danych
- definiowanie złączeń dwukierunkowych (data binding)
- możliwość dołączania innych dyrektyw do danej dyrektywy
- definiowanie sposobu użycia dyrektywy
Spróbujmy napisać prostą dyrektywę, struktura naszej aplikacji to dwa pliki, app.js i index.html:
plik app.js to plik, w którym będziemy definiować naszą dyrektywę:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
(function() { angular.module('ourApp', []) .directive('serwis', function() { return { scope: { title: '=', link: '=adres', content: '=content' }, templateUrl: 'serwisTemplate', controller: function($scope,$element,$attrs) { console.log($scope.title.toUpperCase()); }, }; });; })(); |
Ale po kolei, na samej górze widzimy deklarację nowej dyrektywy serwis, która to zwraca:
- scope – czyli zakres, zmienne, które mają zostać podane do dyrektywy jako argumenty, źródła danych. W naszym przypadku do naszej wizytówki strony internetowej chcemy podawać tytuł, link i opis. Zauważmy, iż pierwsza zmienna posiada jedynie znak równości, oznacza to, iż Angular wartości ma szukać pod taką samą nazwą jak nazwa pola. Dla odmiany w linijce drugiej jawnie podałem, iż wartość zmiennej link znajdzie się w atrybucie adres. Podobnie wartość zmiennej content należy szukać w atrybucie o tej samej nazwie.
- kolejny parametr to templateUrl. W tym miejscu możemy podać faktyczną nazwę pliku html, w której będzie znajdował się kod HTML do naszej dyrektywy, bądź też jak w opisywanym przypadku, odwołać się do warstwy o identyfikatorze równym podanej nazwie. Oznacza to, iż w naszym przypadku dyrektywa będzie szukała kodu HTML w znaczniku o id=”serwisTemplate”
- controller – defniuje kontroler dla danej dyrektywy, to w nim będzię odbywała się cała logika wymagana przez dyrektywę. Kontroler ten przyjmuje 3 argumenty – scope – czyli zakres danych dla danego kontrolera, element – czyli obiekt naszej dyrektywy (przydatny gdy chcemy przypiąć jakąś czynność/event), i attrs – czyli zbiór atrybutów naszej dyrektywy, w naszym wypadku title, adres i content. W tym przypadku warto nadmienić, czym będzie różnił się dostęp do danych za pomocą attrs a za pomocą zakresu? Przede wszystkim tym, iż w attrs zawsze otrzymamy String, niezależnie co będzie nam przekazane. W przypadku dostępu przez $scope otrzymamy sparsowaną przez Angulara wartość.
Czas na nasz widok:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<html ng-app="ourApp"> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.3/angular.js"></script> <script src="../js/app.js"></script> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <serwis title="'Onet'" adres="'http://onet.pl'" content="'Polski portal internetowy założony w 1996 przez spółkę Optimus, od 2012 kontrolowany przez niemiecko-szwajcarski koncern Ringier Axel Springer Polska (100% udziałów); największy polski portal internetowy (2012).'"></serwis> <serwis title="'Wirtualna Polska'" adres="'http://wp.pl'" content="'Nowoczesne medium, porządkuje świat i dostarcza angażujące informacje, rozrywkę i usługi w czasie rzeczywistym. Przewodnik Polaków w wirtualnym świecie.'"></serwis> <serwis title="'TVN24'" adres="'http://tvn24.pl'" content="'Czytaj najnowsze informacje i oglądaj wideo w portalu informacyjnym TVN24! U nas zawsze aktualne wiadomości z kraju, ze świata, relacje na żywo i wiele ...'"></serwis> <div class="container"> <div class="row"> <script type="text/ng-template" id="serwisTemplate"> <div class="col-md-3"> <h1><a ng-href="{{link}}">{{title}}</a></h1> <p>{{content}}</p> </p> </div> </script> </div> </div> </body> </html> |
Standardowo, dołączamy bibliotekę Angulara, a także nasz skrypt app.js. W celu przejrzystości dodałem również bootstrapa i użyłem go do stworzenia prostego layoutu. Co nas jednak najbardziej interesuje to wywołania naszej dyrektywy, jest ich aż trzy, w tym przykładowa:
1 |
<serwis title="'Onet'" adres="'http://onet.pl'" content="'Polski portal internetowy założony w 1996 przez spółkę Optimus, od 2012 kontrolowany przez niemiecko-szwajcarski koncern Ringier Axel Springer Polska (100% udziałów); największy polski portal internetowy (2012).'"></serwis> |
Widzimy, iż wszystkie niezbędne dyrektywie atrybuty podane zostały tuż po otwarciu znacznika serwis. Kilka linijek niżej dociekliwy Czytelnik może również dostrzec warstwę, będącą szablonem naszej dyrektywy. Widzimy w niej kod HTML naszej wizytówki, wraz z umieszczonymi danymi podanymi za pomocą atrybutów.
Uruchomienie aplikacji spowoduje wyświetlenie 3 wizytówek:
W tym miejscu chciałbym skupić się nad sposobem dołączania danych.
Przyjrzyjmy się bliżej deklaracji scope. Zgodnie z Angularem może ona przyjmować 3 wartości:
a) scope: false – oznacza, iż zakresy pomiędzy dyrektywą a resztą kodu będą współdzielone, zakres dyrektywy będzie tym samym, który używany jest dla reszty kodu
b) scope: true – oznacza, iż zakres będzie oddzielny, ale przy inicjalizacji Angular zrobi kopie bieżącego zakresu i użyje go w dyrektywie
c) scope: {var:val…} – oznacza, iż zostanie stworzony całkiem nowy zakres, niedostępny spoza dyrektywy, chyba, że właśnie przy użyciu =, @ i &. Czym się różnią?
W powyższym przykładzie użyłem “=” wraz z nazwą atrybutu, a także w wersji z nazwą domyślną, taką samą jak nazwą pola. Oprócz użytego sposobu, Angular definiuje jeszcze 2 dodatkowe sposoby dołączania danych, zbierzmy je wszystkie:
@ – wiązanie jednokierunkowe. Dyrektywa użyje danych z nadrzędnego zakresu, jednakże nie będzie miała możliwość jego edycji. Ponadto, jeśli jednak w nadrzędnym zakresie zmienna zostanie zaktualizowana, zmiana dosięgnie również zakresu dyrektywy.
= – wiązanie dwukierunkowe, oznacza, iż modyfikacja obiektu lub wartości zmiennej będzie miała odwzorowanie w dyrektywie i poza nią
& – wiązanie wyrażenia, dzięki niemu możemy uruchomić funkcję spoza dyrektywy (czyli z zakresu rodzica), ale już na argumentach z niej pochodzących
W zaprezentowanym przykładzie uwidacznia się także użycie kontrolera. Angular oprócz użycia kontrolerów udostępnia dwa inne sposoby na zarządzanie logiką dyrektywy, są to funkcje compile i link. Czyli oznacza to, iż z semantycznego punktu widzenia moglibyśmy swobodnie zamienić controller na compile lub link. Gdzie tkwi różnica?
a) compile – Pierwszy etap tworzenia dyrektywy, zazwyczaj używany do modyfikacji drzewa DOM szablonu dyrektywy (ale co ważne, jedynie szablonu). Czyli w tym miejscu możemy dokonać modyfikacji kodu HTML szablonu, zasięg dyrektywy jeszcze nie jest zdefiniowany
b) link – drugi etap tworzenia dyrektywy, dane dołączane są do szablonu, używany w przypadku, gdy logika ma wykonywać operacje związane z modyfikowaniem drzewa DOM. W takim wypadku, nie ma sensu tworzenia kontrolera, naszym jedynym celem jest reagowanie na interakcje użytkownika (mouseenter, click itp), kod zawarty w funkcji link uruchamiany jest dopiero po zakończeniu procesu duplikowania szablonu (template) i dołączania danych.
c) controller – najczęściej używane, gdy zachodzi potrzeba interakcji pomiędzy kilkoma dyrektywami, gdy jeden kod dyrektywy jest zależny od wywołania drugiego. Oprócz tego, kontrolery stosuje się wszędzie tam, gdzie logika wykracza poza tylko i wyłącznie modyfikacje drzewa DOM.
Wiemy już, jak definiować logikę dyrektywy, jak dołączyć szablon, jak przekazać parametry. Spróbujmy na chwilę zająć się jakością kodu i dobrymi praktykami, a dokładnie powróćmy do sposobów przekazywania danych. Angular oprócz definiowania zakresu, umożliwia nam przypisanie danych do kontrolera. Co to oznacza? Przede wszystkim większą czytelność kodu, brak zamieszania z przestrzenią nazw, gdyż będzie ona zadeklarowana i zgodna z kontrolerem. W tym celu zmodyfikujmy nieco nasz poprzedni kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
(angular.module('ourApp', []) .directive('serwis', function() { return { bindToController: { title: '=', link: '=adres', content: '=content' }, restrict: 'EA', templateUrl: 'serwisTemplate', scope: {}, controllerAs: 'vm', controller: function() { var vm = this; function init() { vm.link = angular.copy(vm.link); } init(); } }; });; |
Natomiast szablon naszej dyrektywy będzie wyglądał następująco:
1 2 3 4 5 6 7 8 9 |
<script type="text/ng-template" id="serwisTemplate"> <div class="col-md-3"> <h1><a ng-href="{{vm.link}}">{{vm.title}}</a></h1> <p>{{vm.content}}</p> </p> </div> </script> |
Co się zmieniło?
- poprzez dodanie controllerAs nadaliśmy alias dla przestrzeni nazw naszego kontrolera
- dodanie bindToController pozwoliło nam na przypisanie danych do samego kontrolera, a nie zakresu.
- W zdefiniowanym kontrolerze użyliśmy przypisania vm=this, którego już nie raz używaliśmy w przypadku standardowego przypisania aliasów do kontrolerów. Co warto zauważyć, dostęp do danych w kontrolerze musieliśmy zrealizować za pomocą dodatkowej funkcji, gdyż obiekt realizujący dostęp do dyrektywy nie był jeszcze gotów i bez tego otrzymalibyśmy wartości undefined.
- W celu dostępu do danych w widoku użyliśmy przedrostka vm.
- Dodaliśmy również Restrict, który definiuje w jaki sposób można użyć naszej dyrektywy, możliwości:
a) E – jako element, znacznik, przykładowe wywołanie: <serwis></serwis>
b) A – jako atrybut, przykładowe wywołanie: <div serwis></div>
c) C – jako klasę, przykładowe wywołanie: <div class=”serwis”></div>
d) M – jako komentarz, przykładowe wywołanie: <!– directive: serwis –>
Ostatnim elementem, nierozerwalnie związanym z dyrektywami, a którym chciałbym sie dziś zająć jest opcja ‘transclude‘. Wytłumaczę zasadę jej działania na przykładzie.
Przyjrzyjmy się naszej dotychczasowej dyrektywie. Spełnia ona wszelkie nasze wymagania, jednakże co, gdy oprócz zwykłych danych tekstowych, chcielibyśmy również przekazać kod HTML? Który powinien zostać odpowiednio zrenderowany i wyświetlony? W tym miejscu z pomocą przychodzi własnie transclude. Dzięki tej opcji Angular:
- Pobierze zawartość wywołania dyrektywy, np. dla
1<serwis title="'Onet'" adres="'http://onet.pl'"><p>Polski portal internetowy założony w 1996 przez spółkę Optimus, od 2012 kontrolowany przez niemiecko-szwajcarski koncern Ringier Axel Springer Polska (100% udziałów); największy polski portal internetowy (2012).</p></serwis>
zawartością będzie:
1<p>Polski portal internetowy założony w 1996 przez spółkę Optimus, od 2012 kontrolowany przez niemiecko-szwajcarski koncern Ringier Axel Springer Polska (100% udziałów); największy polski portal internetowy (2012).</p> - Pobraną zawartość umieści w dowolnym, wyznaczonym przez programistę miejscu w szablonie, miejsce to definiujemy za pomocą dyrektywy ng-transclude. Nasz nowy widok mógłby wyglądać:
1 2 3 4 5 6 7 |
<script type="text/ng-template" id="serwisTemplate"> <div class="col-md-3"> <h1><a ng-href="{{vm.link}}">{{vm.title}}</a></h1> <ng-transclude></ng-transclude> </div> </script> |
Efekt działania będzie dokładnie ten sam:
Co więc uzyskaliśmy? Możemy swobodnie dołączać kod HTML i wszelkie znaczniki. Nie musimy wszystkiego umieszczać w atrybutach dyrektywy. Ponadto dzięki zastosowaniu transclude, w funkcji link/controller jako 4 parametr (obok, scope, element, attrs), otrzymamy parametr transclude. Dzięki temu możemy go swobodnie umieścić w dowolnym miejscu drzewa DOM.
W celu utrwalenia wiedzy, spróbujmy przygotować dyrektywę, która:
- będzie przyjmowała listę stron internetowych
- będzie wyświetlała otrzymaną listę, każdy z elementów listy jako osobną stronę internetową
- będzie zawierała formularz, który będzie umożliwiał dodawanie nowych stron
W tym celu musimy zmodyfikować nasz kontroller w pliku app.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
angular.module('ourApp', []) .controller('main',function(){ var ctrl = this; ctrl.pages = [{ "title": "Onet", "adres": "http://onet.pl", "content": "Polski portal internetowy założony w 1996 przez spółkę Optimus, od 2012 kontrolowany przez niemiecko-szwajcarski koncern Ringier Axel Springer Polska (100% udziałów); największy polski portal internetowy (2012)." }, { "title": "Wirtualna Polska", "adres": "http://wp.pl", "content": "Nowoczesne medium, porządkuje świat i dostarcza angażujące informacje, rozrywkę i usługi w czasie rzeczywistym. Przewodnik Polaków w wirtualnym świecie." }, { "title": "TVN24", "adres": "http://tvn24.pl", "content": "Czytaj najnowsze informacje i oglądaj wideo w portalu informacyjnym TVN24! U nas zawsze aktualne wiadomości z kraju, ze świata, relacje na żywo i wiele ..." }]; }) .directive('serwis', function() { return { restrict: 'EA', scope: { }, templateUrl: 'serwisTemplate', bindToController: { pages: '=', }, controllerAs: 'vm', controller: function() { var vm = this; function init() { vm.pages = angular.copy(vm.pages); } init(); vm.add = function () { vm.pages.push({ title: vm.title, adres: vm.adres, content: vm.content }); }; } }; });; |
Analizując kod od samej góry można dostrzec:
- definicję kontrolera main z przestrzenią nazw ctrl
- zdefiniowano również pierwotną listę stron, składającą się z 3 wpisów, pod zmienną ctrl.pages
- W kolejnych liniach zdefiniowano nową dyrektywę serwis, do którego kontrolera przypisano za pomocą wiązania dwukierunkowego listę stron (‘pages: =’)
- Kolejny ważny krok to kontroler – w jego ciele umieściliśmy funkcję vm.add, która to pobiera z formularza wartości nowego wpisu i dodaje go do listy.
Całość – banalnie prosta 🙂
Przejdźmy do widoku:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<html ng-app="ourApp" ng-controller="main as ctrl"> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.3/angular.js"></script> <script src="../js/app.js"></script> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <serwis pages="ctrl.pages"></serwis> <div class="container"> <div class="row"> <script type="text/ng-template" id="serwisTemplate"> <div class="col-md-3" ng-repeat="page in vm.pages"> <h1><a ng-href="{{page.adres}}">{{page.title}}</a></h1> {{page.content}} </div> <input ng-model="vm.adres"/> <input ng-model="vm.title"/> <input ng-model="vm.content"/> <input type="submit" value="dodaj" ng-click="vm.add()"/> </script> </div> </div> </body> </html> |
Modyfikacje i ważniejsze elementy kodu:
- nasz szablon skłąda się z dyrektywy ng-repeat, która iteruje po liście wpisów, każdy z wpisów jest wyświetlany w osobnym panelu
- poniżej listy wpisów umieszczono formularz nowego elementu, zatwierdzany przyciskiem z przypisaną akcją vm.add() dla kliknięcia
I to wystraczy by cieszyć się z działającej, prostej dyrektywy. Oczywiście zmiany na liście można swobodnie zintegrować z backendem opartym np. na bazie danych.
Kończąc trochę przydługi już wpis, dyrektywy są techniką niezwykle przydatną, często używaną i wręcz niezbędną przy pisaniu czystego i dobrego kodu.
git: https://gitlab.com/spooky001/blog-angularjs