AngularJS. Pierwsze kroki. eBook. Pdf

Transkrypt

AngularJS. Pierwsze kroki. eBook. Pdf
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości
lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione.
Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie
książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie
praw autorskich niniejszej publikacji.
Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi
bądź towarowymi ich właścicieli.
Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte
w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej
odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne
naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION
nie ponoszą również żadnej odpowiedzialności za ewentualne szkody
wynikłe z wykorzystania informacji zawartych w książce.
Opieka redakcyjna: Ewelina Burska
Projekt okładki: Studio Gravite/Olsztyn
Obarek, Pokoński, Pazdrijowski, Zaprucki
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: [email protected]
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/angupk_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
ISBN: 978-83-283-1590-7
Copyright © Helion 2015
 Poleć książkę na Facebook.com
 Księgarnia internetowa
 Kup w wersji papierowej
 Lubię to! » Nasza społeczność
 Oceń książkę
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Spis treści
Rozdział 1. Wstęp .............................................................................................. 7
Od czego zacząć ............................................................................................................... 9
Biblioteka i ng-app, czyli bez czego nie może się obejść żadna aplikacja ........................ 9
Biblioteka ................................................................................................................... 9
Ng-app ...................................................................................................................... 10
Pierwsza aplikacja .......................................................................................................... 11
Framework SPA ............................................................................................................. 13
Podwójne wiązanie ......................................................................................................... 14
Jednostronne wiązanie .............................................................................................. 14
Dwustronne wiązanie ............................................................................................... 14
AngularJS i MVC ........................................................................................................... 15
Quiz ................................................................................................................................ 16
Rozdział 2. $scope — niepozorny obiekt ........................................................... 17
Wprowadzenie ................................................................................................................ 17
$scope i $rootScope ................................................................................................. 17
Alternatywa dla $scope ............................................................................................ 18
Dziedziczenie ................................................................................................................. 19
Izolowany scope ....................................................................................................... 22
$digest(), $apply() i $watch() ......................................................................................... 22
Nasłuchiwanie oraz $watch() ................................................................................... 22
$digest() ................................................................................................................... 24
$apply() .................................................................................................................... 24
Quiz ................................................................................................................................ 26
Rozdział 3. Moduły .......................................................................................... 27
Wprowadzenie ................................................................................................................ 27
Moduły a kontrolery ....................................................................................................... 28
Moduły a globalna przestrzeń nazw ............................................................................... 29
Zmodularyzowana aplikacja ........................................................................................... 29
Łączenie modułów ................................................................................................... 30
Quiz ................................................................................................................................ 31
Rozdział 4. Dependency Injection — wstrzykiwanie zależności .......................... 33
Wprowadzenie ................................................................................................................ 33
Uzyskiwanie zależności .................................................................................................. 34
Metody wstrzykiwania zależności .................................................................................. 35
DI w praktyce ................................................................................................................. 37
Quiz ................................................................................................................................ 43
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
4
AngularJS. Pierwsze kroki
Rozdział 5. Poznaj potęgę dyrektyw ................................................................. 45
Wprowadzenie ................................................................................................................ 45
Nazewnictwo .................................................................................................................. 48
Wbudowane dyrektywy .................................................................................................. 50
Dyrektywa a ............................................................................................................. 51
Dyrektywa form ....................................................................................................... 51
Dyrektywa input ....................................................................................................... 53
Dyrektywa ngBind ................................................................................................... 54
Dyrektywa ngBindHtml ........................................................................................... 54
Dyrektywa ngBindTemplate .................................................................................... 55
Dyrektywa ngCloak .................................................................................................. 56
Dyrektywy ngBlur i ngFocus ................................................................................... 57
Dyrektywa ngChange ............................................................................................... 57
Dyrektywa ngClass .................................................................................................. 62
Dyrektywa ngRepeat ................................................................................................ 65
Dyrektywa ngClick .................................................................................................. 72
Dyrektywa ngController ........................................................................................... 74
Dyrektywa ngCopy .................................................................................................. 75
Dyrektywa ngCut ..................................................................................................... 76
Dyrektywa ngDblclick ............................................................................................. 78
Dyrektywa ngFocus .................................................................................................. 78
Dyrektywa ngForm .................................................................................................. 79
Dyrektywa ngHref .................................................................................................... 79
Dyrektywa ngIf ........................................................................................................ 80
Dyrektywa ngInclude ............................................................................................... 80
Dyrektywy ngKeydown, ngKeypress i ngKeyup ..................................................... 80
Dyrektywa ngList ..................................................................................................... 81
Dyrektywa ngModel ................................................................................................. 81
Dyrektywa ngModelOptions .................................................................................... 82
Dyrektywy ngMousedown, ngMouseenter, ngMouseleave, ngMousemove,
ngMouseover i ngMouseup ................................................................................... 84
Dyrektywa ngNonBindable ...................................................................................... 84
Dyrektywa ngPaste ................................................................................................... 85
Dyrektywa ngPluralize ............................................................................................. 85
Dyrektywa ngReadonly ............................................................................................ 88
Dyrektywa ngStyle ................................................................................................... 88
Dyrektywa ngSubmit ................................................................................................ 88
Dyrektywa ngSwitch ................................................................................................ 89
Dyrektywa ngTransclude ......................................................................................... 89
Dyrektywa ngValue .................................................................................................. 91
Dyrektywa script ...................................................................................................... 91
Dyrektywa select ...................................................................................................... 93
Dyrektywa textarea .................................................................................................. 96
Quiz ................................................................................................................................ 97
Rozdział 6. Dyrektywy szyte na miarę ............................................................... 99
Wprowadzenie ................................................................................................................ 99
Pierwsza własna dyrektywa ............................................................................................ 99
Właściwości .................................................................................................................. 101
$scope vs. scope ........................................................................................................... 105
Quiz .............................................................................................................................. 107
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Spis treści
5
Rozdział 7. Filtry ............................................................................................ 109
Wprowadzenie .............................................................................................................. 109
Filtry wbudowane ......................................................................................................... 110
Operacje na stringach ............................................................................................. 110
Liczbowe ................................................................................................................ 111
Operacje na datach ................................................................................................. 112
JSON ...................................................................................................................... 113
Filtry dyrektywy ng-repeat ..................................................................................... 113
Linky ...................................................................................................................... 117
Quiz .............................................................................................................................. 118
Rozdział 8. Funkcje ....................................................................................... 119
Wprowadzenie .............................................................................................................. 119
Opis funkcji .................................................................................................................. 119
Funkcja angular.bind .............................................................................................. 119
Funkcja angular.bootstrap ...................................................................................... 120
Funkcja angular.copy ............................................................................................. 120
Funkcja angular.element ........................................................................................ 122
Funkcja angular.equals ........................................................................................... 126
Funkcja angular.extend .......................................................................................... 126
Funkcja angular.forEach ........................................................................................ 127
Funkcje angular.fromJson i angular.toJson ............................................................ 127
Funkcja angular.identity ......................................................................................... 127
Funkcja angular.injector ......................................................................................... 129
Funkcje angular.isArray, angular.isDate, angular.isDefined,
angular.isElement, angular.isFunction, angular.isNumber,
angular.isObject, angular.isString i angular.isUndefined .................................... 131
Funkcje angular.lowercase i angular.uppercase ..................................................... 131
Funkcja angular.module ......................................................................................... 132
Funkcja angular.reloadWithDebugInfo .................................................................. 132
Quiz .............................................................................................................................. 132
Rozdział 9. Routing — lepsza strona nawigacji ............................................... 133
Wprowadzenie .............................................................................................................. 133
Konfiguracja ................................................................................................................. 134
Widoki .......................................................................................................................... 134
Cztery kroki w procesie konfiguracji ............................................................................ 151
Quiz .............................................................................................................................. 151
Rozdział 10. Animacje ..................................................................................... 153
Wprowadzenie .............................................................................................................. 153
Jak to działa .................................................................................................................. 154
Obietnice ...................................................................................................................... 154
CSS3 Transitions .......................................................................................................... 155
Animacje CSS3 i @keyframes ..................................................................................... 158
Animacje JavaScript ..................................................................................................... 161
Quiz .............................................................................................................................. 167
Rozdział 11. Komunikacja z serwerem .............................................................. 169
Wprowadzenie .............................................................................................................. 169
Klasyczne zapytanie XHR a usługa $http ........................................................................ 169
XHR przy użyciu $http ................................................................................................. 170
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
6
AngularJS. Pierwsze kroki
Odpowiedzi http ........................................................................................................... 172
Promises ................................................................................................................. 172
success() i error() .................................................................................................... 172
$q, obietnice i odroczenia ....................................................................................... 173
$q.all ....................................................................................................................... 176
Przechowywanie odpowiedzi ....................................................................................... 176
Pozostałe metody $http ................................................................................................. 177
Parametry metody $http ................................................................................................ 177
Obiekt konfiguracyjny ............................................................................................ 177
Dane ....................................................................................................................... 178
Same origin policy oraz JSONP i CORS na ratunek XHR ........................................... 179
JSON with padding oraz jego ograniczenia ............................................................ 179
CORS — Cross Origin Resource Sharing .............................................................. 179
Trzecie wyjście: proxy ........................................................................................... 180
Quiz .............................................................................................................................. 180
Rozdział 12. Formularze ................................................................................... 181
Wprowadzenie .............................................................................................................. 181
ngFormController ......................................................................................................... 181
Używanie klas CSS ...................................................................................................... 181
Pierwszy formularz ....................................................................................................... 183
Quiz .............................................................................................................................. 184
Rozdział 13. Dobre praktyki ............................................................................. 185
Wprowadzenie .............................................................................................................. 185
Nazewnictwo i podział plików ..................................................................................... 185
Organizacja kodu .......................................................................................................... 188
Wydajność .................................................................................................................... 189
Quiz .............................................................................................................................. 191
Rozdział 14. Testy ........................................................................................... 193
Wprowadzenie .............................................................................................................. 193
Jasmine ......................................................................................................................... 193
Dopasowania ................................................................................................................ 197
Quiz .............................................................................................................................. 204
Rozdział 15. Zakończenie ................................................................................ 205
Skorowidz ..................................................................................... 206
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 1.
Wstęp
Aby opisać AngularJS jednym zdaniem, możemy powiedzieć, iż jest to open-source’owy
framework języka JavaScript wykorzystywany do tworzenia front-endowych aplikacji SPA
(single page application) w oparciu o wzorzec projektowy Model-View-Controller.
Historia frameworka sięga 2009 roku — początkowo był to prywatny projekt pracowników firmy Google: Miško Hevery’ego oraz Adama Abronsa. Angular okazał się na tyle
ciekawy, iż otrzymał od Google oficjalne wsparcie wraz z całym zespołem zajmującym się jego rozwojem. Po raz pierwszy biblioteka ujrzała światło dzienne na początku
2012 roku.
Większość z najistotniejszych atutów Angulara zaczerpnięto z istniejących i sprawdzonych rozwiązań, dzięki czemu stworzono lekkie i efektywne narzędzie deweloperskie, które oferuje szeroką gamę możliwości, prostą strukturę oraz niesamowitą łatwość w testowaniu.
Ogromny wkład w rozwój projektu ma również społeczność internetowa. Poprzez
dzielenie się swoimi doświadczeniami użytkownicy z całego świata biorą czynny
udział w ciągłym ulepszaniu Angulara. Jeżeli pragniesz dołączyć do tego grona, odwiedź oficjalną stronę projektu: https://angularjs.org/, na której można znaleźć poradniki, kursy, opisy API, czyli wszystko, czego może potrzebować głodny wiedzy
deweloper. Rysunek 1.1 przedstawia okienko pobierania „kanciastego” (tak „pieszczotliwie” określamy w naszej książce AngularJS, jeden z najlepszych i najszybciej
rozwijających się frameworków javascript.) ze strony, a rysunek 1.2 pobrane moduły,
o których szerzej opowiemy w dalszych częściach książki.
Dokumentacja techniczna poparta jest wieloma przykładowymi programami ułatwiającymi zrozumienie danego zagadnienia. Dla poszukujących inspiracji polecamy stronę
http://builtwith.angularjs.org, na której znajduje się galeria aplikacji napisanych
w Angularze.
Warto odnotować również fakt, iż Angular operuje na licencji X11/MIT, czyli jest
zupełnie darmowy.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
8
Rysunek 1.1.
Pobieranie
AngularJS
Rysunek 1.2.
Lista pobranych
modułów
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
AngularJS. Pierwsze kroki
Rozdział 1.  Wstęp
9
Od czego zacząć
Angular to framework napisany w JavaScripcie i wykorzystujący język HTML, dlatego
do eksperymentowania z nim w zupełności wystarczą dowolny edytor tekstu, przeglądarka internetowa oraz trochę wolnego czasu. Jednak w celu pełnego wykorzystania potencjału Angulara zalecamy używanie rozbudowanych platform, takich jak Visual
Studio (wersja express for web), WebStorm, Sublime Text 2 czy choćby Notepad++.
Wybór przeglądarki internetowej ma także istotny wpływ na komfort pracy, z tego
powodu polecamy Google Chrome bądź Mozillę Firefox.
Pod względem merytorycznym od czytelnika wymagana jest podstawowa wiedza
z zakresu języków HTML, CSS oraz JavaScript.
Umiejętności, które nabędziesz, studiując AngularJS. Pierwsze kroki, pozwolą Ci na
tworzenie dynamicznych i łatwych w utrzymaniu aplikacji internetowych działających po
stronie klienta. Każde nowe zagadnienie staramy się poprzeć przykładem umożliwiającym
jego dokładne zrozumienie. Najwięcej można się jednak nauczyć poprzez praktykę,
dlatego gorąco zachęcamy do eksperymentowania z przykładami.
Biblioteka i ng-app, czyli bez czego
nie może się obejść żadna aplikacja
Biblioteka
By nasza aplikacja mogła implementować technologię Angulara, wymagane jest podpięcie biblioteki angular.js, dostępnej do pobrania na oficjalnej stronie projektu:
https://angularjs.org/, w sekcji Develop/Download.
Mamy do wyboru dwie wersje biblioteki Angulara: zminimalizowaną angular.min.js
oraz pełną angular.js. Pełna wersja Angulara wyposażona jest w narzędzia ułatwiające debugowanie aplikacji, lecz wpływa dość znacząco na prędkość ładowania biblioteki.
Wersję tę powinniśmy stosować podczas procesu rozwoju aplikacji (fazy developmentu). Specyfikacja tej wersji idealnie nadaje się do testów. Wersja zminimalizowana
natomiast powinna być wykorzystywana w produkcji i jest, jak się domyślasz, lżejsza,
co przekłada się na krótszy czas ładowania.
W celu dodania biblioteki do naszego projektu należy umieścić poniższy skrypt
w dowolnej części kodu pliku html.
<script src="angular.js"></script>
W sytuacji, gdy chcemy wykorzystać wersję zminimalizowaną, wystarczy podmienić
nazwę na angular.min.js.
<script src="angular.min.js"></script>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
10
AngularJS. Pierwsze kroki
Dla przyspieszenia ładowania aplikacji skrypt powinien być zawsze umieszczany w dolnej części znacznika <body>. Dzięki temu pozornie kosmetycznemu zabiegowi proces ładowania szablonu HTML nie będzie blokowany przez ładujący się skrypt z angular.js.
Biblioteka angular.js powinna znajdować się w jednym folderze z naszym programem,
w innym wypadku jako źródło powinniśmy podać dokładną ścieżkę dostępu.
<script src="/angular/angular.js"></script>
Możliwe jest też bezpośrednie załadowanie skryptu ze strony Angulara przy użyciu CDN
(Content Delivery Network), tak jak w przykładzie poniżej.
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"> </script>
W celu załadowania zminimalizowanej wersji wystarczy zmodyfikować końcówkę
ścieżki.
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"> </script>
Sugerujemy stosowanie ostatniej z powyższych metod, ponieważ skrypt przechowywany
w pamięci podręcznej przeglądarki jest dostępny dla wielu aplikacji. Jeżeli użytkownik
odwiedził wcześniej stronę zawierającą link CDN dla bieżącej wersji Angulara (z tygodnia na tydzień z Angulara korzysta coraz więcej aplikacji!), będzie mógł swobodnie przeglądać naszą aplikację bez konieczności ponownego ładowania biblioteki.
Ng-app
Kolejnym elementem, bez którego nie może się obejść żadna aplikacja, jest dyrektywa ng-app, określająca, jaka część drzewa DOM (Document Object Model) będzie zarządzana przez AngularJS. W przypadku gdy tworzymy aplikację w całości zarządzaną
przez nasz framework, powinniśmy umieścić dyrektywę ng-app w znaczniku <html>
w taki oto sposób:
<html ng-app>
...
</html>
Dzięki temu AngularJS wie, że kontroluje wszystkie elementy struktury DOM na tej
stronie.
Kiedy już posiadamy gotową aplikację, w której DOM jest zarządzany przez inną
technologię, np. Rails czy jQuery, możliwe jest zawężenie obszaru kontrolowanego
przez Angulara poprzez umieszczenie ng-app w elemencie wewnątrz struktury. Można
przykładowo wykorzystać <div>. Wówczas AngularJS będzie kontrolował jedynie to,
co dzieje się w obrębie znacznika oraz jego potomków, tak jak pokazano poniżej.
<html>
<!- - niezarządzana przez Angulara - ->
<div ng-app>
<!- - zarządzana przez Angulara - ->
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 1.  Wstęp
11
<div>
<!- - zarządzana przez Angulara - ->
</div>
</div>
<!- - niezarządzana przez Angulara - ->
</html>
Pierwsza aplikacja
Najwyższy czas przełożyć powyższą teorię na praktykę. Pierwszą aplikację zawiera listing 1.1. Nie jest to jednak klasyczne „Hello, World!”.
Listing 1.1. Pierwsza aplikacja
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app>
<body>
<input ng-model='text'>
<p> {{ text }} </p>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
</body>
</html>
Nie umieszczając nawet jednej linijki JavaScriptu, napisaliśmy dynamiczną aplikację.
Kiedy wpiszemy dowolny tekst w przeglądarce, zostanie on dynamicznie wyświetlony. Magia tego procesu zostanie wyjaśniona w dalszej części poradnika.
Była to oczywiście tylko mała prezentacja możliwości AngularJS. Podstawowa aplikacja składa się z co najmniej dwóch plików: widoku (html) oraz kontrolera zawierającego logikę i model. By móc korzystać z kontrolera i „żyjącego” w nim modelu,
należy go dołączyć do widoku, jak podczas procesu z pobieraniem biblioteki. Najlepiej
obrazuje to listing 1.2.
Listing 1.2. Podpinanie kontrolera
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<body>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script src="controller.js"> </script>
</body>
</html>
W sytuacji, gdy nasz kontroler nie znajduje się w tym samym katalogu co widok, należy
sprecyzować dokładną ścieżkę dostępu, tak jak przedstawia to listing 1.3.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
12
AngularJS. Pierwsze kroki
Listing 1.3. Odwołanie do kontrolera
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<body>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script src="/angular/controller.js"></script>
</body>
</html>
Oczywiście każdy poradnik dla programistów nawiązujący do tradycji musi zawierać
przykład z „Hello, World!”. Na listingu 1.4 widać szkielet naszej aplikacji.
Listing 1.4. Szkielet naszej aplikacji
Plik hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<meta charset="utf-8">
</head>
<body>
<div ng-controller="FirstCtrl">
<p>Witaj, {{ message.sentence }} </p>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script src="controller.js"> </script>
</body>
</html>
Plik hello.html to zwyczajny plik HTML uzupełniony o dodatkowe znaczniki odpowiednio interpretowane przez przeglądarkę dzięki własnemu kompilatorowi HTML.
Umiejętność tworzenia własnych dyrektyw, a co za tym idzie, nowych elementów (nawet
dynamicznych!) dla naszego HTML-a to jedna z najważniejszych zalet Angulara!
Nasza aplikacja wykorzystuje jedynie (i to w zupełności wystarczy!) Angulara. Dyrektywę ng-app umieściliśmy w głównym elemencie szablonu, w znaczniku <html>,
dyrektywa ng-controller określa klasę kontrolera. Kontroler oraz żyjący w nim scope zostają związane (ang. binded) w wybranym przez nas elemencie i jego potomstwie.
Obiekt scope (a dokładnie $scope) jest mostem pomiędzy widokiem a kontrolerem,
„transportującym” model w obu kierunkach. Jest on częścią wcześniej wspomnianej
magii Angulara. $scope pełni również wiele innych funkcji, dlatego postanowiliśmy
poświęcić mu osobny rozdział.
Drugi „puzzel” naszej aplikacji to controller.js. Kontroler, jak już wspominaliśmy,
jest miejscem, w którym żyje model i w którym umieszczamy logikę naszej aplikacji.
Przeznaczyliśmy na to zagadnienie osobny rozdział, tutaj przedstawimy tylko jego
ogólny zarys.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 1.  Wstęp
13
Listing 1.5 zawiera logikę oraz model naszej aplikacji.
Listing 1.5. Logika oraz model aplikacji
Plik controller.js
......angular.module('app', [])
.controller('FirstCtrl', ['$scope', function ($scope) {
$scope.message = { sentence: 'świecie!' };
} ]);
Plik controller.js składa się z pojedynczej funkcji FirstCtrl, której wstrzyknęliśmy
$scope. Następnie rozszerzamy go o obiekt message zawierający string. Każdy element
przypisany do modelu staje się od razu dostępny w widoku, w miejscu, gdzie znajduje się
dyrektywa ng-controller.
Jeżeli otworzymy hello.html w przeglądarce, to naszym oczom powinien ukazać się
napis: „Witaj, świecie!”. By zmienić wyświetlaną wiadomość, wystarczy edytować
„komunikat” w klasie controller.js i odświeżyć przeglądarkę.
Przeróbmy jednak ten przykład, aby można było manipulować wyświetlanym napisem dynamicznie i bez potrzeby odświeżania, jak w przypadku pierwszego programu.
W tym celu w listingu 1.6 wykorzystamy ponownie dyrektywę ng-model.
Listing 1.6. Dyrektywa ng-model
Plik hello.html
<html ng-app>
<body>
<input type="text" ng-model="message.sentence">
<p>Witaj, {{ message.sentence }}!</p>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
</body>
</html>
Plik controller.js nie wymaga dodatkowych modyfikacji.
Zadanie przypisania elementów do modelu przejęła od scope właśnie dyrektywa ng-model.
Domyślnie wyświetlona zostanie wiadomość „Witaj, !”, jeżeli jednak wpiszemy dowolną inną wiadomość, to za sprawą ng-model zmiana zostanie natychmiast zarejestrowana przez Angulara i zaimplementowana w kontrolerze.
Framework SPA
AngularJS, jak już powiedzieliśmy, to framework SPA. Technologie SPA posiadają
kilka charakterystycznych cech.
1. Po pierwsze logika zostaje przeniesiona z serwera na klienta. Praca łączenia
szablonu z danymi odbywa się w przeglądarce, nie na back-endzie.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
14
AngularJS. Pierwsze kroki
2. Serwer służy jedynie jako źródło danych dostarczające szablon HTML, CSS
oraz JavaScript podczas ładowania strony. W razie potrzeby dodatkowe dane
z serwera są dosyłane dynamicznie.
3. Strona nie jest przeładowywana za każdym razem, odświeżane są tylko
wybrane jej elementy.
Głównym założeniem tego podejścia jest odciążenie serwera i zwiększenie użyteczności aplikacji (działa ona płynnie i szybko). O tym, jak budować aplikacje SPA, dowiesz
się z rozdziału 9., poświęconego routingowi.
Podwójne wiązanie
Jednostronne wiązanie
Zanim aplikacje bazujące na technologii Ajax stały się popularne, do tworzenia interfejsu użytkownika wykorzystywaliśmy platformy, tj. ASP.NET, PHP, Rails bądź inne. Dane łączono z HTML-em po stronie serwera przed prezentowaniem ich użytkownikowi.
JavaScript bazuje na wcześniej wspomnianym modelu. Znana nam biblioteka jQuery
daje możliwość odświeżania wybranych elementów DOM bez potrzeby ponownego
ładowania całej strony. Szablon HTML łączy się z danymi, a następnie rezultat przesyłany
jest do dowolnie wybranej części DOM poprzez umieszczenie innerHtml na interesującym nas elemencie blokowym.
Wszystko to działa bez zarzutu, ale co w przypadku, gdy chcemy wykorzystać dane
wejściowe wprowadzone przez użytkownika? W one way binding dane są pobierane
z modelu i umieszczane w widoku, który może je jedynie wyświetlić. Nie ma natomiast możliwości, by wpływać na model z widoku.
Dlatego musimy wykonać kilka dodatkowych czynności, aby upewnić się, że dane te
trafią w odpowiednim stanie zarówno do interfejsu użytkownika, jak i do właściwości
klasy JavaScript.
W two way binding zmiany w modelu mogą być wprowadzane po obu stronach — w widoku oraz w kontrolerze.
Dwustronne wiązanie
Two way binding pozwala rozwiązać powyższy problem bez napisania choćby jednej
dodatkowej linijki kodu. Wystarczy, że zadeklarujemy, które elementy po stronie widoku lub kontrolera powinny zostać ze sobą związane. W deklaracji wykorzystujemy
wspomniany już $scope oraz dyrektywę ng-model.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 1.  Wstęp
15
Modyfikacje w widoku powodują natychmiastowe zmiany w modelu i odwrotnie.
Manipulacje elementem drugim wpływają na element pierwszy. Technika ta świetnie
współgra ze wzorcem MVC, ponieważ pozwala eliminować powtarzający się kod (tzw.
boilerplate code) przy przesyłaniu modelu między kontrolerem a widokiem.
O tym, skąd Angular wie, kiedy ma zarejestrować zmianę, a także o sposobie faktycznej propagacji zmian dowiesz się, czytając o nasłuchiwaniu, funkcji $watch, cyklu
$digest() i $apply(). Informacje te zawarte są w rozdziale 2.
AngularJS i MVC
Model-View-Controller to wzorzec projektowy pomagający w organizacji struktury
naszej aplikacji. Tradycyjne podejście do MVC polega na tworzeniu oddzielonych od
siebie komponentów (np. w postaci odrębnych klas) oraz łączeniu ich później za pomocą kodu. Angular natomiast upraszcza powyższy schemat, gdyż sam zajmuje się
łączeniem poszczególnych elementów. Dzięki temu zabiegowi ewidentnie skraca się
czas tworzenia aplikacji, co pozwala deweloperowi skupić się na jej ważniejszych aspektach. Omówimy teraz poszczególne aspekty MVC w ujęciu Angulara.
Kontroler zawiera logikę aplikacji i koordynuje operacje wykonywane pomiędzy
modelem zawierającym dane a widokiem, który te dane pobiera bądź dostarcza użytkownikowi. Kontrolerami są klasy odpowiadające za wskazanie obiektów lub atrybutów składających się na model.
Nie powinniśmy stosować sposobu tworzenia kontrolerów zaprezentowanego w tym
rozdziale. Użyliśmy go ze względu na podobieństwo do tradycyjnego podejścia przy
tworzeniu funkcji. Odpowiedź na pytania o poprawną metodę konstruowania kontrolerów zawarta jest w rozdziale dotyczącym modułów.
Model to reprezentacja danego problemu. W Angularze składają się na niego zwykłe
obiekty JavaScript, tzw. POJO, żyjące w kontrolerze. Nie wymaga się tworzenia specjalnych klas czy dodatkowych funkcji get/set, gdyż manipulacja właściwościami
odbywa się bezpośrednio na obiektach. Jest jednak różnica pomiędzy konstruowaniem
obiektów w czystym JavaScripcie a robieniem tego w Angularze. W celu wykorzystania elementów modelu w widoku musimy je przypisać do obiektu scope ($scope).
Scope służy jako swoisty most łączący model z widokiem, lecz sam w sobie nie posiada żadnych danych.
Przypisanie modelu do obiektu scope wygląda następująco:
function ExampleController($scope) {
$scope.message = { hello : 'Witaj, świecie!'};
var person = [{ name: 'Jan Angularski', age: 32 },
{ name: 'Ola Angularska', age: 29 }
];
$scope.person;
}
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
16
AngularJS. Pierwsze kroki
W ten sposób przypisaliśmy message oraz person do obiektu $scope i możemy się do
niego odwoływać w dowolnej części widoku zarządzanej przez Angulara i zawierającej
dyrektywę ng-controller.
Widok odpowiada za prezentację danych oraz interakcję z użytkownikiem. Definiowany jest za pomocą czystego HTML-a oraz dyrektyw.
Niezbędnym elementem umieszczanym w szablonie HTML jest interpolacja. Jej zadaniem jest wyświetlanie pobieranego modelu. Tworzymy ją poprzez użycie podwójnych nawiasów klamrowych w taki oto sposób:
<div ng-controller="ExampleCtrl">
{{ message.hello }}
</div>
Warto zwrócić uwagę na fakt, że gdy odnosimy się do konkretnego modelu, robimy
to bezpośrednio, bez wykorzystania obiektu scope.
{{ $scope.message.hello
}}
Powyższa próba dobrania się do message.hello nie przyniesie zatem oczekiwanych
rezultatów. Jak już wspomnieliśmy, całą robotę wybierania elementów z modelu po
stronie widoku wykonuje za nas ng-model, dlatego nie ma potrzeby odnoszenia się do
scope.
Możliwe jest również nieco inne zastosowanie interpolacji. Można mianowicie umieścić w niej proste obliczenia:
{{
15 + 85 }}
bądź łańcuchy znaków:
{{ "Angular jest niesamowity!" }}
lub funkcji:
{{ someFunction() }}
Każda z powyższych operacji zostanie bezproblemowo wykonana przez Angulara.
Quiz
1. Co to jest AngularJS?
2. Kto jest twórcą AngularJS?
3. Czym się różni angular.js od angular.min.js?
4. Co to jest ng-app?
5. Co to jest SPA?
6. Czym się różni wiązanie pojedyncze od podwójnego?
7. Co to jest MVC?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 2.
$scope
— niepozorny obiekt
Wprowadzenie
W tym rozdziale zajmiemy się wspomnianym przez nas wcześniej obiektem $scope.
Jego podstawowe zadania to:
 transportowanie modelu pomiędzy widokiem a kontrolerem;
 nasłuchiwanie zdarzeń bądź zmian zachodzących w modelu;
 propagacja zmian modelu.
Mimo że odgrywa wyjątkową rolę, $scope to wciąż zwykły obiekt POJO. Oznacza to,
że możemy dowolnie przypisywać mu oraz modyfikować atrybuty według własnego
uznania. Wyróżnia go fakt, iż w większości przypadków jest on za nas automatycznie
tworzony i wstrzykiwany.
$scope i $rootScope
W fazie ładowania początkowego aplikacji (tzw. bootstrap) AngularJS tworzy wiązanie (binduje) pomiędzy znacznikiem zawierającym dyrektywę ng-app a wszystkim, co
jest zawarte w elementach poniżej.
$rootScope jest rodzicem wszystkich obiektów $scope i znajduje się najwyżej w hierarchii. Instancja $rootScope jest tworzona w momencie bootstrapowania aplikacji.
Każdy program posiada dokładnie jeden taki obiekt, po którym dziedziczą wszystkie
inne obiekty scope. Nie zalecamy przypisywania mu zbyt wielu atrybutów, gdyż jest
on czymś na wzór obiektu globalnego, którego nie powinno się zaśmiecać. Przy wykorzystywaniu więcej niż jednej biblioteki lub frameworka istnieje ryzyko wystąpienia
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
18
AngularJS. Pierwsze kroki
zbieżności nazw atrybutów bądź metod przypisanych do globalnych obiektów. Tego
typu problemy są niezwykle uciążliwe w usuwaniu.
Wspominaliśmy już, że każdy element przypisany do $scope jest od razu dostępny
w widoku. Przypisywanie atrybutów i funkcji do modelu po stronie kontrolera odbywa
się w sposób ukazany w listingu 2.1.
Listing 2.1. Kontroler — przypisanie atrybutów
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>Kontroler – przypisanie atrybutów</title>
</head>
<body>
<div ng-controller="dateCtrl">
Data: {{orginal() | date}}
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.run(function ($rootScope) {
$rootScope.dateOrginal = new Date();
});
app.controller('dateCtrl', function ($rootScope, $scope) {
$scope.orginal = function () {
return $rootScope.dateOrginal;
};
});
</script>
</body>
</html>
W powyższym przykładzie zdefiniowaliśmy w $rootScope właściwość dateOrginal.
Następnie w kontrolerze dateCtrl stworzyliśmy funkcję orginal, która zwraca nam
datę z $rootScope.
Alternatywa dla $scope
Istnieje również możliwość przypisywania atrybutów do modelu po stronie widoku
bez odwoływania się do scope. W tym celu korzystamy z dyrektywy ng-model. Jest ona
dokładnie opisana w rozdziale 5., poświęconym dyrektywom wbudowanym. Na tym
etapie warto zapamiętać, że ng-model inicjuje nam $scope, którego możemy użyć w kontrolerze.
<html ng-app>
...
<div ng-model='wiadomosc'>
<p> {{ wiadomosc }} </p>
</div>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 2.  $scope — niepozorny obiekt
19
Stosując się do dobrych praktyk, powinniśmy w miarę możliwości wybierać wariant
pierwszy, bliższy ideologii MVC.
Dziedziczenie
W przykładzie z listingu 2.1 wykorzystaliśmy wcześniej wspomniany $rootScope. Jak
już mówiliśmy, staramy się nie przypisywać atrybutów do obiektu głównego, lecz do
nowo utworzonego scope znajdującego się w hierarchii poniżej.
app.controller('dateCtrl', function ($scope) {
$scope.wiadomosc = "Przypisujemy wiadomosc do widoku!";
$scope.funkcjaA = function() {
return wiadomosc + "Dodajemy dodatkowe zdanie";
}
}
$scope odwzorowuje strukturę DOM. Oznacza to, że możemy swobodnie zagnieżdżać
jego obiekty.
Korzystanie z obiektów $scope nie wymaga ich wcześniejszej deklaracji. Większość
obiektów $scope tworzona jest dzięki metodzie $new(), wywoływanej za każdym razem, gdy napotykana jest dyrektywa ng-controller.
Nowy obiekt zostaje automatycznie zagnieżdżony poniżej obiektu $rootScope. Poza
jednym wyjątkiem (izolowanym scope) wszystkie obiekty $scope mają dostęp do
obiektów znajdujących się w hierarchii nad nimi. Jeżeli AngularJS nie znajdzie pożądanej informacji w scope na swoim poziomie, to rozpocznie przeszukiwanie obiektu
znajdującego się wyżej, aż dojdzie do $rootScope.
Zobaczmy na listingu 2.2, jak możemy korzystać z dziedziczenia kontrolerów:
Listing 2.2. Kontrolery — dziedziczenie
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>Kontrolery — dziedziczenie</title>
</head>
<body>
<div ng-controller="defaultCtrl">
<div ng-controller="inheritanceCtrl">
<input type="text" ng-model="uczen.imie" placeholder="Imie
Ucznia"></input>
<button ng-click="poprawaTestu()">Poprawa testu </button>
</div>
{{ uczen }}
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
20
AngularJS. Pierwsze kroki
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.uczen = { zdanyTest: false };
});
app.controller('inheritanceCtrl', function ($scope) {
$scope.poprawaTestu = function () {
$scope.uczen.zdanyTest = true;
}
});
</script>
</body>
</html>
Ponieważ inheritanceCtrl związaliśmy w hierarchii niżej niż defaultCtrl, otrzymuje on
dostęp do metod kontrolera bazowego. Tutaj warto odnotować, iż dziedziczenie w Angularze odbywa się w jednym kierunku. W tym wypadku kontroler potomny jest silnie
powiązany z rodzicem, czyli może odwoływać się do jego metod. Jednakże kontroler
bazowy nie może bezpośrednio odwoływać się do potomka. By uzyskać dostęp do owych
metod, należy wykorzystać przesyłanie zdarzeń (ang. event dispatching). W większości przypadków, kiedy musimy odwoływać się do metod potomnych, oznacza to, że
powinniśmy się przyjrzeć naszemu kodowi, gdyż najprawdopodobniej robimy coś źle.
Aby później mieć możliwość odwołania się do naszego scope, musimy umieścić dyrektywę ng-controller w dowolnym elemencie DOM znajdującym się na tym samym
bądź wyższym poziomie hierarchii co model (a konkretnie nasze odwołanie do niego
poprzez interpolację).
<html ng-app>
...
<div ng-controller="Kontroler">
<p> {{ wiadomosc }} </p>
</div>
...
</html>
Dyrektywa ng-controller należy do grupy tzw. tworzących dyrektyw. Za każdym razem, gdy Angular napotyka jedną z takich dyrektyw, zostaje utworzona nowa instancja
scope, dlatego wcześniejsza deklaracja w kontrolerze nie jest wymagana.
Wielu czytelników na pewno zadaje sobie pytanie, jaki jest sens wprowadzenia koncepcji dziedziczenia do scope.
By na nie odpowiedzieć, posłużymy się opisaną w rozdziale 5. dyrektywą ng-repeat.
Przytoczymy krótki opis tej dyrektywy: Ng-repeat pozwala nam iterować po dowolnej kolekcji obiektów, dodatkowo tworzy osobne elementy szablonu DOM dla każdego
z elementów kolekcji.
Listing 2.3 najlepiej nam to zobrazuje.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 2.  $scope — niepozorny obiekt
21
Listing 2.3. Przykład zastosowania dyrektywy ng-repeat
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>Przykład zastosowania dyrektywy ng-repeat</title>
</head>
<body>
<div ng-controller="defaultCtrl">
<ul>
<li ng-repeat="oferta in oferty">
<p> Nazwa: {{ oferta.nazwa }} ||
cena: {{oferta.cena }} </p>
</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.oferty = [
{ nazwa: 'Krzesło', cena: 149.99 },
{ nazwa: 'Stolik', cena: 189.99 },
{ nazwa: 'Szafka', cena: 205.99 },
];
});
</script>
</body>
</html>
Zmienne z każdego obiektu w kolekcji zostaną przypisane do scope, by później zostać
zrenderowanymi przez widok.
Właśnie w tym momencie pojawia się problem. Aby każdą nową zmienną przypisać
do $scope, musielibyśmy nadpisywać poprzednią ze względu na zbieżność nazw atrybutów. Dlatego też każdemu elementowi kolekcji przypisujemy nowy scope. Dana
zmienna będzie „żyć” jedynie w obrębie swojego scope. Wszystkie nowo utworzone
obiekty układają się w hierarchię przypominającą tę ze struktury DOM. Mamy możliwość wykorzystania tej samej nazwy dla zmiennej w różnych obiektach scope.
Podobnie jak w przypadku programowania zorientowanego obiektowo dziedziczenie
pozwala na izolację atrybutów i funkcjonalności poszczególnych elementów modelu.
Dziedziczenie obiektów scope w Angularze odbywa się z użyciem wcześniej wspomnianej metody $new().
var obiektBazowy = $rootScope;
var obiektPochodny = obiektBazowy.$new();
obiektBazowy.imie = 'Marian';
obiektPochodny.nazwisko = 'Kowalski';
W celu zniszczenia danego obiektu scope należy zastosować metodę $destroy(), która
usuwa wszystkie obiekty pochodne (i ich pochodne) z obiektu bazowego. Od tej chwili
dany scope jest gotowy na „odśmiecanie”, czyli tzw. garbage collection.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
22
AngularJS. Pierwsze kroki
Izolowany scope
Możliwe jest również utworzenie tzw. izolowanego scope, który nie dziedziczy po swoich
rodzicach — jest to wcześniej przez nas wspomniany wyjątek. Używamy go podczas
tworzenia komponentów, które chcielibyśmy później kilkakrotnie wykorzystać.
Tworząc izolowany scope, tak naprawdę bawimy się z pewnymi własnościami obiektu
scope.
Wyobraźmy sobie sytuację, iż stworzyliśmy dyrektywę służącą np. do wyświetlania
menu na stronie naszej restauracji. Nasza dyrektywa zawiera szablon dla wyświetlanych
informacji. Podpinamy kontroler do modułu, przypisujemy potrawy do $scope i przypinamy dyrektywę. Gdybyśmy teraz umieścili kilka tagów z dyrektywą wewnątrz kodu
HTML, to wyświetlana byłaby jedna i ta sama informacja. By temu zapobiec, musielibyśmy stworzyć osobny kontroler z nową instancją scope dla każdej potrawy. Pomysł czasochłonny i zmuszający do pisania masy nowego kodu, nie jest to więc najlepsze rozwiązanie. Tutaj właśnie wkraczają izolowane obiekty scope.
Aby odizolować scope, musimy wewnątrz naszej dyrektywy umieścić element scope.
...
return {
scope: {}
}
...
Od tej chwili poszczególne instancje dyrektywy będą izolować swój lokalny scope.
Możemy wiązać różne elementy przypisane do scope.
$digest(), $apply() i $watch()
Jak wcześniej wspominaliśmy, scope nie służy jedynie jako most dla danych. Do jego
obowiązków należy między innymi nasłuchiwanie zmian zachodzących w modelu.
W tym celu wykorzystujemy opisany w dalszej części tego rozdziału $swatch. Scope
posiada również umiejętność wprowadzania (propagacji) zmian w modelu, znajdujących się wewnątrz aplikacji bądź pochodzących spoza niej.
Nasłuchiwanie oraz $watch()
Po przypisaniu $watch do wybranego elementu AngularJS zaczyna oczekiwać na ewentualne zmiany. W momencie ich zajścia wywoływana jest tzw. funkcja nasłuchująca (ang.
listener function), która może reagować na te zmiany. Przyjrzyjmy się bliżej temu, jak
wygląda nasłuchiwanie zmian przez kanciastego, ukazane na listingu 2.4.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 2.  $scope — niepozorny obiekt
23
Listing 2.4. Nasłuchiwanie
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - $watch</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<div class="well">Liczba: {{number}}</div>
<div>
<a class="btn btn-success" href="#" ng-click="add()"> + </a>
<a class="btn btn-danger" href="#" ng-click="dec()"> - </a>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.number = 1;
$scope.$watch('number', function () {
console.log('Liczba: ' + $scope.number);
});
$scope.add = function () {
$scope.number++;
};
$scope.dec = function () {
$scope.number--;
};
});
</script>
</body>
</html>
Wcześniej powiedzieliśmy, że dzięki live binding każda zmiana zachodząca w kontekście Angulara jest przez niego wyłapywana. Naturalnie rodzi się więc pytanie, czy
każdy element przypisany do $scope otrzymuje od razu własny obiekt nasłuchujący. Odpowiedź brzmi: nie, gdyż nasłuchiwanie zmian na wszystkich elementach zajęłoby
zbyt dużo czasu. Mamy do wyboru dwa sposoby zadeklarowania nasłuchiwania wybranych elementów:
 Pierwszy z nich to… interpolacja. Kiedy Angular napotyka interpolację
w widoku, to wie, że automatycznie musi stworzyć obiekt nasłuchujący
(w tym wypadku implicit watcher) na dany element.
<div>
{{ watchedElement }}
</div>
 Istnieje również możliwość tworzenia nasłuchiwaczy własnoręcznie. Struktura
typowego obiektu nasłuchującego (ang. explicit watcher) prezentuje się mniej
więcej tak:
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
24
AngularJS. Pierwsze kroki
$watch('watchedElement', function(newValue, oldValue) {
//functions body…
});
Pierwszy parametr to nazwa elementu modelu będącego pod obserwacją. Drugi parametr
to funkcja nasłuchująca reagująca na zachodzące zmiany — jej wywołanie następuje za każdym razem, gdy wartość obserwowanego elementu ulega zmianie. Porównanie odbywa się poprzez metodę angular.equals(); wykonywana jest również
metoda angular.copy() w celu zapisania obecnej wartości elementu. Obydwa przypadki
zawarte są w naszym poprzednim przykładzie.
Zapewne niejedna osoba zastanawiała się, w jaki sposób Angular dowiaduje się o tych
zmianach zachodzących w modelu. Za ich nasłuchiwanie odpowiada cykl $digest().
$digest()
$digest rozpoczyna się jako efekt wywołania $scope.digest(). Jest to cykl ewaluacji
kolejno wszystkich obiektów nasłuchujących występujących w danym scope oraz jego
potomkach. Ponieważ zachodzące zmiany wywołują tzw. funkcje nasłuchujące, które
mogą modyfikować dowolne elementy modelu (w tym te sprawdzone już wcześniej),
$digest() powtarzany jest dopóty, dopóki owe wezwania nie ustaną. Nawet jeżeli
podczas wykonywania cyklu nie zostanie wezwana żadna funkcja nasłuchująca, zostanie
on powtórzony co najmniej raz w celu upewnienia się, iż nie zaszła żadna zmiana. Jeśli zdarzy się tak, że cykl wpadnie w pętlę nieskończoną, wówczas po 10 iteracjach
zostanie zwrócony błąd.
Wywołanie cyklu następuje automatycznie, np. dzięki dyrektywom ng-model czy
ng-click. Bezpośrednio jednak wywoływany jest najpierw $apply(), który to później
wywołuje $digest().
Może zaistnieć sytuacja, w której trzeba będzie wywołać $apply() manualnie. Angular
zbudowany jest tak, by wychwytywać zmiany zachodzące między widokiem a modelem automatycznie, ale dzieje się to wyłącznie w obrębie jego kontekstu. W sytuacji,
gdy zmiana modelu odbywa się poza kontekstem Angulara, należy go o niej poinformować, wywołując $apply() manualnie — to stąd Angular wie, że musi rozpocząć
nasłuchiwanie. Nie powinniśmy nigdy bezpośrednio wywoływać $digest(). Prawidłowo powinniśmy wywołać $apply(), który później wykona cykl $digest().
$apply()
Usługa $apply zachowuje się jak goniec wysyłany spoza kontekstu Angulara w celu
poinformowania o zaistniałych zmianach. Innymi słowy, $apply() służy do integracji Angulara z innymi frameworkami bądź bibliotekami. $apply() zawiera funkcję pobieraną
jako parametr, za której wykonanie odpowiada $eval. Do jego zadań należy również
sprawdzenie, czy owa funkcja jest wykonywalna, oraz ewentualne poinformowanie
Angulara o wykrytych nieścisłościach poprzez zwrócenie wyjątku. Wykorzystywana jest
tu tzw. obsługa wyjątków z poziomu aplikacji (ang. Application level error handling).
Jej wartość najczęściej doceniana jest wraz ze wzrostem poziomu skomplikowania
aplikacji.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 2.  $scope — niepozorny obiekt
25
Następnie wywoływany jest cykl $digest(). Gdy mówimy o integracji z Angularem,
mamy na myśli właśnie tę usługę: wystarczy otoczyć kod wewnątrz $apply() — prawda,
że proste?
Wiesz już, jak działa $apply(), przejdźmy teraz do przykładu, który pokaże Ci jego
zastosowanie praktyczne. Na pytanie, co stanie się w momencie uruchomienia poniższej
strony, najlepiej odpowie listing 2.5:
Listing 2.5. $watch
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - $watch</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<div class="well">Wiadomość: {{msg}}</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.go = function () {
setTimeout(function () {
$scope.msg = 'Wow, jestem opóźnioną informacją!';
console.log('message:' + $scope.msg);
}, 2000);
}
$scope.go();
});
</script>
</body>
</html>
Wynik wywołania powyższej strony będzie nie do końca zgodny z naszymi oczekiwaniami. Naszym celem było uaktualnienie w widoku {{msg}} po dwóch sekundach. Tak się
jednak nie stało, mimo że teoretycznie program zadziałał i po dwóch sekundach w logu
otrzymaliśmy oczekiwany tekst. Dlaczego widok nie został uaktualniony? Jak rozwiązać ten problem? Do tego posłuży nam $apply(). Przeanalizujmy teraz listing 2.6.
Listing 2.6. $apply()
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - $apply()</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/
3.3.1/css/bootstrap.min.css">
</head>
<body>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
26
AngularJS. Pierwsze kroki
<div ng-controller="defaultCtrl">
<div class="well">Wiadomość: {{msg}}</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5 /angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.go = function () {
setTimeout(function () {
$scope.msg = 'Wow, jestem opóźnioną informacją!';
console.log('message:' + $scope.msg);
$scope.$apply();
}, 2000);
}
$scope.go();
});
</script>
</body>
</html>
Jak widać, dodaliśmy tylko $scope.$apply(), zmieniło to jednak zasadniczo działanie
całej aplikacji. Tym razem otrzymaliśmy odpowiedni log oraz zmianę z dwusekundowym
opóźnieniem po stronie widoku.
Najważniejsze przesłanie płynące z tej części rozdziału jest takie: wszędzie tam, gdzie
AngularJS nie może wykryć zmian samodzielnie, musimy to zrobić ręcznie.
Quiz
1. Co to jest $scope?
2. Czym się różni $scope od $rootScope?
3. Co to jest drzewo DOM?
4. Jak stworzyć izolowany scope?
5. Co to są obiekty nasłuchujące?
6. Jak działa cykl $digest?
7. W jakich sytuacjach należy korzystać z usługi $apply?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 3.
Moduły
Wprowadzenie
Modularyzacja to koncepcja rozbijania złożonych aplikacji na mniejsze wewnętrzne,
współpracujące ze sobą moduły. W modularyzacji możemy wyodrębnić dwie kategorie:
 modularyzację fizyczną, czyli umieszczenie kodu w odrębnych plikach;
 modularyzację logiczną, czyli podzielenie kodu na odrębne moduły
zawierające logikę.
W tym rozdziale zajmiemy się zagadnieniem modularyzacji logicznej. Moduły bazują
na koncepcji hermetyzacji, maskując czysty kod naszej aplikacji. Są one czymś w rodzaju
instrukcji bądź obiektu konfiguracyjnego dla Angulara. Zawierają instrukcje odnośnie do
wstrzykiwania zależności (rozdział 4. o Dependency Injection), mówiąc injectorowi, jakie
kontrolery, filtry czy dyrektywy powinny zostać załadowane w danej aplikacji.
Pisząc programy na produkcji, zazwyczaj dążymy do podzielenia naszej aplikacji na
bloki „kodu” wykonujące podobne zadania. Moduły niosą za sobą wiele udogodnień:
 Dzięki nim nie musimy zaśmiecać globalnej przestrzeni nazw.
 Przeprowadzanie testów jest znacznie łatwiejsze, ponieważ odbywa się
na poszczególnych blokach mających podobne funkcjonalności.
 Możemy dzielić wybrane elementy kodu pomiędzy aplikacjami bez
konieczności „wycinania” poszczególnych kawałków bądź wykorzystywania
niepotrzebnych fragmentów; unikamy tzw. boilerplate code.
 Jesteśmy w stanie ładować wybrane fragmenty kodu w dowolnej kolejności.
 Taka struktura danych wspiera skalowanie aplikacji.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
28
AngularJS. Pierwsze kroki
Moduły a kontrolery
We wcześniejszych przykładach stosowaliśmy uproszczoną formę deklaracji kontrolera. Zależało nam, by przykłady miały przystępną dla czytelnika strukturę i nie odwracały uwagi od podstawowych zasad mechaniki Angulara. Gdy tworzyliśmy naszą
pierwszą aplikację, nasz kontroler wyglądał tak:
function someCtrl($scope) {
$scope.message = 'Hello, World!'
};
Pisząc aplikację dla klienta, czy nawet dla siebie, zrobilibyśmy to w nieco inny sposób. Prawidłowa metoda deklaracji kontrolera została opisana poniżej.
Angular wyposażony jest w API angular.module(), służące do deklaracji modułów.
Owa metoda zawiera dwa ważne parametry. Po pierwsze musimy uwzględnić string
z nazwą modułu, który tworzymy; drugim parametrem jest lista zależności (obiekty
wstrzykiwane). Poniżej znajduje się przykładowy zadeklarowany moduł myApp niezawierający żadnych zależności, dlatego drugi parametr zostawiamy pusty.
var myApp = angular.module('myApp', []);
Gdy umieścimy powyższą metodę w pliku, zostanie utworzona instancja naszego modułu. Stwórzmy teraz plik controller.js i umieśćmy w nim moduł myApp oraz kontroler
someCtrl.
Plik controller.js
var myApp = angular.module('myApp', [ ]);
function someCtrl($scope) {
$scope.message = 'Hello, World!';
};
To, że zarówno moduł, jak i kontroler znajdują się w tym samym pliku, nie oznacza, iż
automatycznie zostaną ze sobą powiązane. By móc przekazać model znajdujący się w kontrolerze do widoku, musimy na wstępie przypisać ten kontroler do wybranego modułu.
Pokażmy najpierw prawidłowy sposób deklaracji kontrolerów.
Plik controller.js
var myApp = angular.module('myApp', []);
myApp.controller( 'someCtrl', function($scope) {
$scope.message = 'Hello, World!';
});
.controller() pobiera dwa argumenty: string z nazwą kontrolera oraz funkcję opisującą
ten kontroler. Przy pomocy powyższej funkcji deklarujemy i przypisujemy kontroler
someCtrl do modułu myApp.
Przypisanie może się również odbyć bezpośrednio przy deklaracji samego modułu.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 3.  Moduły
29
Plik controller.js
var myApp = angular.module('myApp', [])
.controller( 'someCtrl', function($scope) {
$scope.message = 'Hello, World!';
});
Moduły a globalna przestrzeń nazw
Naturalną rzeczą jest teraz zadanie pytania, dlaczego ten sposób, nieco bardziej skomplikowany, jest lepszy od pierwszego, prostszego podejścia. Gdy boostrapujemy Angulara
przy pomocy dyrektywy ng-app, tworzony jest moduł globalny. Jeżeli nie zawracamy
sobie głowy deklarowaniem naszych własnych modułów, wówczas wszystkie filtry,
dyrektywy, usługi czy kontrolery, które stworzymy, zostaną domyślnie przypisane do
globalnego modułu. Niesie to za sobą następujące uniedogodnienia:
 Ryzykujemy możliwość wystąpienia zbieżności nazw elementów.
 Nie mamy możliwości podzielenia naszej aplikacji na mniejsze,
łatwiejsze do zarządzania bloki.
 Nie mamy możliwości ponownego wykorzystania danego modułu.
Zmodularyzowana aplikacja
Najwyższy czas, aby w oparciu o zdobytą wiedzę napisać naszą pierwszą zmodularyzowaną aplikację. W tym celu ponownie wykorzystamy controller.js, lecz wzbogacimy
go o dodatkowy moduł.
Plik controller.js
var myApp = angular.module('myApp', [ ])
myApp.controller( 'someCtrl', function($scope) {
$scope.message = 'Hello, World!';
});
Następnie utwórzmy listing 3.1, zawierający szkielet naszej aplikacji.
Listing 3.1. Szkielet aplikacji
<html ng-app="myApp">
<body ng-controller="someCtrl">
<div ng-model="message">
{{ message }}
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.js">
</script>
<script src="controller.js"> </script>
</body>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
30
AngularJS. Pierwsze kroki
Dokonaliśmy w nim drobnej zmiany — przypisaliśmy dyrektywie ng-app utworzony
chwilę wcześniej moduł. Kiedy decydujemy się przypisać kontroler do modułu, przestaje on istnieć w global namespace. Dyrektywa ng-app sama w sobie nie dostarcza
informacji na temat kontrolerów przypisanych do modułów. Owe dane musimy dostarczyć podczas deklaracji w sposób zaprezentowany poniżej.
<html ng-app="myApp">
Jest to logiczny zabieg, gdyż musimy poinformować Angulara, który moduł będzie
wykorzystywany w aplikacji. Niepodanie nazwy odpowiedniego modułu (przy założeniu, że żaden kontroler nie został zadeklarowany w global namespace) wywoła błąd
„undefined controller”.
Łączenie modułów
Każda angularowa aplikacja może posiadać wyłącznie jedną dyrektywę ng-app, a co
za tym idzie, jeden moduł. Co w takim razie w sytuacji, gdy chcemy połączyć funkcjonalności kilku modułów w jednej aplikacji? Zadanie to jest wbrew pozorom bardzo
łatwe. Listing 3.2 zawiera zmodyfikowany controller.js oraz index.html — zobaczmy,
co się stanie.
Listing 3.2. Łączenie modułów
Plik controller.js
var myApp = angular.module('myApp', ['myApp2']);
var myApp2 = angular.module('myApp2',[ ] );
myApp.controller( 'someCtrl', function($scope) {
$scope.message = 'Hello, Arek!';
});
myApp2.controller('otherCtrl', function($scope) {
$scope.otherMessage = 'Hello, Darek!';
});
Plik index.html
<html ng-app="myApp">
<body ng-controller="someCtrl">
<div ng-model="message">
{{ message }}
{{ otherMessage }}
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5 /angular.js">
</script>
<script src="controller.js"></script>
</body>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 3.  Moduły
Quiz
1. Co to jest moduł?
2. Czym się różni modularyzacja fizyczna od logicznej?
3. Jak deklarujemy moduł w AngularJS?
4. Czy moduły w AngularJS można łączyć?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
31
32
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
AngularJS. Pierwsze kroki
Rozdział 4.
Dependency Injection
— wstrzykiwanie
zależności
Wprowadzenie
Wstrzykiwanie zależności to wzorzec projektowy służący do zarządzania zależnościami
między obiektami. Stosując DI, unikamy zakodowania (tzw. hard coding) owych zależności, dzięki czemu jesteśmy w stanie manipulować nimi na bieżąco w trakcie działania aplikacji. W tym wypadku obiekt po prostu otrzymuje potrzebne mu zależności,
przez co nie musi zajmować się ich tworzeniem.
Wstrzykiwanie zależności niesie za sobą wiele korzyści podsumowanych poniżej:
 Tworzenie oraz konsumpcja zależności są od siebie odseparowane.
 Konsumenta interesuje jedynie sposób wykorzystania danej mu zależności,
a obowiązek jej stworzenia spada na Angulara.
 W każdym momencie mamy możliwość dodania, usunięcia bądź
podmienienia dowolnej zależności.
 Stwarza nam to idealne warunki do testowania, gdyż w celu sprawdzenia
poprawności możemy wstrzyknąć obiekt testowy przed wykorzystaniem
prawdziwych danych.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
34
AngularJS. Pierwsze kroki
Uzyskiwanie zależności
Dostęp do obiektu możemy uzyskać na trzy sposoby:
1. Jeżeli jest zmienną globalną, możemy odwołać się do niej (nie powinno się
zaśmiecać obiektu globalnego!), wywołując ją z namespace.
2. Oczywiście, jeżeli jest zmienną lokalną znajdującą się np. wewnątrz funkcji,
na której operujemy, to również możemy się do niej odwołać (w obrębie tej
funkcji).
3. Możemy przekazać go jako parametr do funkcji wywołania.
Dwa pierwsze podejścia nie są optymalnym rozwiązaniem — jak wcześniej wspomnieliśmy, wymagają one uprzedniego zakodowania, co uniemożliwia ich późniejszą
modyfikację. Wstrzykiwanie zależności dotyczy ostatniego podejścia.
Do tej pory spotykaliśmy się ze wstrzykiwaniem zależności głównie przy $scope.
Każdemu kontrolerowi wstrzykiwana jest nowa instancja $scope. Jedynym zadaniem
kontrolera w tym obszarze jest wyrażenie zapotrzebowania na ten obiekt. Odpowiedzialność za jego stworzenie bądź wykorzystanie już istniejącego spoczywa na wbudowanym silniku Angulara — Dependency Injection Engine.
Ze wstrzykiwaniem obiektu $scope do kontrolera mieliśmy już niejednokrotnie do
czynienia. Listing 4.1 zawiera kilka innych przykładów wykorzystania DI:
Listing 4.1. Przykłady wstrzykiwania zależności
app.controller('defaultCtrl', ['$scope', function ($scope) {
}]);
app.filter('newFilter', ['$http', function ($http) {
return function (data) { }
}]);
app.directive('newDirective', ['$http', function ($http) {
return {}
}]);
app.factory('newService', ['$http', function ($http) {
return {}
}]);
Obiekty wstrzykiwane są w docelowe miejsce, gdy zachodzi taka potrzeba (czyli
w momencie, w którym dana funkcja bądź kontroler zgłoszą takie zapotrzebowanie).
Proces ten odbywa się automatycznie, a odpowiedzialność za wstrzykiwanie spoczywa na usłudze $injector. Tworzy ona nowe instancje zależności w momencie, kiedy
zgłoszone zostanie zapotrzebowanie (nie wcześniej). Injector odpowiada też między
innymi za zarządzanie komponentami Angulara, tj. kontrolerami czy dyrektywami. Gdy
w trakcie cyklu działania aplikacji inicjowany jest moduł, injector zajmuje się dostarczeniem wymaganych zależności.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 4.  Dependency Injection — wstrzykiwanie zależności
35
Metody wstrzykiwania zależności
W Angularze zależności można wstrzykiwać, stosując jeden z poniższych sposobów:
1. Implicit Annotation,
2. Explicit Annotation,
3. wykorzystanie $inject.
Pierwszy z nich i zarazem najłatwiejszy w użyciu polega na wstrzykiwaniu pożądanych
obiektów jako argumentów funkcji chcącej skonsumować zależność. Technika ta nazywa się implicit dependency injection i przedstawia ją listing 4.2:
Listing 4.2. Przykłady zastosowania implicit dependency injection
app.controller('RestaurantCtlr', function ($scope) {
$scope.availableMenu = [{ name: 'Pizza', type: 'Fast Food' },
{ name: 'Pierogi', type: 'Regional Cuisine' },
{ name: 'Lentil Burger', type: 'Vegetarian' }];
});
W powyższym przykładzie napisaliśmy prosty kontroler, któremu wstrzyknęliśmy
$scope. Następnie wykorzystując ten obiekt, przypisaliśmy menu z karty dań do modelu.
RestaurantCtrl nie ma pojęcia, w jaki sposób wstrzyknięte obiekty są stworzone, interesuje go jedynie ich konsumpcja.
Nie ma ograniczeń co do liczby wstrzykiwanych obiektów bądź ich pochodzenia; jako
parametr możemy podać dowolny obiekt, kontroler czy nawet moduł.
app.controller('OtherCtrl', function($scope, otherInjectedObject, someModule) {
//…
});
W czystym JavaScripcie funkcje wymagają dostarczenia parametrów w odpowiedniej
kolejności, możemy za to przypisywać im dowolne nazwy. Kolejność przekazania
owych parametrów w Implicit DI nie ma znaczenia. Argumenty są rozpoznawane po
nazwie! Identyfikacja odbywa się z wykorzystaniem toString(). Oznacza to jednak, że
jesteśmy ograniczeni w nadawaniu im nazw zbieżnych z oryginalnymi.
Implicit dependency injection posiada jednakże pewien mankament — procesy minifikacji oraz obfuskacji (zaciemniania kodu).
Minifikacja to proces usunięcia wszystkich niepotrzebnych znaków z kodu źródłowego
bez naruszania jego funkcjonalności (nierzadko zmieniane są również nazwy argumentów). Gdyby w tej sytuacji zmienione zostały nazwy parametrów funkcji, Angular
nie byłby w stanie odgadnąć, co powinno zostać wstrzyknięte.
W takim wypadku z pomocą przychodzi nam druga technika wstrzykiwania zależności,
explicit dependency injection.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
36
AngularJS. Pierwsze kroki
Explicit dependency injection różni się tym od pierwszego zaprezentowanego sposobu, iż
zamiast dostarczać samą funkcję kontrolera, przekazuje tablicę, której elementami są
nazwy poszczególnych zależności będące stringami oraz owa funkcja. Wspomóżmy
się poprzednim przykładem, modyfikując go nieco w listingu 4.3.
Listing 4.3. Przykłady zastosowania explicit dependency injection
app.controller('RestaurantCtrl', ['$scope', '$http', function ($scope, $http) {
$http.get(url)
.success(function (menu) {
$scope.availableMenu = menu
});
}]);
Mechanika tego sposobu odzwierciedla wcześniej wspomniany czysty JavaScript.
Kolejność umieszczenia parametrów w funkcji wywołania ma znaczenie i musi się
pokrywać z kolejnością wstrzykiwanych zależności w tablicy. Zyskujemy również
dowolność w nadawaniu nazw tym parametrom.
Jako początkowe parametry umieściliśmy w tablicy obiekty $scope oraz $http, następnie przekazaliśmy je funkcji jako parametry wywołania.
app.controller('RestaurantCtrl', ['$scope', '$http', function ($scope, $http) { }]);
Funkcja ta powiąże owe parametry według kolejności ich występowania, czyli $scope
z pierwszym argumentem funkcji oraz $http z drugim. Gdybyśmy zmienili ich kolejność:
app.controller('RestaurantCtrl', ['$scope', '$http', function ($http, $scope) { }]);
wówczas $scope zostałby powiązany z parametrem $http, a obiekt $http z parametrem $scope. Nie zostanie to zgłoszone jako błąd. Wartości zostaną po prostu przypisane odwrotnie. Dlatego w tym miejscu należy szczególnie uważać, by nie pomylić
kolejności.
Ostatnim sposobem, który omówimy, jest DI z wykorzystaniem obiektu $inject.
Przytoczmy i zmodyfikujmy nasz przykład z restauracją po raz kolejny, tak jak prezentuje to listing 4.4.
Listing 4.4. Przykłady zastosowania obiektu $inject
var RestaurantCtlr = function($scope, $http) {
$http.get(url)
.success(function(menu) {
$scope.availableMenu = menu
});
};
RestaurantCtrl.$inject = [ '$scope', '$http' ];
app.controller('RestaurantCtrl', RestaurantCtrl );
Kolejność oraz nazwa argumentów przekazywanych do funkcji nie mają znaczenia. Musimy jedynie pamiętać, żeby powtórzyć ten sam szyk, przypisując kontrolerowi obiekt
$inject. Przypisanie kontrolera do modułu może się odbyć dopiero po przypisaniu $inject.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 4.  Dependency Injection — wstrzykiwanie zależności
37
DI w praktyce
AngularJS posiada kilka podstawowych typów obiektów i komponentów:
 Value,
 Factory,
 Service,
 Provider,
 Constant.
Każdy z tych typów może być wstrzykiwany do pozostałych przy wykorzystaniu mechanizmów AngularJS.
Zacznijmy od Value — w AngularJS jest to prosty obiekt. Może to być ciąg znaków, liczba lub obiekt JavaScript. Obiekty Value najczęściej stosowane są do konfiguracji, wstrzykiwane do fabryk, serwisów bądź kontrolerów.
Poniższy listing 4.5 pokazuje przypadki użycia Value:
Listing 4.5. Value
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - value</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<pre>
$scope.objectValue = {{objectValue}};
$scope.stringValue = {{stringValue}};
$scope.numberValue = {{numberValue}};
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.value("numberValue", 100);
app.value("stringValue", "AngularJS");
app.value("objectValue", { v1: 123, v2: "ABCD", v3: { "v31": "ABCD" } });
app.controller('defaultCtrl',
['$scope', 'objectValue', 'stringValue', 'numberValue',
function ($scope, objectValue, stringValue, numberValue) {
// dostęp na poziomie kontrolera
console.log(objectValue);
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
38
AngularJS. Pierwsze kroki
console.log(stringValue);
console.log(numberValue);
// przypisujemy do scope, by uzyskać widoczność w widoku
$scope.objectValue = objectValue;
$scope.stringValue = stringValue;
$scope.numberValue = numberValue;
}]);
</script>
</body>
</html>
Nazwa Value jest wstrzykiwana do kontrolera. W kontrolerze możemy odwołać się bezpośrednio do wstrzykniętej nazwy. Jeśli chcemy skorzystać z wstrzykniętych wartości
w widoku, musimy je przypisać do obiektu $scope.
Przejdźmy teraz do Factory. Jest to funkcja tworząca obiekt Value. Jeśli serwis, controler
itd. potrzebują obiektu wstrzykiwanego z fabryki, tworzy go ona na żądanie. Raz stworzony obiekt jest dostępny dla wszystkich serwisów, które potrzebują go wstrzyknąć. Factory
różni się od Value tym, że może użyć funkcji do stworzenia obiektu, który zwraca. Możesz wstrzyknąć Value do Factory, gdy tworzony jest obiekt, ale nie można tego samego
zrobić w przypadku Value. Listing 4.6 pokazuje, w jaki sposób możemy korzystać
z Factory.
Listing 4.6. Factory
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - factory</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<pre>
$scope.oneFactory = {{oneFactory}};
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.value("stringValue", "AngularJS");
app.factory("oneFactory", ['stringValue', function (stringValue) {
return "Wartość z fabryki + wartość z value: " + stringValue;
}]);
app.controller('defaultCtrl', ['$scope', 'oneFactory',
function ($scope, oneFactory) {
// dostęp na poziomie kontrolera
console.log(oneFactory);
// przypisujemy do scope, by uzyskać widoczność w widoku
$scope.oneFactory = oneFactory;
}]);
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 4.  Dependency Injection — wstrzykiwanie zależności
39
</script>
</body>
</html>
Value stringValue wstrzyknęliśmy do fabryki, która wykorzystuje go podczas tworzenia obiektu. Przejdźmy teraz do serwisów.
Serwis w AngularJS jest javaScriptowym obiektem singleton zawierającym zbiór
funkcji. Zobaczmy na przykładzie listingu 4.7, jak działa prosty serwis.
Listing 4.7. Service
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - service</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<pre>
$scope.newValue = {{newValue}};
$scope.newValue2 = {{newValue2}};
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.value("stringValue", "AngularJS");
function OneService() {
this.printLog = function () {
console.log("Log z serwisu - AngularJS");
},
this.newValue = function () {
return "Nowa wartość z serwisu!";
}
};
app.service("oneService", OneService);
app.service("twoService", function () {
this.printLog = function () {
console.log("Log z drugiego serwisu - AngularJS");
},
this.newValue = function () {
return "Nowa wartość z drugiego serwisu!";
}
});
app.controller('defaultCtrl', function ($scope, oneService, twoService) {
oneService.printLog();
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
40
AngularJS. Pierwsze kroki
$scope.newValue = oneService.newValue();
twoService.printLog();
$scope.newValue2 = twoService.newValue();
});
</script>
</body>
</html>
W powyższym przykładzie warto przyjrzeć się temu, w jaki sposób zostały stworzone
oneService i twoService. Obydwa serwisy zostały wstrzyknięte do kontrolera i odpowiednio wywołane. Co jednak, jeśli chcemy wstrzyknąć Value do naszego serwisu?
Mechanizm jest bardzo podobny jak w omawianym przypadku fabryki.
Listing 4.7 uzupełniamy o nowe elementy: Value oraz Service. Należy pamiętać, by
podczas dodawania kodu wstrzyknąć threeService jako nowy argument w funkcji
kontrolera, inaczej aplikacja nie zadziała.
app.value("stringValue2", "AngularJS");
app.service("threeService", function (stringValue2) {
this.printLog = function () {
console.log("Log z trzeciego serwisu + value: " + stringValue2);
}
});
app.controller('defaultCtrl', function ($scope, oneService, twoService,
threeService) {
threeService.printLog();
});
Powyższy przykład pokazuje, jak w prosty sposób możemy modularyzować naszą
aplikację. Kolejnym elementem naszych rozważań krążących wokół wstrzykiwania
zależności są Providery. Provider to najbardziej elastyczna metoda tworzenia fabryk.
Przejdźmy od razu do przykładu zawartego w listingu 4.8.
Listing 4.8. Provider
var app = angular.module('app', []);
app.provider("oneProvider", function () {
var provider = {};
provider.$get = function () {
var service = {};
service.doService = function () {
console.log("Log z providera");
}
return service;
}
return provider;
});
Jak widać powyżej, provider() przyjmuje 2 parametry. Pierwszy to nazwa serwisu
lub obiektu, który tworzy Provider, a drugi to funkcja. Obiekt JavaScript Provider
zawiera pojedynczą funkcję $get(). To jest fabryka, która produkuje to, czego zażyczy
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 4.  Dependency Injection — wstrzykiwanie zależności
41
sobie Provider (Service, Value itd.). Na poniższym listingu 4.9 pokazujemy, jak do
Providera wstrzyknąć Value, a następnie wywołać go w kontrolerze.
Listing 4.9. Wstrzykiwanie Value do Providera
var app = angular.module('app', []);
app.value("stringValue", "AngularJS");
app.provider("oneProvider", function () {
var provider = {};
provider.$get = function (stringValue) {
var service = {};
service.doService = function () {
console.log("Log z providera + value: " + stringValue);
}
return service;
}
console.log(provider);
return provider;
});
app.controller('defaultCtrl', function ($scope, oneProvider) {
oneProvider.doService();
});
Idźmy teraz krok dalej. Listing 4.10 konfiguruje nasz Provider w fazie tworzenia modułu. Poprzednie przykłady obrazowały częściowo kolejne możliwości kanciastego,
tym razem zobaczymy, jak działa pełnoprawna aplikacja.
Listing 4.10. Konfiguracja modułu
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - provider</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<pre>
viewTest = {{viewTest}}
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.value("stringValue", "AngularJS");
app.controller('defaultCtrl', function ($scope, oneProv) {
$scope.viewTest = oneProv.viewTest();
oneProv.printLog();
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
42
AngularJS. Pierwsze kroki
});
app.provider("oneProv", function () {
var provider = {};
var config = { paramOne: "jest niesamowity!" };
provider.addConfig = function (paramOne) {
config.paramOne = paramOne;
};
provider.$get = function (stringValue) {
var service = {};
service.printLog = function () {
console.log("Log z providera" + stringValue + config.paramOne);
},
service.viewTest = function () {
return "Log z providera: " + stringValue + config.paramOne;
}
return service;
}
console.log(provider);
return provider;
});
app.config(function (oneProvProvider) {
oneProvProvider.addConfig(" nowa konfiguracja");
});
</script>
</body>
</html>
Uzupełniliśmy nasz Provider o funkcję addConfig, której zadaniem jest dodanie konfiguracji. Należy zwrócić uwagę na nazwy. AngularJS wykorzystuje do identyfikacji
suffix Provider. W naszym przypadku wygląda to następująco: oneProv → oneProv
Provider.
Przejdźmy teraz do stałych. Poprzedni przykład pokazał, jak konfigurować Providery
przy pomocy funkcji module.config(). Niestety nie możemy wstrzykiwać Value do
module.config(), możemy za to wstrzykiwać stałe i tym zajmiemy się teraz. Tworzenie
stałych (constant) wygląda tak:
var app = angular.module('app', []);
app.constant("configValue", "stare wartosci konfiguracji");
Kolejną czynnością jest wstrzyknięcie stałej do naszej funkcji konfigurującej.
app.constant("configValue", "stare wartosci konfiguracji");
app.config(function (oneProvProvider, configValue) {
oneProvProvider.addConfig(" ...nowa konfiguracja... " + configValue);
});
Jak widać powyżej, stworzyliśmy stałą o nazwie configValue, następnie używając powyższej nazwy, wstrzyknęliśmy ją do funkcji config, gdzie została zastosowana w wywołaniu Providera oneProvProvider.addConfig(configValue).
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 4.  Dependency Injection — wstrzykiwanie zależności
43
Ostatnim zagadnieniem, jakie poruszymy w niniejszym rozdziale, jest wstrzykiwanie
zależności pomiędzy modułami. Listing 4.11 pokazuje sposób korzystania z właściwości danego modułu w innym:
Listing 4.11. Właściwości modułu
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - module</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<pre>
viewTest = {{stringValue}}
stringValue2 = {{stringValue2}}
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script>
var app2 = angular.module('app2', []);
app2.value("stringValue2", "Moduł 2");
var app = angular.module('app', ['app2']);
app.value("stringValue", "AngularJS");
app.controller('defaultCtrl', function ($scope, stringValue, stringValue2)
{
$scope.stringValue = stringValue;
$scope.stringValue2 = stringValue2;
});
</script>
</body>
</html>
Najpierw tworzymy moduł o nazwie app2, który nie zawiera żadnych zależności, i przypisujemy mu obiekt Value. Następnie tworzymy drugi moduł, app, i wstrzykujemy mu
app2. Dodatkowo do modułu app przypisujemy nowy obiekt Value. Warto odnotować
fakt, iż kontroler podpięty pod moduł app posiada również obiekt znajdujący się w module
app2. Dzieje się tak dlatego, że tworząc moduł app, wstrzyknęliśmy mu moduł app2.
Quiz
1. Co to jest DI?
2. Jakie są metody wstrzykiwania zależności w AngularJS?
3. Wymień podstawowe typy obiektów w AngularJS.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
44
AngularJS. Pierwsze kroki
4. Czy możemy wstrzyknąć fabrykę do kontrolera?
5. Jak działa funkcja module.config()?
6. Jaka jest różnica pomiędzy fabryką a serwisem?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.
Poznaj potęgę dyrektyw
Wprowadzenie
Dyrektywy są jedną z najpotężniejszych cech AngularJS. Można je sobie wyobrazić jako
bloki czy komponenty wielokrotnego użytku. To właśnie dzięki nim zakochasz się
w AngularJS. Dlaczego? Stosując dyrektywy, nauczysz HTML-a nowych, niespotykanych dotąd sztuczek.
Aby lepiej zrozumieć, jak zachowują się dyrektywy, zatrzymajmy się na chwilę przy
procesie kompilacji ($compile). AngularJS w fazie kompilacji skanuje strukturę DOM
dokumentu HTML. W miejscu występowania dyrektyw wstawia nową funkcjonalność na poziomie danego elementu DOM. Mamy możliwość manipulowania drzewem
DOM poprzez wzbogacanie poszczególnych jego elementów. W skrócie dyrektywy dają
możliwość tworzenia szytych na miarę, dynamicznych elementów HTML. Uczymy w ten
sposób naszego starego dobrego znajomego, HTML-a, nowych sztuczek.
AngularJS w wersji 1.4 posiada 69 wbudowanych dyrektyw. Umożliwia też tworzenie
własnych, szytych na miarę komponentów, o których więcej przeczytasz w dalszej
części książki. Dyrektywy to zdecydowanie jedna z najważniejszych i najbardziej
ekscytujących części Angulara. Podstawę stanowi View, czyli skompilowany DOM
Angulara. View to produkt procesu $compile scalającego szablon HTML ze $scope.
Rysunek 5.1 obrazuje nasz proces.
W sytuacji, gdy wiele dyrektyw przypisanych jest do jednego elementu DOM, czasem
konieczne jest określenie kolejności, w jakiej zostaną one wykonane. Do tego celu
wykorzystywany jest priorytet, np. priority:0.
Warto również nadmienić, iż funkcja compile operuje w dwóch fazach: pre-link oraz
post-link. W fazie pierwszej dyrektywy o większym priorytecie liczbowym są kompilowane w pierwszej kolejności. W fazie drugiej obowiązuje odwrotna kolejność. Jeżeli
nie zdefiniujemy żadnej fazy, wówczas dyrektywa będzie domyślnie operować w fazie
post-link. Hierarchia wykonywania dyrektyw z takim samym priorytetem nie jest zdefiniowana. Domyślny priorytet jest ustawiony na 0. Zobaczmy, jak to wygląda na przykładzie:
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
46
AngularJS. Pierwsze kroki
Rysunek 5.1.
Cykl digest
App.directive('testPriority', function() {
return {
priority: 1001,
restrict: 'A',
compile: function () { },
link: function () { }
}
});
Poszczególne elementy dyrektyw omówimy w rozdziale „Dyrektywy szyte na miarę”.
Powyższy przykład ma za zadanie ułatwić zrozumienie priority.
Kolejnym elementem, z którym należy się zapoznać, jest terminal przyjmujący wartość
true lub false. Właściwość ta ustawiona na true powoduje zatrzymanie wykonywania
dyrektyw znajdujących się w tym samym elemencie i posiadających niższy priorytet.
Pokażmy to na przykładzie:
W kodzie HTML umieśćmy:
<div my-directive2 my-directive3 my-directive4 my-directive1></div>
W kodzie JS umieśćmy:
App.directive('myDirective1', function () {
return {
priority: 1,
terminal: false,
link: function () {
console.log("Dyrektywa 1.");
}
}
});
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
47
App.directive('myDirective2', function () {
return {
priority: 2,
terminal: true,
link: function () {
console.log("Dyrektywa 2.");
}
}
});
App.directive('myDirective3', function () {
return {
priority: 3,
terminal: false,
link: function () {
console.log("Dyrektywa 3.");
}
}
});
App.directive('myDirective4', function () {
return {
priority: 4,
terminal: false,
link: function () {
console.log("Dyrektywa 4.");
}
}
});
Wynik w konsoli:
Dyrektywa 2.
Dyrektywa 3.
Dyrektywa 4.
Jak widać, nie ma znaczenia, w jakiej kolejności ułożyliśmy nasze dyrektywy — ich
wykonanie zależy od priority i terminal.
W związku z tym, że dyrektywa myDirective2 ma ustawioną właściwość terminal=true, dyrektywa myDirective1 o niższym priorytecie nie została wykonana. Należy pamiętać, że dotyczy to tylko dyrektyw zawartych w tym samym elemencie. Nie
ma to natomiast wpływu na kolejność i wykonanie w przypadku, gdy są one wywoływane w osobnych elementach. Posłużmy się przykładem, wykorzystując te same
dyrektywy, tym razem w osobnych elementach:
W kodzie HTML:
<div
<div
<div
<div
my-directive4></div>
my-directive1></div>
my-directive2></div>
my-directive3></div>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
48
AngularJS. Pierwsze kroki
Wynik w konsoli:
Dyrektywa
Dyrektywa
Dyrektywa
Dyrektywa
4.
1.
2.
3.
Nazewnictwo
Zanim zaczniemy korzystać z wbudowanych dyrektyw bądź pisać własne, musimy
zapoznać się z angularowym kompilatorem HTML, który interpretuje to, gdzie i w jaki
sposób stosowana jest dana dyrektywa. AngularJS preferuje nazwy pisane z użyciem
konwencji opartej na uwzględnianiu wielkości liter, tzw. camelCase. Jednakże HTML
nie bierze tego pod uwagę. Twórcy Angulara proponują w to miejsce kilka rozwiązań.
Jednym z nich jest tzw. snake-case, czyli wyrazy rozdzielane kreskami.
Proces normalizacji odbywa się w następujący sposób:
1. Usuwa x-, x:, x_ oraz data-, data:, data_ z początku nazwy elementu lub atrybutu.
2. Konwertuje :, - albo _ na camelCase.
Najlepiej zobrazuje to przykład z listingu 5.1.
Listing 5.1. Nazewnictwo dyrektyw
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - dyrektywy</title>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/
libs/angularjs/1.4.0-beta.5/angular.min.js">
</script>
</head>
<body>
<div ng-controller="defaultCtrl">
ng-model <input ng-model="name" /><br />
ng:model <input ng:model="name" /><br />
ng_model <input ng_model="name" /><br />
x-ng-model <input x-ng-model="name" /><br />
x:ng:model <input x:ng:model="name" /><br />
x_ng_model <input x_ng_model="name" /><br />
data-ng-model <input data-ng-model="name" /><br />
data:ng:model <input data:ng:model="name" /><br />
data_ng_model <input data_ng_model="name" /><br />
<span ng-bind="name"></span> <br />
<span ng:bind="name"></span> <br />
<span ng_bind="name"></span> <br />
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
49
<span x-ng-bind="name"></span> <br />
<span x:ng:bind="name"></span> <br />
<span x_ng_bind="name"></span> <br />
<span data-ng-bind="name"></span> <br />
<span data:ng:bind="name"></span> <br />
<span data_ng_bind="name"></span> <br />
</div>
<script>
angular.module('app', [])
.controller('defaultCtrl', ['$scope', function ($scope) {
$scope.name = "Magiczny tekst";
}]);
</script>
</body>
</html>
Efekt działania aplikacji prezentuje rysunek 5.2. Każda z dyrektyw wypisała tekst
Magiczny tekst, a każda zmiana w jakimkolwiek polu input spowoduje zmianę tekstu
we wszystkich wywołaniach dyrektyw.
Rysunek 5.2.
Metody wywołań
dyrektyw
Dobra praktyka: Do rozdzielania wyrazów w nazwach dyrektyw zawsze używaj -,
np. ng-bind. Mimo że pozostałe sposoby z powodzeniem działają, należy ich unikać. Jeśli chcesz stosować narzędzia do walidacji HTML-a, dodawaj przed nazwą
data-, np. data-ng-bind, co zostanie zinterpretowane jako ngBind.
$compile może dopasować dyrektywy, bazując na nazwie elementu, atrybutu, klasy
czy komentarza, np.:
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
50
AngularJS. Pierwsze kroki
<nowa-dyrektywa></nowa-dyrektywa>
<span nowa-dyrektywa="wyrazenie"></span>
<!-- directive: nowa-dyrektywa wyrazenie -->
<span class="nowa-dyrektywa: wyrazenie;"></span>
Dobra praktyka: Staraj się używać dyrektyw opartych na nazwie elementu lub
atrybutu, pomijając klasy i komentarze. Dyrektywy deklarowane w komentarzach
zostały pierwotnie wymyślone, by można było stosować je w miejscach, gdzie API
DOM ogranicza możliwość tworzenia dyrektyw, które obejmują wiele elementów,
np. <table>. AngularJS 1.2 wprowadza ng-repeat-start oraz ng-repeat-end jako
lepsze rozwiązanie tego problemu.
Podczas kompilacji kompilator dopasowuje teksty i atrybuty za pomocą usługi $interpolate, sprawdzając, czy zawierają osadzone wyrażenia. Wyrażenia te są zarejestrowane
jako watches i zostaną uaktualnione podczas normalnego cyklu digest opisanego w rozdziale 2., dotyczącym $scope. Oto przykład obrazujący kilka typowych przypadków.
W kodzie HTML:
<img data-ng-src="{{ path }}" />
<h1>{{name}}</h1>
W kodzie JS:
$scope.path = 'img/foto.jpg';
$scope.name = 'Adam';
Wbudowane dyrektywy
Przyjrzyjmy się bliżej wbudowanym dyrektywom.
ngApp — wspomniana już w rozdziale 1. dyrektywa jest jedną z najważniejszych. Służy do
automatycznego inicjowania aplikacji. Najczęściej umieszczana jest w pobliżu głównego elementu strony, np. <body> lub <html>. Tylko jedna aplikacja AngularJS może
być automatycznie zainicjowana w dokumencie HTML. Jeśli chcemy uruchomić
większą liczbę aplikacji, musimy zrobić to ręcznie, korzystając z angular.bootstrap.
Przykład automatycznego inicjowania aplikacji:
<!doctype html>
<html ng-app="app">
<body>
<div ng-controller="ExampleController">
{{test}}
</div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/
libs/angularjs/1.4.0-beta.5/angular.min.js"></script>
<script>
var app = angular.module('app', []);
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
51
app.controller('ExampleController', function ($scope) {
$scope.test = '123';
});
</script>
</body>
</html>
Przykład ręcznego inicjowania aplikacji:
<!doctype html>
<html>
<body>
<div ng-controller="ExampleController">
{{test}}
</div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/
libs/angularjs/1.4.0-beta.5/angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('ExampleController', function ($scope) {
$scope.test = '123';
});
angular.bootstrap(document, ['app']);
</script>
</body>
</html>
Dyrektywa a
a — dyrektywa modyfikująca podstawowe zachowanie HTML-owskiego znacznika <a>.
Domyślna akcja znacznika nie jest wykonywana, gdy atrybut href jest pusty. Umożliwia
to proste tworzenie linków akcji z dyrektywą ngClick bez zmiany lokacji czy przeładowywania strony. Zapis HTML prezentuje się następująco:
W kodzie HTML:
<a href="" ng-click="deleteElement()">Usuń element</a>
W kodzie JS:
$scope.deleteElement = function () {
// Kod usuwający element
console.log('Element usunięto!');
}
Dyrektywa a uruchamiana jest z priorytetem 0.
Dyrektywa form
form — dyrektywa tworzy instancję komponentu form.FormController. Jeśli zdefiniowaliśmy atrybut name, FormController jest inicjowany w aktualnym scope pod tą nazwą.
W Angularze możliwe jest zagnieżdżanie formularzy. Formularz nadrzędny jest poprawnie sprawdzony, jeżeli formularz wewnętrzny jest również poprawny.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
52
AngularJS. Pierwsze kroki
Przeglądarki nie pozwalają na zagnieżdżanie znacznika <form>. AngularJS zawiera dyrektywę ngForm, która zachowuje się identycznie jak <form>, ale może być zagnieżdżona. Pozwala to tworzyć struktury formularzy, które są bardzo użyteczne w przypadku
korzystania z dyrektyw sprawdzających w formularzach generowanych dynamicznie
przy pomocy dyrektywy ngRepeat.
Klasy CSS wykorzystywane w formularzach:
 ng-valid, jeśli walidacja pola jest poprawna.
 ng-invalid, jeśli walidacja pola nie jest poprawna.
 ng-pristine, jeśli pole nie było modyfikowane.
 ng-dirty, jeśli pole było modyfikowane.
 ng-submitted, jeśli formularz został wysłany.
Najlepiej zobrazuje to poniższy przykład.
W kodzie CSS:
.my-form.ng-valid
{
background: green;
}
.my-form.ng-invalid {
background: red;
}
W kodzie HTML:
<form name="myForm" class="my-form">
<input name="input1" ng-model="userType" required>
<span ng-show="myForm.input1.$error.required">Pole wymagane!</span><br>
</form>
Jak widać w powyższym przykładzie, w prosty sposób możemy odwołać się do poszczególnych klas.
Dyrektywa form zmienia domyślne zachowanie formularza; jeśli nie został zdefiniowany
atrybut action, strona nie zostanie przeładowana.
Możemy użyć jednego z dwóch poniższych sposobów, by określić, która metoda JavaScript powinna zostać wywołana, gdy formularz zostanie wysłany.
1. dyrektywa ngSubmit, umieszczana w znaczniku <form>;
2. dyrektywa ngClick, umieszczana w pierwszym przycisku.
Aby zapobiec podwójnemu wywoływaniu, należy użyć tylko jednej z powyższych
dyrektyw.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
53
Związane jest to ze specyfikacją HTML-a.
 Jeśli formularz ma tylko jedno pole wejściowe, to naciśnięcie przycisku Enter
na klawiaturze wyzwoli wysłanie formularza (ngSubmit).
 Jeżeli formularz ma 2 i więcej pól wejściowych, a nie posiada przycisku
<button> lub input[type=submit], to naciśnięcie przycisku Enter na klawiaturze
nie spowoduje wyzwolenia wysyłania formularza.
 Jeśli formularz ma jedno albo więcej pól wejściowych oraz jeden bądź więcej
przycisków <button> lub input[type=submit], to naciśnięcie przycisku Enter
na klawiaturze spowoduje wyzwolenie pierwszego przycisku <button> albo
input[type=submit] (ngClick) oraz wywołanie (ngSubmit) w znaczniku <form>.
Używaj ngSubmit, by uzyskać dostęp do zaktualizowanego modelu.
Dyrektywa input
input — modyfikuje standardową funkcjonalność znacznika <input>. Przyjmuje następujące atrybuty:
 ng-model — dyrektywa przypisuje input do scope, korzystając
z ngModelController.
 name — nazwa kontrolki (opcjonalny).
 required — ustawia validation error key, jeśli wartość nie została
wprowadzona (opcjonalny).
 ng-required — ustawia atrybut required, jeżeli ma wartość true
(opcjonalny).
 ng-minlength — ustawia minlength validation error key, jeśli wartość
jest za krótka (opcjonalny).
 ng-maxlength — ustawia maxlength validation error key, jeżeli wartość
jest za długa (opcjonalny).
 ng-pattern — ustawia pattern validation error key, jeśli wyrażenie
nie pasuje do wzorca RegExp (opcjonalny).
 ng-change — wyrażenie angularowe, które jest wykonywane w momencie
wprowadzenia przez użytkownika zmian w polu input (opcjonalny).
 ng-trim — jeśli ustawiony jest na true, AngularJS automatycznie obetnie
spacje na początku i na końcu wprowadzonego tekstu; ignorowany w polach
input[type=password] (opcjonalny).
Dyrektywa input uruchamiana jest z priorytetem 0.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
54
AngularJS. Pierwsze kroki
Dyrektywa ngBind
ngBind — dyrektywa ta zamienia wartość tekstową określonego elementu HTML, wstawiając wynik wyrażenia, i dba o aktualizację, gdy wyrażenie się zmieni.
Poniżej przykład użycia.
W kodzie HTML:
<span ng-bind="test"></span>
W kodzie JS:
$scope.test = '123';
Dyrektywa ngBind najczęściej zastępowana jest interpolacją {{wyrażenie}}. Efekt widoczny dla użytkownika na pierwszy rzut oka będzie dokładne taki sam. Różnicę zauważymy tylko podczas ładowania strony.
Może się zdarzyć, że część bibliotek JS nie załaduje się jednocześnie z dokumentem
HTML, co spowoduje wyświetlenie użytkownikowi surowego wyrażenia {{ test }}.
Zaletą ngBind jest to, że czyni wiązanie niewidocznym dla użytkownika. Alternatywnym rozwiązaniem tego problemu jest zastosowanie dyrektywy ngCloak.
Dyrektywa ngBindHtml
ngBindHtml — jest rozszerzeniem ngBind umożliwiającym wiązanie HTML-a. Aby
dyrektywa działała poprawnie, musimy wstrzyknąć do naszej aplikacji jeden z modułów Angulara, ngSanitize. Kompletny przykład znajduje się w listingu 5.2. Zaczynamy od dodania w naszej aplikacji pliku angular-sanitize.js w następujący sposób:
<script src="js/angular.js"></script>
<script src="js/angular-sanitize.js"></script>
W następnym kroku wstrzykujemy moduł:
var app = angular.module("app", ['ngSanitize'])
Poniżej przykład użycia.
W kodzie HTML:
<span ng-bind-html="test"></span>
W kodzie JS:
$scope.test = '<a href=>123</a>';
Listing 5.2. Przykład kompletnej strony HTML z wykorzystaniem ng-bind-html oraz ngSanitize
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>ng-bind-html</title>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
55
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/
3.2.0/css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div id="testPanel" class="panel">
<h3 class="panel-header">Nazwa</h3>
<pre ng-bind-html="test"></pre>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/
3.2.0/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js"></script>
<script src="https://code.angularjs.org/1.4.0-beta.5/angular-sanitize.js"></script>
<script>
var app = angular.module("app", ['ngSanitize'])
.controller("defaultCtrl", function ($scope) {
$scope.test = '<a href="5.2.html">link</a> inny kod HTML';
});
</script>
</body>
</html>
Efekt działania aplikacji prezentuje rysunek 5.3.
Rysunek 5.3.
ng-bind-html +
ngSanitize
Dyrektywa ngBindTemplate
ngBindTemplate — kolejna dyrektywa z rodziny ngBind, ale w przeciwieństwie do samego
ngBind może zawierać wiele wyrażeń, np.: {{test1}}, {{test2}}, {{test3}}.
Przydaje się to w przypadku, gdy korzystamy z elementów takich jak <title> czy
<option>, które nie mogą zawierać w sobie znacznika <span>. Poniżej przykład użycia.
W kodzie HTML:
<pre ng-bind-template="{{tempText}} – {{name}}! Może też zawierać znaki specjalne
(!@#$%^&*)_+)"></pre>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
56
AngularJS. Pierwsze kroki
W kodzie JS:
$scope.name = "ABC";
$scope.tempText = "123";
Dyrektywa ngCloak
ngCloak — wspomniana wcześniej dyrektywa zapobiega wyświetlaniu w czasie ładowania aplikacji szablonu HTML w surowej, nieskompilowanej postaci. Usuwa też
efekt migotania podczas wyświetlania szablonu HTML.
Dyrektywa ngCloak może być stosowana w elemencie <body>, ale najlepiej używać jej
w mniejszych elementach, tak by umożliwić progresywne ładowanie strony. Działa ona na
znaczniku, w którym została zaimplementowana, oraz na wszystkich jego dzieciach. Zobaczmy to na przykładzie.
W kodzie CSS:
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
}
W kodzie HTML:
<div id="testPanel" class="panel">
<h3 class="panel-header">Nazwa {{name}}</h3>
<span ng-cloak>{{tempText}}</span>
</div>
W kodzie JS:
$scope.name = 'ABC';
$scope.tempText = '123';
Działanie ngCloak oparte jest na wbudowanym w AngularJS stylu:
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
Mamy tu jednak niekonsekwencję. Pisaliśmy wcześniej, że możemy wywoływać dyrektywy, używając kilku sposobów zapisu w kodzie HTML — za pomocą myślników, znaków podkreślenia czy dwukropków do rozdzielania wyrazów. W przypadku
omawianej dyrektywy ograniczeni jesteśmy tylko do myślników. Jeżeli umieścimy
dyrektywę na któryś z następujących sposobów, to ona po prostu nie zadziała:
<span
<span
<span
<span
<span
x:ng:cloak>{{tempText}}</span>
data:ng:cloak>{{tempText}}</span>
ng_cloak>{{tempText}}</span>
x_ng_cloak>{{tempText}}</span>
data:ng:cloak>{{tempText}}</span>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
57
Dyrektywy ngBlur i ngFocus
ngBlur, ngFocus — dwie dyrektywy pozwalające zidentyfikować zachowania użytkownika. Na poniższym przykładzie widać, jak można sterować kolorem pola <input>.
Kliknięcie na polu spowoduje zmianę koloru tła na zielony, a gdy klikniemy obok,
kolor z zielonego zmieni się na czerwony.
W kodzie HTML:
<input type="text"
data-ng-class="{ testFocus: focus, testBlur: blur }"
data-ng-focus="focus=true;blur=false;"
data-ng-blur="blur=true;focus=false;" />
W kodzie JS:
$scope.focus = false;
$scope.blur = false;
W kodzie CSS:
input[type="text"].testFocus {
background-color: green;
}
input[type="text"].testBlur {
background-color: red;
}
W przykładzie wykorzystaliśmy jeszcze jedną wbudowaną dyrektywę, ngClass, o której szerzej napiszemy w dalszej części rozdziału.
Dyrektywa ngChange
ngChange — ta dyrektywa sprawdza, czy użytkownik zmienił wartość. Działanie jest
natychmiastowe, co oznacza, że jest ona wywoływana po wprowadzeniu każdego znaku
we wpisywanej frazie. Najlepiej zobrazuje to poniższy przykład.
W kodzie HTML:
<input type="text" data-ng-change="change()" data-ng-model="testModel" />
W kodzie JS:
$scope.change = function () {
console.log($scope.testModel);
};
Wynik w konsoli po wpisaniu słowa test w polu <input>:
t
te
tes
test
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
58
AngularJS. Pierwsze kroki
ngChange to bardzo potężne narzędzie, które umiejętnie użyte może nam znacznie ułatwić życie. Zobaczmy to na przykładzie. Naszym zadaniem jest pobranie i wyświetlenie trzech różnych JSON-ów. Efekt wykonania listingu 5.3 prezentuje rysunek 5.4.
Pliki JSON:
Plik plik-testowy-1.json
{
"nazwa":"Plik 1.",
"autor":"Jan",
"data":"2015-10-16T17:57:28.556094Z"
}
Plik plik-testowy-2.json
{
"nazwa":"Plik 2.",
"autor":"Andrzej",
"data":"2015-12-19T19:11:33.556004Z"
}
Plik plik-testowy-3.json
{
"nazwa":"Plik 3.",
"autor":"Piotr",
"data":"2015-08-11T14:44:22.556011Z"
}
Listing 5.3. Dyrektywa ngChange
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>Dyrektywa ngChange</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/
3.2.0/css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div>
<div>
Pokaż / ukryj przyciski
<input type="checkbox" data-ng-model="showButtons" /><br />
Pokaż / ukryj listę rozwijalną
<input type="checkbox" data-ng-model="selectionList" />
</div>
<div data-ng-show="showButtons">
<button type="button" class="btn btn-primary btn-sm"
data-ng-repeat="file in files"
data-ng-click="getFileData(file)">{{file.name}}</button>
</div>
<div data-ng-show="selectionList">
<select data-ng-model="file"
data-ng-change="getFileData(file)"
data-ng-options="file as file.name for file in files">
</select>
</div>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
59
<pre data-ng-show="fileData">
Nazwa: {{fileData.data.nazwa}}
Autor: {{fileData.data.autor | uppercase}}
Data: {{fileData.data.data | date}}
URL: {{fileData.config.url}}
</pre>
<pre data-ng-show="fileData">{{fileData|json}}</pre>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.5/angular.min.js">
</script>
<script src=" http://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-resource.js ">
</script>
<script>
var app = angular.module('app', ['ngResource']);
app.controller('defaultCtrl', function ($scope, FileDataService) {
$scope.files = [
{ name: 'Plik 1.', URL: 'plik-testowy-1.json' },
{ name: 'Plik 2.', URL: 'plik-testowy-2.json' },
{ name: 'Plik 3.', URL: 'plik-testowy-3.json' }
];
$scope.getFileData = function (file) {
FileDataService.getFileData(file).then(function (result) {
$scope.fileData = result;
}, function (result) {
alert("Wystąpił błąd!");
});
};
});
app.$inject = ['$scope', 'FileDataService'];
app.factory('FileDataService', ['$http', '$q', function ($http) {
var factory = {
getFileData: function (file) {
console.log(file);
var data = $http({ method: 'GET', url: file.URL });
return data;
}
}
return factory;
}]);
</script>
</body>
</html>
Powyższy przykład pokazuje zachowanie dyrektywy ngChange, jest jednak o wiele bardziej rozbudowany i nasycony dyrektywami, których jeszcze nie omawialiśmy. Jak
wiadomo, przyswajanie wiedzy jest najefektywniejsze, gdy poparta jest ona realnymi,
z życia wziętymi przykładami. Powyższy program jest jak najbardziej realny, a zawarty
w nim kod bardzo często wykorzystywany w aplikacjach SPA (Single Page Application).
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
60
AngularJS. Pierwsze kroki
Rysunek 5.4.
Dyrektywa
ngChange
Po wgraniu plików JSON oraz pliku index.html do tego samego katalogu i następnym
uruchomieniu otrzymamy stronę z dwoma polami wyboru. Pierwsze pole, Pokaż/ukryj
przyciski, pozwala na sterowanie widocznością dynamicznie tworzonych przycisków.
W tym procesie biorą udział cztery dyrektywy (dokładnie omówimy je w dalszej części
książki):
 ng-model (w elemencie checkbox) — tworzy $scope.showButtons.
 ng-show — wyświetla element div, gdy $scope.showButtons jest równy true.
 ng-repeat — tworzy serię przycisków na podstawie $scope.files, a także
$scope.file dla poszczególnych przycisków.
 ng-click — po kliknięciu wykonuje funkcję getFileData(file), która
przyjmuje $scope.file jako parametr.
Pokaż / ukryj przyciski <input type="checkbox" data-ng-model="showButtons" />
<div data-ng-show="showButtons">
<button type="button" class="btn btn-primary btn-sm"
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
61
data-ng-repeat="file in files"
data-ng-click="getFileData(file)">{{file.name}}</button>
</div>
Następne pole wyboru, Pokaż listę rozwijalną, pozwala na sterowanie widocznością
dynamicznie generowanej listy rozwijalnej. Nad procesem czuwają kolejne dyrektywy:
 ng-model (w elemencie checkbox) — tworzy $scope.selectionList.
 ng-show — wyświetla element div, gdy $scope.selectionList jest równy
true.
 ng-model (w elemencie select) — tworzy $scope.file.
 ng-change — w przypadku zmiany wartości w elemencie select wywołuje
funkcję getFileData(file), która przyjmuje jako parametr $scope.file.
 ng-options — dynamicznie tworzy listę rozwijalną.
Pokaż / ukryj listę rozwijalną <input type="checkbox" data-ng-model=
"selectionList" />
<div data-ng-show="selectionList">
<select data-ng-model="file"
data-ng-change="getFileData(file)"
data-ng-options="file as file.name for file in files">
</select>
</div>
Kolejna dyrektywa, ng-show, pokazuje nam elementy <pre>. Tym razem nie przyjmuje
wartości true, ale sprawdza, czy $scope.fileData jest zdefiniowany. Pierwszy element,
<pre>, wyświetla odpowiednio: nazwę, autora, datę i url. Drugi element wyświetla
sformatowany obiekt JSON zawarty w $scope.fileData.
<pre data-ng-show="fileData">
Nazwa: {{fileData.data.nazwa}}
Autor: {{fileData.data.autor | uppercase}}
Data: {{fileData.data.data | date}}
URL: {{fileData.config.url}}
</pre>
<pre data-ng-show="fileData">{{fileData|json}}</pre>
W naszym kontrolerze mamy zdefiniowaną tablicę $scope.files, zawierającą trzy
obiekty przechowujące informacje o nazwie i adresie pliku.
$scope.files = [
{ name: 'Plik 1.', URL: 'plik-testowy-1.json' },
{ name: 'Plik 2.', URL: 'plik-testowy-2.json' },
{ name: 'Plik 3.', URL: 'plik-testowy-3.json' }
];
Nazwy plików możemy zamienić adresami do naszego API.
$scope.getFileData to funkcja, która przyjmuje obiekt file, a następnie korzystając
z serwisu FileDataService, przypisuje wynik wywołania do $scope.fileData.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
62
AngularJS. Pierwsze kroki
$scope.getFileData = function (file) {
FileDataService.getFileData(file).then(function (result) {
$scope.fileData = result;
}, function (result) {
alert("Wystąpił błąd!");
});
};
Ostatnim elementem układanki jest serwis FileDataService, odpowiedzialny za
zwrócenie fabryki tworzącej wywołania $http.
Powyższy przykład obrazuje, jak w prosty sposób, przy pomocy kilku wbudowanych
dyrektyw, możemy stworzyć zaawansowaną logikę na naszej stronie WWW.
Dyrektywa ngClass
ngClass — dyrektywa ta pozwala na dynamiczne ustawianie klas CSS w elementach
HTML <div ng-class="{wyrażenie}">. Działa na trzy różne sposoby, w zależności
od tego, jak zapiszemy przyjmowane wyrażenie.
1. Jeśli wyrażenie jest ciągiem znaków, powinna to być jedna lub więcej nazw
klas CSS rozdzielonych spacjami.
Przykład użycia:
<div data-ng-class="'a b c'"></div>
Wynik w drzewie DOM:
<div class="a b c"></div>
2. Jeżeli wyrażenie jest tablicą, wtedy każdy element tablicy powinien być
ciągiem znaków, reprezentującym nazwy klas CSS rozdzielone przecinkami.
Przykład użycia:
<div data-ng-class="['a', 'b', 'c']"></div>
Wynik w drzewie DOM:
<div class="a b c"></div>
3. Jeśli wyrażenie jest obiektem, każda para klucz-wartość powinna
reprezentować nazwę klasy CSS oraz wartość true lub false.
Przykład użycia:
<div data-ng-class="{'a':true, 'b':true, 'c':false}"></div>
Wynik w drzewie DOM:
<div class="a b"></div>
Zobaczmy w praktyce, jak możemy wykorzystać siłę ngClass.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
63
W kodzie CSS:
.a {
color:red;
font-weight: bold;
border:1px dashed;
}
.b {
color:blue;
font-weight: bold;
text-decoration:underline;
border:1px dotted;
}
.c {
color:green;
font-weight: bold;
text-decoration:overline;
border:1px double;
}
.d {
color:orange;
font-weight: bold;
text-decoration: line-through;
border:1px solid;
}
W kodzie HTML:
<input data-ng-model="name" />
<div data-ng-class="name.length >= 20 ? 'a' : (name.length >= 10 ? 'b' :
(name.length >= 5 ? 'c' : 'd'))">{{name}}</div>
<div>Liczba wpisanych znaków {{name.length}}</div>
Powyższy przykład pokazuje, jak w prosty sposób możemy sterować klasami, bazując
na liczbie wpisanych znaków w polu name. Dla mniej niż 5 znaków zostanie zastosowana
klasa 'd', dla więcej niż 4, ale mniej niż 10 — klasa 'c', dla więcej niż 9 i mniej niż
20 — klasa 'b', dla 20 i więcej — klasa 'a'. W rzeczywistych projektach możemy
np. sterować wielkością kolumn w zależności od ilości tekstu.
Kolejny przykład na listingu 5.4 pokaże, jak użyć ngClass w stosunku do tabel. Wykorzystamy klasy zdefiniowane w bibliotece bootstrap. Załóżmy, że chcemy wyświetlić tabelę zawierającą dwie wartości, nazwę góry oraz jej wysokość. Chcemy zaznaczyć
kolorem tła szczyty o wysokości większej niż 8600 m i mniejszej niż 8500 m. Chcemy również, aby nasze góry były posortowane od najwyższej do najniższej.
Listing 5.4. Dyrektywa ngClass zastosowana dla tabeli
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>Dyrektywa ngClass zastosowana dla tabeli</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/
3.2.0/css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<table class="table table-condensed">
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
64
AngularJS. Pierwsze kroki
<thead>
<tr>
<th>Góra</th>
<th>Metry</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="m in mountainsList | orderBy:sorting"
data-ng-class="{warning: m.metres<8500, danger: m.metres>8600}">
<td>{{m.mountain}}</td>
<td>{{m.metres}}</td>
</tr>
</tbody>
</table>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.4/angular.min.js">
</script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.sorting = '-metres';
$scope.mountainsList = [
{
mountain: "Mount Everest",
metres: 8850
},
{
mountain: "K2",
metres: 8611
},
{
mountain: "Kangczendzonga",
metres: 8598
},
{
mountain: "Lhotse",
metres: 8501
},
{
mountain: "Makalu",
metres: 8463
},
{
mountain: "Cho Oyu",
metres: 8201
}];
});
</script>
</body>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
65
Efekt wywołania kodu prezentuje rysunek 5.5.
Rysunek 5.5.
Dyrektywa
ngClass
zastosowana
dla tabeli
Po raz kolejny skorzystaliśmy z dyrektywy ngRepeat do wyświetlenia poszczególnych
rekordów. Czas najwyższy przyjrzeć się jej bliżej.
Dyrektywa ngRepeat
ngRepeat — jedna z najczęściej używanych dyrektyw. Tworzy instancję szablonu dla
każdego elementu kolekcji. Każda z instancji otrzymuje własny 'scope' z następującymi
właściwościami:
 $index — jest to numeryczny indeks listowanego elementu w zakresie
do 0 do length-1.
 $first — zwraca 'true', jeśli dany element jest pierwszy na liście.
 $middle — zwraca 'true', jeśli dany element jest pomiędzy pierwszym
a ostatnim na liście.
 $last — zwraca 'true', jeśli dany element jest ostatni na liście.
 $even — zwraca 'true', jeśli $index elementu jest parzysty, w przeciwnym
razie zwraca 'false'.
 $odd — zwraca 'true', jeśli $index elementu jest nieparzysty, w przeciwnym
razie zwraca 'false'.
Dyrektywa ngRepeat tworzy nowy 'scope' i jest uruchamiana z priorytetem 1000.
Zanim przejdziemy dalej, popatrzmy na przykład.
W kodzie HTML:
<input type="text" data-ng-model="search" style="width: 80px" />
<ul>
<li data-ng-repeat="mountain in mountainsList | filter:search">
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
66
AngularJS. Pierwsze kroki
{{mountain}}
</li>
</ul>
W kodzie JS:
$scope.mountainsList = ['Mount Everest', 'K2', 'Lhotse', 'Makalu', 'Cho Oyu'];
Dyrektywa ngRepeat przyjmuje wyrażenie "mountain in mountainsList | filter:search",
czyli instrukcję wskazującą źródło oraz opcjonalny sposób filtrowania danych. Następnie tworzy kolejne elementy <li>, wypełniając je kolejnymi elementami tablicy
mountainsList.
Wspomnieliśmy wcześniej o specjalnych właściwościach, jakie posiada każdy nowo
powstały 'scope'. Zobaczmy, jak zadziałają w praktyce. Tym razem stworzymy stronę wyświetlającą tabelkę z nazwami gór, ich wysokościami oraz strzałkami pozwalającymi na przesuwanie elementów naszej tablicy w górę i w dół — listing 5.5. Kliknięcie
w dany rekord umożliwi nam jego edycję. Dodatkowo zastosujmy styl 'info' dla rekordów nieparzystych oraz styl 'danger' dla rekordów parzystych. Efekt wykonania
programu prezentuje rysunek 5.6.
Listing 5.5. Dyrektywa ngRepeat
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>ngRepeat</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/
3.2.0/css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<table class="table">
<tr data-ng-repeat="mountain in mountainsList" data-ng-class=
"{info:$even, danger:$odd}">
<td class="col-sm-1">{{$index}}</td>
<td class="col-sm-4" data-ng-show="!showForms" data-ng-click="showForms=true">
{{mountain.mountain}} </td>
<td class="col-sm-4" data-ng-show="showForms">
<input data-ng-model="mountain.mountain" /></td>
<td class="col-sm-4" data-ng-show="!showForms" data-ng-click="showForms=true">
{{mountain.metres}} </td>
<td class="col-sm-3" data-ng-show="showForms">
<input data-ng-model="mountain.metres" />
<a href="#" data-ng-click="saveChanges($index, mountain.mountain, mountain.metres);
showForms=false" class="glyphicon glyphicon-ok"> Zapisz</a></td>
<td class="col-sm-1">
<a href="#" data-ng-click="showForms=true" class="glyphicon glyphicon-pencil">
</a></td>
<td class="col-sm-1"><a href="#" data-ng-show="!$first" data-ng-click="up($index)"
class="glyphicon glyphicon-arrow-up"></a></td>
<td class="col-sm-1"><a href="#" data-ng-show="!$last" data-ng-click="down($index)"
class="glyphicon glyphicon-arrow-down"></a></td>
</tr>
</table>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/
3.2.0/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.4.0-beta.4/angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850 },
{ mountain: "K2", metres: 8611 },
{ mountain: "Kangczendzonga", metres: 8598 },
{ mountain: "Lhotse", metres: 8501 },
{ mountain: "Makalu", metres: 8463 },
{ mountain: "Cho Oyu", metres: 8201 }];
var drive = function (source, target) {
var t = $scope.mountainsList[target];
$scope.mountainsList[target] = $scope.mountainsList[source];
$scope.mountainsList[source] = t;
};
$scope.up = function (index) {
drive(index, index - 1);
};
$scope.down = function (index) {
drive(index, index + 1);
};
$scope.saveChanges = function (index, mountain, metres) {
$scope.mountainsList[index]= { 'mountain': mountain, 'metres':
metres };
};
});
</script>
</body>
</html>
Rysunek 5.6.
Dyrektywa
ngRepeat
Manipulując indeksami, przesuwamy rekordy w górę i w dół.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
67
68
AngularJS. Pierwsze kroki
Uzyskaliśmy bardzo ciekawy efekt przesuwania elementów w górę i w dół. W pierwszym wpisie nie pokazujemy strzałki w górę, a w ostatnim strzałki w dół. Dodatkowo,
klikając na dany rekord lub ikonkę ołówka, możemy edytować poszczególne wpisy. Dla
lepszego przyswojenia podanej wyżej wiedzy zalecamy uruchomienie powyższego
przykładu i dokładne przeanalizowanie poszczególnych zdarzeń.
Dyrektywa ngRepeat znajduje bardzo szerokie zastosowanie, o czym przekonasz się,
budując kolejne aplikacje oparte na AngularJS.
W następnym listingu, 5.6, wyświetlimy nazwy gór w trzech kolumnach.
Listing 5.6. Dyrektywa ngRepeat — 3 kolumny
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>ngRepeat</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/
3.2.0/css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div data-ng-repeat="mountain in mountainsList">
<div data-ng-switch="" data-on="$index % 3">
<div class="row" data-ng-switch-when="0">
<div class="btn-group btn-group-justified">
<a href="#" class="col-sm-4 btn btn-default" data-ng-click="log($index)"
data-ng-show="mountainsList[$index+0]">{{mountainsList[$index+0]}}</a>
<a href="#" class="col-sm-4 btn btn-default" data-ng-click="log($index)"
data-ng-show="mountainsList[$index+1]">{{mountainsList[$index+1]}}</a>
<a href="#" class="col-sm-4 btn btn-default" data-ng-click="log($index)"
data-ng-show="mountainsList[$index+2]">{{mountainsList[$index+2]}}</a>
</div>
</div>
</div>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.mountainsList = ['Mount Everest', 'K2', 'Kangczendzonga',
'Lhotse', 'Makalu', 'Cho Oyu','Aconcagua',
'Broad Peak', 'Gasherbrum II', 'Shisha Pangma'
];
$scope.log = function (index) {
console.log('index=',index);
};
});
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
69
</script>
</body>
</html>
Efekt wykonania listingu 5.6 prezentuje rysunek 5.7.
Rysunek 5.7. Dyrektywa ngRepeat — 3 kolumny
W efekcie działania powyższego kodu zostały zwrócone trzy kolumny przycisków z nazwami poszczególnych gór. Kliknięcie na przycisk wywoła funkcję log i spowoduje
zalogowanie indeksu na liście. Co jednak, gdy chcemy dynamicznie ograniczyć liczbę
wyświetlanych elementów? Najprostszym sposobem jest dodanie do wyrażenia ngRepeat
odpowiedniego filtru.
Skorzystajmy z przykładu powyżej, dodając do niego dwie rzeczy. Na początek należy
dodać pole <input>, w którym tworzymy ng-model o nazwie 'search'.
<input data-ng-model="search" class="input-sm" />
<div data-ng-repeat="mountain in mountainsList | filter:search">
W drugim kroku w wyrażeniu ng-repeat po znaku | wpisujemy 'filter:search'. I to
wszystko. Jeśli wprowadzimy kolejne znaki w polu <input>, nasz filtr będzie ograniczał
listę wyników.
Napiszmy własny filtr, korzystając z mocy wyrażeń regularnych. Załóżmy, że chcemy
wyszukać nazwy zaczynające się na literę „K”. Nasz kod HTML będzie wyglądał
następująco:
<div data-ng-repeat="mountain in mountainsList | nameLimit:'mountain':'^K'">
{{mountain.mountain}} - {{mountain.metres}}
</div>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
70
AngularJS. Pierwsze kroki
Kompletny kod JS prezentuje się tak:
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850},
{ mountain: "K2", metres: 8611 },
{ mountain: "Kangczendzonga", metres: 8598 },
{ mountain: "Lhotse", metres: 8501 },
{ mountain: "Makalu", metres: 8463 },
{ mountain: "Cho Oyu", metres: 8201 }];
});
app.filter('nameLimit', function () {
return function (input, field, expression) {
var out = [];
var pattern = new RegExp(expression);
for (var i = 0; i < input.length; i++) {
if (pattern.test(input[i][field]))
out.push(input[i]);
}
return out;
};
});
Załóżmy, że mountainsList jest słownikiem składającym się z nazwy i wysokości {'nazwa
a':8000}. Składnia naszego wyrażenia będzie wyglądać trochę inaczej. Zobaczmy to
na przykładzie.
Kod HTML:
<div data-ng-repeat="(mountain, metres) in mountainsList">
{{mountain}} - {{metres}}
</div>
Kod JS:
$scope.mountainsList = {
"Mount Everest": 8850,
"K2": 8611,
"Kangczendzonga": 8598,
"Lhotse": 8501,
"Makalu": 8463,
"Cho Oyu": 8201
};
Kończąc rozważania o ngRepeat, przyjrzyjmy się jeszcze dwóm pochodnym dyrektywom: ng-repeat-start oraz ng-repeat-end. Pozwalają one na zagnieżdżanie wywołań. Zobaczmy to w praktyce na listingu 5.7.
Listing 5.7. Dyrektywy ngStart i ngEnd
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>ngRepeat</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
71
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div class="col-sm-6" data-ng-repeat-start="mountain in mountainsList">
<a href="" data-ng-click="log($index)">{{mountain.mountain}}</a></div>
<div class="col-sm-6" data-ng-repeat-end="">{{mountain.metres}}</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850},
{ mountain: "K2", metres: 8611 },
{ mountain: "Kangczendzonga", metres: 8598 },
{ mountain: "Lhotse", metres: 8501 },
{ mountain: "Makalu", metres: 8463 },
{ mountain: "Cho Oyu", metres: 8201 }];
$scope.log = function (index) {
console.log('index=',index);
};
});
</script>
</body>
</html>
AngularJS wygeneruje następujący kod:
<div class="container">
<!-- ngRepeat: mountain in mountainsList --><div class="col-sm-6 ng-scope"
data-ng-repeat-start="mountain in mountainsList"><a href="" data-ng-click=
"log($index)" class="ng-binding">Mount Everest</a></div>
<div class="col-sm-6 ng-binding ng-scope" data-ng-repeat-end="">8850</div>
<!-- end ngRepeat: mountain in mountainsList --><div class="col-sm-6 ng-scope"
data-ng-repeat-start="mountain in mountainsList"><a href="" data-ng-click=
"log($index)" class="ng-binding">K2</a></div>
<div class="col-sm-6 ng-binding ng-scope" data-ng-repeat-end="">8611</div>
<!-- end ngRepeat: mountain in mountainsList --><div class="col-sm-6 ng-scope"
data-ng-repeat-start="mountain in mountainsList"><a href="" data-ng-click=
"log($index)" class="ng-binding">Kangczendzonga</a></div>
<div class="col-sm-6 ng-binding ng-scope" data-ng-repeat-end="">8598</div>
<!-- end ngRepeat: mountain in mountainsList --><div class="col-sm-6 ng-scope"
data-ng-repeat-start="mountain in mountainsList"><a href="" data-ng-click=
"log($index)" class="ng-binding">Lhotse</a></div>
<div class="col-sm-6 ng-binding ng-scope" data-ng-repeat-end="">8501</div>
<!-- end ngRepeat: mountain in mountainsList --><div class="col-sm-6 ng-scope"
data-ng-repeat-start="mountain in mountainsList"><a href="" data-ng-click=
"log($index)" class="ng-binding">Makalu</a></div>
<div class="col-sm-6 ng-binding ng-scope" data-ng-repeat-end="">8463</div>
<!-- end ngRepeat: mountain in mountainsList --><div class="col-sm-6 ng-scope"
data-ng-repeat-start="mountain in mountainsList"><a href="" data-ng-click=
"log($index)" class="ng-binding">Cho Oyu</a></div>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
72
AngularJS. Pierwsze kroki
<div class="col-sm-6 ng-binding ng-scope" data-ng-repeat-end="">8201</div>
<!-- end ngRepeat: mountain in mountainsList -->
</div>
Efekt wywołania możemy zobaczyć na rysunku 5.8.
Rysunek 5.8. Dyrektywy ngStart i ngEnd
Dyrektywa ngClick
ngClick — dyrektywa używana wielokrotnie w poprzednich przykładach, pozwalająca na
definiowanie niestandardowego zachowania elementu, gdy ten zostanie kliknięty.
Wróćmy na chwilę do ngRepeat — jeszcze raz połączymy siłę obydwu dyrektyw.
Tym razem stworzymy dynamiczną listę przycisków, a kliknięcie na poszczególne przyciski wywoła przypisane do nich funkcje. Kompletny kod znajduje się w listingu 5.8.
Listing 5.8. Dyrektywa ngClick
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>ngClick</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div>
<a class="btn btn-default" data-ng-repeat="execute in mountainsList"
data-ng-click="execute.execute(execute.metres)">{{execute.mountain}}</a>
</div>
<div class="text-danger">
{{result}}
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
73
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.result = null;
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850, execute: function (height)
{ $scope.result= height + ', function 1'; } },
{ mountain: "K2", metres: 8611, execute: function (height) { $scope.result =
height + ', function 2'; } },
{ mountain: "Kangczendzonga", metres: 8598, execute: function (height)
{ $scope.result = height + ', function 3'; } },
{ mountain: "Lhotse", metres: 8501, execute: function (height) { $scope.result =
height + ', function 4'; } },
{ mountain: "Makalu", metres: 8463, execute: function (height) { $scope.result =
height + ', function 5'; } },
{ mountain: "Cho Oyu", metres: 8201, execute: function (height) { $scope.result =
height + ', function 6'; } }];
});
</script>
</body>
</html>
AngularJS tym razem wygeneruje następujący kod:
<div class="container"><div>
<!-- ngRepeat: execute in mountainsList -->
<a class="btn btn-default ng-binding ng-scope" data-ng-repeat="execute in
mountainsList" data-ng-click="execute.execute(execute.metres)">Mount Everest</a>
<!-- end ngRepeat: execute in mountainsList -->
<a class="btn btn-default ng-binding ng-scope" data-ng-repeat="execute in
mountainsList" data-ng-click="execute.execute(execute.metres)">K2</a>
<!-- end ngRepeat: execute in mountainsList -->
<a class="btn btn-default ng-binding ng-scope" data-ng-repeat="execute in
mountainsList" data-ng-click="execute.execute(execute.metres)">Kangczendzonga</a>
<!-- end ngRepeat: execute in mountainsList -->
<a class="btn btn-default ng-binding ng-scope" data-ng-repeat="execute in
mountainsList" data-ng-click="execute.execute(execute.metres)">Lhotse</a>
<!-- end ngRepeat: execute in mountainsList -->
<a class="btn btn-default ng-binding ng-scope" data-ng-repeat="execute in
mountainsList" data-ng-click="execute.execute(execute.metres)">Makalu</a>
<!-- end ngRepeat: execute in mountainsList -->
<a class="btn btn-default ng-binding ng-scope" data-ng-repeat="execute in
mountainsList" data-ng-click="execute.execute(execute.metres)">Cho Oyu</a>
<!-- end ngRepeat: execute in mountainsList -->
</div><div class="text-danger ng-binding">8850, function 1</div></div>
Efekt działania kodu możemy zobaczyć na rysunku 5.9.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
74
AngularJS. Pierwsze kroki
Rysunek 5.9. Dyrektywa ngClick
Dyrektywa ngController
ngController — niezwykle ważna dyrektywa wiążąca klasę kontrolera z widokiem. To
kluczowy aspekt idei wsparcia Angulara dla wzorca projektowego Model-View-Controller.
O samym modelu MVC pisaliśmy już w rozdziale 1. Warto pamiętać o tym, że dyrektywa
ngController tworzy nowy scope i że jest uruchamiana z priorytetem 500.
Jak można było zauważyć w poprzednich przykładach, najczęściej używamy dyrektywy w znaczniku <body>. Nie jest to oczywiście wymagane i możemy ją wykorzystywać w dowolnym innym znaczniku HTML.
Stosując ją w znaczniku <body>, mamy pewność, że nasza dyrektywa kontroluje znacznik <body> i wszystkie jego dzieci. W przypadku gdy chcemy na stronie użyć kilku
kontrolerów, możemy to zrobić, umieszczając je np. w znacznikach <div>. Poniższy
przykład obrazuje zastosowanie paru kontrolerów. Co ważne, kontrolery mogą być
zagnieżdżone oraz mogą się ze sobą komunikować.
<div data-ng-controller="mainCtrl">
<!-- main -->
<div data-ng-controller="childOneCtrl"><!-- one --></div>
<div data-ng-controller="childTwoCtrl">
<!-- two -->
<div data-ng-controller="nextCtrl"><!-- next --></div>
</div>
</div>
Dyrektywę ngController możemy deklarować na dwa różne sposoby. Pierwszy z nich
mogliśmy poznać już w dotychczasowych przykładach. Jest on najczęściej wykorzystywany przez programistów AngularJS. Drugi sposób jest na pierwszy rzut oka trochę
bardziej skomplikowany. Daje on również nieco więcej możliwości. Dyrektywę deklarujemy następująco:
<div ng-controller="tempCtrl as temp1">
Zapis ten pozwala nam na takie podejście do pisania kontrolerów jak przy pisaniu klas.
Przykładowy kontroler może wyglądać tak:
var app = angular.module('app', []);
app.controller('tempCtrl', tempCtrl);
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
75
function tempCtrl() {
this.name = "Piotr";
}
tempCtrl.prototype.showName
alert(this.name);
};
= function () {
Wywołanie funkcji zawsze będzie poprzedzone nową nazwą kontrolera oraz kropką.
Jest to szczególnie ważne w sytuacji, kiedy mamy kilka kontrolerów przypisanych do
jednego elementu.
<a href="" ng-click="temp1.showName()"> Imię </a>
Dyrektywa ngCopy
ngCopy — dyrektywa określająca niestandardowe zachowanie elementu w przypadku
kopiowania. Uruchamiana jest z priorytetem 0. Zobaczmy na listingu 5.9, jak można
wykorzystać dyrektywę ngCopy w połączeniu z ngRepeat.
Listing 5.9. Dyrektywa ngCopy
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>ngCopy</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div data-ng-repeat="mountain in mountainsList">
<div data-ng-copy="copied=true" >{{$index+1}}. {{mountain.mountain}} {{mountain.metres}}</div>
<div class="text-danger" data-ng-show="copied">
Uwaga, tekst <b>"{{mountain.mountain}} - {{mountain.metres}}"</b> o
ID={{$index}} został skopiowany!
</div>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.copied = false;
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850},
{ mountain: "K2", metres: 8611},
{ mountain: "Kangczendzonga", metres: 8598},
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
76
AngularJS. Pierwsze kroki
{ mountain: "Lhotse", metres: 8501},
{ mountain: "Makalu", metres: 8463},
{ mountain: "Cho Oyu", metres: 8201}];
}
);
</script>
</body>
</html>
Efekt wykonania kodu z listingu 5.9 prezentuje rysunek 5.10.
Rysunek 5.10.
Dyrektywa ngCopy
Dyrektywa ngCut
ngCut — dyrektywa określająca niestandardowe zachowanie elementu w przypadku
wycinania. Bardzo podobna do poprzedniczki. Zobaczmy na listingu 5.10, co się stanie,
jeśli spróbujemy skorzystać z Ctrl+X (dla Windowsa) w jednym z pól.
Listing 5.10. Dyrektywa ngCut
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>ngCut</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div
data-ng-repeat="mountain in mountainsList">
<textarea data-ng-cut="cut=true" >{{$index+1}}. {{mountain.mountain}} {{mountain.metres}}</textarea>
<div class="text-danger" data-ng-show="cut">
Uwaga, część tekstu <b>"{{mountain.mountain}} - {{mountain.metres}}"</b> o
ID={{$index}} została wycięta!
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
77
</div>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.cut = false;
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850},
{ mountain: "K2", metres: 8611},
{ mountain: "Kangczendzonga", metres: 8598},
{ mountain: "Lhotse", metres: 8501},
{ mountain: "Makalu", metres: 8463},
{ mountain: "Cho Oyu", metres: 8201}];
});
</script>
</body>
</html>
Efekt wykonania kodu z listingu 5.10 prezentuje rysunek 5.11.
Rysunek 5.11.
Dyrektywa ngCut
Pod właściwym elementem <textarea> wyświetli się informacja, że część tekstu o danym
ID została wycięta. Dyrektywa może stać się bardzo przydatna w projektowaniu zaawansowanych formularzy.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
78
AngularJS. Pierwsze kroki
Dyrektywa ngDblclick
ngDblclick — dyrektywa określająca niestandardowe zachowanie elementu w przypadku
podwójnego kliknięcia. Oznacza to, że możemy zdefiniować przycisk działający tylko
w sytuacji podwójnego szybkiego kliknięcia.
Przykład użycia:
<button ng-dblclick="count = count + 1" ng-init="count=0"> +1 </button>
Dyrektywa ngFocus
ngFocus — dyrektywa określająca niestandardowe zachowanie elementu w przypadku
ustawieniu w nim fokusa. Modyfikując listing 5.10, możemy uzyskać taki oto ciekawy
efekt — listing 5.11.
Listing 5.11. Dyrektywa ngFocus
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>ngFocus</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div
data-ng-repeat="mountain in mountainsList">
<textarea data-ng-cut="cut=true" data-ng-focus="f=true">{{$index+1}}.
{{mountain.mountain}} - {{mountain.metres}}</textarea>
<div class="text-danger" data-ng-show="cut">
Uwaga, część tekstu <b>"{{mountain.mountain}} - {{mountain.metres}}"</b> o
ID={{$index}} została wycięta!
</div>
<div class="text-success" data-ng-show="f">
Jesteś wewnątrz elementu o ID={{$index}}!
</div>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.cut = false;
$scope.f = false;
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850},
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
{
{
{
{
{
mountain:
mountain:
mountain:
mountain:
mountain:
79
"K2", metres: 8611},
"Kangczendzonga", metres: 8598},
"Lhotse", metres: 8501},
"Makalu", metres: 8463},
"Cho Oyu", metres: 8201}];
});
</script>
</body>
</html>
Efekt wykonania kodu z listingu 5.11 prezentuje rysunek 5.12.
Rysunek 5.12. Dyrektywa ngFocus
Dyrektywa ngForm
ngForm — HTML nie pozwala na zagnieżdżanie formularzy. Twórcy AngularJS postanowili to zmienić, dając nam do dyspozycji dyrektywę będącą aliasem standardowego
znacznika <form>.
Dyrektywa ngHref
ngHref — kolejna warta uwagi dyrektywa pozwalająca na stosowanie wyrażeń
{{wyrażenie}} w linkach. Dyrektywa uruchamiana jest z priorytetem 99. Poniżej pokazu-
jemy, jak nie stosować wyrażeń i jak je poprawnie stosować w linkach.
Wersja niepoprawna:
<a href="http://www.adres.pl/{{produkty}}">Produkty</a>
Wersja poprawna:
<a data-ng-href="http://www.adres.pl/{{produkty}}">Produkty</a>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
80
AngularJS. Pierwsze kroki
Dyrektywa ngIf
ngIf — dyrektywa usuwająca lub dodająca cześć struktury drzewa DOM poprzez bazowanie na wyrażeniu. Jeśli wyrażenie zwraca false, element jest usuwany, a jeśli wyrażenie zwraca true, klon elementu jest dodawany do drzewa DOM. Uwaga, w momencie
usunięcia elementu z drzewa DOM jego scope jest niszczony, a w przypadku przywrócenia jest tworzony nowy.
Przykład użycia:
<div data-ng-init="checked=true">
<a hreh="#" data-ng-click="checked=false"> Ukryj </a>
<a hreh="#" data-ng-click="checked=true"> Pokaż </a>
<span data-ng-if="checked"> Text </span>
</div>
Możemy w prosty sposób ukrywać i pokazywać dowolny element.
Dyrektywa ngInclude
ngInclude — dyrektywa pobierająca, kompilująca i wstrzykująca zewnętrzny HTML.
Domyślnie możemy korzystać z linków do zewnętrznych HTML-i w obrębie naszego
serwera. Pobieranie szablonów HTML z innych serwerów wymaga odpowiedniego
obsłużenia. Przyjrzyjmy się bliżej naszej dyrektywie.
<ng-include
src=""
onload="" autoscroll=""> </ng-include>
 src — jak łatwo się domyślić, jest to link do naszego szablonu.
 onload — jest to opcjonalny parametr, wykonywany w przypadku ładowania
szablonu.
 autoscroll — opcjonalny parametr: jeśli jest zdefiniowany lub (i) jego
wyrażenie zwraca true, załadowany szablon może być przewijany.
Przykład użycia:
<div>
<ng-include src="side1">Tekst pokazywany w czasie ładowania.</ng-include>
</div>
<div data-ng-include='side2' autoscroll=""></div>
Definicja stron po stronie skryptu:
$scope.side1 = '1.html';
$scope.side2 = '2.html';
Dyrektywy ngKeydown, ngKeypress i ngKeyup
ngKeydown, ngKeypress, ngKeyup — dyrektywy określające niestandardowe zachowanie
elementu w przypadku kolejnych akcji klawiszy klawiatury.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
81
Przykład użycia:
<input data-ng-keydown="wyrazenie">
<input data-ng-keypress="wyrazenie">
<input data-ng-keyup="wyrazenie">
Drugi przykład pokaże nam, jak w prosty sposób możemy odczytać kody poszczególnych
klawiszy:
<input data-ng-keyup="event=$event">
<p>Zdarzenie keyCode: {{ event.keyCode }}</p>
<p>Zdarzenie altKey: {{ event.altKey }}</p>
Dyrektywa ngList
ngList — dyrektywa konwertująca string rozdzielony domyślnie przecinkami na tablicę
stringów. Domyślny przecinek możemy zmienić na nasz własny separator w następujący sposób: data-ng-list=" | ".
Co ciekawe, dyrektywa działa w dwóch kierunkach. Jak zobaczymy na poniższym przykładzie, możemy dynamicznie tworzyć tablicę, wpisując kolejne nazwy w polu <imput>
i rozdzielając je wybranymi separatorami. W tym samym czasie pozostałe pola są automatycznie aktualizowane i rozdzielane ich własnymi separatorami.
W kodzie HTML:
<input data-ng-model="mountainsList" data-ng-list> separator "," <br />
<input data-ng-model="mountainsList" data-ng-list="|"> separator "|" <br />
<input data-ng-model="mountainsList" data-ng-list="@"> separator "@" <br />
{{mountainsList}}
W kodzie JS:
$scope.mountainsList = [
"Mount Everest",
"K2",
"Kangczendzonga",
"Lhotse",
"Makalu",
"Cho Oyu"];
});
Dyrektywa ta może stać się bardzo przydatna podczas tworzenia zaawansowanych interfejsów użytkownika.
Dyrektywa ngModel
ngModel — dyrektywa tworzy powiązanie (binduje) pomiędzy <input>, <select>,
<textarea> lub inną spersonalizowaną kontrolką z właściwością w scope, korzystając
z NgModelController. Dyrektywa ta zapewnia ponadto walidację (np. pole wymagane,
e-mail, liczba), pamiętając o aktualnym stanie, w jakim znajduje się kontrolka, np. czy
przeszła poprawną walidację, czy nie. Co za tym idzie, możemy skorzystać z takich oto
klas CSS: ng-valid, ng-invalid, ng-dirty, ng-pristine, ng-touched, ng-untouched.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
82
AngularJS. Pierwsze kroki
Poniżej prezentujemy, jak w praktyce wykorzystać możliwości dyrektywy ngModel.
W kodzie CSS:
.my-input {
-webkit-transition:all linear 0.7s;
transition:all linear 0.7s;
background: green;
}
.my-input.ng-invalid {
color:white;
background: red;
}
W kodzie HTML:
Wpisz liczbę:<input data-ng-model="number" data-ng-pattern="/^\d+$/" name=
"num" class="my-input" />
W powyższym przykładzie do sprawdzenia poprawności wprowadzanych danych użyliśmy kolejnej bardzo przydatnej dyrektywy, ng-pattern, o której szerzej napiszemy w dalszej części książki. Teraz przypatrzmy się bliżej następnej niezwykle użytecznej dyrektywie, bezpośrednio powiązanej z omawianą wyżej ngModel.
Dyrektywa ngModelOptions
ngModelOptions — dyrektywa ta pozwala na określenie niestandardowej listy zdarzeń,
które wyzwoli aktualizacja modelu i (lub) np. timer. Oznacza to, że wartość wyświe-
tlana w widoku może być inna niż ta w modelu. Daje nam to możliwość ręcznego sterowania momentem, w którym model ma zostać uaktualniony.
Aktualizacja modelu po przejściu użytkownika do innego pola, a nie za każdym razem,
gdy wpisze kolejny znak, może się przełożyć na znaczną poprawę wydajności. Warto
o tym pamiętać szczególnie przy tworzeniu rozbudowanych formularzy. Aby tego dokonać, należy skorzystać z metody $rollbackViewValue.
Popatrzmy na poniższy listing 5.12, prezentujący trzy różne możliwości użycia dyrektywy ng-model-options.
Listing 5.12. Dyrektywa ng-model-options
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>AngularJS – ng-model-options</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<form name="editUser">
<!--Model uaktualniany jest po opuszczeniu kontrolki-->
Imię: <input class="form-control" type="text" name="userName" data-ng-model=
"user.name" data-ng-model-options="{ updateOn: 'blur' }" /><br />
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
83
<!--Model uaktualniany jest po 2 sekundach od wpisania ostatniego znaku-->
Płeć: <input class="form-control" type="text" name="userSex" data-ng-model=
"user.sex" data-ng-model-options="{debounce: 2000}" /><br />
<!--Przykład użycia jako getterSetter-->
Wiek: <input class="form-control" type="number" name="userAge" data-ng-model=
"user.age" data-ng-model-options="{ getterSetter: true }" /><br />
</form>
<pre>
user.name = {{user.name}}
user.sex = {{user.sex}}
user.age = {{user.age()}}
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
var _age = 25;
$scope.user = {
age: function (newAge) {
return angular.isDefined(newAge) ? (_age = newAge) : _age;
}
};
});
</script>
</body>
</html>
Pierwszy przypadek, updateOn: 'blur', aktualizuje model po opuszczeniu przez
użytkownika kontrolki <input>. Jeśli chcemy, aby nasz model był aktualizowany np.
po dwóch sekundach od zmiany, w polu <input> zastosujemy debounce: 2000. Trzeci,
a zarazem ostatni przypadek pokazuje, w jaki sposób możemy korzystać z getterSetter.
Ostateczny efekt można zobaczyć na rysunku 5.13.
Rysunek 5.13. Dyrektywa ng-model-options
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
84
AngularJS. Pierwsze kroki
Dyrektywy ngMousedown, ngMouseenter,
ngMouseleave, ngMousemove, ngMouseover
i ngMouseup
ngMousedown, ngMouseenter, ngMouseleave, ngMousemove, ngMouseover, ngMouseup — są
to dyrektywy określające niestandardowe zachowanie elementu w przypadku kolejnych
akcji myszy. Zobaczmy na przykładzie, jak działają poszczególne z nich.
W kodzie HTML:
<div style="padding:4px; background:red;" data-ng-mousedown="log('ngMousedown');">
ngMousedown</div>
<div style="padding:4px; background:green;" data-ng-mouseenter="log('ngMouseenter');">
ngMouseenter</div>
<div style="padding:4px; background:blue;" data-ng-mouseleave="log('ngMouseleave');">
ngMouseleave</div>
<div style="padding:4px; background:red;" data-ng-mousemove="log('ngMousemove');">
ngMousemove</div>
<div style="padding:4px; background:green;" data-ng-mouseover="log('ngMouseover');">
ngMouseover</div>
<div style="padding:4px; background:blue;" data-ng-mouseup="log('ngMouseup');">
ngMouseup</div>
W kodzie JS:
$scope.log = function (text) {
console.log(text);
};
Dyrektywa ngNonBindable
ngNonBindable — dyrektywa, której zadaniem jest informowanie Angulara, by nie
wiązał danego elementu drzewa DOM. Najprościej można to zobrazować w następujący
sposób:
W kodzie HTML:
Imię:<input type="text" data-ng-model="name"/><br />
Wiek:<input type="text" data-ng-model="age"><br />
<div>Bindable: {{name + " " + age}}</div>
<div data-ng-non-bindable>Non-Bindable:{{name + " " + age}}</div>
Dyrektywa ngNonBindable może się okazać bardzo przydatna, gdy na naszej stronie
chcemy pokazać np. fragmenty kodu.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
85
Dyrektywa ngPaste
ngPaste — dyrektywa określająca niestandardowe zachowanie elementu w przypadku
wklejania. Niezwykle prosta w użyciu, podobna do omawianych wcześniej ngCut i ngCopy.
Dyrektywa ngPluralize
ngPluralize — dyrektywa pozwalająca na zmianę wyświetlanej informacji w zależności od reguł lokalizacyjnych. Co to oznacza? Najlepiej pokaże to prosty przykład:
<div ng-repeat="i in [1,2,3]">
{{i}} - <ng-pluralize count='i'
when="{
'1': 'programista',
'other':'programistów'
}">
</ng-pluralize>
</div>
W efekcie otrzymamy następujący wynik:
1 - programista
2 - programistów
3 - programistów
Nie są to wszystkie możliwości dyrektywy ngPluralize — jedną z jej ciekawszych
opcji jest parametr offset.
Co daje użycie go i jak można go implementować, pokazuje kolejny przykład.
<div ng-repeat="i in [1,2,3]">
Liczba osób: {{i}} - lider zespołu <ng-pluralize offset=1 count='i'
when="{
'2': '+ {} programista',
'other':'+ {} programistów'
}">
</ng-pluralize></div>
</div>
Efekt wykonania kodu:
Liczba osób: 1 - lider zespołu + 0 programistów
Liczba osób: 2 - lider zespołu + 1 programista
Liczba osób: 3 - lider zespołu + 2 programistów
W bardzo prosty sposób możemy sterować wyświetlanymi informacjami. Jak łatwo
zauważyć, 'when' możemy dowolnie rozbudowywać o kolejne wartości liczbowe. Dodatkowo możemy korzystać z dwóch zdefiniowanych stringów: 'one' oraz używanego
w przykładzie 'other'.
Poznałeś już zasadę działania, przejdźmy zatem do kolejnego listingu, 5.13, obrazującego przekazaną dotychczas wiedzę.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
86
AngularJS. Pierwsze kroki
Listing 5.13. Dyrektywa ngPluralize
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>ngPluralize</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<h2 class="bg-primary">
<ng-pluralize count="mountainsList.length" offset="3" when="{
'0': '{{text.t0}}',
'1': '{{mountain1}} {{text.t1}}',
'2': '{{mountain1}} oraz {{mountain2}} {{text.t2}}',
'3': '{{mountain1}}, {{mountain2}}, {{mountain3}} {{text.t3}}',
'one': '{{mountain1}}, {{mountain2}}, {{mountain3}} + {{text.one}}',
'other': '{{mountain1}}, {{mountain2}}, {{mountain3}} + {} {{text.other}}'}">
</ng-pluralize></h2>
<p>Wpisz nazwy gór, rozdzielając je przecinkiem (,). </p>
<textarea data-ng-model="mountainsList" ng-list="," class="form-control">
</textarea><br />
<div class="well well-lg">
<span data-ng-repeat="p in mountainsList track by $index">{{$index}} - {{p}}
<br /></span>
</div>
<div class="btn-group">
<button data-ng-click="clear()" class="btn btn-danger">Wyczyść formularz</button>
<button data-ng-click="reset()" class="btn btn-success">Przywróć dane wejściowe
</button></div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.mountainsList = [
"Mount Everest",
"K2",
"Kangczendzonga",
"Lhotse",
"Makalu",
"Cho Oyu"];
$scope.text = {
't0': 'Tablica jest pusta.',
't1': 'znajduje się w tablicy.',
't2': 'znajdują się w tablicy.',
't3': 'znajdują się w tablicy!',
'one': 'jeszcze jedna nazwa, znajdują się w tablicy.',
'other': 'nazwy znajdują się w tablicy.',
};
$scope.orginalMountainsList = [];
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
87
angular.copy($scope.mountainsList, $scope.orginalMountainsList);
$scope.reset = function () {
$scope.mountainsList = $scope.orginalMountainsList;
};
$scope.clear = function () {
$scope.mountainsList = [];
};
$scope.$watch('mountainsList', function () {
$scope.mountain1 = ($scope.mountainsList[0] ? $scope.mountainsList[0] : null);
$scope.mountain2 = ($scope.mountainsList[1] ? $scope.mountainsList[1] : null);
$scope.mountain3 = ($scope.mountainsList[2] ? $scope.mountainsList[2] : null);
});
});
</script>
</body>
</html>
Po uruchomieniu powyższego kodu mamy stronę wyświetlającą listę nazw szczytów
na samej górze w następujący sposób:
Mount Everest, K2, Kangczendzonga + 3 nazwy znajdują się w tablicy.
Niżej znajduje się okno formularza, w którym możemy wpisywać kolejne nazwy lub
usuwać istniejące. Całość obrazuje rysunek 5.14. Angular dzięki dyrektywie ngPluralize
automatycznie dostosuje typ wyświetlanego tekstu do liczby elementów w tablicy.
Dla urozmaicenia sobie życia dodaliśmy też przyciski pozwalające na wyczyszczenie
tablicy oraz przywrócenie początkowych danych wejściowych.
Rysunek 5.14. Dyrektywa ngPluralize
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
88
AngularJS. Pierwsze kroki
Popatrzmy na jeszcze jeden przykład. Tym razem użyjemy naszej dyrektywy w nieco
inny sposób, modyfikując listę wyboru.
W kodzie HTML:
<select ng-model="selectedNumber">
<option value="">Wybierz liczbę produktów ...</option>
<option
ng-repeat="number in [1, 2, 3, 4]"
ng-pluralize
count="number"
when="{1: '{{number}} produkt', other: '{{number}} produkty'}"
>{{number}}</option>
</select>
Wybrano: {{selectedNumber}}
Ta prosta modyfikacja wyświetla listę rozwijaną z kolejnymi numerami. Do każdego
z numerów dodane jest słowo „produkt” lub „produkty”, zależnie od kontekstu.
Dyrektywa ngReadonly
ngReadonly — dyrektywa przyzwalająca na ustawienie atrybutu "readonly".
<input ng-model="name" ng-readonly="true"/>
W przypadku wyrażenia zwracającego true element jest dostępny tylko do odczytu.
Dyrektywa ngStyle
ngStyle — dyrektywa pozwalająca na ustawienie stylu CSS w danym elemencie
HTML warunkowo. Zobaczmy, jak wygląda jej zastosowanie.
W kodzie HTML:
<h1 data-ng-style="redStyle">Text</h1>
W kodzie JS:
$scope.redStyle = { color: 'red' };
Dyrektywa ngSubmit
ngSubmit — dyrektywa pozwalająca na wiązanie w przypadku wysłania formularza
i zapobiegająca jednocześnie domyślnemu przeładowaniu strony, ale tylko wtedy, gdy
formularz nie ma zdefiniowanego atrybutu 'action', 'data-action' lub 'x-action'.
Dyrektywa uruchamiana jest z priorytetem 0. Zobaczmy, jak to działa w praktyce.
W kodzie HTML:
<form data-ng-submit="submit()">
<input type="text" data-ng-model="text" name="text" />
<input type="submit" id="submit" value="Wyślij" />
</form>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
89
W kodzie JS:
$scope.submit = function () {
if ($scope.text) {
console.log($scope.text);
}
else{
console.log('Brak');
}
};
Klikając przycisk Wyślij, nie spowodujemy przeładowania strony, lecz wywołamy
funkcję submit.
Dyrektywa ngSwitch
ngSwitch — dyrektywa ta służy do warunkowego zmieniania struktury drzewa DOM.
ngSwitch przyjmuje wyrażenie, na bazie którego (korzystając z pomocy ng-switch-when
oraz ng-switch-default) manipuluje elementami w drzewie. Pozwala na wyświetlanie
i ukrywanie ich w zależności od rezultatu wyrażenia.
Inaczej mówiąc, ngSwitch umożliwia wstawianie na sztywno warunków w naszym szablonie. Uwaga, dyrektywa tworzy nowy scope. Warto też wiedzieć, że jest uruchamiana
z priorytetem 1200. Posłużmy się prostym przykładem, by lepiej wyjaśnić zasadę
działania.
W kodzie HTML:
wpisz liczbę z zakresu od 1 do 5:<br />
<input data-ng-model="number" class="form-control" />
<div data-ng-switch="" data-on="number">
<div data-ng-switch-when="1">Szablon {{number}}</div>
<div data-ng-switch-when="2">Szablon {{number}}</div>
<div data-ng-switch-when="3">Szablon {{number}}</div>
<div data-ng-switch-when="4">Szablon {{number}}</div>
<div data-ng-switch-when="5">Szablon {{number}}</div>
<div data-ng-switch-default="">Brak</div>
</div>
W zależności od wpisanej liczby AngularJS wyświetla właściwy szablon.
Dyrektywa ngTransclude
ngTransclude — dyrektywa ta wyznacza punkt wstawienia dla nadpisanego DOM,
dyrektywy nadrzędnej używającej transkluzji. Możemy w ten sposób poszerzać elementy
DOM o nowe wartości.
Szerzej omówimy ten przypadek przy okazji dyrektyw tworzonych przez użytkownika.
Na tym etapie możemy zobaczyć różnicę działania dwóch bardzo podobnych dyrektyw,
pokazaną na listingu 5.14. Jedna z nich ma ustawiony parametr transclude na true, a druga
na false.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
90
AngularJS. Pierwsze kroki
Listing 5.14. Dyrektywa ngTransclude
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>AngularJS</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div data-test-transclude-false="" class="panel panel-default">
<div class="panel-body">{{testData}}</div>
</div>
<div data-test-transclude-true="" class="panel panel-default">
<div class="panel-body">{{testData}}</div>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.testData = 'Jakiś tekst';
});
app.directive('testTranscludeFalse', function () {
return {
restrict: 'A',
template: '<div><span>Ten tekst zostanie zamieniony</span><div>Ten tekst
nie zostanie zmieniony</div></div>',
transclude: false,
link: function (scope, element, attrs, ctrl, transclude) {
element.find('span').replaceWith(transclude());
}
};
});
app.directive('testTranscludeTrue', function () {
return {
restrict: 'A',
template: '<div><span>Ten tekst zostanie zamieniony</span><div>Ten tekst
nie zostanie zmieniony</div></div>',
transclude: true,
link: function (scope, element, attrs, ctrl, transclude) {
element.find('span').replaceWith(transclude());
}
};
});
</script>
</body>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
91
Powyższy przykład zawiera dwie niewbudowane dyrektywy: testTranscludeFalse
oraz testTranscludeTrue. Więcej informacji o możliwościach samodzielnego tworzenia
dyrektyw znajduje się w rozdziale 6., „Dyrektywy szyte na miarę”. Ten przykład niech
posłuży tylko jako obraz możliwości samej dyrektywy ngTransclude.
Dyrektywa ngValue
ngValue — dyrektywa ta wiąże wartość input[select] lub input[radio], gdy element
jest wybrany. ngValue jest szczególnie użyteczna w przypadku dynamicznego generowania list bądź radio-buttonów. Poniższy przykład rozwieje wszelkie wątpliwości.
W kodzie HTML:
<label ng-repeat="number in [1,2,3,4,5,6]" for="{{number}}">{{number}}
<input type="radio"
ng-model="dd.favorite"
ng-value="number"
id="{{number}}"
name="favorite">
</label><div>Wybrany numer: {{dd.favorite}}</div>
W kodzie JS:
$scope.dd = { favorite: '3' };
Dyrektywa script
script — dyrektywa ta ładuje zawartość <script> do $templateCache, który może być
używany przez ngInclude, ngView lub dyrektywy. Typ elementu <script> musi być
określony jako text/ng-template.
Posłużmy się przykładem, który pozwoli lepiej wyjaśnić możliwości dyrektywy script
oraz pokaże, w jaki sposób możemy ją wykorzystać w naszej codziennej pracy. Tym
razem stworzymy sobie aplikację zawierającą proste menu, do czego zastosujemy
style dobrze znanego już bootstrapa. Klikając poszczególne przyciski, użytkownik
zobaczy treść zawartą w odpowiednich szablonach (listing 5.15). Każdy szablon sterowany jest przez swój własny kontroler.
Listing 5.15. Dyrektywa script
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>script</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div>
<h3>{{defaultValue}}</h3>
</div>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
92
AngularJS. Pierwsze kroki
<div class="btn-group">
<a data-ng-click="currentTpl='/left.html'" class="btn btn-default">
<span class="glyphicon glyphicon-align-left"></span></a>
<a data-ng-click="currentTpl='/center.html'" class="btn btn-default">
<span class="glyphicon glyphicon-align-center"></span></a>
<a data-ng-click="currentTpl='/right.html'" class="btn btn-default">
<span class="glyphicon glyphicon-align-right"></span></a>
<a data-ng-click="currentTpl='/justify.html'" class="btn btn-default">
<span class="glyphicon glyphicon-align-justify"></span></a>
</div>
<div id="tpl-content" ng-include src="currentTpl"></div>
</div>
<script type="text/ng-template" id="/left.html">
<div data-ng-controller="leftCtrl">
<p class="text-left">{{leftValue}}</p></div>
</script>
<script type="text/ng-template" id="/center.html" >
<div data-ng-controller="centerCtrl">
<p class="text-center">{{centerValue}}</p></div>
</script>
<script type="text/ng-template" id="/right.html">
<div data-ng-controller="rightCtrl">
<p class="text-right">{{rightValue}}</p></div>
</script>
<script type="text/ng-template" id="/justify.html">
<div data-ng-controller="justifyCtrl">
<p class="text-justify">{{justifyValue}}</p></div>
</script>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.defaultValue = "Tekst z kontrolera domyślnego";
});
app.controller('leftCtrl', function ($scope) {
$scope.leftValue = "Kliknąłeś przycisk 'left'";
});
app.controller('centerCtrl', function ($scope) {
$scope.centerValue = "Kliknąłeś przycisk 'center'";
});
app.controller('rightCtrl', function ($scope) {
$scope.rightValue = "Kliknąłeś przycisk 'right'";
});
app.controller('justifyCtrl', function ($scope) {
$scope.justifyValue = "Kliknąłeś przycisk 'justify'";
});
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
93
</script>
</body>
</html>
Możemy oczywiście użyć również jednego kontrolera do obsługi poszczególnych
szablonów; kontrolery 'leftCtrl', 'centerCtrl', 'rightCtrl', 'justifyCtrl' są potomkami kontrolera 'defaultCtrl', co oznacza, że każdy z nich może korzystać z jego zasięgu.
Całość prezentuje rysunek 5.15.
Rysunek 5.15. Dyrektywa script
Dyrektywa select
select — dyrektywa pozwalająca Angularowi na wiązanie HTML-owskiego elementu
select. Wykorzystując ngOptions, możemy dynamicznie generować listę <option>
dla elementu <select>, opartą na tablicy lub obiekcie. W momencie, gdy zostanie wybrana jedna z opcji menu <select>, element tablicy bądź właściwość obiektu zostają powiązane z modelem zdefiniowanym w ngModel.
Nasuwa się pytanie, czy możemy dynamicznie tworzyć elementy <option> przy użyciu omawianej już dyrektywy ngRepeat. Oczywiście możemy — AngularJS daje nam
wiele możliwości dynamicznego tworzenia obiektów. Poniższy przykład pokaże, jak
możemy osiągnąć ten sam efekt przy pomocy znanej już dyrektywy ngRepeat oraz
omawianej właśnie select.
W kodzie HTML:
 przy wykorzystaniu ng-repeat:
<div>
<select ng-model="name">
<option value="">Wybierz nazwę</option>
<option value="{{name}}" ng-repeat="name in mountainsList">{{name}}</option>
</select>
</div>
 przy wykorzystaniu ng-options:
<div>
<select ng-model="name" name="temp" ng-options="name as name for name
in mountainsList">
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
94
AngularJS. Pierwsze kroki
<option value="">Wybierz nazwę</option>
</select>
</div>
<div><h1>{{name}}</h1></div>
W kodzie JS:
$scope.mountainsList = ['Mount Everest', 'K2', 'Kangczendzonga', 'Lhotse', 'Makalu'];
Co ciekawe, w obydwu przypadkach działa podwójne wiązanie. Wybór elementu z pierwszej listy skutkuje natychmiastową zmianą w drugiej i na odwrót — wybór w drugiej
skutkuje automatyczną zmianą w pierwszej. Dodatkowo nazwa wybranej góry wyświetlana jest poniżej.
Jak widać powyżej, możemy w bardzo prosty sposób budować dynamiczne listy rozwijane. Wróćmy jeszcze na chwilę do ngRepeat — możemy z niej korzystać w przypadkach, gdy nasza tablica zawiera tylko ciągi znaków. Jeżeli zawiera ona obiekty, musimy użyć dyrektywy select. Przyjrzyjmy się kolejnemu listingowi, 5.16, tym razem
trochę bardziej złożonemu. Efekt wykonania programu prezentuje rysunek 5.16.
Listing 5.16. Dyrektywa select
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>AngularJS - dyrektywa select</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div>
<h3>Nazwa góry: {{climbing.currentMountain.mountain}}, stopień
trudności: {{climbing.grade}} </h3>
<div>
Wybierz nazwę
<select data-ng-model="climbing.currentMountain"
data-ng-options="mountain.mountain +' (' + mountain.metres
+ ')' group
by mountain.country for mountain in mountainsList"></select>
Wybierz stopień trudności
<select data-ng-model="climbing.grade"
data-ng-options="g for g in grades"></select>
</div>
</div>
<pre>{{climbing|json}}</pre>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/
bootstrap.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script>
var app = angular.module('app', []);
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
app.controller('defaultCtrl', function ($scope) {
$scope.grades = [
'Bardzo łatwe',
'Łatwe',
'Trudne',
'Bardzo trudne'
];
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850, country: 'Nepal-Chiny' },
{ mountain: "K2", metres: 8611, country: 'Pakistan-Chiny' },
{ mountain: "Kanczendzonga", metres: 8598, country: 'Nepal-Indie' },
{ mountain: "Lhotse", metres: 8501, country: 'Nepal-Chiny' },
{ mountain: "Makalu", metres: 8463, country: 'Nepal-Chiny' },
{ mountain: "Czo Oju", metres: 8201, country: 'Nepal-Chiny' },
{ mountain: 'Dhaulagiri', metres: 8167, country: 'Nepal' },
{ mountain: 'Manaslu', metres: 8163, country: 'Nepal' },
{ mountain: 'Nanga Parbat', metres: 8125, country: 'Pakistan' },
{ mountain: 'Annapurna', metres: 8091, country: 'Nepal' },
{ mountain: 'Sziszapangma', metres: 8012, country: 'Chiny' }
];
$scope.climbing = {
grade: $scope.grades[2],
currentMountain: $scope.mountainsList[1]
};
});
</script>
</body>
</html>
Rysunek 5.16. Dyrektywa select
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
95
96
AngularJS. Pierwsze kroki
Powyższy przykład doskonale obrazuje potęgę dyrektywy select. Wykorzystując
dwie różniące się strukturą tablice, tworzymy listy rozwijane. Zmiana w każdej z nich
skutkuje automatycznym wiązaniem, którego efekt widać w nagłówku <h3> oraz w elemencie <pre>. Nazwy gór zostały pogrupowane według pola „państwo”, a wyświetlanie
zostało rozszerzone o wysokość.
Warto przyjrzeć się bliżej konstrukcji poszczególnych wyrażeń w ng-options. Należy
pamiętać, że nie są one parsowane przez $parse. Wyrażenia stosowane w ng-option
są dostosowane specjalnie do wymagań dyrektywy select.
Zmieńmy teraz naszą tablicę w obiekt.
W kodzie HTML:
<select data-ng-model="gradeValue" data-ng-options="name +' ('+ value +')'
for (name, value) in grade"></select>
<select data-ng-model="gradeValue" data-ng-options="name for (name, value)
in grade"> </select>
<div>{{gradeValue}}</div>
W kodzie JS:
$scope.grade = {
'Bardzo łatwe': 'Easy',
'Łatwe':'M',
...
'Trudne':'HVS',
'Bardzo trudne': 'E'
};
Po uruchomieniu powyższego kodu AngularJS dynamicznie utworzy listę rozwijaną
zawierającą nazwy oraz przypisane do nich wartości, np. Trudne (HVS).
Dyrektywa textarea
textarea — dyrektywa pozwalająca Angularowi na wiązanie HTML-owskiego elementu <textarea>. Wiązanie oraz właściwości walidacji są dokładnie takie same jak
w elemencie <input>.
Tak oto dotarliśmy do końca rozważań na temat wbudowanych dyrektyw. Po zapoznaniu się z niniejszym rozdziałem czytelnik otrzymał solidną porcję wiadomości
popartych wieloma zróżnicowanymi przykładami. Przerobienie ich gwarantuje zdobycie rzetelnej wiedzy praktycznej. Zalecamy eksperymentowanie i rozbudowywanie
poszczególnych przykładów.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 5.  Poznaj potęgę dyrektyw
Quiz
1. Która z wbudowanych dyrektyw może być użyta w aplikacji tylko raz?
2. Jaka jest różnica pomiędzy 'ngBind="Test"' a zapisem '{{Test}}'?
3. Jaka jest różnica pomiędzy ngSwitch a ngIf?
4. Czym się różni ng-valid od ng-dirty?
5. W jakich sytuacjach należy stosować dyrektywę ngCloak?
6. Jak działa dyrektywa ngRepeat i z jakim priorytetem jest uruchamiana?
7. Jakie są sposoby deklaracji dyrektywy ngController?
8. Która z dyrektyw pozwala na zmianę wyświetlanych informacji w zależności
od ustawionych reguł lokalizacyjnych?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
97
98
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
AngularJS. Pierwsze kroki
Rozdział 6.
Dyrektywy szyte na miarę
Wprowadzenie
Dyrektywy od kanciastego uczą HTML-a nowych sztuczek. Czy to jest prawda? Przekonajmy się „na własnej skórze”. AngularJS nie byłby tym samym frameworkiem bez
dyrektyw. To one stanowią jego siłę i to one najczęściej przyciągają nowych fanów
tego narzędzia. Omówiliśmy już dosyć szeroko dyrektywy wbudowane.
Na przykładach można było zobaczyć, jak dużo funkcjonalności one pokrywają. Przychodzi jednak taki moment w życiu każdego ngDevelopera, kiedy zaczyna zastanawiać
się nad własnymi, uszytymi na miarę, idealnie dopasowanymi, działającymi dokładnie tak, jak tego wymaga jego jedyna i niepowtarzalna aplikacja. Czy to źle? Czy to
oznacza, że kanciasty jest za mało kanciasty? Nie, absolutnie nie, każdy z nas ma inne
potrzeby, każdy z nas chce stworzyć coś unikalnego, niepowtarzalnego, coś, co przyciągnie i zachwyci użytkownika.
Pewnie zastanawiasz się, co to znaczy, że dyrektywy uczą HTML-a nowych sztuczek.
To bardzo dobre pytanie. AngularJS w fazie kompilacji ($compile) skanuje drzewo DOM
dokumentu HTML, a następnie w miejscach wystąpień dyrektyw podpina funkcjonalności na poziomie naszego drzewa DOM. Funkcjonalności te obejmują sam element, dla którego zdefiniowaliśmy dyrektywę, oraz wszystkie jego dzieci. Dyrektywy
pozwalają w ten sposób budować komponenty gotowe do wielokrotnego użytku. Umożliwiają też manipulowanie drzewem DOM, co daje nam bardzo duże możliwości ingerencji w znany i lubiany HTML.
Pierwsza własna dyrektywa
Jak już wiesz, Angular posiada wiele wbudowanych dyrektyw, a jednocześnie zaprojektowany jest tak, byśmy mogli tworzyć nasze własne. Dyrektywę możemy określić
jako komponent czy element UI wielokrotnego użytku. Stwórzmy naszą pierwszą dyrektywę, nasz pierwszy uszyty na miarę element UI.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
100
AngularJS. Pierwsze kroki
W kodzie HTML:
<div my-first-directive></div>
W kodzie JS:
var app = angular.module('app', []);
angular.module('app.directives', []).
directive('myFirstDirective', function (injectables) {
return function (scope, element, attrs) {
// zrób coś
}
});
A teraz pójdźmy o krok dalej i „uszyjmy” naszą pierwszą dyrektywę — pełny kod na
listingu 6.1.
Listing 6.1. Pierwsza własna dyrektywa
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS - pierwsza dyrektywa szyta na miarę</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/
3.2.0/css/bootstrap.css" />
</head>
<body>
<div class="container">
<div data-color-changer="red"><h1>Tło czerwone</h1></div>
<div data-color-changer="yellow"><h1>Tło żółte</h1></div>
<div data-color-changer="green"><h1>Tło zielone</h1></div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.directive("colorChanger", function () {
return function (scope, element, attrs) {
element.bind("mouseenter", function () {
element.css("background", attrs.colorChanger);
});
element.bind("mouseleave", function () {
element.css("background", "none");
});
}
});
</script>
</body>
</html>
Powyższy przykład przedstawia bardzo prostą dyrektywę zmieniającą tło po najechaniu
myszą na dany div. Jak łatwo zauważyć, wykorzystaliśmy funkcję element.bind. Jej
użycie jest możliwe dzięki wbudowanej w AngularJS bibliotece jqLite. Więcej informacji
na jej temat znajduje się w rozdziale 8. Wynik działania naszego programu można
zobaczyć na rysunku 6.1.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 6.  Dyrektywy szyte na miarę
101
Rysunek 6.1.
Pierwsza własna
dyrektywa
Właściwości
Zanim przejdziemy do rozłożenia dyrektyw na czynniki pierwsze, warto zwrócić uwagę
na fakt, że konstruktor dyrektywy zwraca funkcję, która pobiera element i modyfikuje
go zgodnie z parametrami określonymi w tym zakresie. W przykładzie z listingu 6.1
nazwa naszej dyrektywy została użyta jako atrybut. Jest to domyślne działanie AngularJS,
niewymagające deklaracji. Przejdźmy teraz do bardziej zaawansowanych możliwości
dyrektyw. Listing 6.2 pokazuje możliwości, jakie daje nam AngularJS przy tworzeniu
własnych dyrektyw.
Listing 6.2. Przykładowe właściwości przyjmowane przez tworzoną dyrektywę
var app = angular.module('app', []);
// deklarujemy dyrektywę
app.directive("test", function() {
return {
restrict: "E",
// dyrektywa jest elementem, nie atrybutem
require: '^test2', //wymagane inne dyrektywy
scope: {
// tworzymy izolowany zakres dyrektywy (isolated scope)
name: "@",
// 'name' przekazywane przez wartość (string, w jedną stronę)
number: "=", // 'number' przekazywane przez referencję (dwukierunkowo)
save: "&"
// 'save' akcja
},
template:
// szablon HTML (możesz użyć w nim powyższego scope)
"<div>" +
" {{name}}: <input ng-model='number' />" +
" <button ng-click='save()'>Zapisz</button>" +
"</div>",
replace: true,
// zastępuje oryginalny element DOM szablonem danej dyrektywy
transclude: false, // nie kopiuje oryginalnego HTML-a
controller: [ "$scope", function ($scope) {
}],
link: function (scope, element, attrs, controller) {
}
}
});
Konstruktor dyrektywy zwraca obiekt z kilkoma właściwościami. Warto się im przyjrzeć
bliżej. Pierwsza właściwość określa sposób, w jaki będziemy wywoływać dyrektywę.
Przyjmuje ona następujące opcje: "A", "E", "C", "M". Pierwsza z nich jest domyślna i oznacza atrybut, druga, równie często używana "E" , oznacza element. Trzecią i czwartą
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
102
AngularJS. Pierwsze kroki
pominiemy z premedytacją — nie zalecamy ich stosowania, gdyż powstały przed dyrektywami ng-repeat-start i ng-repeat-end, a wykorzystując je, zamazujemy kod.
Kolejna właściwość, require, pozwala określić zależności w postaci innych dyrektyw,
które wymagane są do działania naszej. W tym przypadku dyrektywa test wymaga
test2 do prawidłowego działania.
<div test test2="text"></div>
Jeśli dyrektywa test2 nie zostanie wywołana, to otrzymamy informację o błędzie.
Znak ^ umieszczony przed nazwą test2 informuje AngularJS, by szukał wywołania
ngModel również poza elementem, w którym została zdefiniowana dyrektywa test.
Istnieje także druga opcja ?, która spowoduje, że w przypadku braku zdefiniowanej
zależności nie otrzymamy błędu kompilacji dyrektywy.
Przejdźmy do kolejnej właściwości scope — umożliwia ona tworzenie izolowanego
zakresu zmiennych (lokalny scope), który jest niezbędny w przypadku generowania
elementów wielokrotnego użytku.
scope: {
parameter1: "@",
parameter2: "=",
parameter3: "&"
},
Dla tak zdefiniowanego scope możemy stworzyć następujące wywołanie:
<div test
parameter1="{{ nazwa }}"
parameter2="nazwa2"
parameter3="nazwa3()">
</div>
Przejdźmy teraz do realnego przykładu. Załóżmy, że chcemy zbudować prosty system
oceny wydarzeń — listing 6.3. Użytkownik powinien mieć możliwość kliknięcia na
przycisk plus i minus, by zmienić ocenę. Domyślnie ustawimy ocenę na 5. Dyrektywa
jest wielokrotnego użytku.
Listing 6.3. System ocen
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS - pierwsza dyrektywa szyta na miarę</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.css" />
</head>
<body>
<div class="container">
<form>
<h3>Oceń wydarzenie</h3>
<div class="row">
<div class="col-md-4">
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 6.  Dyrektywy szyte na miarę
103
<events-evaluation text="Lokalizacja" data-ng-model="number">
</events-evaluation>
</div>
<div class="col-md-4">
<events-evaluation text="Hotel" data-ng-model="number">
</events-evaluation>
</div>
<div class="col-md-4">
<events-evaluation text="Jedzenie" data-ng-model="number">
</events-evaluation>
</div>
</div>
</form>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.directive("eventsEvaluation", function () {
return {
restrict: "E",
scope: {
text: "@",
},
template:
"<div>" +
" {{text}}: <input ng-disabled='true' type='number' data-ng-model=
'number' class='form-control' />" +
" <a ng-disabled='number<1' class='btn btn-default' href='#' data-ng-click=
'reduce()'><span class='glyphicon glyphicon-minus'></span></a> " +
" <a ng-disabled='number>9' class='btn btn-default' href='#' data-ng-click=
'increase()'><span class='glyphicon glyphicon-plus'></span></a> " +
"</div>",
replace: true,
transclude: false,
controller: function ($scope) {
$scope.number = 5;
$scope.increase = function () {
$scope.number++;
};
$scope.reduce = function () {
$scope.number--;
};
},
}
});
</script>
</body>
</html>
Tym sposobem stworzyliśmy dość zaawansowany system ocen. Rysunek 6.2 prezentuje efekt działania naszego programu.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
104
AngularJS. Pierwsze kroki
Rysrunek 6.2.
Wynik wywołania
strony z systemem
ocen
Nasz własny element HTML prezentuje się następująco:
<events-evaluation text="Lokalizacja" data-ng-model="number"></events-evaluation>
<events-evaluation text="Hotel" data-ng-model="number"></events-evaluation>
<events-evaluation text="Jedzenie" data-ng-model="number"></events-evaluation>
Parametr text przekazuje do szablonu nazwę wyświetlaną nad oceną. Listing 6.4
przedstawia kod generowany przez każdy z naszych elementów — aby go podejrzeć,
należy zbadać dany element w przeglądarce.
Listing 6.4. Kod generowany przez kompilator AngularJS
<div text="Lokalizacja" data-ng-model="number" class="ng-binding ng-isolate-scope
ng-pristine ng-untouched ng-valid">
Lokalizacja:
<input ng-disabled="true" type="number" data-ng-model="number" class=
"form-control ng-pristine ng-untouched ng-valid" disabled="disabled">
<a ng-disabled="number<1" class="btn btn-default" href="#" data-ng-click=
"reduce()">
<span class="glyphicon glyphicon-minus"></span></a>
<a ng-disabled="number>9" class="btn btn-default" href="#" data-ng-click=
"increase()" disabled="disabled">
<span class="glyphicon glyphicon-plus"></span></a>
</div>
Dynamicznie zmienia się tylko zawartość parametru text. To jest oczywiście bardzo
prosty przykład, ale pokazuje, jaka siła drzemie w dyrektywach. Raz napisaną dyrektywę
możemy wykorzystywać dowolną liczbę razy. Sama dyrektywa może być o wiele bardziej
skomplikowana, co nie będzie miało wpływu na ostateczną czytelność naszego kodu, ponieważ my zawsze będziemy używać jedynie <events-evaluation></events-evaluation>.
Wróćmy jeszcze do listingu 6.2 i omówmy pozostałe właściwości. Po scope przyszedł
czas na replace. Przy ustawieniu tej właściwości na true AngularJS zmienia ciało
elementu, dla którego jest zdefiniowany, na szablon dyrektywy. Jeśli wartość zmienimy na false, zostanie zwrócony rezultat, jak na listingu 6.5. Jak we wcześniejszym
przypadku również tutaj należy zbadać dany element w przeglądarce.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 6.  Dyrektywy szyte na miarę
105
Listing 6.5. Kod generowany przez kompilator AngularJS w przypadku ustawiania wartości replace
na false
<events-evaluation text="Lokalizacja" data-ng-model="number"
class="ng-isolate-scope ng-pristine ng-untouched ng-valid">
<div class="ng-binding">
Lokalizacja:
<input ng-disabled="true" type="number" data-ng-model="number"
class="form-control ng-pristine ng-untouched ng-valid" disabled="disabled">
<a ng-disabled="number<1" class="btn btn-default"
href="#" data-ng-click="reduce()">
<span class="glyphicon glyphicon-minus"></span></a>
<a ng-disabled="number>9" class="btn btn-default" href="#"
data-ng-click="increase()">
<span class="glyphicon glyphicon-plus">
</span></a> </div></events-evaluation>
Należy jednak pamiętać, że w takiej sytuacji starsze przeglądarki mogą mieć problem
z wyświetlaniem „nieznanych” im elementów, np. <nazwa-wlasna></nazwa-wlasna>.
Czy warto wspierać starsze przeglądarki, czy nie warto? Oto jest pytanie. Odpowiedź
na nie każdy musi znaleźć sam, a zależy ona w dużej mierze od tego, do kogo kierujemy
nasz serwis.
Kolejna właściwość, której poświęcimy naszą uwagę, nazywa się transclude. W przypadku podania wartości true AngularJS pobierze ciało elementu, w którym została zdefiniowana dyrektywa, i wstawi je do szablonu.
$scope vs. scope
Zastanawiasz się na pewno, jaka jest różnica pomiędzy scope z dyrektywy a $scope
z kontrolera. Jeśli w kontrolerze wydrukujemy sobie zawartość $scope i w dyrektywie
zrobimy to samo dla scope, otrzymamy dokładnie taki sam wynik — obydwa wydruki
będą identyczne i będą dotyczyć tego samego $scope. Najlepiej obrazuje to listing 6.6.
Listing 6.6. Kod prezentujący różnicę pomiędzy $scope a scope
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<meta charset="utf-8">
<title>AngularJS - $scope vs. scope</title>
</head>
<body ng-controller="defaultCtrl">
<div test-scope></div>
<div test-scope2></div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
console.log('Controller=', $scope);
});
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
106
AngularJS. Pierwsze kroki
app.directive("testScope", function () {
return {
link: function (scope) {
console.log('testScope=', scope);
}
};
});
app.directive("testScope2", function () {
return {
scope: {},
link: function (scope) {
console.log('testScope2=', scope);
}
};
});
</script>
</body>
</html>
Wynik jest dokładnie taki, jakiego się spodziewaliśmy. Scope z dyrektywy testScope
nie jest izolowany, w związku z czym wynik jej wydruku jest identyczny jak wydruk z kontrolera. Dodając w drugiej dyrektywie właściwość scope: {}, spowodowaliśmy, że jej scope został wyizolowany. Co to oznacza? To, że postawiliśmy mur pomiędzy
$scope i scope.
Rysunek 6.3.
Widok wydruku z konsoli,
scope z kontrolera
i z pierwszej dyrektywy
mają to samo id:2
(to ten sam scope),
scope z drugiej
dyrektywy ma id:3
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 6.  Dyrektywy szyte na miarę
107
Skoro jesteśmy przy link, warto zapamiętać, że kolejność parametrów zwracanej
funkcji ma znaczenie i nie można jej zmienić.
link: function (scope, element, attrs) {}
Jeśli zamienimy kolejność, np. w taki sposób:
link: function (element, attrs, scope) {}
a następnie spróbujemy wydrukować zawartość element, otrzymamy zawartość scope.
Oznacza to, że nazewnictwo nie ma znaczenia, liczy się kolejność. Równie dobrze zamiast
naszego scope moglibyśmy wstawić $scope.
Kolejność zawsze jest następująca: scope, element, atrybuty. Jest to zupełne przeciwieństwo tego, z czym mamy do czynienia w kontrolerach, w których nie możemy
zmienić nazwy wstrzykiwanego $scope, nie musimy też dbać o kolejność. Dzieje się
tak dlatego, że $scope wstrzykiwany do kontrolera jest przedefiniowany, a co za tym
idzie, jego nazwa jest niezmienna.
Quiz
1. Co to jest dyrektywa?
2. Czy dyrektywy mogą być wielokrotnego użytku?
3. Do czego służy właściwość restrict?
4. Czy dyrektywa może mieć własny kontroler?
5. Do czego służy link?
6. Jak jest różnica pomiędzy $scope i scope?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
108
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
AngularJS. Pierwsze kroki
Rozdział 7.
Filtry
Wprowadzenie
Najprościej rzecz ujmując — filtry służą do formatowania danych wyświetlanych użytkownikowi. Filtr bierze jedną tablicę i tworzy z niej drugą. Angular oferuje kilka wbudowanych filtrów oraz możliwość łatwego tworzenia własnych.
Filtry wykonują trzy zadania:
 formatowanie,
 sortowanie danych,
 filtrowanie danych.
W HTML-u wywołujemy filtr, używając znaku | (pipe) wewnątrz definicji szablonu
{{}}. O szablonach i wyrażeniach mówiliśmy już przy okazji podwójnego wiązania
w rozdziale 1.
Można stosować wiele filtrów w tym samym czasie, rozdzielając je kolejnymi znakami |.
{{ wyrażenie | filtr1 | filtr2 | ... }}
Filtry mogą posiadać wiele argumentów.
{{ wyrażenie | filtr:argument1:argument2:... }}
Więcej informacji na ten temat znajduje się w dalszej części tego rozdziału, poświęconej budowaniu własnych filtrów.
Na poniższym przykładzie można zobaczyć nasz pierwszy filtr w akcji.
<div ng-app="app">
<div ng-controller="demoCtrl">
<input ng-model="nazwa" type="text" />
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
110
AngularJS. Pierwsze kroki
<p>{{nazwa | lowercase}}</p>
</div>
</div>
Czy jest możliwe dodawanie filtrów również po stronie JavaScriptu? Oczywiście, że
tak! Wystarczy użyć serwisu $filter.
app.controller('demoCtrl', ['$scope', '$filter',
function ($scope, $filter) {
$scope.nazwa = $filter('lowercase')('Chcę być małą literą!');
}]);
Filtry możemy wstrzykiwać do kontrolerów, serwisów i dyrektyw. Daje nam to ogromne możliwości manipulacji danymi, zanim zostaną wyświetlone użytkownikowi. W dalszej części rozdziału pokażemy dokładnie, jak je wykorzystać.
Filtry wbudowane
Zatrzymajmy się na chwilę przy filtrach wbudowanych. Możemy podzielić je na:
 operacje na stringach,
 liczbowe,
 operacje na datach,
 JSON,
 filtry dyrektywy ng-repeat.
Operacje na stringach
lowercase — zmienia litery w wyrażeniu na małe.
Przykład zastosowania w HTML-u:
{{wyrażenie | lowercase}}
Przykład zastosowania w JavaScripcie:
$filter('lowercase’) (‘Wyrażenie do zmiany na małe litery')
uppercase — zmienia litery w wyrażeniu na duże.
Przykład zastosowania w HTML-u:
{{ wyrażenie | uppercase}}
Przykład zastosowania w JavaScripcie:
$filter('uppercase')('Wyrażenie do zmiany na duże litery')
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 7.  Filtry
111
Liczbowe
number — formatuje liczbę jak tekst. Jako parametr przekazujemy liczbę miejsc po
przecinku, do jakiej ma zostać zaokrąglona liczba. Jeśli w wyrażeniu przekażemy znak
nienumeryczny, filtr zwróci pusty string.
{{ 123456789 | number }} <!-- 1,234,567,890 -->
{{ 1.234567 | number:2 }} <!-- 1.23 -->
{{1234.777777|number:3}} <!-- 1,234.778 -->
{{-12|number:4}} <!-- -12.0000 -->
Przykład zastosowania w HTML-u:
{{ wyrażenie | number : miejsca_po_przecinku}}
Przykład zastosowania w JavaScripcie:
$filter('number')(number, miejsca_po_przecinku)
currency — formatuje liczbę jako walutę. Jako parametr przyjmuje symbol lub identyfikator waluty. W przypadku braku parametru zostanie zwrócona domyślna waluta lokalna.
Przykład zastosowania w HTML-u:
{{ wyrażenie | currency : symbol_lub_identyfikator}}
Przykład zastosowania w JavaScripcie:
$filter('currency')(kwota, symbol_lub_identyfikator)
Wykorzystajmy nasz kod w praktyce. Ponieważ nie podaliśmy parametru waluty, Angular
wyświetlił nam wartość domyślną waluty w $.
{{ 1000000 | currency }} <!-- $1,000,000.00 -->
Zanim przejdziemy dalej, powiemy kilka słów o internacjonalizacji w filtrach. Angular
wspiera internacjonalizację w filtrach dla dat, waluty i liczb. Jak możemy w prosty sposób
zmienić walutę $ na zł? Wystarczy dołączenie biblioteki i18n/angular-locale_pl-pl.js.
Poniżej znajduje się przykład zastosowania biblioteki i18n/angular-locale_pl-pl.js.
Dołączamy bibliotekę z polską lokalizacją (należy pamiętać, aby zawsze była dołączana
po Angularze):
<script src="https://code.angularjs.org/1.4.0-beta.5/i18n/angular-locale_pl-pl.js">
</script>
Domyślnie wyświetlany jest polski złoty. Jeśli chcemy skorzystać z innych walut,
przekazujemy je w parametrze.
{{
{{
{{
{{
1000000
1000000
1000000
1000000
|
|
|
|
currency
currency
currency
currency
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
}}
: '$'
: '£'
: '€'
<!-- 1,000,000.00 zł -->
}}
<!-- 1,000,000.00 $ -->
}}
<!-- 1,000,000.00 £ -->
}}
<!-- 1,000,000.00 € -->
112
AngularJS. Pierwsze kroki
Operacje na datach
date — formatuje datę w oparciu o dostarczony argument. Jeżeli nie ma argumentu,
wyświetlany jest domyślny format mediumDate.
Oto wbudowane lokalizowane formaty.
Zacznijmy od stworzenia $scope.teraz i przypiszmy do niego aktualną datę w naszym pliku js.
angular.module('app', [])
.controller('ExampleController', ['$scope', function ($scope) {
$scope.teraz = Date.now();
}]);
Następnie w pliku html wywołajmy filtry.
{{
{{
{{
{{
{{
{{
{{
{{
teraz
teraz
teraz
teraz
teraz
teraz
teraz
teraz
|
|
|
|
|
|
|
|
date:'medium' }} <!-- 19 lip 2014 16:43:36 -->
date:'short' }} <!-- 19.07.2014 16:43 -->
date:'fullDate' }} <!-- sobota, 19 lipca 2014 -->
date:'longDate' }} <!-- 19 lipca 2014 -->
date:'mediumDate' }} <!-- 19 lip 2014 -->
date:'shortDate' }} <!-- 19.07.2014 -->
date:'mediumTime' }} <!-- 16:43:36 -->
date:'shortTime' }} <!-- 16:43 -->
W związku z tym, że korzystamy z i18n/angular-locale_pl-pl.js, format naszej daty
jest dokładnie taki, jakiego oczekujemy. Jeśli nie użyjemy biblioteki lokalizacyjnej,
daty zostaną wyświetlone w formacie domyślnym, anglosaskim.
Mamy też możliwość tworzenia niestandardowych dat.
Czterocyfrowy rok: {{ teraz | date:'yyyy' }} <!-- 2014 -->
Dwucyfrowy rok: {{ teraz | date:'yy' }} <!-- 14 -->
Miesiąc w roku: {{ teraz | date:'MMMM' }} <!-- lipca -->
Miesiąc w roku skrót: {{ teraz | date:'MMM' }} <!-- lip -->
Dwucyfrowy miesiąc: {{ teraz | date:'MM' }} <!-- 07 -->
Miesiąc: {{ teraz | date:'M' }} <!-- 7 -->
Dwucyfrowy dzień: {{ teraz | date:'dd' }} <!-- 09 -->
Dzień:{{ teraz | date:'d' }} <!-- 9 -->
Dzień tygodnia: {{ teraz | date:'EEEE' }} <!-- sobota -->
Dzień tygodnia skrót: {{ teraz | date:'EEE' }} <!-- sob. -->
Dwucyfrowa godzina (00-23) : {{ teraz | date:'HH' }} <!-- 07 -->
Godzina (0-23) : {{ teraz | date:'H' }} <!-- 7 -->
Dwucyfrowa godzina (01-12) :{{ teraz | date:'hh' }} <!-- 05 -->
Godzina (1-12) : {{ teraz | date:'h' }} <!-- 5 -->
Dwucyfrowa minuta: {{ teraz | date:'mm' }} <!-- 03 -->
Minuta: {{ teraz | date:'m' }} <!-- 3 -->
Dwucyfrowa sekunda: {{ teraz | date:'ss' }} <!-- 08 -->
Sekunda: {{ teraz | date:'s' }} <!-- 8 -->
Milisekunda z kropką: {{ teraz | date:'.sss' }} <!-- .700 -->
Milisekunda z przecinkiem: {{ teraz | date:',sss' }} <!-- ,700 -->
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 7.  Filtry
113
Przykłady użycia:
{{teraz | date:'d MMMM yyyy' }} <!-- 19, lipca 2014 -->
{{teraz | date:'d, EEEE' }} <!-- 19, sobota -->
{{teraz | date:'hh:mm:ss.sss' }} <!-- 05:44:13.618 -->
JSON
json — konwertuje obiekt JavaScript do formatu json string.
Przykład zastosowania w HTML-u:
{{ obiekt_javascript | json}}
Przykład zastosowania w JavaScripcie:
$filter('json')(obiekt_javascript)
Filtry dyrektywy ng-repeat
Omawiana w rozdziale 5. dyrektywa ng-repeat jest jednym z kluczowych elementów
AngularJS. Przy jej użyciu możemy iterować przez kolekcje (obiekty lub tablice) i powtarzać fragmenty kodu dla każdego z elementów.
Filtr orderBy
orderBy — sortuje pola tablicy. Kierunek sortowania możemy ustalić na dwa sposoby. Pierwszym jest zastosowanie znaku + bądź – przed nazwą pola, np. -nazwa_pola
(gdzie plus to domyślne sortowanie rosnące, a minus to sortowanie malejące). Druga
metoda to ustawienie kierunek_sortowania na true lub false (gdzie true to domyślne
sortowanie rosnące, a false to sortowanie malejące).
Przykład zastosowania w HTML-u:
{{ wyrażenie | orderBy : nazwa_pola : kierunek_sortowania}}
Przykład zastosowania w JavaScripcie:
$filter('orderBy')(tablica, nazwa_pola, kierunek_sortowania)
Poniższy przykład obrazuje działanie filtru orderBy. W pliku HTML tworzymy tabelę,
która posłuży nam jako szablon.
<div data-ng-controller="FiltryCtrl">
<table class="table">
<thead>
<tr>
<td>Imię</td>
<td>Wiek</td>
<td>Miasto</td>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="uzytkownik in uzytkownicy | orderBy: [ 'imie', '-wiek', 'miasto']">
<td>{{uzytkownik.imie}}</td>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
114
AngularJS. Pierwsze kroki
<td>{{uzytkownik.wiek}}</td>
<td>{{uzytkownik.miasto}}</td>
</tr>
</tbody>
</table>
</div>
Dyrektywa ng-repeat pobiera dane z tablicy użytkowników, następnie posługując się
filtrem orderBy, sortuje je w następujący sposób:
 imię — rosnąco;
 wiek — malejąco;
 miasto — rosnąco.
Należy pamiętać, że sortowanie odbywa się w kolejności pól przekazywanych do filtru, czyli na początku sortowane jest imię, następnie wiek i na końcu miasto.
Jak już wspominaliśmy, dyrektywa ng-repeat jest bardzo użyteczna i daje ogromne
możliwości. Stosunkowo małym nakładem pracy możemy stworzyć rozbudowaną aplikację. Jednym z problemów, na jakie możemy się natknąć, jest aspekt optymalizacji. ngrepeat najlepiej sprawdza się w małych i średnich tablicach. Jeśli mamy do obróbki
zbiory liczone w tysiącach rekordów, warto zastanowić się nad paginacją i pobieraniem
wybranych rekordów na żądanie.
W pliku js definiujemy tablicę użytkowników:
var app = angular.module('app', []);
app.controller('FiltryCtrl', function ($scope) {
$scope.uzytkownicy = [
{ "id": "1", "imie": "Marcin", "wiek": "18", "miasto": "Warszawa" },
{ "id": "2", "imie": "Adrian", "wiek": "22", "miasto": "Radom" },
{ "id": "3", "imie": "Arkadiusz", "wiek": "19", "miasto": "Koszalin" },
{ "id": "4", "imie": "Jan", "wiek": "33", "miasto": "Poznań" },
{ "id": "5", "imie": "Longin", "wiek": "45", "miasto": "Gdańsk" },
{ "id": "6", "imie": "Dariusz", "wiek": "31", "miasto": "Kraków" },
{ "id": "7", "imie": "Tomasz", "wiek": "55", "miasto": "Katowice" },
{ "id": "8", "imie": "Paweł", "wiek": "26", "miasto": "Wrocław" },
{ "id": "9", "imie": "Adam", "wiek": "22", "miasto": "Szczecin" }
]});
W wyniku otrzymaliśmy listę użytkowników posortowanych zgodnie z założeniami
naszego filtru:
Imię
Adam
Adrian
Arkadiusz
Dariusz
Jan
Longin
Marcin
Paweł
Tomasz
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Wiek
22
22
19
31
33
45
18
26
55
Miasto
Szczecin
Radom
Koszalin
Kraków
Poznań
Gdańsk
Warszawa
Wrocław
Katowice
Rozdział 7.  Filtry
115
Filtr limitTo
limitTo — kolejny użyteczny filtr dyrektywy ng-repeat, za pomocą którego możemy
ograniczyć liczbę wyświetlanych rekordów. Jeśli mamy np. 10 rekordów, a zależy nam na
wyświetlaniu tylko 2, do akcji wkracza limitTo.
Przykład zastosowania w HTML-u:
{{ limitTo_wyrażenie| limitTo : limit}}
Przykład zastosowania w JavaScripcie:
$filter('limitTo')(input, limit)
Rozbudujmy nasz poprzedni przykład o filtr limitTo i ograniczmy liczbę wyświetlanych rekordów do 2. Nasze wyrażenie będzie się przedstawiać następująco:
data-ng-repeat="uzytkownik in uzytkownicy | orderBy:[ 'imie', '-wiek', 'miasto'] |
limitTo:2"
Po odświeżeniu strony otrzymamy tabelkę z 2 pierwszymi rekordami:
Imię
Adam
Adrian
Wiek
22
22
Miasto
Szczecin
Radom
Filtr filter
filter — to bardzo potężne narzędzie pozwala na wyszukiwanie rekordów tablicy
zawierających określone frazy.
Wyrażeniem może być string, np. Adam, obiekt, np. {imię:"Adam", wiek:"22"}, lub
funkcja function(wartość), która zostanie wywołana dla każdego elementu tablicy.
Zatrzymajmy się jeszcze przez chwilę przy obiektach, w których mamy do dyspozycji
specjalną wartość $. Możemy ją wykorzystać w następujący sposób: {$:"text"}. W tym
przypadku zostaną przeszukane wszystkie pola.
Przykład zastosowania w HTML-u:
{{ filtr_wyrażenie | filter : wyrażenie : komparator}}
Przykład zastosowania w JavaScripcie:
$filter('filter')(tablica, wyrażenie, komparator)
Dodajmy powyższy filtr do naszego przykładu. Załóżmy, że chcemy wyszukać rekordy,
w których występuje fraza 'sz':
data-ng-repeat="uzytkownik in uzytkownicy | orderBy:[ 'imie', '-wiek', 'miasto'] |
limitTo:8 | filter:{$:'sz'}"
W efekcie otrzymujemy posortowaną tablicę z rekordami, których pola zawierają
szukaną frazę.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
116
AngularJS. Pierwsze kroki
Imię
Adam
Arkadiusz
Dariusz
Marcin
Wiek
22
19
31
18
Miasto
Szczecin
Koszalin
Kraków
Warszawa
Jak już niejednokrotnie wspominaliśmy, siłą AngularJS jest podwójne wiązanie. Dyrektywa ng-repeat w połączeniu z filtrem filter jest tego najlepszym dowodem. Załóżmy,
że chcemy ograniczać listę wyświetlanych wyników dynamicznie w zależności od
wpisanego przez użytkownika tekstu w polu Wyszukaj. Na początku dodajmy do naszego
przykładu znacznik:
<input data-ng-model="wyszukaj" />
Poniżej wyświetlamy listę użytkowników.
<div data-ng-repeat="uzytkownik in uzytkownicy | filter:wyszukaj">
<div>{{uzytkownik.imie}}</div>
<div>{{uzytkownik.wiek}}</div>
<div>{{uzytkownik.miasto}}</div>
</div>
W momencie, gdy użytkownik rozpocznie wpisywanie tekstu w polu Wyszukaj, nasza
lista użytkowników zacznie się automatycznie zawężać. To użyteczne rozwiązanie może
nam uprościć życie.
Wiesz już, jak korzystać z wbudowanych filtrów, zróbmy więc kolejny krok i zbudujmy
własny filtr. Przez cały czas pozostajemy przy naszym przykładzie — teraz dołożymy
jeszcze jedno ograniczenie, które pozwoli nam na wyświetlanie rekordów od trzeciego do
piątego. Jak to zrobić? Na początku stwórzmy nowy filtr i nazwijmy go zakres.
app.filter('zakres', function () {
return function (arr, poczatek, koniec) {
return arr.slice(poczatek, koniec);
};
});
Nasze wyrażenie wygląda teraz następująco:
data-ng-repeat="uzytkownik in uzytkownicy | orderBy:[ 'imie', '-wiek', 'miasto'] |
limitTo:7 | filter:{$:'a'} | zakres:poczatek:koniec"
W zależności od tego, jakie wartości przypiszemy zmiennym początek i koniec, otrzymamy odpowiednio wybrane rekordy. Jeżeli zmiennej początek przypiszemy liczbę
ujemną, uzyskamy rekordy liczone od końca tablicy.
Powiedzieliśmy już o filtrach wbudowanych. Wiesz też, jak stworzyć swój własny, szyty
na miarę filtr. Teraz przyszedł czas na moduły, czyli gotowce. AngularJS udostępnia
kilka bardzo przydatnych modułów, z którymi warto się zaprzyjaźnić. Jednym z nich
jest ngSanitize, służący do bezpiecznego analizowania i przetwarzania danych w formacie HTML w naszej aplikacji. Samymi modułami zajmiemy się szerzej w dalszych
częściach książki, na tym etapie skupmy się na filtrze linky.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 7.  Filtry
117
Linky
Filtr linky wyszukuje w tekście linki i zmienia je na format HTML. Obsługuje http, https,
ftp, mailto oraz zwykłe adresy e-mail.
Przykład zastosowania w HTML-u:
<span ng-bind-html="linky_wyrazenie | linky"></span>
Przykład zastosowania w JavaScripcie:
$filter('linky')(tekst, target) // target: _blank, _self, _parent lub _top
Posłużmy się przykładem, który pozwoli Ci lepiej zrozumieć specyfikę filtru linky.
W pliku js definiujemy scope z tekstem zawierającym linki:
var app = angular.module('app', ['ngSanitize']);
app.controller('FiltryCtrl', function ($scope) {
$scope.tekst = 'Tekst zawierający linki: http://angularjs.org/,\n' +
'mailto:[email protected], [email protected],\n' +
'nasz ftp: ftp://127.0.0.1/.';
});
W naszym HTML-u dodajemy dwie rzeczy. Po pierwsze moduł sanitize — pamiętaj,
aby był on zadeklarowany po deklaracji samego AngularJS, a nie przed nią.
<script src="https://code.angularjs.org/1.4.0-beta.5/angular-sanitize.js"></script>
W body strony dodajemy odpowiednio:
<div ng-bind-html="tekst | linky:'_blank'"></div>
Pełny kod aplikacji prezentuje listing 7.1.
Listing 7.1 Filtr linky
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS - Filtr linky</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body>
<div data-ng-controller="defaultCtrl">
<div ng-bind-html="tekst | linky:'_blank'"></div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script src="https://code.angularjs.org/1.4.0-beta.5/angular-sanitize.js">
</script>
<script>
var app = angular.module('app', ['ngSanitize']);
app.controller('defaultCtrl', function ($scope) {
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
118
AngularJS. Pierwsze kroki
$scope.tekst = 'Tekst zawierający linki: http://angularjs.org/,\n' +
'mailto:[email protected], [email protected],\n' +
'nasz ftp: ftp://127.0.0.1/.';
});
</script>
</body>
</html>
Efekt wykonania kodu z listingu 7.1 można zobaczyć na rysunku 7.1.
Rysunek 7.1. Wykorzystanie filtru Linky
Przed rozpoczęciem pisania nowej funkcjonalności warto rozejrzeć się w sieci, bo istnieje bardzo duże prawdopodobieństwo, że znajdziemy gotowy, czasami wymagający
drobnych modyfikacji moduł do natychmiastowego wykorzystania.
Quiz
1. Jakie są trzy podstawowe zadania filtrów?
2. Jak możemy podzielić filtry wbudowane ze względu na ich funkcjonalność?
3. Wymień filtry dyrektywy ng-repeat.
4. Którego z filtrów możemy użyć do posortowania tablicy?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 8.
Funkcje
Wprowadzenie
AngularJS oferuje nam wiele wbudowanych funkcji, które możemy wykorzystywać,
budując nowoczesne, funkcjonalne i łatwe w utrzymaniu aplikacje. Celem tego rozdziału jest zapoznanie czytelnika z najważniejszymi z nich. Przyjrzymy się ich charakterystycznym cechom oraz zobaczymy, jak działają w praktyce.
Opis funkcji
Funkcja angular.bind
angular.bind — zwraca funkcję, która wywołuje inną funkcję, fn, powiązaną z self
(self staje się this dla fn). Opcjonalnie możemy dodać parametr args, który zostanie
powiązany na początku procesu z funkcją. Cecha ta jest również znana jako partial
application. Brzmi to trochę jak masło maślane, zobaczmy więc, jak to działa.
Przykład wykorzystania:
angular.bind(self, fn, args);
W kodzie JS:
$scope.test = {
text: 'Witaj, świecie!',
printText: function () {
console.log('Napis: ' + this.text);
}
};
$scope.test.printText(); // wynik: 'Napis: Witaj, świecie!'
$scope.logText = $scope.test.printText;
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
120
AngularJS. Pierwsze kroki
$scope.logText(); // wynik: undefined
$scope.logText1 = angular.bind($scope.test, $scope.test.printText);
$scope.logText1(); // wynik: 'Napis: Witaj, świecie!'
Jak widać w powyższym przykładzie, wywołanie funkcji w następujący sposób:
$scope.test.printText(); powoduje wyświetlenie logu 'Napis: Witaj, świecie!'.
Jeśli jednak przypiszemy $scope.test.printText; do $scope.logText, tracimy kontekst i nasz this.text jest niezdefiniowany. W kolejnej linii przypisujemy wynik
angular.bind do $scope.logText1 — tym razem nasz log jest już zgodny z oczekiwaniami. Po wczytaniu uzyskamy następujący wynik:
Napis: Witaj, świecie!
Napis: undefined
Napis: Witaj, świecie!
Funkcja angular.bootstrap
angular.bootstrap — funkcja ta pozwala na ręczną inicjalizację Angulara. Jak zapewne
pamiętasz, możemy dokonać takiej inicjalizacji również za pomocą wbudowanej dyrektywy ngApp.
Poniższy przykład, listing 8.1, pokazuje, jak możemy zrobić to ręcznie.
Listing 8.1. Ręczna inicjalizacja AngularJS
<!doctype html>
<html>
<body>
<div ng-controller="defaultCtrl">
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', [])
.controller('defaultCtrl', function ($scope) {
console.log('app');
});
angular.bootstrap(document, ['app']);
</script>
</body>
</html>
Funkcja angular.copy
angular.copy — tworzy dokładną kopię (deep copy) źródła, którym może być obiekt
lub tablica. Jest to bardzo przydatna i często wykorzystywana funkcja. Używamy jej
w następujący sposób:
angular.copy(źródło, [cel]);
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 8.  Funkcje
121
Podczas implementacji warto pamiętać o kilku zasadach. Jeśli nie wskażemy celu, tworzona jest po prostu kopia obiektu bądź tablicy. Jeżeli cel zostanie wskazany, wszystkie
jego elementy zostaną usunięte, a następnie na ich miejsce zostanie skopiowana zawartość źródła. Jeśli źródło nie jest obiektem lub tablicą, to zostanie zwrócone (null,
undefined). Jeżeli źródło jest takie samo jak cel, zostanie zwrócony wyjątek.
Załóżmy, że chcemy stworzyć aplikację, w której zmiany w formularzu będą widoczne
dla użytkownika dopiero po zapisie. Listing 8.2 pokazuje, jak możemy to osiągnąć, korzystając z angular.copy. Po uruchomieniu aplikacji możemy dokonać zmiany nazwy —
w momencie jej wprowadzenia automatycznie zostanie wyświetlony przycisk Zmień.
Po aktywowaniu przycisku wyzwalamy metodę save(), przyjmującą index danego elementu tablicy oraz nową nazwę. Zostanie ona zapisana, a $scope.changeMountainsList
zostanie automatycznie odświeżony.
Listing 8.2. Funkcja angular.copy
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>AngularJS - funkcje - angular.copy</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<div>
<table class="table" data-ng-init="showButton=false">
<thead>
<tr>
<th>Nazwa zmieniana automatycznie</th>
<th>Zmień nazwę</th>
</tr>
</thead>
<tr data-ng-repeat="mountain in mountainsList track by $index">
<td class="col-lg-6">{{mountain}}</td>
<td class="col-lg-6">
<input data-ng-model="mountain" data-ng-change=
"showButton=true" />
<button class="btn btn-danger" data-ng-show="showButton"
data-ng-click="save($index, mountain); showButton=
false">Zmień</button>
</td>
</tr>
</table>
</div>
<h4>Dane po zapisie</h4>
<div class="well">
<ul>
<li data-ng-repeat="mountain in changeMountainsList track by $index">
{{mountain}}
</li>
</ul>
</div>
<h4>Dane wejściowe</h4>
<div class="well">
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
122
AngularJS. Pierwsze kroki
<ul>
<li data-ng-repeat="mountain in masterMountainsList track by $index">
{{mountain}}
</li>
</ul>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller("defaultCtrl", function ($scope, mountainsList) {
$scope.mountainsList = mountainsList;
$scope.masterMountainsList = angular.copy($scope.mountainsList);
$scope.changeMountainsList = angular.copy($scope.mountainsList);
$scope.save = function (index, newName) {
$scope.changeMountainsList[index] = newName
};
});
app.factory('mountainsList', function () {
return ["Mount Everest", "K2", "Kangczendzonga", "Lhotse"];
});
</script>
</body>
</html>
Efekt działania aplikacji można zobaczyć na rysunku 8.1. W czasie wprowadzania
zmian automatycznie zostanie wyświetlony przycisk Zmień.
Funkcja angular.element
angular.element — obudowuje element DOM lub HTML-owski string jako element
jQuery. Jeśli do naszego projektu dołączymy bibliotekę jQuery, angular.element stanie się aliasem jej funkcji.
Jeżeli nie zdecydujemy się tego zrobić, wówczas AngularJS udostępni nam wbudowaną okrojoną wersję jQuery, zwaną jqLite. Jest ona bardzo lekka i zawiera najczęściej
używane funkcjonalności.
jqLite zapewnia jedynie następujące metody jQuery:
 addClass();
 after();
 append();
 attr() — nie wspiera funkcji jako parametrów;
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 8.  Funkcje
Rysunek 8.1.
Formularz
wykorzystujący
możliwości funkcji
angular.copy
 bind() — nie obsługuje przestrzeni nazw, selektorów czy eventData;
 children() — nie obsługuje selektorów;
 clone();
 contents();
 css() — przyjmuje tylko inline-styles, nie obsługuje getComputedStyle();
 data();
 detach();
 empty();
 eq();
 find() — ograniczone do wyszukiwania po nazwie znacznika;
 hasClass();
 html();
 next() — nie obsługuje selektorów;
 on() — nie obsługuje przestrzeni nazw, selektorów czy eventData;
 off() — nie obsługuje przestrzeni nazw, selektorów;
 one() — nie obsługuje przestrzeni nazw, selektorów;
 parent() — nie obsługuje selektorów;
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
123
124
AngularJS. Pierwsze kroki
 prepend();
 prop();
 ready();
 remove();
 removeAttr();
 removeClass();
 removeData();
 replaceWith();
 text();
 toggleClass();
 triggerHandler();
 unbind() — nie obsługuje przestrzeni nazw;
 val();
 wrap().
Zobaczmy na listingu 8.3, jak możemy wykorzystać funkcję angular.element.
Listing 8.3. Funkcja angular.element
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>AngularJS - funkcje - angular.element</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<h4>W polu numer 1 wpisz liczbę 1000, następnie w polu numer 2 wpisz
liczbę 2000.</h4>
<div data-lucky-number=""></div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) { });
app.directive("luckyNumber", function () {
var alertElement = angular.element(
"<div class=\"well\">{{data.number1}}<br />{{data.number2}}</div>");
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 8.  Funkcje
125
var link = function (scope) {
scope.$watch("data.number1", function (value) {
if (value === "1000") {
alertElement.fadeOut(800);
}
})
scope.$watch("data.number2", function (value) {
if (value === "2000") {
alertElement.fadeIn(800);
}
})
}
return {
restrict: "AE",
replace: true,
template: "<div>Numer 1: <input class=\"form-control\" type=
\"text\" ng-model=\"data.number1\"><br />Numer 2: <input class=
\"form-control\" type=\"text\" ng-model=\"data.number2\"><div>",
compile: function (tElem) {
tElem.append(alertElement);
return link;
}
}
})
</script>
</body>
</html>
Na rysunku 8.2 widać efekt działania aplikacji — po wpisaniu liczby 1000 znika dolny
div, a po wpisaniu w drugim polu liczby 2000 pojawia się.
Rysunek 8.2.
Aplikacja
wykorzystująca
funkcję
angular.element
W powyższym przykładzie stworzyliśmy własną dyrektywę luckyNumber. Więcej o możliwościach tworzenia własnych dyrektyw przeczytasz w rozdziale 6., „Dyrektywy szyte
na miarę”. Na tym etapie warto zapamiętać, że nie należy manipulować drzewem DOM
w kontrolerach, ale właśnie w dyrektywach.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
126
AngularJS. Pierwsze kroki
Funkcja angular.equals
angular.equals — określa, czy dwa obiekty lub dwie wartości są równe. Obsługuje
typy wartości, wyrażenia regularne, tablice i obiekty.
Sposób użycia:
angular.equals(obiekt1, obiekt2);
Poniżej prosty przykład. Porównajmy 3 obiekty.
W kodzie JS:
$scope.obj1 = { key1: "wartosc1", key2: "wartosc2", key3: { a: "a1", b: "b1",
c: "c1" } };
$scope.obj2 = { key3: { a: "a1", b: "b1", c: "c1" }, key2: "wartosc2",
key1: "wartosc1" };
$scope.obj3 = { key3: { a: "a1", b: "b1", c: "c1" }, key2: "wartosc1",
key1: "wartosc2" }
$scope.test1 = angular.equals($scope.obj1, $scope.obj2); // true
$scope.test2 = angular.equals($scope.obj1, $scope.obj3); // false
Jak łatwo się zorientować, kolejność wartości nie ma znaczenia; obj1 jest równy obj2.
Funkcja angular.extend
angular.extend — rozszerza obiekt docelowy, kopiując właściwości. Co ciekawe,
można podawać wiele źródeł. Jeśli chcemy zachować obiekty oryginalne, należy przekazać pusty obiekt jako cel.
Sposób użycia z przekazaniem pustego obiektu jako celu:
var objekt = angular.extend({}, object1, object2)
Zobaczmy na przykładzie, czym różni się rozszerzanie od omówionego już kopiowania.
W kodzie JS:
var obj1 = { wartosc1: 1, wartosc2: {} };
var obj2 = angular.copy(obj1);
console.log(obj2.wartosc2 === obj1.wartosc2); // false — Angular wykonał głębokie
// kopiowanie, mamy 2 niezależne obiekty
console.log(angular.equals(obj1, obj2)); // true — obydwa obiekty są takie same
obj2 = {}; // czyścimy obiekt 2.
angular.extend(obj2, obj1); // rozszerzamy obiekt 2. za pomocą funkcji extend
console.log(obj2.wartosc2 === obj1.wartosc2); // true — Angular wykonał płytkie kopiowanie,
// oba elementy wskazują na ten sam obiekt
console.log(angular.equals(obj1, obj2)); // true — obydwa obiekty są sobie równe
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 8.  Funkcje
127
Funkcja angular.forEach
angular.forEach — wywołuje funkcję iteratora dla każdego obiektu kolekcji, który
jest obiektem lub tablicą.
Przykład użycia:
angular.forEach(obj, funkcja_iterator, [kontekst]);
Na poniższym przykładzie zobaczysz, w jaki sposób możemy zalogować poszczególne
wartości obiektu $scope.mountainsList.
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850, country: 'Nepal-Chiny' }, { mountain:
"K2", metres: 8611, country: 'Pakistan-Chiny' },
{ mountain: "Kangczendzonga", metres: 8598, country: 'Nepal-Indie' }, { mountain:
"Lhotse", metres: 8501, country: 'Nepal-Chiny' },
{ mountain: "Makalu", metres: 8463, country: 'Nepal-Chiny' }
];
angular.forEach($scope.mountainsList, function (value, index) {
console.log(index, value.mountain, value.metres, value.country);
})
Otrzymany log:
0
1
2
3
4
"Mount Everest" 8850 "Nepal-Chiny"
"K2" 8611 "Pakistan-Chiny"
"Kangczendzonga" 8598 "Nepal-Indie"
"Lhotse" 8501 "Nepal-Chiny"
"Makalu" 8463 "Nepal-Chiny"
Funkcje angular.fromJson i angular.toJson
angular.fromJson, angular.toJson — są to funkcje, odpowiednio, deserializujące
oraz serializujące do formatu JSON.
Sposoby użycia:
angular.fromJson(json);
angular.toJson(obj, [ładny]); // ładny — opcjonalne ustawienie, jeśli jest true, JSON zawiera
// znaki nowej linii oraz białe znaki.
Funkcja angular.identity
angular.identity — funkcja, która zwraca swój pierwszy argument. Przydatna podczas pisania kodu w stylu funkcjonalnym. Co to oznacza? W programowaniu funkcjonalnym nie ma zmiennych globalnych. Wszystko, czego chcesz użyć, musisz przekazać
lub wstrzyknąć.
Zobaczmy, jak w praktyce możemy skorzystać z możliwości oferowanych przez tę funkcję. Stwórzmy sobie prostą aplikację (listing 8.4), której zadaniem będzie przerobienie
normalnej wypowiedzi na styl szanownego Yody.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
128
AngularJS. Pierwsze kroki
Listing 8.4. Aplikacja wykorzystująca funkcję angular.identity
<!doctype html>
<html data-ng-app="app">
<head>
<title>AngularJS - funkcje - angular.identity</title>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-2">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body>
<div data-ng-controller="defaultCtrl">
<h1>Wersja normalna: <b>{{n | uppercase}}</b></h1>
<h1>Wersja według Yody: <b>{{y | uppercase}}</b></h1>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
$scope.yoda = function (a, b, c) {
return c + b + a;
};
$scope.normal = function (a, b, c) {
return a + b + c;
};
$scope.result = function (fn, a, b, c) {
return (fn || angular.identity)(a, b, c);
};
$scope.a = 'to ';
$scope.b = 'jest ';
$scope.c = 'tekst sklejony ';
$scope.n = $scope.result($scope.normal, $scope.a, $scope.b, $scope.c);
$scope.y = $scope.result($scope.yoda, $scope.a, $scope.b, $scope.c);
});
</script>
</body>
</html>
Po uruchomieniu nasza przeglądarka wyświetli następujący tekst:
Wersja normalna: TO JEST TEKST SKLEJONY
Wersja według Yody: TEKST SKLEJONY JEST TO
Efekt można zobaczyć na rysunku 8.3.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 8.  Funkcje
129
Rysunek 8.3.
Efekt działania
aplikacji
wykorzystującej
funkcję
angular.identity
Funkcja angular.injector
angular.injector — tworzy obiekt, który może być użyty do pobierania serwisów lub do
wstrzykiwania zależności. Serwis może być automatycznie wstrzykiwany (po nazwie)
do kontrolera przy pomocy $injector.
Zobaczmy, jak możemy wykorzystać injector w praktyce. Naszym celem jest stworzenie
aplikacji pobierającej tablicę nazw z serwisu (listing 8.5), poza tym możemy dodawać
nowe nazwy i usuwać istniejące.
Listing 8.5. Aplikacja wykorzystująca funkcję angular.injector
<!doctype html>
<html data-ng-app="app">
<head>
<title>AngularJS - funkcje - angular.injector</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body>
<div data-ng-controller="defaultCtrl">
<input class="form-control" data-ng-model="data.name" />
<button class="btn btn-success" data-ng-click="add(data.name);
data.name=''">Dodaj</button>
<ul>
<li data-ng-repeat="name in list() track by $index">
{{name}} <a href="#" data-ng-click="rem($index)"> Usuń </a>
</li>
</ul>
<pre>
list = {{list()}}
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
130
AngularJS. Pierwsze kroki
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope, $injector) {
$scope.list = function () {
var mountain = $injector.get('mountain');
return mountain.mountainsList
}
$scope.rem = function (index) {
var mountain = $injector.get('mountain');
mountain.mountainsList.splice(index, 1);
};
$scope.add = function (name) {
var mountain = $injector.get('mountain');
mountain.mountainsList.push(name);
}
});
app.service('mountain', function () {
this.mountainsList = ["Mount Everest", "K2", "Kangczendzonga",
"Lhotse", "Makalu"];
});
</script>
</body>
</html>
Za każdym razem ręcznie wstrzykujemy serwis mountain do funkcji list, rem i add.
Efekt działania aplikacji prezentuje rysunek 8.4.
Rysunek 8.4.
Aplikacja
wykorzystująca
funkcję
angular.injector
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 8.  Funkcje
131
Funkcje angular.isArray, angular.isDate,
angular.isDefined, angular.isElement,
angular.isFunction, angular.isNumber,
angular.isObject, angular.isString
i angular.isUndefined
angular.isArray, angular.isDate, angular.isDefined, angular.isElement, angular.is
Function, angular.isNumber, angular.isObject, angular.isString, angular.is
Undefined — zestaw funkcji określających, odpowiednio, czy dana referencja to tabli-
ca, czy data, czy jest zdefiniowana, czy jest elementem drzewa DOM, czy jest funkcją,
czy to numer, czy to obiekt (pamiętaj, że null nie jest obiektem), czy to string, czy
jest niezdefiniowana.
Funkcje te są bardzo przydatne i niezwykle proste w użyciu. Zdefiniujmy prostą tablicę,
a następnie wywołajmy kolejno poszczególne funkcje.
W kodzie JS:
$scope.test = [1,2,3];
console.log(angular.isArray($scope.test));
// true
console.log(angular.isDate($scope.test));
// false
console.log(angular.isDefined($scope.test)); // true
console.log(angular.isElement($scope.test)); // false
console.log(angular.isFunction($scope.test)); // false
console.log(angular.isNumber($scope.test)); // false
console.log(angular.isObject($scope.test)); // true
console.log(angular.isString($scope.test)); // false
console.log(angular.isUndefined($scope.test));// false
Jak widać w naszym logu, za każdym razem otrzymaliśmy true lub false, w zależności od spełnienia danego warunku. Dzięki temu możemy budować następujące konstrukcje:
if (angular.isArray($scope.test)) {
$scope.test.push(4);
}
console.log($scope.test);
Na początku sprawdzamy, czy $scope.test jest tablicą. Jeśli tak, to dodajemy do niej
kolejny element. W wyniku otrzymamy: [1, 2, 3, 4].
Funkcje angular.lowercase i angular.uppercase
angular.lowercase oraz angular.uppercase — jest to zestaw funkcji pozwalających
sterować wielkością liter przyjmowanego ciągu znaków. Sposób użycia jest analogiczny jak w poprzednich przypadkach.
console.log(angular.lowercase('AngularJS'));
console.log(angular.uppercase('AngularJS'));
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
// angularjs
// ANGULARJS
132
AngularJS. Pierwsze kroki
Funkcja angular.module
angular.module — to globalne miejsce do tworzenia, rejestracji oraz pobierania mo-
dułów AngularJS.
Przykłady użycia:
// stworzenie modułu module1
var module1 = angular.module('module1', []);
// stworzenie serwisu
module1.service( 'nazwaSerwisu', function );
// stworzenie fabryki
module1.factory( 'nazwaFabryki', function );
// stworzenie nowego providera
module1.provider( 'nazwaProvidera', function );
Funkcja angular.reloadWithDebugInfo
angular.reloadWithDebugInfo — pozwala na uruchomienie naszej aplikacji w trybie
debug.
Jest to już ostatnia z funkcji, którymi zajęliśmy się w tym rozdziale. Naszym celem
było pokazanie możliwości, jakie daje ten wspaniały framework. Za każdym razem,
gdy tylko to możliwe, staramy się poprzeć naszą tezę odpowiednimi przykładami. Po ich
przeanalizowaniu czytelnik powinien dość swobodnie poruszać się po krainie kanciastych funkcji.
Quiz
1. Która z funkcji służy do ręcznej inicjalizacji kanciastego?
2. Czy AngularJS korzysta z biblioteki jQuery?
3. Która z funkcji pozwala na rozszerzanie obiektów?
4. Która z funkcji służy do wstrzykiwania zależności?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.
Routing — lepsza strona
nawigacji
Wprowadzenie
Świat aplikacji SPA stoi przed nami z otwartymi ramionami, uśmiecha się i zaprasza
do tańca. Tym, którym umknęła informacja, co to jest SPA (Single Page Applications),
przypominamy, że są to aplikacje oparte na tzw. pojedynczej stronie. Oznacza to, że
nasza przeglądarka pobiera stronę tylko raz, przechodząc na kolejne podstrony, doładowywane są tylko niezbędne elementy, szkielet pozostaje ten sam.
Co to daje? Po pierwsze nie odświeżamy całej strony za każdym razem, gdy zaistnieje
potrzeba przejścia np. do szczegółów produktu, a odświeżamy jedynie sekcję z danym
produktem. Do serwera w tle wysyłane jest zapytanie o właściwy JSON i to wszystko.
Reszta dzieje się po stronie przeglądarki. Nasz serwer tylko raz serwuje pełny HTML
strony domowej, a kolejne zapytania dotyczą już wyłącznie wybranych fragmentów
kodu lub danych w formacie JSON. Tym samym zmniejszamy znacznie obciążenie
serwera, a użytkownik otrzymuje o wiele bardziej komfortową możliwość nawigowania po serwisie.
Raz załadowana do pamięci przeglądarki strona będzie działać dużo płynniej. AngularJS
pozwala w bardzo łatwy sposób zbudować w pełni funkcjonalną aplikację SPA. Zanim przejdziemy do szczegółów, zobaczmy, jak wygląda prosta aplikacja. Na początku
należy dodać referencje do oddzielnego modułu AngularJS o nazwie ngRoute. Robimy
to następująco:
<script src=" https://code.angularjs.org/1.4.0-beta.5/angular-route.js"></script>
Możemy również korzystać z naszych lokalnych zasobów — my w tym przypadku
skorzystamy z zasobu zewnętrznego, z pełnej, niezminimalizowanej wersji.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
134
AngularJS. Pierwsze kroki
W następnym kroku należy poinformować aplikację o tym, że chcemy wykorzystać
niesamowite możliwości kanciastego w dziedzinie routingu w taki oto sposób:
var app = angular.module('app', ['ngRoute']);
To w zasadzie tyle. Routing w naszej pierwszej aplikacji SPA jest prawie gotowy do
pracy. Oczywiście należy go jeszcze odpowiednio skonfigurować, ale to za chwilę.
Konfiguracja
Zacznijmy od $routeProvider, służącego do konfiguracji $route. Poniższy przykład
pokaże, jak możemy tworzyć konfigurację:
app.config(function ($routeProvider) {
$routeProvider
.when('/',
{
templateUrl: "/default.html",
controller: "defaultCtrl"
})
.when('/list',
{
templateUrl: "/list.html",
controller: "listCtrl"
})
...
...
...
.otherwise({
template: "Brak strony!"
})
});
Na początku w naszym module app tworzymy config. Następnie w funkcji when podajemy adres strony zawierającej szablon oraz kontroler, który chcemy dla niego wykorzystać. Po umieszczeniu wszystkich szablonów możemy dodać sekcję otherwise, zawierającą odwołanie do szablonu informującego, że użytkownik zastosował odwołanie
niezdefiniowane. when daje $routeProvider o wiele więcej możliwości, jednak na tym
etapie studiowania Angulara pozostaniemy przy tych najważniejszych.
Widoki
Dochodzimy teraz do długo oczekiwanego momentu, w którym stworzymy naszą pierwszą aplikację SPA, pozwalającą zarządzać naszymi zadaniami. Nie skorzystamy z serwera, a cała funkcjonalność znajdzie się po stronie przeglądarki. Komunikacji z serwerem
poświęciliśmy oddzielny rozdział i zachęcamy do zapoznania się z nim.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
135
W naszej aplikacji wykorzystamy znaną już dobrze z wcześniejszych przykładów bibliotekę CSS Bootstrap, a dodatkowo możliwości biblioteki jQuery i opartej na niej
jQuery UI. Przy pomocy możliwości dyrektyw kanciastego oraz jQuery UI wzbogacimy nasz formularz o ładny i funkcjonalny kalendarz.
To jednak za kilka chwil, tymczasem skupmy się na strukturze naszej aplikacji.
Zacznijmy od struktury katalogów — rysunek 9.1.
Rysunek 9.1.
Struktura katalogów
Pierwszy katalog css zawiera plik style.css, w którym definiujemy styl .done-true dla
zadań oznaczonych, jako wykonane. Nadpisujemy style bootstrapa dla elementów oznaczonych jako disabled oraz readonly. Watro ustawić pole kalendarza na tylko do odczytu,
co uniemożliwi wpisanie daty w nieakceptowanym przez nas formacie. Kliknięcie na
tym polu spowoduje wyświetlenie kalendarza. Po wybraniu daty pole zostanie automatycznie wypełnione, a kalendarz zamknięty.
Plik style.css
.done-true {
text-decoration: line-through;
color: #ddd;
}
.form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control
{
cursor: pointer;
background-color:white;
}
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
136
AngularJS. Pierwsze kroki
W folderze script mamy katalogi common oraz core. W katalogu common znajdują się
wszystkie współdzielone elementy naszej aplikacji. Plik directives/ng-date-picker.js zawiera dyrektywę ngDatePicker, której zadaniem jest rozszerzenie pola formularza daty, by
po kliknięciu wyświetlany był przyjazny kalendarz. Pole to jest ustawione na readonly, co uniemożliwia użytkownikowi wpisanie niepoprawnego formatu daty. Dyrektywa wykorzystuje element element.datepicker, który jest częścią jQuery UI. Więcej na
temat tworzenia dyrektyw można znaleźć w rozdziale 6., „Dyrektywy szyte na miarę”.
Plik ng-date-picker.js6
app.directive('ngDatePicker', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
element.datepicker({
changeYear: true,
changeMonth: true,
showWeek: true,
firstDay: 1,
dayNames: "pl",
dateFormat: 'dd/m/yy',
onSelect: function (date) {
ctrl.$setViewValue(date);
scope.$apply();
}
});
}
};
});
Efekt działania dyrektywy widać na rysunku 9.2.
Rysunek 9.2. Kalendarz jQuery UI
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
137
ngDatePicker przyjmuje restrict: 'A', co oznacza, że możemy wywoływać ją przy
użyciu atrybutu. W naszym przypadku wywołanie wygląda następująco:
data-ng-date-picker=""
Wrócimy do niego przy okazji prezentacji pliku edit.html z katalogu core/edit.
Kolejny plik, filters/filters.js, zawiera filtr rangeTime, który przyjmuje 2 parametry: pierwszy to zakres, dla jakiego ma zostać stworzona tablica, a drugi pozwala na ustawienie
tzw. połówek. Jeśli przyjmuje on wartość true, nasza tablica wygląda tak: [0.5, 1,
1.5, 2, 2.5,...], a jeśli przyjmuje wartość false, zwracana tablica wygląda następująco: [1, 2, 3, 4,...]. Tablicę wykorzystujemy do zbudowania listy rozwijanej
(estymacja czasu wykonania zadania) w formularzu dodawania zadań.
Plik filters.js
app.filter('rangeTime', function () {
return function (input, total, halfHour) {
total = parseInt(total);
for (var i = 1; i < total; i++) {
if (halfHour) {
input.push(i - 0.5);
}
input.push(i);
}
return input;
};
});
Ostateczny efekt można zobaczyć na rysunku 9.3.
Rysunek 9.3. Lista rozwijana wykorzystująca filtr rangeTime
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
138
AngularJS. Pierwsze kroki
Przejdźmy teraz do kolejnej części aplikacji, której zadaniem jest przechowywanie danych. Zaczynamy od services/categories-data.js. Plik ten zawiera kategorie zadań, a do
każdej kategorii przypisana jest odpowiednia ikonka.
Plik categories-data.js
app.factory('categories', function () {
return [
{ name: 'Personalne', 'gico': 'heart' },
{ name: 'Zdrowie', 'gico': 'tint' },
{ name: 'Nauka', 'gico': 'book' },
{ name: 'Biznes', 'gico': 'usd' },
{ name: 'Dom', 'gico': 'home' },
{ name: 'Inne', 'gico': 'paperclip' }];
});
Kolejny plik, services/todos-data.js, zawiera serwis przechowujący nasze zadania. W wersji produkcyjnej należy oczywiście wykorzystać back-end obsługujący wywołania. Na
potrzeby tego ćwiczenia nie będziemy łączyć się z back-endem. Nasz serwis zwraca
JSON z następującymi polami:
 title — tytuł zadania;
 done — true/false: określa, czy zadanie zostało już wykonane (true),
czy jeszcze nie (false);
 type — typ zadania składający się z dwóch pól: name (nazwa typu)
oraz gico (przechowuje typ ikony);
 estimates — szacowany czas wykonania zadania;
 date — data.
Plik todos-data.js
app.factory('todos', function () {
return [
{
'title': 'Randka z Julitą', 'done': false, "type": { "name":
"Personalne", "gico": "heart" }, 'estimates': 3, "date": "11/11/2015"
},
{
'title': 'Siłownia', 'done': false, "type": { "name": "Zdrowie", "gico":
"tint" }, 'estimates': 2, "date": "12/11/2015"
},
{
'title': 'AngularJS następne kroki', 'done': false, "type": { "name":
"Nauka", "gico": "book" }, 'estimates': 4, "date": "14/11/2015"
},
{
'title': 'Spotkanie z Janem', 'done': false, "type": { "name": "Biznes",
"gico": "usd" }, 'estimates': 1, "date": "15/11/2015"
}
];
});
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
139
Przejdźmy teraz do katalogu core, czyli tzw. rdzenia aplikacji. W pierwszym podkatalogu, default, mamy plik default.html. Jest to bardzo prosty szablon z komunikatem
powitalnym.
Plik default.html
<h2>Dzięki <strong>Lista Zadań Online</strong> możesz zarządzać zadaniami w każdym
miejscu!</h2>
Katalog edit przechowuje 2 pliki: pierwszy to kontroler editCtrl w pliku edit-ctrl.js,
a drugi to szablon edit.tpl.html, zawierający formularz dodawania nowych zadań. Kontroler editCtrl składa się z następujących elementów:
 $scope.categories — tablica wykorzystywana do generowania listy
rozwijanej Typ;
 $scope.formData — ustawiający domyślne wartości w listach rozwijanych;
 $scope.addTodo — metoda dodająca nowe zadanie, ustawiająca pole Nazwa
na puste oraz przenosząca użytkownika do widoku listy zadań.
Plik edit-ctrl.js
app.controller('editCtrl', function ($scope, $location, categories) {
$scope.categories = categories;
$scope.formData = { type: $scope.categories[0], estimates: $scope.estimates = 1 };
$scope.addTodo = function () {
$scope.$parent.todos.push({
'title': $scope.formData.newTodo,
'done': false,
'type': $scope.formData.type,
'estimates': $scope.formData.estimates,
'date': $scope.formData.date
});
$scope.formData.newTodo = '';
$location.path('/list')
};
});
Kontroler editCtrl przypisany jest do szablonu edit.tpl.html. Szablon zawiera formularz pozwalający na dodanie nowego zadania.
Plik edit.tpl.html
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">Dodaj zadanie!</h3>
</div>
<div class="panel-body">
<form name="f" data-ng-submit="addTodo()">
Nazwa:
<textarea class="form-control" name="newTodo" data-ng-model="formData.newTodo"
required></textarea>
Typ:
<select class="form-control" name="type" data-ng-model="formData.type"
data-ng-options="value.name for value in categories" required>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
140
AngularJS. Pierwsze kroki
</select>
Estymowany czas:
<select class="form-control" name="estimates" data-ng-model=
"formData.estimates"
data-ng-options="value +' h' for value in [] | rangeTime:9:true" required>
</select>
Data:
<input class="form-control" type="text" data-ng-model="formData.date"
data-ng-date-picker="" name="date" required readonly="readonly">
<br />
<button class="btn btn-success" data-ng-disabled="f.$invalid">Add <span
class="glyphicon glyphicon-ok"></span></button>
</form>
</div>
</div>
Warto zwrócić uwagę na to, w jaki sposób budowane są listy rozwijane. Pierwsza,
Typ, wyświetla użytkownikowi nazwy typów. Kod HTML generowany przez AngularJS wygląda następująco:
<select class="form-control ng-pristine ng-valid ng-valid-required ng-touched"
name="type"
data-ng-model="formData.type" data-ng-options="value.name for value in categories"
required="">
<option value="0" selected="selected">Personalne</option>
<option value="1">Zdrowie</option>
<option value="2">Nauka</option>
<option value="3">Biznes</option>
<option value="4">Dom</option>
<option value="5">Inne</option>
</select>
Zaznaczony jest pierwszy element tablicy, zgodnie z oczekiwaniem.
$scope.formData = { type: $scope.categories[0], estimates: $scope.estimates = 1 };
Value wysyłane przez formularz przy wybraniu np. pierwszej opcji, "Personalne",
wygląda tak:
{
"name": "Personalne",
"gico": "heart"
}
Oznacza to, że value="0" wskazuje na pierwszy element tablicy categories, a nie na
zero.
HTML generowany dla listy rozwijanej Estymowany czas wygląda następująco:
<select class="form-control ng-pristine ng-untouched ng-valid ng-valid-required"
name="estimates" data-ng-model="formData.estimates"
data-ng-options="value +' h' for value in [] | rangeTime:9:true" required="">
<option value="0">0.5 h</option>
<option value="1" selected="selected">1 h</option>
<option value="2">1.5 h</option>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
<option
<option
<option
<option
<option
<option
<option
<option
<option
<option
<option
<option
<option
</select>
141
value="3">2 h</option>
value="4">2.5 h</option>
value="5">3 h</option>
value="6">3.5 h</option>
value="7">4 h</option>
value="8">4.5 h</option>
value="9">5 h</option>
value="10">5.5 h</option>
value="11">6 h</option>
value="12">6.5 h</option>
value="13">7 h</option>
value="14">7.5 h</option>
value="15">8 h</option>
Jak widać, zaznaczony jest drugi element tablicy, a odpowiada za to kod kontrolera:
$scope.formData = { type: $scope.categories[0], estimates: $scope.estimates = 1 };
Ciekawostką jest tu wykorzystanie pustej tablicy oraz filtru rangeTime.
Filtr przyjmuje dwa parametry. Pierwszy odpowiada za liczbę generowanych godzin,
drugi pozwala na dodanie tzw. połówek. Przyjrzyjmy się bliżej ng-options:
data-ng-options="value +' h' for value in [] | rangeTime:9:true"
 value — to wartość wyświetlana użytkownikowi, my dodaliśmy do niej
jeszcze symbol godziny, h.
 for value — jest to wartość wysyłana przez formularz.
 in [] — odwołujemy się do pustej tablicy, dla której zdefiniowaliśmy filtr.
Jak wiadomo, może pobierać daną wartość, zmieniać ją i zwracać, może
również pobierać pustą tablicę i zwracać nową, zbudowaną na podstawie
wywołanych parametrów — tak też dzieje się w naszym przypadku.
 rangeTime:9:true — jest to odwołanie do naszego filtru z prośbą o zwrócenie
tablicy ośmiogodzinnej [0.5, 1, …. , 8] w przedziale co pół godziny.
Przejdźmy dalej. Kolejny szablon, json/json.tpl.html, jest bardzo prosty i odpowiada
za wyświetlenie listy zadań w formacie json. Formatowanie realizujemy tu, korzystając z wbudowanego filtru AngularJS — json.
Plik json.tpl.html
<pre>
{{todos | json}}
</pre>
Pewnie zadajesz sobie pytanie, dlaczego szablon json.tpl.html nie ma przypisanego kontrolera. Odpowiedź jest bardzo prosta: w tym przypadku korzystamy z kontrolera ojca
indexCtrl, nie ma więc potrzeby tworzenia osobnego kontrolera dla tej funkcjonalności.
Rysunek 9.4 prezentuje stronę wyświetlającą aktualny json.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
142
AngularJS. Pierwsze kroki
Rysunek 9.4. Strona wyświetlająca listę zadań w formacie JSON
Następny na liście jest listCtrl, znajdujący się w list/list-ctrl.js. Kontroler ten odpowiada
za obsługę szablonu list.tpl.html. Funkcjonalność w nim zawarta pozwala na usuwanie
wykonanych zadań.
Plik list-ctrl.js
app.controller('listCtrl', function ($scope) {
$scope.deleteCompleted = function () {
$scope.$parent.todos = $scope.$parent.todos.filter(function (item) {
return !item.done;
});
};
});
Plik list.tpl.html
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Zadania</h3>
</div>
<div class="panel-body">
<table class="table table-striped">
<thead>
<tr>
<th>Lp.</th>
<th>Nazwa</th>
<th>Estymacja</th>
<th>Data</th>
<th>Ico</th>
<th>Zaznacz jako wykonane</th>
</tr>
</thead>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
143
<tr data-ng-repeat="todo in todos">
<td>{{$index+1}}. </td>
<td><span class="done-{{todo.done}}">{{todo.title}}</span></td>
<td><span class="done-{{todo.done}}">({{todo.estimates}}h)
</span></td>
<td><span class="done-{{todo.done}}">{{todo.date}}</span></td>
<td><span class="glyphicon glyphicon-{{todo.type.gico}}
done-{{todo.done}}"></span></td>
<td><input type="checkbox" data-ng-model="todo.done" title=
"Mark Complete" /></td>
</tr>
</table>
</div>
</div>
<div class="panel panel-default">
<div class="panel-body">
<button class="btn btn-danger" data-ng-click="deleteCompleted()">
Usuń wykonane <span class="glyphicon glyphicon-remove"></span></button>
</div>
</div>
Jak widać na powyższym listingu, wyświetlanie listy zadań realizowane jest za pomocą dyrektywy data-ng-repeat="todo in todos". Kod wygenerowany dla pierwszego rekordu wygląda następująco:
<!-- ngRepeat: todo in todos -->
<tr data-ng-repeat="todo in todos" class="ng-scope">
<td class="ng-binding">1. </td>
<td><span class="done-false">Randka z Julitą</span></td>
<td><span class="done-false">(3h)</span></td>
<td><span class="done-false">11/11/2015</span></td>
<td><span class="glyphicon glyphicon-heart done-false"></span></td>
<td><input type="checkbox" data-ng-model="todo.done" title="Mark Complete"
class="ng-pristine ng-untouched ng-valid"></td>
</tr><!-- end ngRepeat: todo in todos -->
Dyrektywa ngRepeat generuje dla każdego rekordu nową sekcję <tr>, wypełniając ją
danymi — rysunek 9.5.
Kolejny plik, app.mdl.js, stanowi trzon naszej aplikacji. To tu zdefiniowany jest moduł app.
Plik app.mdl.js
var app = angular.module('app', ['ngRoute']);
Przejdźmy teraz do znacznie ciekawszego app.rout.js. Plik ten zawiera konfigurację routingu naszej aplikacji. Pisaliśmy już trochę na ten temat na początku rozdziału, teraz
zobaczymy, jak taki plik wygląda w praktyce.
Plik app.rout.js
app.config(function ($routeProvider) {
$routeProvider
.when('/', // w przypadku wywołania "/" użyj szablonu default.html
{
templateUrl: "script/core/default/default.html"
})
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
144
AngularJS. Pierwsze kroki
Rysunek 9.5. Lista zadań
.when('/list', // w przypadku wywołania "/list" użyj szablonu "list.html" oraz kontrolera "listCtrl"
{
templateUrl: "script/core/list/list.tpl.html",
controller: "listCtrl"
})
.when('/edit',
{
templateUrl: "script/core/edit/edit.tpl.html",
controller: "editCtrl"
})
.when('/json',
{
templateUrl: "script/core/json/json.tpl.html",
})
.otherwise({ // w przypadku innego wywołania wyświetl szablon "Brak strony!"
template: "Brak strony!"
})
});
Pierwszy szablon, script/core/default/default.tpl.html, zawiera treść, którą użytkownik
widzi po wejściu na naszą stronę. Kolejny szablon, script/core/list/list.tpl.html, wyświetlany jest użytkownikowi po kliknięciu na link w menu Twoje zadania. Następny,
script/core/edit/edit.tpl.html, odpowiada za wyświetlenie strony z formularzem umożliwiającym dodanie nowego zadania.
W związku z tym, że jest to aplikacja napisana w celach edukacyjnych, pozwoliliśmy
sobie dodać kolejny szablon, prezentujący nasze zadania w formacie JSON. W ostatniej sekcji, otherwise, nie odwołujemy się do szablonu zdefiniowanego w innym pliku
— szablon jest bardzo prosty i możemy go zdefiniować w pliku konfiguracyjnym.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
145
app.rout.js to serce naszego routingu, dzięki któremu AngularJS wie, jak ma się zachować, gdy użytkownik wybierze jakiś link.
Tym samym doszliśmy do dwóch ostatnich plików. Pierwszy z nich to kontroler index-ctrl.js, a drugi to strona główna naszej aplikacji.
Plik index-ctrl.js
app.controller('indexCtrl', function ($scope, $location, todos) {
$scope.todos = todos;
$scope.getClass = function (path) {
if ($location.path().substr(0, path.length) == path) {
return "active"
} else {
return ""
}
}
});
Zadaniem powyższego kontrolera, poza przypisaniem danych do $scope.todos, jest
kontrolowanie menu. Funkcja getClass ustawia klasę CSS active dla elementu strony,
na której znajduje się użytkownik.
Plik index.html
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>AngularJS - $routeParams</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
<link rel="stylesheet"
href="http://code.jquery.com/ui/1.11.2/themes/smoothness/jquery-ui.css">
<link href="css/style.css" rel="stylesheet" />
</head>
<body data-ng-controller="indexCtrl">
<div class="container">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#/">Lista Zadań Online</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li data-ng-class="getClass('/list')"><a href="#/list">Twoje zadania
</a></li>
<li data-ng-class="getClass('/edit')"><a href="#/edit">Dodaj zadanie
</a></li>
<li data-ng-class="getClass('/json')"><a href="#/json">JSON</a></li>
</ul>
</div>
</div>
</nav>
<div data-ng-view=""></div>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
146
AngularJS. Pierwsze kroki
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="http://code.jquery.com/ui/1.11.2/jquery-ui.js"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-route.js"></script>
<script
<script
<script
<script
<script
src="script/app.mdl.js"></script>
src="script/app.rout.js"></script>
src="script/index-ctrl.js"></script>
src="script/core/list/list-ctrl.js"></script>
src="script/core/edit/edit-ctrl.js"></script>
<script
<script
<script
<script
</body>
</html>
src="script/common/directives/ng-date-picker.js"></script>
src="script/common/services/todos-data.js"></script>
src="script/common/services/categories-data.js"></script>
src="script/common/filters/filters.js"></script>
W ostatnim pliku należy zwrócić uwagę na jeden z jego najważniejszych elementów:
<div data-ng-view=""></div>. Dzięki dyrektywie ngView AngularJS wyświetla jeden
z wybranych szablonów.
Powyższy przykład jest dosyć rozbudowany, ale zachęcamy do zapoznania się z nim
i eksperymentowania. Nasuwa się jeszcze jedno pytanie związane z aplikacją SPA, a mianowicie: jak obsłużyć linki kończące się np. id wybranego artykułu?
Wyobraźmy sobie, że chcemy stworzyć stronę zawierającą listę nazw gór. Po kliknięciu
w daną nazwę przejdziemy do strony zawierającej pełny opis. Na stronie tej chcemy mieć
możliwość powrotu do listy lub usunięcia danej góry. Jak zwykle posłużymy się przykładem, tym razem jednak zastosujemy nieco inną taktykę. Zamiast używać szablonów
umieszczonych w oddzielnych plikach HTML (co zalecamy w systemach produkcyjnych), na potrzeby tego ćwiczenia skorzystamy z dyrektywy select — listing 9.1.
Listing 9.1. Możliwości użycia dyrektywy select
<!DOCTYPE html>
<html data-ng-app="app">
<head>
<title>AngularJS - routing</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body data-ng-controller="defaultCtrl">
<div class="container">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="collapse navbar-collapse" id=
"bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
147
<li data-ng-class="getClass('/home')">
<a href="#/home">Strona główna</a></li>
<li data-ng-class="getClass('/mountain')">
<a href="#/mountain">Góry</a></li>
</ul>
</div>
</div>
</nav>
<div data-ng-view=""></div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-route.js"></script>
<script>
var app = angular.module("app", ['ngRoute']);
app.config(function ($routeProvider) {
$routeProvider
.when("/home", { templateUrl: "home.html" })
.when("/mountain", { templateUrl: "mountain.html", controller:
"listCtrl" })
.when("/mountain/:id", { templateUrl: "details.html", controller:
"detailsCtrl" })
.otherwise({ redirectTo: "/home" });
});
app.controller("defaultCtrl", function ($scope, $location, mountainsList) {
$scope.getClass = function (path) {
if ($location.path().substr(0, path.length) == path) {
return "active"
} else {
return ""
}
}
});
app.controller("listCtrl", function ($scope, mountainsList) {
$scope.mountains = mountainsList.getAll();
});
app.controller("detailsCtrl", function ($scope, $routeParams,
mountainsList, $location) {
$scope.mountain = mountainsList.getById($routeParams.id);
$scope.delete = function (id) {
mountainsList.deleteById(id);
$location.path('/mountain')
};
});
app.factory("mountainsList", function () {
var mountains = [
{ id: "1", mountain: "Mount Everest", metres: 8850, country:
'Nepal-China' },
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
148
AngularJS. Pierwsze kroki
{ id: "2", mountain: "K2", metres: 8611, country: 'Pakistan-China' },
{ id: "3", mountain: "Kangczendzonga", metres: 8598, country:
'Nepal-India' },
{ id: "4", mountain: "Lhotse", metres: 8501, country: 'Nepal-China' },
{ id: "5", mountain: "Makalu", metres: 8463, country: 'Nepal-China' },
{ id: "6", mountain: "Cho Oyu", metres: 8201, country: 'Nepal-China' },
{ id: "7", mountain: 'Dhaulagiri', metres: 8167, country: 'Nepal' },
{ id: "8", mountain: 'Manaslu', metres: 8163, country: 'Nepal' },
{ id: "9", mountain: 'Nanga Parbat', metres: 8125, country: 'Pakistan' },
{ id: "10", mountain: 'Annapurna', metres: 8091, country: 'Nepal' },
{ id: "11", mountain: 'Shishapangma', metres: 8012, country: 'China' }
];
return {
getAll: function () {
return mountains;
},
getById: function (id) {
var result = null;
angular.forEach(mountains, function (m) {
if (m.id == id) result = m;
});
return result;
},
deleteById: function (id) {
angular.forEach(mountains, function (m, i) {
if (id == m.id) {
mountains.splice(i, 1);
}
});
}
};
});
</script>
<script type="text/ng-template" id="home.html">
<h1>Jesteś na stronie głównej.</h1>
</script>
<script type="text/ng-template" id="mountain.html">
<div class="panel panel-default">
<div class="panel-body">
<h3>Lista</h3>
<p data-ng-repeat="mountain in mountains track by $index">
{{$index+1}}. <a href="#/mountain/{{mountain.id}}">
{{mountain.mountain}}
<span class="glyphicon glyphicon-info-sign"></span>
</a>
</p>
</div>
</div>
</script>
<script type="text/ng-template" id="details.html">
<div class="panel panel-default">
<div class="panel-body">
<h3>Szczegóły</h3>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
149
<div class="list-group">
<a href="#" class="list-group-item active">
{{mountain.mountain}}
</a>
<a href="#" class="list-group-item">
Wysokość: <b>
{{mountain.metres}}
</b>
</a>
<a href="#" class="list-group-item">
Państwo: <b>
{{mountain.country}}
</b>
</a>
</div>
<a href="#/mountain" class="btn btn-default">Powrót do listy</a>
<a href="" ng-click="delete(mountain.id)" class="btn btn-danger">
Usuń {{mountain.mountain}}</a>
</div>
</div>
</script>
</body>
</html>
Efekty działania aplikacji można zobaczyć kolejno na rysunkach 9.6, który prezentuję
listę gór, oraz 9.7, który przedstawia szczegóły danej góry.
Rysunek 9.6. Lista
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
150
AngularJS. Pierwsze kroki
Rysunek 9.7. Szczegóły dla góry Mount Everest, z przyciskiem umożliwiającym usunięcie pozycji
Przypatrzmy się, jak realizowana jest funkcjonalność pozwalająca na odwoływanie się do
określonych artykułów po id. Tak wygląda składnia pliku konfiguracyjnego:
.when("/mountain/:id", { templateUrl: "details.html", controller: "detailsCtrl" })
Dwukropek przed nazwą parametru wskazuje AngularJS, że jest to dynamiczna część
linku. Oznacza to, że jeśli odwołamy się np. w taki sposób: mountain/5, to zostaną nam
wyświetlone szczegóły dla góry Makalu. Tak jak w poprzednim przykładzie każdy
szablon strony kontrolowany jest przez swój kontroler. Warto zwrócić uwagę na to, jak
tym razem zorganizowaliśmy fabrykę mountainsList, która zwraca nam trzy funkcje:
 getAll,
 getById,
 deleteById.
Wszystkie powyższe funkcje wykonują działania na tablicy mountains: pierwsza zwraca
wszystkie obiekty tablicy, druga obiekt o podanym id, a trzecia usuwa z tablicy obiekt
o podanym id. Jak widać w obydwu powyższych przykładach, stworzenie aplikacji SPA
przy użyciu AngularJS jest bardzo proste.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 9.  Routing — lepsza strona nawigacji
151
Cztery kroki w procesie konfiguracji
Oto najważniejsze kroki, które należy wykonać, aby nasza aplikacja zadziałała:
1. Dołączamy do naszej strony angular-route.js.
2. Wstrzykujemy do modułu ngRoute w następujący sposób:
angular.module('myApp', ['ngRoute']);.
3. Dodajemy ng-view <div ng-view></div>.
4. Konfigurujemy:
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/home.html',
controller: 'homeCtrl'
})
}]);
Wykonanie czterech powyższych czynności pozwala nam cieszyć się zaletami routingu po stronie klienta. AngularJS robi większość rzeczy za nas, dzięki czemu możemy
skupić się na tworzeniu nowych, unikalnych funkcjonalności, którymi pozytywnie zaskoczymy naszych użytkowników.
Quiz
1. Do czego służy funkcja when?
2. Jak możemy skonfigurować domyślny szablon?
3. Która z dyrektyw służy do wyświetlania szablonów?
4. Czy szablon może posiadać własny kontroler?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
152
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
AngularJS. Pierwsze kroki
Rozdział 10.
Animacje
Wprowadzenie
Twórcy AngularJS zbudowali moduł ngAnimate, by ułatwić angularowym aplikacjom
korzystanie z CSS-a oraz JavaScriptu. Animacje w kanciastym możemy tworzyć na
kilka sposobów:
1. używając CSS3 Transitions;
2. korzystając z animacji CSS;
3. stosując JavaScript.
Zanim przejdziemy do tworzenia własnych animacji, przyjrzyjmy się bliżej powyższym
możliwościom. Zacznijmy od wstrzyknięcia do naszej aplikacji modułu ngAnimate —
listing 10.1.
Listing 10.1. Wstrzykiwanie modułu animacji
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>Wstrzykiwanie modułu animacji</title>
</head>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-animate.js"></script>
<script>
var app = angular.module('app', ['ngAnimate']);
</script>
</body>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
154
AngularJS. Pierwsze kroki
Po pierwsze należy dodać do naszego projektu odwołanie do angular-animate.js. Należy
je umieścić po uprzednim dodaniu samego angular.js. Następnie do naszego modułu
wstrzykujemy ngAnimate. OK, teraz jesteśmy gotowi do dodania animacji do naszej
angularowej aplikacji.
Jak to działa
Wszystko, czego potrzebujemy, żeby zobaczyć animację, to zdefiniowanie odpowiedniego
CSS-a lub zadeklarowanie animacji JavaScript przy użyciu funkcji myModule.animation().
Dyrektywami, które automatycznie obsługują animacje, są: ngRepeat, ngInclude, ngIf,
ngSwitch, ngShow, ngHide, ngView oraz ngClass. Dyrektywy niestandardowe mogą skorzystać z animacji przy pomocy serwisu $animate.
Poniżej, w tabeli 10.1, znajduje się szczegółowy podział zdarzeń animacji obsługiwanych przez wyżej wymienione dyrektywy.
Tabela 10.1. Dyrektywy — wspierane akcje
Dyrektywy
Wspierane akcje
ngRepeat
enter, leave i inne
ngView
enter, leave
ngInclude
enter, leave
ngSwitch
enter, leave
ngIf
enter, leave
ngClass
add, remove
ngShow i ngHide
add, remove
form i ngModel
add, remove (dirty, pristine, valid, invalid i inne)
ngMessages
add, remove
ngMessage
enter, leave
Wspomniany już serwis $animate dodaje wybrane klasy, bazując na zdarzeniach
emitowanych przez dyrektywę. Chodzi o takie operacje DOM jak: enter, leave oraz move.
W przypadku zaistnienia któregokolwiek z tych zdarzeń serwis $animate zbada wszystkie
zdefiniowane animacje JavaScript (są one zdefiniowane za pomocą $animateProvider),
jak również wszystkie animacje CSS, zdefiniowane w klasach CSS przypisanych do
danego obiektu.
Obietnice
W AngularJS 1.4 każda z metod animacji w serwisie $animate po wywołaniu zwraca
obietnicę. Obietnica zostaje spełniona po zakończeniu się animacji, gdy zostanie ona
odwołana lub gdy jest pominięta.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 10.  Animacje
155
$animate.enter(element, container).then(function () {
// wywołanie nastąpi w momencie, gdy animacja zostanie wykonana
});
Ze względu na charakter obietnicy, chcąc wywołać jakiś specyficzny kod AngularJS
(zmiana w modelu, zmiana lokalizacji strony itp.), należy pamiętać, by wywołać
$scope.$apply(...);
$animate.leave(element).then(function () {
$scope.$apply(function () {
$location.path('/nowa-strona');
});
});
Animację możemy odwołać poprzez wywołanie metody $animate.cancel(promise);
z dostarczonej obietnicy, zwróconej, gdy animacja się rozpoczęła.
var promise = $animate.addClass(element, 'super-long-animation').then(function () {
// to nadal będzie aktywne, nawet w przypadku wywołania $animate.cancel
});
element.on('click', function () {
// przerwij animację
$animate.cancel(promise);
});
Należy pamiętać, że odwoływanie obietnic jest unikalne dla usługi $animate. Inne
obietnice nie mogą być odwoływane.
CSS3 Transitions
CSS3 Transitions to zdecydowanie najprostszy sposób na wzbogacanie aplikacji animacjami. Działa na wszystkich przeglądarkach, z wyjątkiem IE9 i starszych. Użytkownicy
przeglądarek nieobsługujących CSS3 Transitions zobaczą nieanimowaną wersję naszej strony.
Pamiętajmy, że nasza animacja będzie działać dla tego elementu DOM, dla którego
została zdefiniowana.
W poniższym przykładzie klasa zostanie zastosowana do elementu div.
<div class="fade-out"></div>
CSS3 Transitions jest w całości oparty na klasach, co oznacza, że animacja będzie
wykonywana w przeglądarce tak długo, jak długo będziemy mieli animację podpiętą
pod element HTML.
Przejdźmy do przykładu, który pozwoli lepiej zrozumieć zasadę działania AngularJS
w połączeniu z CSS3 Transitions. Stworzymy prostą stronę wyświetlającą listę stopni
trudności. Listę będzie można filtrować, dzięki czemu będzie się ona skracać i rozszerzać
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
156
AngularJS. Pierwsze kroki
w zależności od wpisanego szukanego słowa. Użyjemy do tego dyrektywy ngRepeat,
która wyzwala trzy zdarzenia animacji: 'enter', 'leave' oraz 'move'. W pliku CSS
obsługujemy te zdarzenia w następujący sposób:
.pierwsza-animacja.ng-enter,
.pierwsza-animacja.ng-leave,
.pierwsza-animacja.ng-move {
-webkit-transition: 1s linear all;
transition: 1s linear all;
}
Następnie należy dodać styl określający start animacji:
.pierwsza-animacja.ng-enter,
.pierwsza-animacja.ng-move {
opacity: 0;
}
i jej kierunek:
.pierwsza-animacja.ng-enter.ng-enter-active,
.pierwsza-animacja.ng-move.ng-move-active {
opacity: 1;
}
Start animacji 'leave':
.pierwsza-animacja.ng-leave {
opacity: 1;
}
Koniec animacji 'leave':
.pierwsza-animacja.ng-leave.ng-leave-active {
opacity: 0;
}
Teraz pozostaje nam dodanie klasy pierwsza-animacja do elementu, w którym została
zdefiniowana dyrektywa ngRepeat:
<li data-ng-repeat="grade in grades | filter:search" class="pierwsza-animacja">
Nasz przykład w całości prezentuje listing 10.2, a efekt działania aplikacji można zobaczyć na rysunku 10.1.
Listing 10.2. Pierwsza animacja
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS - Animacje #1</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
<style>
.pierwsza-animacja.ng-enter,
.pierwsza-animacja.ng-leave,
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 10.  Animacje
157
.pierwsza-animacja.ng-move {
-webkit-transition: 1s linear all;
transition: 1s linear all;
}
.pierwsza-animacja.ng-enter,
.pierwsza-animacja.ng-move {
opacity: 0;
}
.pierwsza-animacja.ng-enter.ng-enter-active,
.pierwsza-animacja.ng-move.ng-move-active {
opacity: 1;
}
.pierwsza-animacja.ng-leave {
opacity: 1;
}
.pierwsza-animacja.ng-leave.ng-leave-active {
opacity: 0;
}
</style>
</head>
<body>
<div data-ng-controller="defaultCtrl">
<input placeholder="Szukaj" data-ng-model="search" class="form-control" />
<ol>
<li data-ng-repeat="grade in grades | filter:search"
class="pierwsza-animacja">
{{ grade }}
</li>
</ol>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-animate.js"></script>
<script>
var app = angular.module('app', ['ngAnimate']);
app.controller('defaultCtrl', function ($scope) {
$scope.grades = ['Niedostateczny', 'Dopuszczający', 'Dostateczny',
'Dobry', 'Bardzo dobry',
'Celujący'];
});
</script>
</body>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
158
AngularJS. Pierwsze kroki
Rysunek 10.1. Pierwsza animacja — podczas wpisywania kolejnych znaków zawężamy liczbę
wyświetlanych pozycji, które płynnie znikają
Animacje CSS3 i @keyframes
Animacje CSS są bardziej rozbudowane niż animacje oparte na CSS3 Transitions. Są
wspierane przez wszystkie przeglądarki, z wyłączeniem IE9 i starszych. Wykorzystując
CSS-owską regułę @keyframes, możemy stworzyć animację, która stopniowo zmienia
jeden zestaw stylów na drugi. Podczas animacji styl można zmieniać wielokrotnie.
Zmianę animacji możemy określić na dwa sposoby: w procentach lub przy pomocy
słów kluczowych from i to. W naszym kolejnym przykładzie użyjemy tej drugiej metody. Tym razem naszym celem jest zbudowanie aplikacji, która w momencie przełączania się pomiędzy stronami będzie pokazywać schludną animację — kod aplikacji
prezentuje listing 10.3. Wykorzystajmy naszą wiedzę dotyczącą routingu, budowy
kontrolerów oraz szablonów opartych na dyrektywie script. Efekt działania aplikacji
prezentuje rysunek 10.2.
Listing 10.3. Animacja pomiędzy stronami z użyciem CSS3 i @keyframes
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS - Animacje #2</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
<style>
.main-container {
height: 300px;
position: relative;
}
.main-animation.ng-enter {
-webkit-animation: enter 2s;
animation: enter 2s;
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 10.  Animacje
159
left: 100%;
}
.main-animation.ng-leave {
-webkit-animation: leave 2s;
animation: leave 2s;
left: 19px;
}
.main-animation.ng-leave,
.main-animation.ng-enter {
position: absolute;
top: 19px;
width: 100%;
}
@keyframes enter {
from {
left: 100%;
}
to {
left: 19px;
background: red;
}
}
@-webkit-keyframes enter {
from {
left: 100%;
}
to {
left: 19px;
background: red;
}
}
@keyframes leave {
from {
left: 19px;
}
to {
left: -100%;
}
}
@-webkit-keyframes leave {
from {
left: 19px;
}
to {
left: -100%;
}
}
</style>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
160
AngularJS. Pierwsze kroki
</head>
<body>
<div>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#/">CSS3 Keyframe</a>
</div>
<ul class="nav navbar-nav">
<li><a href="#/grades">Stopnie trudności</a></li>
</ul>
</div>
</nav>
<div class="main-container well">
<div data-ng-view="" class="main-animation"></div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-animate.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-route.js"></script>
<script>
var app = angular.module('app', ['ngAnimate', 'ngRoute']);
app.controller('gradesCtrl', function ($scope) {
$scope.grades = ['Bardzo łatwe', 'Łatwe', 'Trudne', 'Bardzo trudne'];
});
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'default.html'
});
$routeProvider.when('/grades', {
templateUrl: 'grades.html',
controller: 'gradesCtrl'
});
}]);
</script>
<script type="text/ng-template" id="default.html">
<h1>Witaj!</h1>
</script>
<script type="text/ng-template" id="grades.html">
<div data-ng-repeat="grade in grades">{{grade}}</div>
</script>
</body>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 10.  Animacje
161
Rysunek 10.2. Animacja pomiędzy stronami
Powyższy przykład pokazuje, w jaki sposób możemy dość mocno zmodyfikować wrażenia towarzyszące użytkownikowi podczas poruszania się po naszej aplikacji. Możemy oczywiście w tym samym czasie zmieniać wiele różnych stylów — w naszym
przypadku dla zwiększenia efektu dodaliśmy czerwone tło 'background: red'. Nowa
strona, przesuwając się z prawej strony do brzegu lewej, równocześnie nabiera coraz
mocniejszego koloru tła. Po zakończeniu animacji kolor znika.
Animacje JavaScript
Animacje JavaScript różnią się znacznie od tych poprzednio omawianych. Tym razem
przypisujemy właściwości do drzewa DOM, używając JavaScriptu. Zbudujmy szkielet pierwszej animacji:
var app = angular.module('app', ['ngAnimate']);
app.animation('.nazwa-animacji', function () {
return {
event: function (elem, done) {
// logika animacji
done();
return function (cancelled) {
// callback zamykający lub odwołujący
}
}
};
});
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
162
AngularJS. Pierwsze kroki
Oto kilka rzeczy, o których warto pamiętać podczas pisania animacji JavaScript w AngularJS:
1. Nazwa animacji zaczyna się od kropki (.).
2. Każda akcja animacji przyjmuje dwa parametry:
 Obiekt, będący aktualnym elementem drzewa DOM, w którym zostanie
zastosowana animacja. Jest to obiekt jqLite, jeśli nie korzystamy z biblioteki
jQuery (nie załadowaliśmy jej przed AngularJS). W przeciwnym razie jest
to obiekt jQuery.
 Funkcja zwrotna, która jest wywoływana po zakończeniu animacji.
Działanie dyrektywy zostanie wstrzymane, jeżeli zostanie wywołana
funkcja done().
Istnieje wiele bibliotek (np. Anima.js) wspomagających tworzenie animacji z wykorzystaniem JavaScriptu. Na potrzeby naszych przykładów pozostaniemy przy jQuery.
Przejdźmy teraz do przykładu. Zacznijmy od ngView. Korzystaliśmy już wielokrotnie
z tej użytecznej dyrektywy, tym razem stworzymy animację generowaną za pomocą
JavaScriptu w momencie przejścia pomiędzy stronami. Do div zawierającego dyrektywę
dodamy klasę powodującą powolne pojawianie się i przesuwanie wybranych szablonów.
<div data-ng-view="" class="view-fade-in"></div>
Następnie w naszym module tworzymy taką animację:
var app = angular.module('app', ['ngAnimate', 'ngRoute']);
app.animation('.view-fade-in', function () {
return {
enter: function(element, done) {
element.css({
opacity: 0,
position: "relative",
left: "100px"
})
.animate({
top: 0,
left: 0,
opacity: 1
}, 2000, done);
}
};
});
Bezpośrednie wywołanie animate() bez konwersji elementu do obiektu jQuery jest
możliwe, ponieważ załadowaliśmy bibliotekę jQuery przed kanciastym. Zobaczmy
pełny przykład na listingu 10.4, by lepiej zrozumieć zachodzące zależności.
Listing 10.4. Animacja pomiędzy stronami z użyciem JavaScriptu
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS - Animacje JavaScript</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 10.  Animacje
</head>
<body>
<div>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#/">Animacje JavaScript</a>
</div>
<ul class="nav navbar-nav">
<li><a href="#/grades">Stopnie trudności</a></li>
<li><a href="#/mountain">Góry</a></li>
</ul>
</div>
</nav>
<div class="main-container well">
<div data-ng-view="" class="view-fade-in"></div>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-animate.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-route.js"></script>
<script>
var app = angular.module('app', ['ngAnimate', 'ngRoute']);
app.controller('gradesCtrl', function ($scope) {
$scope.grades = ['Bardzo łatwe', 'Łatwe', 'Trudne', 'Bardzo trudne'];
});
app.controller('secondCtrl', function ($scope) {
$scope.mountainsList = [
{ mountain: "Mount Everest", metres: 8850 },
{ mountain: "K2", metres: 8611 },
{ mountain: "Kangczendzonga", metres: 8598 },
{ mountain: "Lhotse", metres: 8501 }];
});
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'default.html'
});
$routeProvider.when('/grades', {
templateUrl: 'grades.html',
controller: 'gradesCtrl'
});
$routeProvider.when('/mountain', {
templateUrl: 'mountain.html',
controller: 'secondCtrl'
});
}]);
app.animation('.view-fade-in', function () {
return {
enter: function (element, done) {
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
163
164
AngularJS. Pierwsze kroki
element.css({
opacity: 0,
position: "relative",
left: "100px"
})
.animate({
top: 0,
left: 0,
opacity: 1
}, 2000, done);
}
};
});
</script>
<script type="text/ng-template" id="default.html">
<h1>Witaj!</h1>
</script>
<script type="text/ng-template" id="grades.html">
<div data-ng-repeat="grade in grades">{{grade}}</div>
</script>
<script type="text/ng-template" id="mountain.html">
<div data-ng-repeat="mountain in mountainsList">
{{mountain.mountain}} - {{mountain.metres}}
</div>
</script>
</body>
</html>
Efekt działania prezentuje rysunek 10.3.
Rysunek 10.3. Animacja z użyciem JavaScriptu
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 10.  Animacje
165
Przejdźmy teraz do listingu 10.5 i jednej z najczęściej używanych dyrektyw, ngRepeat,
by stworzyć dla niej animację opartą na JavaScripcie.
Listing 10.5. Animacja dyrektywy ngRepeat z użyciem JavaScriptu oraz filtru
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS -CSS3-Transitions</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body>
<div data-ng-controller="defaultCtrl">
<input placeholder="Szukaj" data-ng-model="search" class="form-control" />
<ol>
<li data-ng-repeat="grade in grades | filter:search" class=
"moving-sideways">
{{ grade }}
</li>
</ol>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-animate.js"></script>
<script>
var app = angular.module('app', ['ngAnimate']);
app.controller('defaultCtrl', function ($scope) {
$scope.grades = ['Niedostateczny', 'Dopuszczający', 'Dostateczny', 'Dobry',
'Bardzo dobry', 'Celujący'];
});
app.animation('.moving-sideways', function () {
return {
enter: function (element, done) {
var width = element.width();
element.css({
position: 'relative',
right: -100,
opacity: 0
});
element.animate({
right: 0,
opacity: 1
}, done);
},
leave: function (element, done) {
element.css({
position: 'relative',
right: 0,
opacity: 1
});
element.animate({
right: -100,
opacity: 0
}, done);
},
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
166
AngularJS. Pierwsze kroki
move: function (element, done) {
element.css({
right: "100px",
opacity: 0
});
element.animate({
right: "0px",
opacity: 1
}, done);
}
};
});
</script>
</body>
</html>
Następny listing, 10.6, pokaże, w jaki sposób możemy ukrywać i z powrotem pokazywać
wybrane elementy naszej strony. Wykorzystamy do tego dyrektywy ngShow i ngHide.
Zobaczmy to na przykładzie:
Listing 10.6. Ukrywanie elementów
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS -CSS3-Transitions</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body>
<div data-ng-controller="defaultCtrl">
<div ng-init="checked=true">
<label>
<input type="checkbox" ng-model="checked" style="float:left;
margin-right:10px;"> Pokaż / ukryj
</label>
<div class="show-hide" ng-show="checked" style="clear:both;">
<ol>
<li data-ng-repeat="grade in grades">
{{ grade }}
</li>
</ol>
</div>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-animate.js"></script><script>
var app = angular.module('app', ['ngAnimate']);
app.controller('defaultCtrl', function ($scope) {
$scope.grades = ['Niedostateczny', 'Dopuszczający', 'Dostateczny',
'Dobry', 'Bardzo dobry',
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 10.  Animacje
167
'Celujący'];
});
app.animation('.show-hide', function () {
return {
beforeAddClass: function (element, className, done) {
if (className === 'ng-hide') {
console.log('beforeAddClass');
element.animate({
opacity: 0
}, 800, done);
} else {
done();
}
},
removeClass: function (element, className, done) {
if (className === 'ng-hide') {
console.log('removeClass');
element.css('opacity', 0);
element.animate({
opacity: 1
}, 800, done);
} else {
done();
}
}
};
});
</script>
</body>
</html>
W powyższym przykładzie dyrektywa ngRepeat generuje listę nazw. Korzystając z możliwości, jakie daje ng-show, możemy ją ukryć (podczas tego działania uruchamiana
jest nasza animacja). Zwraca ona dwie funkcje: beforeAddClass oraz removeClass.
Pierwsza powoduje powolne zanikanie elementu, druga jego powolne pojawianie się.
Dodatkowo w konsoli możemy zobaczyć, kiedy zostaje wywołana pierwsza funkcja,
a kiedy druga.
Quiz
1. Do czego służy moduł ngAnimate?
2. Jakie są trzy sposoby tworzenia animacji w AngularJS?
3. Które z dyrektyw automatycznie obsługują animacje?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
168
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
AngularJS. Pierwsze kroki
Rozdział 11.
Komunikacja z serwerem
Wprowadzenie
AJAX umożliwia komunikację z serwerem bez konieczności odświeżania strony. Jego
zaimplementowanie było krokiem milowym dla deweloperów i zrewolucjonizowało
aplikacje webowe. Oczywiście Angular jako framework SPA również korzysta z AJAX,
dodatkowo robi to automatycznie przy pomocy wbudowanych usług, zdejmując ten
ciężar z barków programisty.
Usługa odpowiedzialna za komunikację z serwerem to $http. Angular dostarcza nam niezwykle użyteczne API upraszczające korzystanie z dwóch popularnych metod komunikacji: XHR (XmlHttpRequest) oraz JSONP (JSON with padding).
Dodatkowym zagadnieniem, któremu się tutaj przyjrzymy, są obietnice (promises) zajmujące się obsługą asynchronicznych zapytań. Warto wiedzieć, czym różnią się one
od tradycyjnego callbacku i dlaczego odsłaniają przed nami nowe możliwości.
Rozdział ten skupia się na różnorodnych technikach komunikacji z back-endowymi
serwerami HTTP, opisując ją z perspektywy aplikacji front-endowych. Nie zawiera
on informacji dotyczących konfiguracji serwera.
Klasyczne zapytanie XHR a usługa $http
Na początek omówimy udogodnienia płynące z korzystania z usługi $http. Wysyłając
zapytanie XHR, musimy poradzić sobie z takimi aspektami jak:
 wysyłanie zapytania;
 przetwarzanie odpowiedzi;
 sprawdzanie błędów.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
170
AngularJS. Pierwsze kroki
Przy zastosowaniu tradycyjnej formy zapytań XHR czekałoby nas sporo roboty. Bazując
nawet na tak prostym przykładzie, jesteś w stanie zobrazować sobie przewagę wynikającą z użycia $http.
Wcześniej jednak powiemy kilka słów o JSON-ie. JSON (JavaScript Object Notation) to
specyficzna składnia do przechowywania i wymiany danych. Jest to przystępniejsza
alternatywa XML. Wprawdzie JSON wywodzi się oryginalnie z języka JavaScript,
lecz jest niezależny i szeroko wykorzystywany w innych językach programistycznych.
Plik data.json
{"var1": "Test"}
Tradycyjne wywołanie XHR wygląda następująco:
var params = "param1=test";
var http = new XMLHttpRequest();
http.open('GET', 'data.json', true);
http.onreadystatechange = function () {
if (http.readyState == 4 && http.status == 200) {
var data = http.responseText;
console.log(data);
}
else if (http.status = 400) {
// obsługa błędów
}
}
http.send(params);
Status odpowiedzi w zakresie od 200 do 299 oznacza powodzenie.
Nawet tak prosta operacja wymaga od nas sporej, jak na stopień trudności tego zadania,
ilości kodu. Przyjrzyjmy się teraz, jak to samo wywołanie wygląda przy wykorzystaniu $http.
XHR przy użyciu $http
Jak już wcześniej wspominaliśmy, $http to proste w obsłudze API stworzone w celu
wywoływania żądań JSONP oraz XHR bez konieczności pisania uciążliwego kodu.
Listing 11.1 zawiera przykładową aplikację.
Listing 11.1. Przykładowe wywołanie $http
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS – XHR /title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 11.  Komunikacja z serwerem
171
<body>
<div ng-controller="defaultCtrl">
<pre>
data = {{data}}
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope, $http, conn) {
conn.getData().then(function (data) {
$scope.data = data;
});
});
app.factory('conn', function ($http) {
return {
getData: function () {
return $http.get('data.json')
.then(function (result) {
return result.data;
});
}
}
});
</script>
</body>
</html>
Plik data.json
{
"nazwa":"Plik 1",
"autor":"Jan",
"data":"2015-10-16T17:57:28.556094Z"
}
Wynik wywołania listingu 11.1 można zobaczyć na rysunku 11.1.
Rysunek 11.1. Wynik wywołania $http
Samo wywołanie metody GET zajęło nam 3 linijki. Wystarczy porównać oba podejścia,
by zrozumieć, jak dużo robi za nas AngularJS.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
172
AngularJS. Pierwsze kroki
Odpowiedzi http
Promises
Tradycyjnym sposobem zarządzania asynchronicznymi operacjami jest callback. AngularJS, jak już wcześniej wspomnieliśmy, korzysta z obietnic. Odpowiedź zwracana
przez $http to obietnica (ang. promise). Jako że nie mamy gwarancji, iż zostaną nam
zwrócone odpowiednie dane, wynikiem może być oczekiwany obiekt bądź wyrzucony wyjątek. W Angularze do obchodzenia się z obietnicami służy API $q.
Zasadniczo nie ma nic złego w wykorzystywaniu callback w sytuacji, kiedy mamy do
czynienia z małą liczbą asynchronicznych zapytań. Niestety wraz ze wzrostem złożoności zadania wzrasta również trudność w implementowaniu callback i występuje
problem tzw. callback hell, czyli zagnieżdżenia zapytań. W żadnym wypadku jednak
nie zakazujemy czytelnikowi stosowania callback, gdyż istnieją techniki pozwalające
na jego optymalizację. Staramy się jedynie przedstawić alternatywne rozwiązanie wykorzystane w Angularze.
Obietnice posiadają cechy funkcji synchronicznych, ale są pełnoprawnymi funkcjami
asynchronicznymi. Obsługa wyjątków w dowolnym momencie działania aplikacji jest
jednym z takich przykładów. Dzięki ich asynchronicznej naturze nie musimy się
przejmować blokowaniem wątków.
W Angularze istnieją dwa sposoby obchodzenia się z odpowiedzią. Pierwszy polega
na wykorzystaniu dwóch metod, success() i error(), a drugi wymaga jednej metody,
then(), którą pokazaliśmy w przykładzie powyżej.
success() i error()
Funkcja przyjmowana przez te metody zawiera cztery opisane poniżej parametry:
 data — zawiera dane powiązane z zapytaniem.
 status — informuje o statusie odpowiedzi.
 headers — umożliwia dostęp do nagłówków.
 config — obiekt konfiguracyjny, który jest wysyłany wraz z zapytaniem.
Składnia takiego wywołania wygląda następująco:
$http.get('data.json')
.success(function (data, status, headers, config) {
console.log(data);
})
.error(function (data, status, headers, config) {
console.log("ERROR!");
});
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 11.  Komunikacja z serwerem
173
W przypadku braku zdefiniowania którejś z metod odpowiedzi jest ona najzwyczajniej pomijana.
Różnica pomiędzy metodą success() i error() a then() polega na tym, iż wybierając
wariant drugi, otrzymujemy jeden obiekt zwrotny. Wariant pierwszy natomiast zwraca
nam odpowiedź rozbitą na opisane powyżej parametry.
$q, obietnice i odroczenia
AngularJS opracował własną implementację obietnic, bazującą na bibliotece q stworzonej
przez Krisa Kowala. $q można rozłożyć na dwa elementy: odroczenia (deferreds)
oraz obietnice (promises). Pierwszy obiekt reprezentuje zadanie, które nie zostało
jeszcze ukończone, i służy do informowania o postępie, zakończeniu oraz rezultacie
zadania. Tworzenie odroczenia wygląda tak:
var defferedObject = $q.defer();
Zaraz po stworzeniu obiekt otrzymuje status pending. Oznacza to, iż oczekuje na wynik,
sukces bądź porażkę.
Odroczenia posiadają dwie metody, resolve() oraz reject(), mające za zadanie zwrócić
odpowiedni wynik. Pierwsza metoda sygnalizuje pomyślność wykonania zadania.
function deferredTimer(success) {
var deferred = $q.defer();
$timeout(function () {
if (success) {
deferred.resolve({ message: "Udało się!" });
} else {
deferred.reject({ message: "Nie udało się :<" });
}
}, 2000);
return deferred.promise;
}
deferred posiada również obiekt promise, reprezentujący obietnice. Możemy stworzyć
obietnicę i przypisać jej odpowiednie operacje w zależności od sukcesu bądź porażki.
var promiseObject = deferred.promise;
promiseObject
.then(function (someData) {
//obsługa zapytania
}, function (someError) {
//obsługa błędu
});
Jeżeli natomiast planujemy wykonać tę samą operację, niezależnie od wyniku zapytania musimy posłużyć się metodą promise.finally().
Skoro wiesz już, jak działają obiekty deferred, przejdźmy teraz do listingu 11.2 i zobaczmy ich działanie w pełnej aplikacji. Uzyskany efekt prezentuje rysunek 11.2.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
174
AngularJS. Pierwsze kroki
Listing 11.2. Obiekty deferred
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS - obietnice/title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<pre>
result = {{result}}
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope, $q, $timeout) {
$scope.deferTest = function (success) {
deferredTimer(success).then(
function (data) {
$scope.result = "Udało się: " + data.message;
},
function (data) {
$scope.result = "Ups! : " + data.message;
}
);
};
function deferredTimer(success) {
var deferred = $q.defer();
$timeout(function () {
if (success) {
deferred.resolve({ message: "Jest OK!" });
} else {
deferred.reject({ message: "Nie jest OK!" });
}
}, 2000);
return deferred.promise;
}
$scope.deferTest(true);
});
</script>
</body>
</html>
Obietnice mogą się łączyć w tzw. reakcje łańcuchowe. Oznacza to, że kilka operacji
może zostać wykonanych jedna po drugiej. Przykład ten zawarliśmy w listingu 11.3.
Ostatecznie konsola wypisze string „Hello, world!”.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 11.  Komunikacja z serwerem
175
Rysunek 11.2. Obiekty deferred
Listing 11.3. Reakcje łańcuchowe obietnic
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app">
<head>
<title>AngularJS – łańcuchy obietnic/title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/
css/bootstrap.min.css">
</head>
<body>
<div ng-controller="defaultCtrl">
<pre>
</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope, $q) {
$scope.helperFunction = function (someValue) {
var deferred = $q.defer();
deferred.resolve(someValue);
return deferred.promise;
}
var somePromise = $scope.helperFunction('Hello')
.then(function (x) {
return x + ', world';
})
.then(function (x) {
return x + '!';
})
.then(function (x) {
console.log(x);
});
$scope.helperFunction();
});
</script>
</body>
</html>
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
176
AngularJS. Pierwsze kroki
$q.all
Funkcja all pozwala nam połączyć kilka obietnic w jedną, dzięki czemu nasz kod będzie wyglądał nieco bardziej elegancko.
Zobaczmy, co się stanie, gdy zechcemy zsynchronizować kilka asynchronicznych zapytań. Wywołujemy trzy razy metodę GET, by otrzymać trzy JSON-y.
var full = [];
$http.get('data1.json').success(function (data) {
full.push(data);
$http.get('data2.json').success(function (data) {
full.push(data);
$http.get('data3.json').success(function (data) {
full.push(data);
$scope.result = full.join(", ");
});
});
});
Zagnieżdżone zapytania nie są zbyt eleganckim rozwiązaniem. Powinniśmy wykorzystać $q.all.
var data1 = $http.get('data1.json'),
data2 = $http.get('data1.json'),
data3 = $http.get('data1.json');
$q.all([data1, data2, data3]).then(function (result) {
var full = [];
angular.forEach(result, function (response) {
full.push(response.data);
});
return full;
}).then(function (fullResult) {
$scope.combinedResult = fullResult.join(", ");
});
Powyższy przykład pokazuje dobre praktyki synchronizacji kilku asynchronicznych
wywołań.
Przechowywanie odpowiedzi
AngularJS umożliwia przechowywanie odpowiedzi wysyłanych przez serwer. By
skorzystać z tej usługi, należy ją odblokować w następujący sposób:
$http.get('naszeDane.json',{ cache: true })
.success(function(data, status, headers, config) {
}
Od tej pory wszystkie odpowiedzi na zapytania pochodzące z tego samego adresu
URL są pobierane przez AngularJS z pamięci podręcznej.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 11.  Komunikacja z serwerem
177
Warto też wiedzieć, że w sytuacji, gdy występuje więcej niż jedno zapytanie z tego samego adresu, AngularJS pobiera odpowiedź tylko raz i wykorzystuje ją, by odpowiedzieć na
każde z zapytań. Skupiając się na poprawie efektywności, autorzy przeoczyli jednak
jeden dość istotny szczegół, mianowicie dane prezentowane użytkownikowi mogą zostać podmienione przez nowe, np. w momencie zatwierdzenia przez użytkownika
wykonywania jakiejś ważnej operacji. Skutkować to może przypadkowymi błędami.
Pozostałe metody $http
Metoda GET wykorzystana w powyższych przykładach jest jedną z kilku metod służących do wykonywania żądań XHR. Sposób korzystania z tych metod nie różni się
zbytnio od sposobu wywoływania GET. Owe metody, w które wyposażono $http, to:
 POST — $http.post(url, data, [config]);
 PUT — $http.put(url, data, [config]);
 DELETE — $http.delete(url, [config]);
 HEAD — $http.head(url, [config]);
 JSONP — $http.jsonp(url, [config]);
 PATCH - $http.json(url, data, [config]).
Lista parametrów przyjmowana przez $http różni się w zależności od wywoływanej
metody HTTP. Poniżej wymieniamy te parametry:
 url — to nic innego jak adres URL powiązany z wywołaniem;
 data — dane wysyłane wraz z zapytaniem;
 config — obiekt javaScriptowy zawierający dodatkowe informacje na temat
konfiguracji, wpływające na obsługę wywołań. Parametr ten jest opcjonalny.
Element pierwszy jest obowiązkowy i znajduje się w każdej z powyższych metod.
Drugi element jest opcjonalny, gdyż wykorzystują go jedynie metody POST, PATCH oraz
PUT, służące do przenoszenia danych. Każdemu z nich poświęcimy nieco więcej miejsca
w dalszej części tego rozdziału.
Parametry metody $http
Obiekt konfiguracyjny
Jego zadaniem jest modyfikowanie domyślnych standardów zapytań i odpowiedzi
w taki sposób, by spełniały wymagania stawiane przez dewelopera. Obiekt konfiguracyjny dokonuje tych zmian na podstawie atrybutów przesłanych razem z nim. Poniżej przedstawiamy ich listę wraz z opisem:
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
178
AngularJS. Pierwsze kroki
 url — jest to wcześniej opisany adres celu.
 header — dzięki temu parametrowi jesteśmy w stanie załączyć do naszego
zapytania dodatkowy nagłówek.
 params — mapa (nie lista!) parametrów występujących po sobie, oddzielonych
przecinkami. Może zostać załączona wraz z adresem URL. Przybiera formę
„atrybut i wartość”, np. [{ atr1: 'wartość1', atr2: 'wartość2'}].
 cache — pozwala na cachowanie zapytań.
 data — obiekt bądź łańcuch, który jest wysyłany wraz z zapytaniem
lub odpowiedzią.
 timeout — mierzony w milisekundach czas, po którym usługa zostanie
wycofana.
 transformRequest — funkcja transformacyjna, która służy do preprocesowania
danych z serwera.
 transformResponse — funkcja transformacyjna, która służy do postprocesowania
danych z serwera.
 xsrfHeaderName — zawiera nazwę nagłówka potrzebnego przy tokenie XSRF.
 xsrfCookieName — zawiera nazwę ciasteczka przetrzymującego token XSRF.
 withCredentials — jest to wyrażenie boolowskie.
 responseType — zwraca typ zapytania.
 method — precyzuje, jaka metoda HTTP zostanie wykonana.
Obiekt konfiguracyjny wysyłany jest jako ostatni argument usługi $http. Pozwala to
wykorzystać go w więcej niż jednej metodzie (GET, POST itp.).
Dane
Akceptowanymi danymi dla metod PUT i POST są dowolne obiekty JavaScript. Zostają
one automatycznie przekonwertowane do formatu tekstowego JSON. Jako dane
możemy też przesyłać łańcuchy (string) danych, które oczywiście nie podlegają żadnym zmianom.
Mechanizm służący do konwertowania danych na format JSON ignoruje wszelakie
zmienne zaczynające się znakiem dolara ($).
Usługa $http spróbuje przeprowadzić operację zmiany JSON w obiekt JavaScript, zanim zostanie sprawdzony rodzaj wywołania zwrotnego.
Poniżej prezentujemy niezwykle prosty przykład korzystający z metody POST. Jest to
żądanie utworzenia nowego produktu.
var nowyProdukt = {
nazwa: 'Zakochaj się w AngularzeJS',
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 11.  Komunikacja z serwerem
179
typ: 'Podręcznik',
kodKreskowy: '00021254552'
};
Użycie metody POST:
$http.post('/url', nowyProdukt);
Same origin policy oraz JSONP i CORS
na ratunek XHR
Koncepcja same origin narodziła się w latach 90. XX w. Służy ona do zwiększania bezpieczeństwa danych wędrujących w aplikacjach internetowych. Zezwala skryptom na
dostęp do elementów DOM znajdujących się na tej samej stronie internetowej, lecz
ogranicza komunikację z DOM innych stron.
Wykorzystanie XHR do operacji na usługach spoza „domowej” strony nie należy do
najłatwiejszych zadań. Z pomocą przychodzą jednak przeróżne techniki obchodzenia
tego problemu. Postanowiliśmy przedstawić tu opis tych najpopularniejszych.
JSON with padding oraz jego ograniczenia
Istnieje pewien sposób na obejście polityki same-origin. JSONP nie wywołuje zapytań
XHR, lecz generuje skrypt <script>, którego źródło zawiera referencję do interesującego
nas „obcego” serwera. JSONP wykorzystuje tutaj fakt, iż możemy bezkarnie pobierać
elementy znajdujące się wewnątrz znacznika <script>. Po wygenerowaniu i umieszczeniu <script> wewnątrz DOM wywoływany jest serwer. Odpowiedź jest „owijana”
(ang. padding) funkcją wywołania znajdującą się w naszej aplikacji.
Niestety dla nas, JSONP ma pewne ograniczenia. Po pierwsze może zostać wykorzystany jedynie do zapytań z metodą GET. Po drugie, ponieważ przeglądarka nie jest w stanie
wyświetlić statusu odpowiedzi HTTP (wszystko przez ten <script>!), utrudnione jest
rozwiązywanie problemów. Ponadto serwer może wygenerować dowolny kod JavaScript i umieścić go w JSONP. Zostanie on automatycznie załadowany do przeglądarki
i wykonany w sesji użytkownika. Odpowiednio skonfigurowany serwer potrafi więc np.
wykraść cenne dane, przejąć sesję bądź narobić innych kosztownych szkód. Zaleca
się stosowanie tej metody tylko na zaufanych stronach.
CORS — Cross Origin Resource Sharing
Możliwe jest również rozwiązanie omawianego problemu w bezpieczniejszy sposób.
CORS polega na zainicjowaniu współpracy przeglądarki z obcym serwerem poprzez
wysyłanie zapytań i odpowiedzi w celu tymczasowego umożliwienia komunikacji.
By jednak stało się to możliwe, serwer, z którym chcemy się skomunikować, musi
być należycie skonfigurowany, aby mógł poprawnie interpretować zapytania.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
180
AngularJS. Pierwsze kroki
Dzięki CORS nie musimy ograniczać się jedynie do GET. Możemy dowolnie wybierać
z pełnej listy metod (opisanych w poprzedniej części tego rozdziału). Niestety ma to swoją
cenę, gdyż wymagana jest nowoczesna przeglądarka internetowa. Dodatkowo należy
wprowadzić odpowiednie konfiguracje zarówno po stronie klienta, jak i serwera.
Trzecie wyjście: proxy
W sytuacji, gdy chcemy uniknąć stosowania JSONP czy CORS lub nie możemy z nich
skorzystać, nie pozostaje nam nic innego, jak skonfigurować nasz serwer lokalny przy
pomocy proxy. Sposób ten zadziała w każdej przeglądarce i nie narazi nas na dodatkowe niebezpieczeństwa. Wymaga natomiast dodatkowego skonfigurowania serwera.
Quiz
1. Co to jest AJAX?
2. Za co odpowiada usługa $http?
3. Co to jest XHR?
4. Do czego służy #q.all?
5. Co to jest CORS?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 12.
Formularze
Wprowadzenie
Kontrolki (input, select, textarea) pozwalają użytkownikowi na wprowadzanie danych. Formularz jest zbiorem powiązanych kontrolek. Dla polepszenia użyteczności
formularz i kontrolki świadczą usługę sprawdzania poprawności wprowadzonych danych.
Użytkownik otrzymuje natychmiastową informację zwrotną o tym, jak naprawić błąd.
Sprawdzanie błędów po stronie przeglądarki można obejść, w związku z czym nie jesteśmy zwolnieni z powtórzenia tej czynności po stronie serwera. Sprawdzać błędy i informować o nich użytkownika możemy na wiele różnych sposobów.
ngFormController
Przyjrzyjmy się bliżej temu, jak AngularJS może nam w tym pomóc. Poniższa tabela 12.1 prezentuje właściwości ngFormController wspomagające obsługę błędów w formularzach.
Używanie klas CSS
Aby umożliwić nam korzystanie z CSS-a, ngModel dodaje następujące klasy:
 ng-valid: model jest poprawny.
 ng-invalid: model nie jest poprawny.
 ng-valid-[key]: dla każdego poprawnego klucza, który został dodany
przez $setValidity.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
182
AngularJS. Pierwsze kroki
Tabela 12.1. Właściwości ngFormController
Właściwość
Typ
Opis
$pristine
boolean
true, jeśli formularz nie został zmieniony (żadne pole nie zostało
zmienione); false, jeżeli któreś z pól zostało zmienione.
$dirty
boolean
Odwrotność $pristine; true, jeśli użytkownik skorzystał
z formularza.
$valid
boolean
true, jeżeli wszystkie pola formularza zostały prawidłowo
wypełnione.
$invalid
boolean
Odwrotność $valid; true, jeśli któreś z pól formularza nie zostało
poprawnie wypełnione.
$submitted
boolean
Użytkownik wysłał formularz.
$error
Object
Obiekt zawierający referencje do kontrolek lub formularzy, które
nie przeszły prawidłowej weryfikacji.
Wbudowany w tokeny:
email
max
maxlength
min
minlength
number
pattern
required
url
date
datetimelocal
time
week
month
 ng-invalid-[key]: dla każdego niepoprawnego klucza, który został dodany
przez $setValidity.
 ng-pristine: kontrolka nie została jeszcze użyta.
 ng-dirty: kontrolka została użyta.
 ng-touched: użytkownik opuścił kontrolkę.
 ng-untouched: użytkownik nie opuścił kontrolki.
 ng-pending: wszystkie $asyncValidators, które nie zostały wykonane.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 12.  Formularze
183
Pierwszy formularz
Przejdźmy teraz do praktycznego wykorzystania zdobytej wiedzy. W ramach ćwiczenia stworzymy formularz kontaktowy — listing 12.1. Nie będzie on przesyłał danych
na serwer, a jedynie kopiował je i wyświetlał w postaci JSON.
Listing 12.1. Formularz kontaktowy
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" data-ng-app="app">
<head>
<title>AngularJS - form</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/
css/bootstrap.css" />
</head>
<body>
<div data-ng-controller="defaultCtrl">
<form name="ContactForm">
Imię:
<input type="text" ng-model="contact.firstName" name="firstName" class=
"form-control" required />
Nazwisko:
<input type="text" ng-model="contact.lastName" class="form-control"
required />
E-mail:
<input type="email" ng-model="contact.email" class="form-control"
required />
Wiadomość:
<textarea ng-model="contact.info" class="form-control" required>
</textarea>
Płeć:
<input type="radio" ng-model="contact.gender" value="male" class=
"radio radio-inline" />mężczyzna
<input type="radio" ng-model="contact.gender" value="female" class=
"radio radio-inline" />kobieta
<br /><br />
Wiek:
<input type="number" ng-model="contact.age" class="form-control"
required />
<button class="btn btn-success" ng-click="update(contact)" ng-disabled=
"form.$invalid">Zapisz</button>
</form>
<pre>form = {{contact | json}}</pre>
<pre>contactForm = {{contactForm | json}}</pre>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script>
var app = angular.module('app', []);
app.controller('defaultCtrl', function ($scope) {
var date = new Date();
$scope.contactForm = {
date: date
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
184
AngularJS. Pierwsze kroki
};
$scope.update = function (contact) {
$scope.contactForm = angular.copy(contact);
};
});
</script>
</body>
</html>
Jak widać na rysunku 12.1, otrzymaliśmy formularz kontaktowy z dosyć zaawansowaną
walidacją. Przy próbie wysłania formularza użytkownik zostanie poproszony o uzupełnienie kolejnych pól.
Rysunek 12.1.
Formularz
kontaktowy
Formularze możemy tworzyć na dziesiątki, o ile nie setki różnych sposobów. Warto poeksperymentować, tak by zdobyć jak największe doświadczenie. Formularze są nieodłączną częścią zdecydowanej większości witryn internetowych, dlatego prędzej czy
później każdy twórca stron internetowych się z nimi zetknie.
Quiz
1. Wymień właściwości ngFormController.
2. Jaka jest różnica pomiędzy $pristine a $dirty?
3. Jakie wbudowane tokeny posiada właściwość $error?
4. Jakie klasy pozwalające korzystać z CSS-a udostępnia ngModel?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 13.
Dobre praktyki
Wprowadzenie
Wielu z nas, programistów, stawia sobie za cel być coraz lepszym pracownikiem, coraz lepszym człowiekiem w ogóle. Poświęcamy mnóstwo czasu na naukę, doświadczenia oraz pracę. Nie zawsze jednak ciężka praca równa jest pracy błyskotliwej. Jak
mawia Brian Tracy, nieważne, skąd przyszedłeś, ważne, dokąd zmierzasz. Przenosząc
to zdanie na nasze podwórko, możemy powiedzieć: nieważne, jak pisałeś kod dotychczas, ważne, jak zamierzasz go pisać w przyszłości. Zachęcamy do korzystania z istniejących wzorców lub wypracowanych sposobów radzenia sobie z różnego rodzaju
wyzwaniami. Ten rozdział poświęcony jest dobrym praktykom, z którymi warto się
zapoznać. Być może znajdziesz tu coś, co pomoże Ci przejść na kolejny poziom tworzenia aplikacji przy użyciu kanciastego. Wszystko zależy od Ciebie.
Nazewnictwo i podział plików
Dobra aplikacja to aplikacja zaprojektowana optymalnie. Nie od dziś wiadomo, że czas
poświęcony na planowanie zwraca się dziesięciokrotnie. Czas poświęcony na projektowanie nie tylko zwróci się dziesiątki, a może i setki razy, ale przede wszystkim może zadecydować o sukcesie lub porażce aplikacji. Źle zaprojektowana, a przez to bardzo trudna
w utrzymaniu aplikacja umrze śmiercią naturalną przy pierwszej większej aktualizacji.
Nazewnictwo i podział plików to jedne z pierwszych zagadnień, z jakimi stykamy się
podczas projektowania nowej aplikacji. Warto przyjrzeć się temu, jak sobie z tym poradzili twórcy AngularJS: wyciągnąć wnioski i część rzeczy zaadaptować, a o części
szybko zapomnieć. Na stronie angularjs.org możemy znaleźć dużo gotowych rozwiązań
i wskazówek, jak poradzić sobie w określonych sytuacjach. O ile samo nazewnictwo
jest potraktowane poważnie i można z powodzeniem przenieść je do własnych projektów,
o tyle podział plików jest niestety uproszczony tak bardzo, że aż sprzeczny z główną
filozofią modułowości, na której opiera się framework.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
186
AngularJS. Pierwsze kroki
Zatrzymajmy się chwilę przy nazewnictwie. O czym należy pamiętać, tworząc nazwy
plików i katalogów? Po pierwsze o ich czytelności i jednoznaczności. Popatrzmy na poniższy przykład:
FolderJeden/FolderZawierajacyPliki/PlikZawierajacyFunkcjeiKontrolery
DlaStronyGlownej.js
Pokazuje on, jak nie należy konstruować nazw. Wyobraźmy sobie, że mamy dwadzieścia
tak nazwanych katalogów i w każdym po dwadzieścia takich plików. Odnalezienie
czegokolwiek graniczyłoby z cudem. Nazwy powinny być jednoznaczne i jak najkrótsze.
Zalecamy stosowanie małych liter z wyrazami rozdzielanymi myślnikami. Dobrym
sposobem jest stosowanie skrótów:
.srv
.fltr
.const
.val
.mock
.mdl
.tpl
itd.
Jeśli nie mamy jakiegoś szczególnego powodu, by zrobić to inaczej, zalecamy korzystanie
z angielskich nazw plików, katalogów, zmiennych, funkcji itd. Ważne, aby nasz kod
był czytelny dla jak największego grona programistów, a używanie krótkich, jednoznacznych nazw ułatwia zarządzanie aplikacją i jej rozwój. Przyjrzyjmy się poniższemu
przykładowi.
Małe litery, wyrazy rozdzielane myślnikami, tzw. snake-case:
my-folder/my-file.css
Nazwy plików z wykorzystaniem skrótów:
app.mdl.js
car-list.ctrl.js
car-list.tpl.html
car-list.css
car-box.div.js
car-box.div.test.js
Tak mógłby wyglądać moduł car w dobrze zaprojektowanej aplikacji. Nazwy są jasne
i bez trudu możemy się domyślić, co dany plik zawiera. Jeśli korzystamy np. z GruntJS,
znacznie ułatwimy mu w ten sposób filtrowanie.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 13.  Dobre praktyki
187
Wróćmy teraz do struktury katalogów oraz jasnego podziału plików. W przypadku
mikroskopijnych aplikacji (strona główna i jedna podstrona) możemy zastosować podział
zaproponowany w przykładach na stronie domowej Angulara. Wszystkie kontrolery
umieszczamy w jednym pliku controllers.js, a wszystkie serwisy w pliku services.js.
Jeśli jednak budujemy aplikację większą niż mikroskopijna, a zakładamy, że większość stron na świecie taka właśnie jest, należy zapomnieć o tym, co proponują twórcy naszego ukochanego frameworka, i zrobić krok naprzód. Najlepszym sposobem
podziału katalogów jest podział funkcjonalny.
Zacznijmy od początku. W większości aplikacji mamy pliki, których kod jest wykorzystywany wielokrotnie w różnych miejscach, i pliki, które są odpowiedzialne za
jedną konkretną funkcjonalność w jednym konkretnym miejscu. Podzielmy więc naszą strukturę na dwie części: pierwszą, common, w której znajdą się wszystkie pliki
zawierające kod współdzielony, oraz drugą, core.
Dla lepszego objaśnienia tego zagadnienia posłużmy się przykładem z rysunku 13.1.
Rysunek 13.1.
Struktura aplikacji
Rozdzieliliśmy części wspólne od części głównej naszej aplikacji. Takie podejście
znacznie ułatwia wykrywanie błędów oraz zapobiega dublowaniu nazw plików i katalogów. Pamiętaj, aby trzymać wszystkie pliki danej funkcjonalności razem. Co to
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
188
AngularJS. Pierwsze kroki
daje? Bardzo dużo — jeśli chcemy np. usunąć moduł, możemy to zrobić, usuwając
linki, które prowadzą do niego i katalogu z modułem. Jeżeli chcemy coś zmodyfikować
i rozbudować, wszystko mamy pod ręką, dzięki czemu wyszukiwanie staje się banalnie
proste.
Organizacja kodu
Kolejnym krokiem na drodze do sukcesu jest prawidłowa organizacja kodu. Każdy o niej
mówi, ale nie każdy wie, gdzie znajduje się zaklęcie do nieskazitelnego kodu.
Zacznijmy od nazewnictwa komponentów. Zadaniem każdego dobrego architekta czy
programisty jest dbanie o unikalność nazw. Jest to bardzo ważne, szczególnie że prędzej
czy później podejmujemy decyzję o dołączeniu do aplikacji zewnętrznych bibliotek.
Oznacza to, że łatwo może dojść do kolizji nazw, a co za tym idzie, do niepoprawnego
działania aplikacji. W nazwach komponentów, inaczej niż w nazwach plików, stosujemy notację camelCase — pierwszy wyraz pisany jest od małej litery, każdy następny
dołączony do poprzedniego zaczyna się od dużej litery. Dodatkowo kropkami rozdzielamy kolejne zagłębienia w strukturze.
Teraz ważna informacja: nazwy komponentów powinny jednoznacznie obrazować funkcjonalność danego komponentu oraz jego położenie w strukturze plików. Na podstawie doświadczenia wyniesionego z pracy przy dużych i bardzo dużych projektach opartych na AngularJS śmiało możemy powiedzieć, że jest to jedna z najistotniejszych
i najużyteczniejszych wskazówek ułatwiających życie deweloperowi.
Popatrzmy na przykład:
script/core/main.ctrl.js
Definicja kontrolera w AngularJS:
angular.module('carApp')
.controller('carApp.core.mainCtrl',
[$scope, function($scope){
}]);
Wiesz już, jak nazywać poszczególne moduły, by uniknąć kolizji nazw i poprawić ich
czytelność. Następnym krokiem jest podział kodu na moduły przy zachowaniu zasady
jednej odpowiedzialności. Oznacza to, że w jednym pliku mamy jeden moduł, który
odpowiada za jedną funkcjonalność.
Posłużmy się przykładem:
scripts/core/cars/cars.mdl.js
.module('carApp.core.cars', [])
.controller('carApp.core.mainCtrl'...)
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 13.  Dobre praktyki
189
Podsumowując: podczas projektowania i programowania aplikacji należy zwrócić
szczególną uwagę na stosowane nazewnictwo oraz modułowy podział kodu.
Wydajność
W dobie urządzeń mobilnych wydajność jest kluczowym zagadnieniem związanym
z każdą dobrą aplikacją. To wydajność świadczy w głównej mierze o sukcesie bądź
porażce danego projektu. Użytkownicy oczekują, że otrzymają informacje w najkrótszym
możliwym czasie i przy minimalnym wysiłku. Aplikacja, która wolno odpowiada,
wiesza się, nie odświeża na czas, jest z góry skazana na klęskę.
AngularJS mimo wielu wbudowanych rozwiązań wydajnościowych nie pokonuje
wszystkich problemów. Nieważne, jak szybki jest sam framework — to od nas zależy, jak
go wykorzystamy. Naszym zadaniem jest stworzenie kodu przejrzystego, a co za tym
idzie, łatwego w rozwoju i utrzymaniu, oraz kodu zoptymalizowanego pod kątem wydajności. Warto poznać kilka prostych zasad, które pozwolą nam uniknąć niepotrzebnych
trudności.
Kluczem do zwiększenia wydajności jest zminimalizowanie ilości $watchers wewnątrz AngularaJS, co przekłada się na poprawę wydajności cyklu $digest. To bardzo
istotne, by nasza aplikacja działała szybko, a jej użytkownicy czuli się komfortowo.
Za każdym razem, gdy model jest aktualizowany, użytkownik wprowadza dane w widoku
lub serwis zwraca dane do kontrolera, AngularJS uruchamia coś, co nazywamy cyklem
$digest.
Ten cykl to wewnętrzna pętla aplikacji, która przechodzi przez powiązania, wiąże i sprawdza, czy zmieniły się jakieś wartości. Jeśli wartości zostały zmienione, AngularJS
uaktualnia model. Dokładny opis tego cyklu znajduje się w rozdziale 2.
Każde nowe wiązanie to kolejne $watchers i kolejne obiekty $scope wydłużające pętlę
$digest. Przy rozwijaniu aplikacji powinniśmy być świadomi, jak wiele obiektów
$scope i wiązań stworzyliśmy i jak się to przełoży na szybkość pętli $digest.
AngularJS w wersji beta 1.4.0 posiada bardzo użyteczną możliwość jednokrotnego wiązania. Oznacza to, że mimo zmian w modelu nasza wartość nie jest brana pod uwagę
podczas przebiegu pętli $digest.
Tak deklarowaliśmy nasze wartości dotychczas:
{{ wyrażenie }}
Wiązanie jednorazowe wymaga postawienia podwójnego dwukropka przed wyrażeniem:
{{ ::wyrażenie }}
Pamiętaj: im mniej pracy ma do wykonania nasza aplikacja, tym jest ona szybsza.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
190
AngularJS. Pierwsze kroki
Kolejnym krokiem ku lepszemu zrozumieniu wewnętrznych zależności AngularJS
jest poznanie różnic pomiędzy $scope.$apply() i $scope.$digest(). Każdy z nas, projektantów, prędzej czy później zaczyna korzystać z różnego rodzaju wtyczek. Bardzo
często zdarza się, że dana wtyczka ma własny system aktualizacji drzewa DOM i dzieje
się to bez wiedzy AngularJS. To jest dokładnie ten moment, kiedy z pomocą przychodzi nam $scope.$apply(), który nakazuje AngularJS uruchomienie pętli $digest.
Inaczej mówiąc, $scope.$apply() informuje AngularJS o tym, że zaszły zmiany w modelu poza wewnętrznym cyklem. Wywołanie $scope.$apply() powoduje uruchomienie pętli $digest z poziomu $rootScope.$digest(), czyli w pętli brane są pod uwagę
wszystkie elementy zawarte w $rootScope i ich dzieci do końca hierarchii. Jeśli jednak
nasz element nie ma połączenia ze swoim rodzicem i nie ma sensu wymuszać pętli
$digest dla całej aplikacji, możemy zastosować $scope.$digest(), który działa dokładnie tak jak $scope.$apply(), z tą różnicą, że pętla wywoływana jest tylko dla tego
elementu i jego dzieci, z którego została wywołana. Ma to oczywiście duży wpływ na
wydajność aplikacji — należy pamiętać, że aktualizowany jest jedynie element, z którego wywołana jest pętla $digest, natomiast jego rodzice nie.
Wiesz już, jak działa cykl odświeżania danych w naszej aplikacji. Wiesz również, kiedy
zastosować $scope.$apply(), a kiedy $scope.$digest().
W następnym kroku skupimy się na jednym z najpopularniejszych i najużyteczniejszych
elementów, czyli na dyrektywie ng-repeat. To, że jest ona najpopularniejsza, nie oznacza,
że jest też najwydajniejszym rozwiązaniem. Jeśli to tylko możliwe, powinniśmy jej
unikać. Nie mówimy tu o tym, by jej nie stosować, ale mówimy, żeby jej nie nadużywać.
Bądź świadomy, jak rozrasta się Twoja aplikacja, jak poszczególne elementy wpływają
na proces odświeżania.
Kolejnymi niszczycielami wydajności są dyrektywy ng-show i ng-hide. Jeżeli nie jest
to konieczne, starajmy się ich unikać. To samo w przypadku filtrów, które są bardzo proste w użyciu, jednak podczas przejścia pętli w cyklu $digest każdy filtr jest sprawdzany dwa razy. Za pierwszym razem sprawdzane jest, czy zaszły jakieś zmiany, a za
drugim, czy istnieją jeszcze jakieś wartości wymagające aktualizacji.
Zanim zaczniemy pisać kod, warto dobrze przemyśleć architekturę aplikacji. Unikaj dużej
liczby obiektów. Należy stworzyć ich tyle, ile jest absolutnie konieczne, i ani jednego
więcej. Bądź świadomy, jak działa system odświeżania danych i silnik AngularJS.
Mniejsza liczba obiektów nasłuchujących to po prostu szybsza aplikacja. Kanciasty to
znakomity framework: jest szybki, dość prosty do opanowania i daje rozwiązania w 99%
sytuacji, z którymi spotkasz się, tworząc aplikacje web. Nie oznacza to jednak, że jest
on do wszystkiego — zawsze należy dokładnie zbadać potrzeby aplikacji, a następnie
wybrać jak najlepsze rozwiązanie i jak najlepsze narzędzia.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 13.  Dobre praktyki
Quiz
1. O czym należy pamiętać podczas tworzenia nazw plików i katalogów?
2. Co to jest snake-case i czym się różni od camelCase?
3. Jaka jest różnica pomiędzy $scope.$apply() i $scope.$digest()?
4. Jak dyrektywa ng-repeat wpływa na wydajność aplikacji?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
191
192
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
AngularJS. Pierwsze kroki
Rozdział 14.
Testy
Wprowadzenie
Dlaczego potrzebujesz pisać testy? Odpowiedź na to pytanie jest dość prosta: Ponieważ
nie jesteś Chuckiem Norrisem, a tylko jego kod testuje się sam i zawsze trwa to 0 ms.
Pisanie testów jest bardzo ważne, bezpośrednio przekłada się na jakość naszej aplikacji.
Klienci nie płacą nam za pisanie testów, klienci płacą za jakość. Tworząc kod, podejmujemy codziennie setki różnego rodzaju decyzji. Testy mają na celu sprawdzenie, czy
nasze decyzje były trafne, czy czegoś nie przeoczyliśmy, czy wszystko działa zgodnie
z naszymi założeniami.
Jasmine
AngularJS został napisany z myślą o testach, co czyni go jeszcze atrakcyjniejszym,
zwłaszcza gdy myślimy o dużych aplikacjach. Zanim przejdziemy do testów samego
AngularJS, przyjrzyjmy się bliżej jednemu z najpopularniejszych frameworków do testowania JavaScriptu — Jasmine. Naszą przygodę z testami zaczniemy od przygotowania
odpowiedniego środowiska pracy. Jasmine jest w pełni niezależnym frameworkiem,
który do poprawnego działania nie wymaga dodatkowych bibliotek. Nie musimy nic
instalować, wystarczy pobrać ze strony https://github.com/jasmine/jasmine/releases/
odpowiednią wersję — w naszym przypadku 2.1.3 — i można rozpocząć testowanie
kodu. Wygląd strony pobierania prezentuje rysunek 14.1.
Po rozpakowaniu archiwum zip uruchamiamy SpecRunner.html, a tym samym nasz
pierwszy test.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
194
AngularJS. Pierwsze kroki
Rysunek 14.1. Strona, z której można pobrać wersję Jasmine 2.1.3
Jeśli korzystasz z Visual Studio, do swojego projektu możesz pobrać Jasmine przy
użyciu NuGet Package Manager. Wystarczy kliknąć prawym przyciskiem myszy na
projekcie, a następnie w polu wyszukiwania wpisać jasmine — po kilku sekundach
wyświetli się lista dostępnych rozszerzeń (rysunek 14.2). W naszym przypadku wybierzemy Jasmine Test Framework.
Rysunek 14.2. NuGet Package Manager
Po krótkiej instalacji nasze środowisko jest gotowe do pracy. Czytelnicy korzystający
z node.js mogą zainstalować pakiet Jasmine przy pomocy poniższej komendy:
npm install -g jasmine
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 14.  Testy
195
Następnie inicjujemy projekt, stosując komendę:
jasmine init
Przyjrzyjmy się teraz bliżej, jak działa Jasmine. Po uruchomieniu pliku SpecRunner
otrzymaliśmy stronę WWW z pierwszym testem — rysunek 14.3.
Rysunek 14.3.
Pierwszy test
Kod HTML strony SpecRunner prezentuje się tak:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.1.3</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.1.3/
jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine-2.1.3/jasmine.css">
<script src="lib/jasmine-2.1.3/jasmine.js"></script>
<script src="lib/jasmine-2.1.3/jasmine-html.js"></script>
<script src="lib/jasmine-2.1.3/boot.js"></script>
<!-- include source files here... -->
<script src="src/Player.js"></script>
<script src="src/Song.js"></script>
<!-- include spec files here... -->
<script src="spec/SpecHelper.js"></script>
<script src="spec/PlayerSpec.js"></script>
</head>
<body>
</body>
</html>
W katalogu source znajdują się pliki Player.js oraz Song.js. Zawierają one kod, który
zostanie przetestowany po uruchomieniu, natomiast pliki w katalogu spec zawierają
testy. Jest to bardzo prosta konstrukcja. Przejdźmy zatem dalej i napiszmy nasz pierwszy test. Zacznijmy od stworzenia w katalogu spec pliku firstTestSpec.js, w którym napiszemy test dla funkcji firstTest(), zwracającej tekst Pierwszy test!.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
196
AngularJS. Pierwsze kroki
Kod JS w pliku firstTestSpec.js wygląda tak:
describe("Pierwszy test", function () {
it("powinien zwrócić tekstPierwszy test!'", function () {
expect(firstTest()).toEqual("Pierwszy test!");
});
});
W pliku HTML usuwamy odwołania do Player.js, Song.js, SpecHelper.js oraz Player
Spec.js, a w to miejsce wstawiamy odwołanie do naszego nowo stworzonego firstTest
Spec.js. Po uruchomieniu testu otrzymamy komunikat o błędzie — rysunek 14.4.
Rysunek 14.4. Test zakończony niepowodzeniem
Funkcja firstTest nie jest zdefiniowana, co oczywiście jest prawdą. W następnym kroku
dodamy plik firstTest.js i zdefiniujemy w nim prostą funkcję firstTest, zwracającą
tekst Pierwszy test!.
var firstTest = function(){
return "Pierwszy test!";
}
Funkcja jest gotowa. W pliku HTML należy dodać jeszcze odwołanie do nowo powstałego firstTest.js i ponownie uruchomić test. Tym razem strona wygląda zupełnie
inaczej — rysunek 14.5.
Rysunek 14.5. Test zakończony powodzeniem
Test trwał 0,005 s i zakończył się sukcesem. Jak widać na rysunku 14.5, Jasmine na
podstawie describe tworzy sekcję z naszym testem, nazwaną po prostu Pierwszy test.
Następnie, wykorzystując funkcję it, w powyższej sekcji opisuje dany test i wywołuje
funkcję testującą expect. Być może brzmi to trochę skomplikowanie, ale z biegiem
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 14.  Testy
197
czasu wszystko stanie się jasne. Wiesz już, jak napisać prosty test, przejdźmy teraz do
rozpoznania możliwości, jakie daje nam Jasmine, i dokładnie przyjrzyjmy się poszczególnym dopasowaniom. Jedno z nich, toEqual, pokazaliśmy w pierwszym przykładzie.
Za chwilę omówimy je bardziej szczegółowo.
Dopasowania
Jasmine oferuje następujące dopasowania:
 toBe,
 toBeCloseTo,
 toBeDefined,
 toBeFalsy,
 toBeGreaterThan,
 toBeLessThan,
 toBeNaN,
 toBeNull,
 toBeTruthy,
 toBeUndefined,
 toContain,
 toEqual,
 toHaveBeenCalled,
 toHaveBeenCalledWith,
 toMatch,
 toThrow,
 toThrowError.
Każde z powyższych dopasowań możemy zaprzeczyć, używając not. Na przykład zaprzeczenie dla toBe będzie wyglądało tak:
expect(example).not.toBe(true);
Jasmine składa się ze Spec (specyfikacji) oraz Suite (zestawu specyfikacji). Specyfikacja
to funkcja javaScriptowa. Definiujemy ją poprzez wywołanie funkcji it() z podaniem
opisu i funkcji, którą ma wywołać. Oczekiwanie (expectation) danego zachowania
aplikacji można wyrazić poprzez wywołanie funkcji expect() z jednym z warunków
dopasowania (matcher). Zestaw specyfikacji lub sekcja to zbiór specyfikacji zgrupowanych przy pomocy funkcji describe(). Uwaga: założenia są wykonywane w takiej
kolejności, w jakiej zostały zdefiniowane.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
198
AngularJS. Pierwsze kroki
Na pewno zastanawiasz się, jak działają poszczególne dopasowania i jak można wykorzystać je w praktyce. To dobry moment, by poznać je bliżej. Zacznijmy od toBe.
Na pierwszy rzut oka dopasowanie to jest identyczne z toEqual. Różnica między nimi
jest jednak znacząca i polega na tym, że w przeciwieństwie do toEqual, który jest najprostszym dopasowaniem, polegającym na porównaniu dwóch elementów w celu
sprawdzenia, czy są sobie równe, toBe sprawdza, czy dwa elementy są tymi samymi
obiektami. Aby lepiej wyjaśnić, jak działają poszczególne dopasowania, stworzymy
przykład wykorzystujący każde z nich. Tym razem zaczniemy od utworzenia pliku
secondTest.js, w którym umieścimy następujący kod:
var example = {
exampleTrue: true,
exampleDecimalNumber: 0.1,
exampleObject: { foo: function () { } },
exampleUndefined: undefined,
exampleFalse: false,
exampleNumber: 2,
exampleNaN: NaN,
exampleNull: null,
exampleArray: ['Jan', 'Adam', 'Ola'],
exampleText: "tekst testowy",
exampleFileName: "test.png",
};
Następnie stworzymy plik secondTestSpec.js, w którym umieścimy nasze testy.
Omawiany już test toBe będzie wyglądał tak:
describe("toBe", function () {
it("powinien zwrócić true", function () {
expect(example.exampleTrue).toBe(true);
});
it("nie powinien zwrócić true", function () {
expect(example.exampleFalse).not.toBe(true);
});
});
describe("toEqual", function () {
it("wartości są sobie równe", function () {
expect(example.exampleTrue).toEqual(true);
});
it("wartości nie są sobie równe", function () {
expect(example.exampleTrue).not.toEqual(false);
});
});
Tak jak już wspomnieliśmy, każde z dopasowań można zaprzeczyć przy pomocy not.
Przeanalizujmy powyższy przykład. Describe tworzy sekcję o nazwie toBe. Nazwa oczywiście może być dowolna, ważne, by celnie opisywała zawartość danej sekcji. W naszym
przypadku jest to po prostu nazwa dopasowania, które jest przedmiotem naszych
rozważań. Warto pamiętać o tym, że sekcje można zagnieżdżać, tworząc tym samym
rozbudowaną strukturę. Wewnątrz describe mamy dwa testy: pierwszy informuje nas
o tym, że zwracana wartość example.exampleTrue powinna być równa true, w drugim
przypadku, w związku z zastosowanym zaprzeczeniem, oczekujemy wartości false.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 14.  Testy
199
Kolejnym użytecznym dopasowaniem, nad którym się pochylimy, jest toBeCloseTo.
Pozwala ono na sprawdzenie, czy dana liczba dziedziczna jest w pobliżu, czy nie. Sama
bliskość określana jest drugim parametrem, który wskazuje liczbę miejsc po przecinku.
Kod takiego porównania przedstawia się następująco:
describe("toBeCloseTo", function () {
it("liczba jest blisko", function () {
expect(example.exampleDecimalNumber).toBeCloseTo(0.12 ,1);
});
it("liczba jest blisko", function () {
expect(example.exampleDecimalNumber).not.toBeCloseTo(0.12, 2);
});
it("liczba jest blisko", function () {
expect(example.exampleDecimalNumber).not.toBeCloseTo(0, 1);
});
});
Tak jak poprzednio naszą sekcję nazwaliśmy jak dopasowanie. Teraz funkcję it wywołujemy trzy razy, za każdym razem testując naszą liczbę w trochę inny sposób.
Kolejne dopasowanie, toBeDefined, sprawdza, czy dany element jest zdefiniowany:
describe("toBeDefined", function () {
it("wartość powinna być zdefiniowana", function () {
expect(example.exampleObject).toBeDefined();
});
it("wartość nie powinna być zdefiniowana", function () {
expect(example.exampleUndefined).not.toBeDefined();
});
});
Możemy też dopasować element niezdefiniowany:
describe("toBeUndefined", function () {
it("wartość nie powinna być zdefiniowana", function () {
expect(example.exampleUndefined).toBeUndefined();
});
it("wartość powinna być zdefiniowana", function () {
expect(example.exampleFalse).not.toBeUndefined();
});
});
Następne użyteczne dopasowania to toBeFalsy oraz toBeTruthy:
describe("toBeFalsy", function () {
it("powinien zwrócić false", function () {
expect(example.exampleFalse).toBeFalsy();
});
it("nie powinien zwrócić false", function () {
expect(example.exampleTrue).not.toBeFalsy();
});
});
describe("toBeTruthy", function () {
it("powinien zwrócić true", function () {
expect(example.exampleTrue).toBeTruthy();
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
200
AngularJS. Pierwsze kroki
});
it("nie powinien zwrócić true", function () {
expect(example.exampleFalse).not.toBeTruthy();
});
});
W pierwszym przypadku oczekujemy false, a po zanegowaniu true, natomiast w drugim
na odwrót.
Kolejne dopasowania, toBeGreaterThan oraz toBeLessThan, pomogą nam sprawdzić,
czy dana liczba jest większa, czy mniejsza od oczekiwanej:
describe("toBeGreaterThan", function () {
it("liczba powinna być większa od", function () {
expect(example.exampleNumber).toBeGreaterThan(1);
});
it("liczba nie powinna być większa od", function () {
expect(example.exampleNumber).not.toBeGreaterThan(4);
});
});
describe("toBeLessThan", function () {
it("liczba powinna być mniejsza od", function () {
expect(example.exampleNumber).toBeLessThan(4);
});
it("liczba nie powinna być mniejsza od", function () {
expect(example.exampleNumber).not.toBeLessThan(1);
});
});
Teraz sprawdźmy, przy pomocy toBeNaN oraz toBeNull, czy oczekiwana wartość jest
NaN lub null.
describe("toBeNaN", function () {
it("powinien zwrócić NaN", function () {
expect(example.exampleNaN).toBeNaN();
});
it("nie powinien zwrócić NaN", function () {
expect(example.exampleNumber).not.toBeNaN();
});
});
describe("toBeNull", function () {
it(" powinien zwrócić null", function () {
expect(example.exampleNull).toBeNull();
});
it("nie powinien zwrócić null", function () {
expect(example.exampleNumber).not.toBeNull();
});
});
Następnie zobaczmy, czy zdefiniowana tablica x zawiera wartość y. Możemy to zrobić przy pomocy toContain.
describe("toContain", function () {
it("powinien zawierać element w tablicy", function () {
expect(example.exampleArray).toContain('Ola');
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 14.  Testy
201
});
it("nie powinien zawierać elementu w tablicy", function () {
expect(example.exampleArray).not.toContain('Kasia');
});
});
Nasza tablica exampleArray zawiera trzy imiona: Jan, Adam i Ola. Pierwszy test sprawdza, czy w tablicy znajduje się wartość Ola: tak, znajduje się — test kończy się powodzeniem. Drugi sprawdza, czy w tablicy nie znajduje się wartość Kasia: oczywiście
nie ma takiej wartości — test również kończy się powodzeniem.
Kolejne dwa dopasowania, którym należy się bacznie przyjrzeć, to toHaveBeenCalled
oraz toHaveBeenCalledWith.
describe("toHaveBeenCalled", function () {
var exampleSpy;
beforeEach(function () {
exampleSpy = jasmine.createSpy('exampleSpy');
exampleSpy("Example", "Spy");
});
it("sprawdzenie nazwy", function () {
expect(exampleSpy.identity).not.toEqual('exampleSpy')
});
it("śledzi, czy spy został wywołany", function () {
expect(exampleSpy).toHaveBeenCalled();
});
it("śledzi liczbę wywołań", function () {
expect(exampleSpy.calls.length).not.toEqual(0);
});
it("śledzi wywoływane argumenty", function () {
expect(exampleSpy).toHaveBeenCalledWith("Example", "Spy");
});
});
describe("toHaveBeenCalled – wywołanie wielu", function () {
var colors;
beforeEach(function () {
colors = jasmine.createSpyObj('colors', ['red', 'green', 'blue',
'orange']);
colors.red();
colors.green();
colors.blue(10);
});
it("dla każdego wywołania", function () {
expect(colors.red).toBeDefined();
expect(colors.green).toBeDefined();
expect(colors.blue).toBeDefined();
expect(colors.orange).toBeDefined();
});
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
202
AngularJS. Pierwsze kroki
it("sprawdza wywołania", function () {
expect(colors.red).toHaveBeenCalled();
expect(colors.green).toHaveBeenCalled();
expect(colors.blue).toHaveBeenCalled();
expect(colors.orange).not.toHaveBeenCalled();
});
it("sprawdza argumenty wywołania", function () {
expect(colors.blue).toHaveBeenCalledWith(10);
});
});
Następne dopasowanie, toMatch, oparte jest na wyrażeniach regularnych.
describe("toMatch", function () {
it("powinien zwrócić true", function () {
expect(example.exampleText).toMatch(/text/);
expect(example.exampleFileName).toMatch(/\w+.(jpg|gif|png|svg)/i);
expect(example.exampleNumber).toMatch(/\d/);
});
it("nie powinien zwrócić true, function () {
expect(example.exampleNumber).not.toMatch(/3/);
});
});
Jak widać w powyższym przykładzie, korzystając z wyrażeń regularnych, możemy np.
sprawdzić konkretny tekst, nazwę pliku, wartość liczbową itp.
Ostatnie dopasowanie, którym się zajmiemy, to toThrow:
describe('toThrow', function () {
it('sprawdza, czy oczekiwany błąd', function () {
var object = {
doSomething: function () {
throw new Error("Nieoczekiwany błąd!")
}
};
expect(object.doSomething).toThrow(new Error("Nieoczekiwany błąd!"));
});
});
W ten sposób doszliśmy do końca rozważań nad poszczególnymi dopasowaniami.
Warto wiedzieć, że można również tworzyć własne kryteria dopasowania.
Poszczególne funkcje mogą być wyłączone poprzez wywołanie xit() zamiast it(). To
samo w przypadku sekcji, gdzie wywołujemy xdescribe() zamiast describe(). W sekcjach możemy stosować funkcje beforeEach() i afterEach(), w zależności od potrzeb.
Przedstawiliśmy już rodzaje dopasowań oraz możliwości ich wykorzystania, możemy
więc przejść do sedna sprawy, czyli rozpocząć testy kanciastego — listing 14.1.
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 14.  Testy
203
Listing 14.1. Test AngularJS
Plik index.html
<!DOCTYPE html>
<html ng-app="app">
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.1.3</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.1.3/
jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine-2.1.3/jasmine.css">
<script src="lib/jasmine-2.1.3/jasmine.js"></script>
<script src="lib/jasmine-2.1.3/jasmine-html.js"></script>
<script src="lib/jasmine-2.1.3/boot.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/
angular-mocks.js"></script>
<!-- include source files here... -->
<script src="src/firstTest.js"></script>
<!-- include spec files here... -->
<script src="spec/firstTestSpec.js"></script>
</head>
<body>
</body>
</html>
Plik firstTest.js
var app = angular.module('app', []);
app.factory('testFactory', function () {
return ['Ala','Ola','Tomek'];
});
app.controller('defaultCtrl', function ($scope, testFactory) {
$scope.text = 'Hello, World!';
$scope.testFactory = testFactory;
});
Plik firstTestSpec.js
describe('MainCtrl', function(){
var scope; // scope, z którego będziemy korzystać podczas testów
// mock app umożliwiający wstrzyknięcie zależności
beforeEach(angular.mock.module('app'));
// mock kontrolera umożliwiający wstrzykiwanie zależności oraz zawarcie $rootScope i $controller
beforeEach(angular.mock.inject(function($rootScope, $controller){
// tworzymy pusty scope
scope = $rootScope.$new();
// deklarujemy kontroler i wstrzykujemy pusty scope
$controller('defaultCtrl', { $scope: scope });
}));
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
204
AngularJS. Pierwsze kroki
// startujemy z testami
it('powinien zawierać wartość', function(){
expect(scope.text).toBe("Hello World!");
});
it('tablica powinna zawierać wartość', function () {
expect(scope.testFactory).toContain("Ala");
});
it('tablica nie powinna zawierać wartości', function () {
expect(scope.testFactory).not.toContain("Ida");
});
});
Powyższy przykład pokazuje, jak możemy wykorzystać Jasmine w testach AngularJS.
Tym samym kończymy nasze rozważania na temat testów. Mamy nadzieję, że wiedza
zdobyta podczas studiowania tego rozdziału pomoże Ci w pisaniu lepszych jakościowo
aplikacji AngularJS, a czas poświęcony na testy zredukuje do minimum liczbę błędów, które mogą się pojawić się po wdrożeniu aplikacji do środowiska produkcyjnego.
Quiz
1. Dlaczego warto pisać testy?
2. Co to jest Jasmine?
3. Jakie dopasowania oferuje Jasmine?
4. Czy dopasowania można zaprzeczać?
5. Jak działa dopasowanie toBeCloseTo?
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Rozdział 15.
Zakończenie
Dziękujemy za czas poświęcony na przeczytanie naszej książki.
Każda, nieważne, czy duża, czy mała, aplikacja rozpoczęła swoje istnienie w umyśle pojedynczej osoby. Na samym początku jest myśl, którą stopniowo rozwijamy, by ostatecznie otrzymać gotowy produkt. AngularJS to narzędzie, nie rozwiązanie. To od nas
zależy, do czego wykorzystamy Angulara, jaką aplikację napiszemy, jakie narzędzia
i rozwiązania zastosujemy. Kanciasty jest bardzo wszechstronny — jego funkcjonalność, prostota użycia i bardzo szybki rozwój przemawiają za tym, aby poważnie podejść do rozpoznania jego możliwości. Pisząc niniejszą książkę, postawiliśmy sobie
za cel przybliżenie tej fascynującej technologii jak najszerszemu gronu programistów.
Na co dzień każdy z nas pisze aplikacje, w których wykorzystujemy większość przedstawionych w książce rozwiązań. Prowadzimy wykłady, warsztaty, bierzemy udział
w konferencjach. Mamy nadzieję, że nasza wiedza i doświadczenie przekazane w tych
kilkunastu rozdziałach pomogą zrobić czytelnikowi nie tylko pierwsze, ale i kolejne
kroki podczas przygody zwanej AngularJS, czego szczerze wszystkim życzymy.
Zapraszamy na nasz github: https://gist.github.com/kalbarczyk, gdzie znaleźć można
wiele ciekawych przykładów, również tych omawianych na łamach książki.
Drogi Czytelniku, przygotowaliśmy dla Ciebie dodatkowe materiały wideo, stanowiące
uzupełnienie książki. Zeskanuj poniższy kod QR i przejdź do filmu:
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
Skorowidz
$apply(), 24, 25
$digest(), 24
$inject, 35
$q, 173
$q.all, 176
$routeProvider, 134
$scope, 105
$watch(), 22, 25
@keyframes, 158
A
AJAX, 169
akcja animacji, 162
animacja
dyrektywy ngRepeat, 165
pomiędzy stronami, 158–162
animacje, 153
CSS3, 153, 158
JavaScript, 153, 161
API angular.module(), 28
aplikacje
SPA, 7, 134
zmodularyzowane, 29
B
biblioteka
angular.js, 9
Bootstrap, 17, 135
jQuery, 135
C
callback, 169, 172
callback hell, 172
camelCase, 48
CDN, Content Delivery Network, 10
CORS, Cross Origin Resource
Sharing, 179
CSS, 181
CSS3 Transitions, 153, 155
cykl
$digest, 25, 46
ewaluacji, 24
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
D
data, 18
definicja szablonu, 109
Dependency Injection Engine, 34
DI, Dependency Injection, 33
dobre praktyki
nazewnictwo plików, 185
organizacja kodu, 188
podział plików, 185
wydajność, 189
dodawanie
biblioteki, 9
filtrów, 110
konfiguracji, 42
dokumentacja, 7
DOM, Document Object Model,
10
dopasowania, 197
dopasowanie
toBe, 198
toBeCloseTo, 199
toBeDefined, 199
toBeFalsy, 199
toBeGreaterThan, 200
toBeLessThan, 200
toBeNaN, 200
toBeNull, 200
toBeTruthy, 199
toHaveBeenCalled, 201
toHaveBeenCalledWith, 201
toMatch, 202
toThrow, 202
dyrektywa, 45
ngBlur, 57
ngEnd, 70
a, 51
form, 51
input, 53
ng-app, 10
ngBind, 54
ngBindHtml, 54
ngBindTemplate, 55
ngChange, 57
ngClass, 62
ngClick, 72
ngCloak, 56
ng-controller, 12, 13, 20
ngController, 74
ngCopy, 75
ngCut, 76
ngDblclick, 78
ngFocus, 57, 78
ngForm, 79
ngHref, 79
ngIf, 80
ngInclude, 80
ngKeydown, 80
ngKeypress, 80
ngKeyup, 80
ngList, 81
ng-model, 13
ngModel, 81
ngModelOptions, 82
ngMousedown, 84
ngMouseenter, 84
ngMouseleave, 84
ngMousemove, 84
ngMouseover, 84
ngMouseup, 84
ngNonBindable, 84
ngPaste, 85
ngPluralize, 85
ngReadonly, 88
ng-repeat, 21, 113
ngRepeat, 65, 167
ngStart, 70
ngStyle, 88
ngSubmit, 88
ngSwitch, 89
ngTransclude, 89
ngValue, 91
ngView, 146
script, 91
select, 93
textarea, 96
Skorowidz
207
dyrektywy
wbudowane, 50
własne, 99
działanie
Jasmine, 195
serwisu, 39
dziedziczenie, 19
E
Explicit Annotation, 35
explicit watcher, 23
F
fabryka, 38
fabryka mountainsList, 150
filtr, 109
filter, 115
limitTo, 115
linky, 117
orderBy, 113
rangeTime, 137, 141
filtry wbudowane, 110
dyrektywy ng-repeat, 113
JSON, 113
liczbowe, 111
operacje na datach, 112
operacje na stringach, 110
format JSON, 178
formularz, 181
formularz kontaktowy, 183
framework
Jasmine, 193
SPA, 13
funkcja, 119
$get(), 40
addConfig, 42
all, 176
angular.bind, 119
angular.bootstrap, 120
angular.copy, 120
angular.element, 122
angular.equals, 126
angular.extend, 126
angular.forEach, 127
angular.fromJson, 127
angular.identity, 127
angular.injector, 129
angular.isArray, 131
angular.isDate, 131
angular.isDefined, 131
angular.isElement, 131
angular.isFunction, 131
angular.isNumber, 131
angular.isObject, 131
angular.isString, 131
angular.isUndefined, 131
angular.lowercase, 131
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
angular.module, 132
angular.reloadWithDebugInfo,
132
angular.toJson, 127
angular.uppercase, 131
compile, 45
FirstCtrl, 13
firstTest, 196
getClass, 145
module.config(), 42
myModule.animation(), 154
nasłuchująca, listener
function, 22
when, 134
G
garbage collection, 21
global namespace, 30
globalna przestrzeń nazw, 29
H
hard coding, 33
I
Implicit Annotation, 35
Implicit DI, 35
implicit watcher, 23
izolowany scope, 22
J
Jasmine, 193
JSON, JavaScript Object
Notation, 113, 170
JSONP, JSON with padding, 169,
179
K
kalendarz, 135, 136
klasy CSS, 181
kompilator HTML, 48
komunikacja z serwerem, 169
komunikat o błędzie, 196
konfiguracja, 134, 151
$route, 134
modułu, 41
konstruktory dyrektyw, 101
kontroler, 15, 28
bazowy, 20
controller.js, 12
dateCtrl, 18
dziedziczenie, 19
potomny, 20
someCtrl, 28
kontrolki, 181
L, Ł
lista, 149
lista rozwijana, 140
live binding, 23
logika aplikacji, 13, 15
ładowanie początkowe aplikacji,
17
łączenie modułów, 30
M
metoda
$animate.cancel(), 155
$destroy(), 21
$new(), 19, 21
angular.copy(), 24
angular.equals(), 24
angular.module(), 28
error(), 172
DELETE, 177
GET, 176
HEAD, 177
JSONP, 177
PATCH, 177
POST, 177
promise.finally(), 173
PUT, 177
reject(), 173
resolve(), 173
success(), 172
metody
$http, 177
wstrzykiwania zależności, 35
wywołań dyrektyw, 49
minifikacja, 35
model, 15
model aplikacji, 13
moduł, 8, 27
app, 43
ngAnimate, 153
MVC, Model-View-Controller, 15
N
nasłuchiwanie, 22
nazewnictwo dyrektyw, 48
notacja camelCase, 188
O
obiekt
$inject, 35, 36
$rootScope, 17
$scope, 12
$Scope, 17
Factory, 38
konfiguracyjny, 177
208
AngularJS. Pierwsze kroki
obiekt
promise, 173
Provider, 40
Value, 37
obiekty
deferred, 174, 175
nasłuchujące, 23
obietnice, promises, 154, 169, 173
obsługa
animacji, 154
błędów, 181
wyjątków, 24
odpowiedzi, 176
odpowiedzi http, 172
odroczenia, deferreds, 173
odśmiecanie, 21
odwołanie do kontrolera, 12
operacje
na datach, 112
na stringach, 110
organizacja kodu, 188
P
parametry metody $http, 177
pierwsza aplikacja, 11
plik, 185
app.mdl.js, 143
app.rout.js, 143
categories-data.js, 138
controller.js, 13, 28, 29
data.json, 170, 171
default.html, 139
directives/ng-date-picker.js,
136
edit.tpl.html, 139
edit-ctrl.js, 139
filters.js, 137
filters/filters.js, 137
firstTest.js, 203
firstTestSpec.js, 196, 203
index.html, 30, 145, 203
index-ctrl.js, 145
json.tpl.html, 141
list.tpl.html, 142
list-ctrl.js, 142
ng-date-picker.js6, 136
secondTest.js, 198
secondTestSpec.js, 198
style.css, 135
todos-data.js, 138
pliki JSON, 58
pobieranie
AngularJS, 8
Jasmine, 194
podpinanie kontrolera, 11
podwójne wiązanie, 14
prezentacja danych, 16
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]
proces konfiguracji, 151
propagacja, 22
przechowywanie odpowiedzi, 176
przestrzeń nazw, 29
przesyłanie zdarzeń, event
dispatching, 20
przypisanie atrybutów, 18
R
reakcje łańcuchowe obietnic, 175
reguła @keyframes, 158
reprezentacja problemu, 15
routing, 133
S
same origin policy, 179
scope, 105
serwer proxy, 180
serwis, 39
serwis $animate, 154
snake-case, 48
SPA, Single Page Applications, 7,
13, 133
specyfikacja, 197
string, 110
struktura
aplikacji, 187
katalogów, 135
system ocen, 102
szablon default.tpl.html, 144
szkielet
animacji, 161
aplikacji, 12, 29
T
test, 193
test zakończony
niepowodzeniem, 196
powodzeniem, 196
testowanie JavaScriptu, 193
tworzenie
animacji, 153
CSS, 153
CSS3 Transitions, 153
JavaScript, 153
dyrektyw, 101
fabryk, 40
konfiguracji, 134
stałych, 42
typy obiektów, 37
U
ukrywanie elementów, 166
usługa
$apply, 24
$http, 169
uzyskiwanie zależności, 34
W
wartość
NaN, 200
null, 200
wiązanie
dwustronne, 14
jednostronne, 14
widok, 16
właściwości
dyrektyw, 101
modułu, 43
ngFormController, 182
właściwość dateOrginal, 18
wstrzykiwanie
modułu animacji, 153
stałej, 42
Value do Factory, 38
Value do Providera, 41
zależności, DI, 33
wydajność, 189
wyjątek, 24
wyrażenia regularne, 202
wywołanie $http, 170
X
XHR, XmlHttpRequest, 169
Z
zaciemnianie kodu, 35
zagnieżdżenie zapytań, 172
zapytanie XHR, 169
zarządzanie
asynchronicznymi
operacjami, 172
zależnościami, 33
zastosowanie
dyrektywy ng-repeat, 21
dyrektywy select, 146
explicit dependency injection,
36
implicit dependency injection,
35
obiektu $inject, 36
Value, 37
zestaw specyfikacji, 197
zmodularyzowana aplikacja, 29
znak
|, 109
dwukropka, 150
Ebookpoint.pl kopia dla: Dawid Karwot [email protected]

Podobne dokumenty