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]