3. Klient Angular JS
3.1. Odnośniki do frameworka Angular JS
Na początku niniejszego dokumentu podano dwa źródła dotyczące frameworka Angular JS. Przytaczamy je ponownie:
- [ref1]: książka „Pro AngularJS” autorstwa Adama Freemana, wydana przez wydawnictwo Apress. Jest to doskonała pozycja. Kody źródłowe przykładów zawartych w tej książce są dostępne bezpłatnie na stronie URL [http://www.apress.com/downloadable/download/sample/sample_id/1527/];
- [ref2]: oficjalna dokumentacja Angular JS [https://docs.angularjs.org/guide];
Angular JS zasługuje na osobną książkę. Książka Adama Freemana liczy ponad 600 stron, a żadna z nich nie jest zmarnowana. Opiszemy aplikację Angular, a w trakcie tego opisu poruszymy podstawy tego frameworka. Niemniej jednak ograniczymy się wyłącznie do wyjaśnień niezbędnych do zrozumienia proponowanego rozwiązania. Angular to niezwykle bogaty framework i istnieje wiele sposobów na osiągnięcie tego samego rezultatu. Stanowi to pewną trudność, ponieważ na początku nie wiadomo, czy stosowane rozwiązanie jest gorsze, czy lepsze od innych. Tak jest w przypadku rozwiązania przedstawionego tutaj. Można by je napisać inaczej, być może z uwzględnieniem lepszych praktyk.
3.2. Architektura klienta Angular
Architektura klienta Angular przypomina architekturę klasycznej aplikacji internetowej MVC, z kilkoma różnicami. Na przykład aplikacja internetowa Spring MVC ma następującą architekturę:
![]() |
Przetwarzanie żądania od klienta przebiega w następujący sposób:
- żądanie – żądane URL mają postać http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] to klasa Springa, która przetwarza przychodzące URL. „Przekierowuje” ona URL do akcji, która ma ją przetworzyć. Akcje te są metodami określonych klas o nazwach [Contrôleurs]. Litera „C” w MVC oznacza tutaj ciąg znaków [Dispatcher Servlet, Contrôleur, Action]. Jeśli nie skonfigurowano żadnej akcji do przetworzenia przychodzącego URL, serwlet [Dispatcher Servlet] odpowie, że żądany URL nie został znaleziony (błąd 404 NOT FOUND);
- przetwarzanie
- wybrana akcja może wykorzystać parametry parami, które przekazała jej serwlet [Dispatcher Servlet]. Mogą one pochodzić z różnych źródeł:
- ścieżki [/param1/param2/...] z URL,
- parametrów [p1=v1&p2=v2] z URL,
- parametrów przesłanych przez przeglądarkę wraz z żądaniem;
- podczas przetwarzania żądania użytkownika akcja może wymagać warstwy [metier] [2b]. Po przetworzeniu żądania klienta może ono wywołać różne odpowiedzi. Klasycznym przykładem jest:
- strona błędu, jeśli żądanie nie mogło zostać poprawnie przetworzone
- w przeciwnym razie strona potwierdzenia
- akcja żąda wyświetlenia określonego widoku [3]. Widok ten wyświetli dane, które nazywamy modelem widoku. To właśnie litera M w MVC. Akcja utworzy ten model M o nazwie [2c] i zażąda wyświetlenia widoku V o nazwie [3];
- odpowiedź – wybrany widok V wykorzystuje model M utworzony przez akcję do zainicjowania dynamicznych części odpowiedzi HTML, którą musi wysłać do klienta, a następnie wysyła tę odpowiedź.
Architektura naszego klienta Angular będzie analogiczna, choć terminologia będzie nieco inna. Po pierwsze, aplikacje Angular są zazwyczaj aplikacjami internetowymi typu single-page (APU) lub Single Page Application (SPA):

- użytkownik żąda początkowej strony aplikacji URL w postaci: http://machine:port/contexte. Przeglądarka wysyła zapytanie do serwera WWW, aby uzyskać żądany dokument. Jest to strona HTML, której styl został określony za pomocą CSS, a dynamikę zapewnia JavaScript;
- następnie użytkownik będzie wchodził w interakcję z wyświetlanymi widokami. Można wyróżnić różne rodzaje interakcji:
- te, które nie wymagają żadnej interakcji z otoczeniem, na przykład ukrywanie/wyświetlanie elementów widoku. Są one obsługiwane przez wbudowany JavaScript;
- te, które wymagają danych pochodzących ze zdalnej usługi internetowej. Dane te zostaną pobrane poprzez wywołanie AJAX (Asynchronous JavaScript and XML), następnie zostanie zbudowany model i wyświetlony widok;
- te, które wymagają innego widoku niż widok początkowy. Zostanie on zażądany poprzez wywołanie Ajax do serwera, który dostarczył stronę początkową. Następnie powyższy proces się powtórzy. Uzyskana strona zostanie zapisana w pamięci podręcznej przeglądarki. Przy następnym wywołaniu nie będzie już żądana od zdalnego serwera HTML;
Ostatecznie przeglądarka wykonuje tylko jedno wywołanie HTTP – to, które pobiera stronę początkową. Kolejne wywołania HTTP, kierowane do serwera stron HTML lub zdalnych usług internetowych, są realizowane przez kod JavaScript osadzony w stronach.
Przedstawiamy teraz architekturę aplikacji w przeglądarce. Pomijamy serwer HTML, który dostarcza strony HTML aplikacji. Dla wyjaśnienia można założyć, że wszystkie one znajdują się w pamięci podręcznej przeglądarki.
![]() |
Na początek należy zorientować się w tej architekturze:
- w [1] znajdujemy się w przeglądarce;
- w [2] użytkownik wchodzi w interakcję z widokami wyświetlanymi przez przeglądarkę;
- w [3] dane są pobierane z sieci, często z serwisów internetowych;
Użytkownik wchodzi w interakcję z widokami: wypełnia formularze i je zatwierdza. Wyjaśnijmy ten proces na przykładzie powyższego widoku V1. Załóżmy, że jest to widok początkowy aplikacji. Uzyskano go w następujący sposób:
- użytkownik żąda początkowego widoku aplikacji URL w postaci: http://machine:port/contexte;
- przeglądarka zażądała dokumentu powiązanego z tym URL. Otrzymał stronę HTML / CSS / JS z widoku V1;
- wbudowany w stronę kod JavaScript przejął wówczas kontrolę i przekazał ją kontrolerowi C1 [5];
- ten z kolei utworzył model M1 [8] [9] widoku V1. Utworzenie tego modelu mogło wymagać wykorzystania usług wewnętrznych [6] oraz wywołania usług zewnętrznych [7];
Użytkownik ma teraz przed sobą widok V1. Załóżmy, że jest to formularz. Wypełnia go, a następnie zatwierdza:
- w widoku [4] użytkownik zatwierdza formularz;
- w widoku [5] zdarzenie to zostanie przetworzone przez jedną z metod kontrolera C1;
jeśli zdarzenie powoduje jedynie prostą zmianę w widoku V1 (ukrycie/wyświetlenie pól), kontroler C1 zmodyfikuje model M1 widoku V1, a następnie ponownie wyświetli widok V1. W tym celu może potrzebować jednej z usług warstwy [services] lub [6].
Jeśli zdarzenie wymaga danych zewnętrznych:
- w widoku [6] kontroler C1 zwróci się do warstwy [DAO] o ich pobranie;
- w [7] warstwa ta wykona jedno lub więcej wywołań AJAX w celu ich uzyskania;
- w [8] i [9] model M1 zostanie zmodyfikowany, a widok V1 wyświetlony;
Jeśli zdarzenie powoduje zmianę widoku, w obu powyższych przypadkach, zamiast wyświetlać widok V1, kontroler C1 zażąda nowego widoku URL [10]. Jest to widok URL wewnętrzny dla przeglądarki. Nie powoduje on natychmiastowego wywołania widoku HTTP na serwerze stron HTML. Ta zmiana URL jest przetwarzana przez router skonfigurowany w taki sposób, że każdemu wewnętrznemu URL odpowiada widok V i jego kontroler C. Router powoduje wówczas wyświetlenie nowego widoku Vn. Przed wyświetleniem kontrolę przejmuje jej kontroler Cn, który tworzy model Mn, a następnie powoduje wyświetlenie widoku Vn o nazwie [11]. Gdyby strona HTML widoku Vn nie znajdowała się w pamięci podręcznej przeglądarki, zostanie ona zażądana od serwera stron HTML.
Warstwa [Présentation] tej architektury jest zbliżona do architektury JSF (Java Server Faces):
- widok V odpowiada widokowi typu Facelet w JSF;
- kontroler C odpowiada beanowi JSF, klasie Java zawierającej zarówno model M widoku V, jak i jego obsługi zdarzeń;
Warstwa [Services] różni się od znanych nam warstw [Services]. W programowaniu stron internetowych po stronie serwera najczęściej spotyka się następującą architekturę warstwową:
![]() |
W powyższym przykładzie warstwa [web] komunikuje się z warstwą [DAO] wyłącznie za pośrednictwem warstwy [métier]. Nic nie stoi na przeszkodzie, aby w warstwie [web] umieścić odwołanie do warstwy [DAO], co umożliwiłoby tę komunikację. Jednak tego nie robimy.
W przypadku Angulara nie nakładamy na siebie takiego ograniczenia. Architektura wygląda wówczas następująco:
![]() |
- w warstwie [1] warstwa [présentation] może komunikować się bezpośrednio z dowolną usługą;
- w [2] usługi znają się nawzajem. Jedna usługa może korzystać z jednej lub kilku innych.
3.3. Widoki klienta Angular
Widoki klienta Angular zostały już przedstawione w punkcie 1.3.3. Aby ułatwić lekturę tego nowego rozdziału, przytaczamy je tutaj ponownie. Pierwszy widok wygląda następująco:
![]() |
- w [6] – strona startowa aplikacji. Jest to aplikacja do umawiania wizyt u lekarzy;
- w [7] – pole wyboru, które pozwala włączyć lub wyłączyć tryb [debug]. Ten ostatni charakteryzuje się obecnością ramki [8], która wyświetla szablon bieżącego widoku;
- w [9] – sztuczny czas oczekiwania w milisekundach. Domyślnie wynosi 0 (brak oczekiwania). Jeśli N jest wartością tego czasu oczekiwania, każda akcja użytkownika zostanie wykonana po upływie czasu oczekiwania wynoszącego N milisekund. Pozwala to na obserwację sposobu zarządzania oczekiwaniem zaimplementowanego przez aplikację;
- w [10] – identyfikator serwera Spring 4 (URL). Zgodnie z powyższym jest to [http://localhost:8080];
- w [11] i [12] – identyfikator i hasło osoby, która chce korzystać z aplikacji. Istnieją dwaj użytkownicy: admin/admin (login/hasło) z rolą (ADMIN) oraz user/user z rolą (USER). Tylko rola ADMIN ma uprawnienia do korzystania z aplikacji. Rola USER służy wyłącznie do pokazania, jaką odpowiedź zwraca serwer w tym przypadku użycia;
- w [13] znajduje się przycisk umożliwiający połączenie z serwerem;
- w [14] – język aplikacji. Dostępne są dwa: domyślnie francuski oraz angielski.
![]() |
- w [1] nawiązuje się połączenie;
![]() |
- po zalogowaniu można wybrać lekarza, u którego chce się umówić na wizytę [2] oraz dzień wizyty [3];
- w [4] prosimy o wyświetlenie terminarza wybranego lekarza na wybrany dzień;
![]() |
- po uzyskaniu kalendarza lekarza można zarezerwować termin [5];
![]() |
- w [6] wybiera się pacjenta na wizytę i potwierdza ten wybór w [7];
![]() |
Po zatwierdzeniu wizyty następuje automatyczny powrót do kalendarza, w którym nowa wizyta jest już zapisana. Wizytę tę można później usunąć za pomocą kodu [7].
Opisano już główne funkcje. Są one proste. Funkcje, które nie zostały opisane, to opcje nawigacyjne służące do powrotu do poprzedniego widoku. Na koniec omówimy zarządzanie językiem:
![]() |
- w [1] przechodzi się z języka francuskiego na angielski;
2

- na [2] – widok przełącza się na język angielski, w tym kalendarz;
3.4. Konfiguracja projektu Angular
Będziemy stopniowo tworzyć nasz klient Angular. Korzystamy z IDE Webstorm.
Utwórzmy pusty folder [rdvmedecins-angular-v1], a następnie otwórzmy go w Webstormie:
![]() |
- w [1] otwieramy folder;
- w [2] wskazujemy utworzony przez nas folder;
- w pliku [3] otrzymujemy pusty projekt WebStorm;
![]() |
- w [4] konfigurację projektu przeprowadza się za pomocą opcji [File / Settings];
- W plikach [5] i [6] konfiguruje się właściwość [Spelling], która odpowiada za sprawdzanie pisowni. Domyślnie funkcja ta jest włączona. Ponieważ pobrane oprogramowanie jest w języku angielskim, nasze komentarze w języku francuskim dotyczące programów będą zaznaczane jako potencjalne błędy ortograficzne. Wyłączamy zatem tę funkcję sprawdzania pisowni [7];
![]() |
- w pliku [8] tworzymy nowy plik;
- w [9] decydujemy się utworzyć plik [package.json], który opisuje aplikację przy użyciu składni JSON;
- w pliku [10] modyfikujemy wygenerowany plik zgodnie z instrukcją podaną w pliku [11];
- w pliku [12] zapisujemy ten plik zarówno w [package.json], jak i w [bower.json];
![]() |
- w pliku [13] ponownie konfiguruje się projekt;
![]() |
- w pliku [14] należy skonfigurować właściwość [Javascript / Bower], która pozwoli nam zadeklarować potrzebne biblioteki JavaScript;
- w [15] należy wskazać plik [bower.json], który właśnie utworzyliśmy;
![]() |
- w pliku [16] dodajmy bibliotekę JavaScript;
- w pliku [17] wyświetlane są wszystkie biblioteki JavaScript dostępne do pobrania;
- w pliku [18] możemy wprowadzić kryterium filtrujące listę z pliku [17]. Tutaj wskazujemy, że chcemy bibliotekę [Angular JS];
- w [19] pojawiają się parametry biblioteki. Widzimy tutaj, że zostanie pobrana wersja 1.2.18 Angulara;
- w [20] następuje jej pobieranie;
![]() |
- w pliku [21] widać, że została pobrana;
- w pliku [22] widoczna jest pobrana wersja. W rzeczywistości jest to więc wersja 1.2.19;
- w [23] widać najnowszą dostępną wersję;
![]() |
- w [24], postępując tak samo jak poprzednio, pobieramy następujące biblioteki:
w celu zakodowania ciągu „user:password” w Base64; | ||
w celu internacjonalizacji kalendarza | ||
w celu przekierowania wewnętrznych elementów aplikacji URL do właściwego kontrolera i widoku; | ||
umożliwia internacjonalizację widoków. Jest to projekt niezależny od Angulara. W tym przypadku będą używane dwa języki: francuski i angielski; | ||
zapewnia komponenty wizualne zgodne z Bootstrapem. W tym przypadku wykorzystamy jego kalendarz; | ||
framework CSS Bootstrap. Zostanie wykorzystany do tworzenia widoków; | ||
zapewnia komponent wizualny typu „tabela”. Jest on „responsywny” w tym sensie, że może dostosowywać się do rozmiaru ekranu; | ||
udostępnia komponent typu „lista rozwijana”; |
![]() |
- w [25] pobrane biblioteki zostały zainstalowane w folderze [bower_components];
- w [26] widać, że biblioteka JQuery została pobrana. Dzieje się tak, ponieważ korzysta z niej Bootstrap. System instalacji zależności JavaScript w projekcie działa podobnie jak w przypadku Mavena w środowisku Java: jeśli pobrana biblioteka ma własne zależności, są one automatycznie pobierane;
Plik [bower.json] uległ zmianie:
Wszystkie pobrane zależności zostały zapisane w pliku.
3.5. Strona startowa klienta Angular
Tworzymy pierwszą wersję strony startowej klienta Angular:
![]() |
- o nazwach [1] i [2], tworzymy plik o nazwie HTML, o nazwach [app-01], [3] i [4];
Plik [app-01.html] będzie przez jakiś czas naszą stroną główną. Skonfigurujemy w nim import plików CSS i JS, których potrzebuje aplikacja:
<!DOCTYPE html>
<html>
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tahé">
<!-- CSS -->
<link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
</div>
<!-- Rdzeń Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
- wiersze 11–12: pliki CSS dla Bootstrap;
- wiersz 13: plik CSS dla komponentu [boostrap-select];
- wiersz 14: plik CSS dla komponentu [footable];
- wiersze 21–24: pliki JS dla komponentów Bootstrap;
- wiersz 21: komponenty Bootstrap są obsługiwane przez plik JQuery;
- wiersz 22: plik JS z Bootstrap;
- wiersz 23: plik JS dla komponentu [boostrap-select];
- wiersz 24: plik JS dla komponentu [footable];
- wiersze 26–30: pliki JS z Angular oraz powiązanych z nim projektów;
- wiersz 26: plik JS z biblioteki Angular. Należy go załadować po pliku JQuery, jeśli ta biblioteka jest używana;
- wiersz 27: plik JS projektu [angular-ui-bootstrap];
- wiersz 28: plik JS routera [angular-route];
- wiersz 29: plik JS z modułu internacjonalizacji aplikacji Angular;
- wiersz 30: plik JS modułu [angular-base64];
Można sprawdzić poprawność pliku [app-01.html]:
![]() |
- w pliku [1] żądamy sprawdzenia kodu;
- w pliku [2] przedstawiono wynik w przypadku, gdy wszystko przebiega prawidłowo;
Zaleca się takie systematyczne sprawdzanie kodu przed jego wykonaniem. W tym przypadku pozwala to wykryć wszelkie błędy odwołań w plikach CSS i JS. Jeśli ścieżka jest nieprawidłowa, narzędzie do sprawdzania kodu to zgłosi.
- W pliku [3] stronę można załadować w przeglądarce za pomocą debuggera. W przeglądarce otrzymujemy następujący wynik:
![]() |
- w pliku [4] strona [app-01.html] została dostarczona przez serwer wewnętrzny WebStormu działający tutaj na porcie 63342;
- w przypadku [5] – konsola debuggera. Gdyby wystąpiły błędy, pojawiłyby się właśnie tutaj. To również tutaj trafiają zrzuty ekranu generowane przez instrukcję [console.log(expression)] w JavaScript. Będziemy intensywnie korzystać z tej możliwości;
Tryb debugowania pozwala modyfikować stronę w Webstormie i obserwować efekty tych zmian w przeglądarce bez konieczności ponownego ładowania strony. Jeśli więc dodamy poniższy wiersz 3:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2>Version 1</h2>
</div>
i wrócimy do przeglądarki, zauważymy, że strona uległa zmianie:
![]() |
3.6. Odkrywanie Bootstrapa
Teraz zilustrujemy niektóre cechy Bootstrapa wykorzystywane w aplikacji. Mam jedynie ograniczoną wiedzę na temat tego frameworka, zdobytą poprzez kopiowanie i wklejanie kodu znalezionego w Internecie. Wyjaśnię rolę klas CSS, które, jak sądzę, rozumiem. Pominę komentarze dotyczące pozostałych.
3.6.1. Przykład 1
W Angularze operacje pobierające informacje z zewnętrznych źródeł są asynchroniczne. Oznacza to, że operacja zostaje uruchomiona, a widok natychmiast powraca do stanu, w którym użytkownik może kontynuować interakcję. Aplikacja jest powiadamiana o zakończeniu operacji za pomocą zdarzenia. Zdarzenie to jest obsługiwane przez funkcję JS, która może następnie wzbogacić bieżący widok lub go zmienić. Jeśli operacja może potrwać długo, warto dać użytkownikowi możliwość jej anulowania. Będziemy mu tę opcję oferować systematycznie. W tym celu wykorzystamy pasek informacyjny Bootstrap:

Aby uzyskać ten efekt, duplikujemy [app-01.html] do [app-02.html] i modyfikujemy następujące wiersze:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="alert alert-warning">
<h1>Opération en cours. Veuillez patienter...
<button class="btn btn-primary pull-right">Annuler</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
</div>
- wiersz 1: klasa CSS [container] definiuje obszar wyświetlania wewnątrz przeglądarki;
- wiersz 3: klasa CSS [alert] wyświetla kolorowy obszar. Klasa [alert-warning] wykorzystuje predefiniowany kolor;
- wiersz 5: klasa [btn] nadaje styl przyciskowi. Klasa [btn-primary] nadaje mu określony kolor. Klasa [pull-right] umieszcza go po prawej stronie paska ostrzegawczego;
- wiersz 6: animowany obrazek oczekiwania;
3.6.2. Przykład 2
Różne widoki aplikacji będą miały wspólny tytuł:

Aby uzyskać ten efekt, powielamy klasę [app-01.html] do klasy [app-03.html] i modyfikujemy następujące wiersze:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- Jumbotron Bootstrap -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="assets/images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>Les Médecins associés</h1>
</div>
</div>
</div>
</div>
- zakreślony obszar uzyskuje się dzięki klasie [jumbotron] z wiersza 4;
- wiersz 5: klasa [row] definiuje wiersz o 12 kolumnach;
- wiersz 6: klasa [col-md-2] definiuje obszar o szerokości dwóch kolumn w tym wierszu;
- wiersz 7: w tych dwóch kolumnach umieszcza się obraz;
- wiersze 9–11: w pozostałych 10 kolumnach umieszcza się tekst;
3.6.3. Przykład 3
Widoki będą miały górny pasek sterowania. Znajdą się na nim opcje sterowania, linki lub przyciski. Znajdą się tam również elementy formularza. Na przykład:
![]() |
Aby uzyskać ten efekt, powielamy element [app-01.html] do elementu [app-04.html] i modyfikujemy następujące wiersze:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<!-- tryb debugowania -->
<label style="width: 100px">
<input type="checkbox">
<span style="color: white">Debug</span>
</label>
<!-- formularz logowania -->
<div class="form-group">
<input type="text" class="form-control" placeholder="Temps d'attente"
style="width: 150px"/>
<input type="text" class="form-control" placeholder="URL du service web"
style="width: 200px"/>
<input type="text" class="form-control" placeholder="Login"
style="width: 100px"/>
<input type="password" class="form-control" placeholder="Mot de passe"
style="width: 100px"/>
</div>
<button class="btn btn-success">
Connexion
</button>
</form>
</div>
<button class="btn btn-success">
Connexion
</button>
</form>
</div>
</div>
</div>
</div>
- wiersz 4: klasa [navbar] określa styl paska nawigacyjnego. Klasa [navbar-inverse] nadaje jej czarne tło. Klasa [navbar-fixed-top] sprawi, że podczas przewijania strony wyświetlanej przez przeglądarkę pasek nawigacyjny pozostanie u góry ekranu;
- wiersze 6–14: definiują obszar [1]. Jest to typowy zestaw klas, którego nie rozumiem. Korzystam z tego komponentu w obecnej postaci;
- wiersz 15: definiuje „responsywny” obszar paska poleceń. Na smartfonie obszar ten znika w obszarze menu;
- wiersz 16: klasa [navbar-form] nadaje styl formularzowi na pasku poleceń. Klasa [navbar-right] przesuwa go na prawą stronę tego formularza;
- wiersze 23–32: cztery pola wprowadzania danych w formularzu z wiersza 17 [3]. Znajdują się one wewnątrz klasy [form-group], która formatuje elementy formularza, a każda z nich posiada klasę [form-control];
- wiersz 33: klasa [btn], z którą już się spotkaliśmy, wzbogacona o klasę [btn-success], która nadaje jej zielony kolor;
3.6.4. Przykład 4
Pasek sterowania umożliwi zmianę języka za pomocą listy rozwijanej:

Aby uzyskać ten efekt, powielamy klasę [app-01.html] do klasy [app-05.html] i dodajemy następujące wiersze do paska sterowania:
<button class="btn btn-success">
Connexion
</button>
<!-- języki -->
<div class="btn-group">
<button type="button" class="btn btn-danger">
Langues
</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="">Français</a>
</li>
<li>
<a href="">English</a>
</li>
</ul>
</div>
</form>
Dodane wiersze to wiersze 4–21.
- wiersz 5: klasa [btn-group] określa styl grupy przycisków. Są dwa w wierszach 6 i 9;
- wiersze 6–8: pierwszy przycisk określa etykietę listy rozwijanej. Klasa [btn-danger] nadaje mu kolor czerwony;
- wiersze 9–12: drugi przycisk to przycisk listy rozwijanej. Jest on umieszczony obok pierwszego, co sprawia wrażenie, jakby stanowiły one jeden element;
- wiersz 10: wyświetla strzałkę w dół wskazującą, że przycisk jest listą rozwijaną;
- wiersz 11: dla programów do odczytu ekranu;
- wiersze 13–20: elementy listy rozwijanej stanowią elementy listy nieuporządkowanej;
3.6.5. Przykład 5
Aby zatwierdzić formularz lub poruszać się po stronie, użytkownik ma do dyspozycji w pasku sterowania opcje lub przyciski, jak pokazano poniżej:
![]() |
Opcje menu zostały skonfigurowane w pliku [1]. Aby uzyskać ten efekt, powielamy plik [app-01.html] do pliku [app-06.html] i dodajemy następujące wiersze:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
...
</div>
<!-- opcje menu -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span>Home</span>
</a>
</li>
<li class="active">
<a href="">
<span>Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span>Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span>Annuler</span>
</a>
</li>
</ul>
<!-- przyciski po prawej stronie -->
<form class="navbar-form navbar-right" role="form">
...
</form>
</div>
</div>
</div>
</div>
- Opcje menu są generowane przez wiersze 8–29. Są to ponownie elementy listy <ul>. Klasa [active] sprawia, że tekst jest podświetlony, co wskazuje, że można kliknąć daną opcję.
3.6.6. Przykład 6
Lekarzy i klientów przedstawimy w rozwijanych listach, jak poniżej:
![]() |
Używana lista rozwijana nie jest natywnym komponentem Bootstrap. Jest to komponent [bootstrap-select] (http://silviomoreto.github.io/bootstrap-select/). Aby uzyskać ten efekt, duplikujemy [app-01.html] do [app-07.html] i dodajemy następujące wiersze:
<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2><label for="medecins">Médecins</label></h2>
<select id="medecins" data-style="btn btn-primary" class="selectpicker">
<option value="1">Mme Marie PELISSIER</option>
<option value="1">Mr Jacques BROMARD</option>
<option value="1">Mr Philippe JANDOT</option>
<option value="1">Mme Justine JACQUEMOT</option>
</select>
</div>
<!-- rdzeń Bootstrap JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- skrypt lokalny -->
<script>
$('.selectpicker').selectpicker();
</script>
</body>
</html>
- wiersz 5: należy zaimportować arkusz stylów z pliku [bootstrap-select];
- wiersz 13: atrybut [data-style] jest wykorzystywany przez [bootstrap-select]. Służy on do nadania stylu liście rozwijanej. W tym przypadku nadajemy jej wygląd niebieskiego przycisku [btn-primary];
- wiersz 13: atrybut [class] jest wykorzystywany w wierszu 23. Może mieć dowolną wartość;
- wiersze 14–17: elementy listy rozwijanej. Znajdują się tu klasyczne tagi HTML;
- wiersz 22: należy zaimportować JS z [bootstrap-select];
- wiersze 24–26: skrypt JS uruchamiany po zakończeniu ładowania strony;
- wiersz 25: instrukcja JQuery. Metoda [selectpicker] (selectpicker()) jest stosowana do wszystkich elementów o klasie [selectpicker] ($('.selectpicker')). Jest tylko jeden taki element – tag <select> w wierszu 13. Metoda [selectpicker] pochodzi z pliku JS, do którego odwołuje się wiersz 22;
3.6.7. Przykład 7
Aby wyświetlić kalendarz lekarza, użyjemy „responsywnej” tabeli dostarczonej przez bibliotekę JS [footable]:
![]() |
- w [1]: tabela z normalnym wyświetlaniem;
- w [2]: tabela po zmniejszeniu rozmiaru okna przeglądarki. Kolumna [Action] automatycznie przechodzi do następnego wiersza. Nazywa się to komponentem „responsywnym” lub po prostu dostosowującym się.
Duplikujemy [app-01.html] do [app-08.html] i dodajemy następujące wiersze:
...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="row alert alert-warning">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
- wiersze 2 i 60 są już obecne w pliku [app-01.html]. Są to pliki CSS i JS dostarczone przez bibliotekę [footable];
- wiersz 3 odwołuje się do następującego pliku CSS:
@CHARSET "UTF-8";
#przedziały th {
text-align: center;
}
#odstępy w elementach td {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Style [status-*] pochodzą z przykładu wykorzystania tabeli [footable] znalezionego na stronie biblioteki.
- wiersz 8: umieszcza tabelę w jednym wierszu [row] oraz w kolorowej ramce [alert alert-warning];
- wiersz 9: tabela zajmie 6 kolumn [col-md-6];
- wiersz 10: tabela HTML jest sformatowana za pomocą Bootstrap [class='table'];
- wiersz 13: atrybut [data-toggle] wskazuje kolumnę, w której znajduje się symbol [+/-] służący do rozwijania/zwijania wiersza;
- wiersz 19: atrybut [data-hide='phone'] wskazuje, że kolumna ma zostać ukryta, jeśli rozmiar ekranu odpowiada rozmiarowi ekranu telefonu. Można również użyć wartości „tablet”;
3.6.8. Przykład 8
Aby pomóc użytkownikowi, utworzymy dymki pomocy (tooltip) wokół głównych elementów widoków:
![]() |
Aby uzyskać ten efekt, powielamy element [app-01.html] do [app-09.html] i dodajemy następujące wiersze:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<!-- opcje menu -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- skrypt lokalny -->
<script>
// --------------------- moduł Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>
Dymki pomocy są dostarczane przez bibliotekę [angular-ui-bootstrap], która z kolei opiera się na bibliotece [angular]. W wierszu 50 importowana jest biblioteka [angular-ui-bootstrap]. Aby zaimplementować komponenty biblioteki [angular-ui-bootstrap], musimy utworzyć moduł Angular. Odbywa się to w wierszach 52–55. Wiersze te definiują moduł Angular o nazwie [rdvmedecins] (pierwszy parametr). Moduł Angular może korzystać z innych modułów Angular. Nazywa się to zależnościami modułu. Są one podane w tablicy jako drugi parametr funkcji [angular.module]. W tym przypadku moduł o nazwie [ui.bootstrap] jest dostarczany przez bibliotekę [angular-ui-bootstrap]. To właśnie ten moduł zapewni nam dymki pomocy.
Wiersz 54 definiuje moduł Angular. Domyślnie nie ma to żadnego wpływu na stronę. Wskazujemy, że strona ma być zarządzana przez Angular, przypisując ją do modułu Angular. Tak właśnie zrobiono w wierszu 2. Atrybut [ng-app='rdvmedecins'] przypisuje stronę do modułu utworzonego w wierszu 54. Strona zostanie następnie przeanalizowana przez Angular. Atrybuty [tooltip] zostaną wykryte i przetworzone przez moduł [ui.bootstrap].
Składnia dymku pomocy jest następująca:
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
Powyżej dodajemy dymek pomocy do tekstu [Home]:
- [tooltip]: określa treść dymku pomocy;
- [tooltip-placement]: określa jego położenie (bottom, top, left, right);
Angular JS umożliwia dodawanie nowych tagów lub atrybutów do tych już istniejących w języku HTML. To rozszerzenie języka HTML realizowane jest za pomocą dyrektyw Angular. W tym przypadku atrybuty [tooltip] i [tooltip-placement] są atrybutami utworzonymi przez [angular-ui-bootstrap].
3.6.9. Przykład 9
Aby pomóc użytkownikowi w wyborze dnia spotkania, udostępnimy mu kalendarz:

Podobnie jak w przypadku dymków pomocy, kalendarz ten jest dostarczany przez bibliotekę [angular-ui-bootstrap]. Aby uzyskać ten efekt, powielamy [app-01.html] w pliku [app-10.html] i dodajemy następujące wiersze:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
</div>
</div>
</div>
</div>
</div>
</div>
...
<!-- skrypt lokalny -->
<script>
// --------------------- moduł Angular
angular.module("rdvmedecins", ['ui.bootstrap'])
</script>
</body>
</html>
Podobnie jak poprzednio, strona jest powiązana z modułem Angular (wiersze 2 i 28). Kalendarz jest zdefiniowany przez tag <datepicker> w wierszu 16, zdefiniowany przez bibliotekę [angular-ui-bootstrap]:
- [show-weeks='true']: do wyświetlania numerów tygodni;
- [class='well']: w celu otoczenia kalendarza szarym polem z zaokrąglonymi narożnikami;
- [ng-model='jour']: atrybuty [ng-*] są atrybutami Angulara. Atrybut [ng-model] określa dane, które zostaną umieszczone w modelu widoku. Gdy użytkownik kliknie datę, zostanie ona umieszczona w zmiennej [jour] modelu. Zmienna ta jest wykorzystywana w wierszu 10. Składnia {{wyrażenie}} pozwala na obliczenie wyrażenia złożonego z elementów modelu. W tym przypadku {{dzień}} wyświetli wartość zmiennej [jour] z modelu. Jedną z głównych zalet Angulara jest to, że widok automatycznie śledzi zmiany w zmiennej [jour]. Dzięki temu, gdy użytkownik zmieni daty, zmiany te zostaną natychmiast wyświetlone w wierszu 10. Ogólnie rzecz biorąc, działa to w następujący sposób:
- widok V jest powiązany z modelem M;
- Angular obserwuje model M i automatycznie aktualizuje widok V, gdy nastąpi zmiana w modelu M;
Składnia {{dzień|data}} nazywana jest filtrem. Wyświetlana nie jest wartość zmiennej [jour], lecz wartość zmiennej [jour] przefiltrowana przez filtr o nazwie [date]. Filtr ten jest predefiniowany w Angularze. Służy on do formatowania dat. Przyjmuje parametry określające pożądany format. Tak więc wyrażenie {{dzień | data:'fullDate'}} oznacza, że chcemy uzyskać pełny format daty, w tym przypadku [Friday, June 20, 2014], ponieważ kalendarz domyślnie jest w języku angielskim. Wkrótce omówimy kwestię jego internacjonalizacji.
3.6.10. Podsumowanie
Przedstawiliśmy elementy frameworka CSS Bootstrap, z których będziemy korzystać. Były to komponenty pasywne: ich zdarzenia nie były obsługiwane. W związku z tym kliknięcie przycisków lub linków nie powodowało żadnej reakcji. Zdarzenia te będą obsługiwane w JavaScript. Możliwe jest korzystanie z tego języka bez pomocy frameworków, ale podobnie jak miało to miejsce po stronie serwera, po stronie klienta niektóre frameworki stają się niezbędne. Dotyczy to frameworka Angular JS, który wprowadza nowe podejście do tworzenia aplikacji JavaScript uruchamianych w przeglądarce. Przedstawimy go teraz.
3.7. Poznajmy Angular JS
Przedstawimy teraz niektóre cechy frameworka Angular JS wykorzystywane w aplikacji. Z niektórymi z nich mieliśmy już do czynienia:
- strona HTML jest oparta na Angularze JS, jeśli dołączy się do niej moduł:
<html ng-app="rdvmedecins">
- Angular umożliwia tworzenie nowych tagów i atrybutów HTML za pomocą dyrektyw:
- Angular umożliwia tworzenie filtrów:
- Widok V wyświetla model M. Angular monitoruje model M i automatycznie aktualizuje widok V w przypadku zmiany w modelu M. Wartość zmiennej modelu M jest wyświetlana w widoku V za pomocą:
Zaczniemy od dokładniejszego omówienia implementacji wzorca projektowego Model–Widok–Kontroler w Angularze. Przypomnijmy sobie powiązania między nimi z punktu widzenia architektury:
![]() |
- widok V1 wyświetla model M1 utworzony przez kontroler C1. Ten ostatni zawiera nie tylko model M1, ale również procedury obsługi zdarzeń widoku V1. Znajdujemy się w cyklu 5, 8, 9:
- [5]: w widoku V1 występuje zdarzenie. Jest ono przetwarzane przez kontroler C1;
- ten wykonuje swoje zadanie [6-7], a następnie tworzy szablon M1 [8];
- [9]: widok V1 wyświetla nowy model M1. Jak już wspomnieliśmy, ten ostatni etap przebiega automatycznie. Nie ma tu, jak w innych frameworkach, wyraźnego „push” (C1 przesyła szablon M1 do V1) ani wyraźnego „pull” (widok V1 pobiera model M1 z C1). Ma miejsce domyślne przesłanie, którego programista nie widzi;
- następnie cykl 5, 8, 9 powtarza się;
3.7.1. Przykład 1: szablon MVC w Angularze
Wróćmy do przykładu kalendarza. Widzieliśmy już dyrektywę, która go generuje:
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
Dyrektywa ta dopuszcza inne atrybuty niż te przedstawione powyżej, między innymi atrybut [min-date], który określa minimalną datę, jaką można wybrać w kalendarzu. Będzie to dla nas przydatne. Gdy użytkownik wybiera datę spotkania, musi ona być równa lub późniejsza od daty bieżącego dnia. Napiszemy zatem:
<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>
gdzie [dateMin] będzie zmienną szablonu strony, której wartością będzie dzisiejsza data. W ten sposób otrzymamy następującą stronę:
![]() |
- gdzie [1] oznacza, że jest 19 czerwca 2014 r. Kursor wskazuje, że można wybrać 19 czerwca;
- w [2] kursor wskazuje, że nie można wybrać 18 czerwca;
Duplikujemy plik [app-10.html] do pliku [app-11.html] i wprowadzamy następujące zmiany:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
</div>
</div>
</div>
<!-- rdzeń Bootstrap JavaScript ================================================== -->
...
<!-- skrypt lokalny -->
<script>
// --------------------- moduł Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
// kontroler
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// data minimalna
$scope.minDate = new Date();
}]);
</script>
</body>
</html>
Najpierw przyjrzyjmy się lokalnemu skryptowi w wierszach 26–37:
- wiersz 28: utworzenie modułu [rdvmedecins] wraz z jego zależnością od modułu [ui.bootstrap], który dostarcza kalendarz;
- wiersze 30–35: utworzenie kontrolera. To właśnie on będzie przechowywał szablon naszej strony. Nie będzie tu żadnego menedżera zdarzeń;
- wiersze 30–31: kontroler [rdvMedecinsCtrl] należy do modułu [rdvmedecins]. Do modułu można dodać dowolną liczbę kontrolerów. W naszej aplikacji będziemy mieli:
- moduł zarządzający aplikacją;
- jeden kontroler na widok;
- drugi parametr funkcji [controller] to tablica o postaci ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)]. Ostatnim parametrem jest funkcja implementująca kontroler. Jej parametry to obiekty, które Angular JS przekaże tej funkcji.
Wróćmy do architektury aplikacji Angular:
![]() |
Powyżej kontroler C1 zawiera wszystkie obsługi zdarzeń widoku V1, a także model M1 tego widoku. Obsługujące zdarzenia mogą potrzebować jednej lub kilku usług [6] do działania. Przekazuje się je wszystkie jako parametry funkcji konstruktorowej kontrolera:
Usługi Si są singletonami. Angular tworzy ich tylko jeden egzemplarz. Są one identyfikowane za pomocą nazwy Si. Dlaczego występują one dwukrotnie w powyższej tabeli? Podczas działania skrypty JS są minifikowane. W wyniku tego procesu minifikacji powyższa tabela przyjmuje następujący kształt:
Parametry tracą swoje nazwy. A przecież to właśnie nazwy usług. Dlatego ważne jest, aby zachować te nazwy. Z tego powodu są one przekazywane jako ciągi znaków jako parametry poprzedzające funkcję. Ciągi znaków nie ulegają zmianie w procesie minifikacji. Kiedy Angular będzie tworzył kontroler z nową tablicą, zastąpi a1 na S1, a2 na S2, ... Kolejność parametrów jest zatem istotna. Musi ona odpowiadać kolejności usług poprzedzających definicję funkcji.
Wróćmy do definicji kontrolera [rdvMedecinsCtrl]:
// kontroler
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// data minimalna
$scope.minDate = new Date();
}]);
- wiersze 3–4: jedynym obiektem wstrzykniętym do kontrolera jest obiekt $scope. Jest to obiekt predefiniowany, który reprezentuje model M widoków powiązanych z kontrolerem. Aby wzbogacić model widoku, wystarczy dodać pola do obiektu $scope;
- co zostało zrobione w wierszu 6. Tworzy się pole [minDate] o wartości odpowiadającej dzisiejszej dacie;
Widok V wykorzystuje ten model M w następujący sposób:
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
...
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
...
</div>
...
- wiersz 1: treść strony jest powiązana z kontrolerem [rdvMedecinsCtrl] za pomocą atrybutu [ng-controller]. Oznacza to, że wszystko, co znajduje się w tagu <body>, będzie korzystać z kontrolera [rdvMedecinsCtrl] w celu zarządzania swoimi zdarzeniami i pobierania swojego modelu M. Strona HTML może zależeć od wielu kontrolerów, które mogą być ze sobą zagnieżdżone lub nie:
Powyżej:
- treść pliku [div1] (wiersze 1–10) wyświetla szablon M1 zarządzany przez kontroler c1. Tagi w tym obszarze mogą odwoływać się do obsługi zdarzeń kontrolera c1;
- zawartość pliku [div11] (wiersze 3–4) wyświetla szablon M11 zarządzany przez kontroler c11, a także szablon M1. Występuje dziedziczenie szablonów. Tagi w tym obszarze mogą odwoływać się zarówno do menedżerów zdarzeń kontrolera c11, jak i do menedżerów zdarzeń kontrolera c1. Nie mogą one odwoływać się ani do szablonu M12 kontrolera c12, ani do jego menedżerów zdarzeń. Kontroler c12 nie jest bowiem rozpoznawany w wierszach 3–5;
- wiersze 7–9: można zastosować rozumowanie analogiczne do poprzedniego;
Wróćmy do kodu kalendarza:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
Atrybut [min-date] jest inicjowany wartością [minDate] z modelu. Domyślnie [$scope.minDate]. Pole jest zawsze wyszukiwane w obiekcie $scope.
3.7.2. Przykład 2: lokalizacja dat
Na razie kalendarz nie jest dla nas zbyt przydatny, ponieważ jest to kalendarz angielski. Można go dostosować do lokalizacji:
![]() |
- w [1] mamy kalendarz w języku francuskim;
- w [2] przełączamy go na język angielski;
- w [3] mamy kalendarz angielski;
Duplikujemy stronę [app-11.html] do [app-12.html], a następnie modyfikujemy tę ostatnią w następujący sposób:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- kalendarz-->
<div class="col-md-4">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
<!-- języki -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
</html>
Zmian jest niewiele. Dodano jedynie wiersze 21–31 z listy rozwijanej języków. Po raz pierwszy spotykamy się z obsługą zdarzeń w wierszach 27–28:
- wiersz 27: atrybut [ng-click] jest atrybutem Angular, który wskazuje procedurę obsługi zdarzenia, która ma zostać wykonana po kliknięciu elementu posiadającego ten atrybut. W tym przypadku zostanie wykonana funkcja [$scope.setLang('fr')]. Spowoduje ona przełączenie kalendarza na język francuski;
- wiersz 28: w tym miejscu ustawiamy kalendarz na język angielski;
- wiersz 35: ponieważ kod JavaScript kontrolera jest dość obszerny, umieszczamy go w pliku [rdvmedecins.js];
Angular zarządza lokalizacją widoków za pomocą modułu o nazwie [ngLocale]. Definicja naszego modułu [rdvmedecins] będzie zatem następująca:
// --------------------- moduł Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);
W wierszu 2 nie wolno zapomnieć o zależnościach, ponieważ Angular bywa mało precyzyjny w swoich komunikatach o błędach. Pominięcie zależności jest zatem szczególnie trudne do wykrycia. W tym przypadku mamy nową zależność od modułu [ngLocale].
Domyślnie Angular obsługuje jedynie lokalizację dat, liczb itp., które mają lokalne warianty. Nie obsługuje natomiast internacjonalizacji tekstów. W tym celu wykorzystamy bibliotekę [angular-translate]. Zarządzanie lokalizacją odbywa się za pomocą biblioteki [angular-i18n]. Biblioteka ta zawiera tyle plików, ile jest wariantów dat, liczb itp.
![]() |
W przypadku kalendarza francuskiego użyjemy pliku [angular-locale_fr-fr.js], a dla kalendarza angielskiego – pliku [angular-locale_en-us.js]. Przyjrzyjmy się na przykład zawartości pliku [angular-locale_fr-fr.js]:
'use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
$provide.value("$locale", {
"DATETIME_FORMATS": {
"AMPMS": [
"AM",
"PM"
],
"DAY": [
"dimanche",
"lundi",
"mardi",
"mercredi",
"jeudi",
"vendredi",
"samedi"
],
"MONTH": [
"janvier",
"f\u00e9vrier",
"mars",
"avril",
"mai",
"juin",
"juillet",
"ao\u00fbt",
"septembre",
"octobre",
"novembre",
"d\u00e9cembre"
],
"SHORTDAY": [
"dim.",
"lun.",
"mar.",
"mer.",
"jeu.",
"ven.",
"sam."
],
"SHORTMONTH": [
"janv.",
"f\u00e9vr.",
"mars",
"avr.",
"mai",
"juin",
"juil.",
"ao\u00fbt",
"sept.",
"oct.",
"nov.",
"d\u00e9c."
],
"fullDate": "EEEE d MMMM y",
"longDate": "d MMMM y",
"medium": "d MMM y HH:mm:ss",
"mediumDate": "d MMM y",
"mediumTime": "HH:mm:ss",
"short": "dd/MM/yy HH:mm",
"shortDate": "dd/MM/yy",
"shortTime": "HH:mm"
},
"NUMBER_FORMATS": {
"CURRENCY_SYM": "\u20ac",
"DECIMAL_SEP": ",",
"GROUP_SEP": "\u00a0",
"PATTERNS": [
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 3,
"minFrac": 0,
"minInt": 1,
"negPre": "-",
"negSuf": "",
"posPre": "",
"posSuf": ""
},
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 2,
"minFrac": 2,
"minInt": 1,
"negPre": "(",
"negSuf": "\u00a0\u00a4)",
"posPre": "",
"posSuf": "\u00a0\u00a4"
}
]
},
"id": "fr-fr",
"pluralCat": function (n) { if (n >= 0 && n <= 2 && n != 2) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
}]);
Widzimy tam elementy pozwalające na utworzenie kalendarza francuskiego:
- wiersze 10–18: tabela dni tygodnia;
- wiersze 19–32: tabela miesięcy w roku;
- wiersze 33–41: tabela skróconych nazw dni tygodnia;
- wiersze 42–55: tabela skróconych nazw miesięcy;
- wiersze 56–63: formaty daty i godziny. W wierszu 62 rozpoznajemy format „dd/mm/rr” stosowany w datach francuskich;
- wiersze 65–95: informacje dotyczące formatowania liczb. Nie interesuje nas to w tym przypadku;
- wiersz 96: identyfikator „fr-fr” ustawień regionalnych pliku (fr-fr: francuski z Francji, fr-ca: francuski z Kanady, ...)
W pliku [angular-locale_en-us.js] mamy dokładnie to samo, ale tym razem dla angielskiej wersji USA (en-us).
Powyższy kod nie jest zbyt łatwy do odczytania. Po uważnym przeczytaniu okazuje się, że cały ten kod definiuje zmienną [$locale] z linii 4. To właśnie poprzez zmianę wartości tej zmiennej uzyskujemy internacjonalizację dat, liczb, walut itp. Co ciekawe, Angular nie przewidział możliwości zmiany zmiennej [$locale] w trakcie działania aplikacji. Definiuje się ją raz na zawsze poprzez zaimportowanie pliku z wybraną lokalizacją:
<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>
Nie ma sensu importować wszystkich plików z wybranych ustawień regionalnych, ponieważ każdy plik, jak widzieliśmy, wykonuje tylko jedną czynność: definiuje zmienną [$locale]. Obowiązuje ostatni zaimportowany plik i nie ma już możliwości zmiany ustawień regionalnych.
Przeglądając internet w poszukiwaniu rozwiązania tego problemu, nie znalazłem żadnego. Proponuję tutaj własne rozwiązanie: [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. Pomysł polega na umieszczeniu różnych potrzebnych nam ustawień regionalnych w słowniku. To właśnie stamtąd będziemy je pobierać, gdy zajdzie potrzeba zmiany. Kod JavaScript z [rdvmedecins.js] ma następującą strukturę:
![]() |
Jeśli pominąć definicję ustawień regionalnych, która zajmuje 200 wierszy (wiersze 15–215 powyżej), kod jest prosty:
- wiersz 6: definiuje moduł [rdvmedecins] i jego zależności;
- wiersze 8–10: definiują kontroler strony [rdvMedecinsCtrl];
- wiersz 9: funkcja tworząca kontroler przyjmuje dwa parametry:
- $scope: do utworzenia szablonu widoku;
- $locale: jest to zmienna zarządzająca lokalizacją kalendarza. To właśnie ją należy zmienić przy zmianie języka;
- wiersz 13: zmienna [minDate] w szablonie jest inicjowana bieżącą datą;
- wiersz 15: definiuje słownik [locales]. Należy zauważyć, że nie wpisano [$scope.locales]. Zmienna [locales] nie jest bowiem częścią szablonu udostępnianego widokowi;
- wiersze 15–215: definiują słownik {'fr':locale-fr-fr, 'en':locale-en-us}. Wartości [locale-fr-fr] i [locale-en-us] są pobierane odpowiednio z plików JS, [angular-locale_fr-fr.js] i [angular-locale_en-us.js]. Najtrudniejsze jest to, aby nie pomylić się w bardzo licznych nawiasach tego słownika...
- wiersz 217: inicjujemy zmienną $locale wartością locales['fr'], czyli francuską wersją ustawień regionalnych. Nie można po prostu napisać [$locale=locales['fr']], co przypisałoby do $locale adres locales['fr']. Należy wykonać kopiowanie wartości. Można to zrobić za pomocą predefiniowanej funkcji [angular.copy];
- wiersz 219: zmienna [jour] w szablonie jest inicjowana bieżącą datą. Powoduje to, że kalendarz zostanie wyświetlony z datą ustawioną na tę datę;
- wiersze 223–230: definiują procedurę obsługi zdarzenia, która jest wywoływana podczas zmiany języka. Należy zwrócić uwagę na składnię:
służy do zdefiniowania procedury obsługi zdarzenia o nazwie [nom_fonction], która przyjmuje parametry [param1, param2, ...];
Przypomnijmy kod HTML z listy rozwijanej:
<!-- języki -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
- wiersz 8: wybór języka francuskiego powoduje wywołanie [setLang('fr')];
- wiersz 9: wybór języka angielskiego powoduje wywołanie kodu [setLang('en')];
- wiersz 3: atrybut [is-open] jest wartością logiczną, która kontroluje otwarcie (true) lub zamknięcie (false) listy rozwijanej. Jest on inicjowany zmienną [isopen] z modelu widoku;
Wróćmy do kodu [rdvmedecins.js]:
- wiersz 225: zmieniamy wartość zmiennej [$locale] na odpowiednią wartość ze słownika [locales];
- wiersz 227: ustalono, że gdy model M widoku V ulegnie zmianie, widok V jest automatycznie odświeżany z nowym modelem. W wierszu 225 zmieniono wartość zmiennej [$locale], która nie jest częścią modelu M wyświetlanego przez widok V. Należy znaleźć sposób na zmianę tego modelu M, aby kalendarz odświeżył się i zaczął korzystać z nowych ustawień regionalnych. W tym miejscu zmieniamy zmienną [jour] w modelu kalendarza. Inicjujemy ją nowym wskaźnikiem (new), który wskazuje na datę identyczną z wyświetlaną. [$scope.jour.getTime()] to liczba milisekund, która upłynęła od 1 stycznia 1970 r. do daty wyświetlanej przez kalendarz. Na podstawie tej liczby odtwarzamy nową datę. Oczywiście otrzymamy tę samą datę, a kalendarz pozostanie ustawiony na wyświetlanej wcześniej dacie. Jednak wartość [$scope.jour], która w rzeczywistości jest wskaźnikiem, ulegnie zmianie, a kalendarz zaktualizuje się;
- wiersz 229: przypisujemy do false wartość zmiennej [isopen] z szablonu. Zmienna ta kontroluje jeden z atrybutów listy rozwijanej:
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
...
</div>
W wierszu 1 powyżej atrybut [is-open] zmieni się na false, co spowoduje zamknięcie listy rozwijanej.
3.7.3. Przykład 3: internacjonalizacja tekstów
Wróćmy do lokalizacji kalendarza:
![]() |
W [3] widzimy, że kalendarz jest w języku angielskim, ale nie dotyczy to tekstów [Calendrier, Langues]. Domyślnie Angular nie oferuje narzędzia do internacjonalizacji komunikatów. W tym przypadku skorzystamy z biblioteki [angular-translate] (https://github.com/angular-translate/angular-translate).
Opracujemy następujący przykład:
![]() |
- w [1] – widok w języku francuskim;
- w [2] – wersja angielska;
Przyjrzyjmy się konfiguracji niezbędnej do internacjonalizacji. Skrypt [rdvmedecins.js] zostaje zmodyfikowany w następujący sposób:
// --------------------- moduł Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// konfiguracja i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// komunikaty w języku francuskim
$translateProvider.translations("fr", {
'msg_header': 'Gabinet lekarski <br/> Les Médecins Associés',
'msg_langues': 'Języki',
'msg_agenda': 'Kalendarz {{tytuł}} {{imię}} {{nazwisko}}<br/>{{dzień}}',
'msg_calendrier': 'Kalendarz',
'msg_jour': 'Wybrany dzień: ',
'msg_meteo': „Dzisiaj będzie padać...”
});
// komunikaty w języku angielskim
$translateProvider.translations("en", {
'msg_header': 'The Associated Doctors',
'msg_langues': „Języki”,
'msg_agenda': „{{tytuł}} Dziennik {{imię}} {{nazwisko}}<br/> z dnia {{dzień}}”,
'msg_calendrier': 'Kalendarz',
'msg_jour': 'Wybrany dzień: ',
'msg_meteo': 'Dzisiaj będzie padać...'
});
// domyślny język
$translateProvider.preferredLanguage("fr");
}]);
- wiersz 2: pierwszą zmianą jest dodanie nowej zależności. Internacjonalizacja aplikacji wymaga modułu Angular [pascalprecht.translate];
- wiersze 5–26: definiują funkcję [config] modułu [rdvmedecins]. Podczas uruchamiania aplikacji Angular framework tworzy instancje wszystkich usług niezbędnych dla aplikacji – zarówno tych predefiniowanych przez Angular, jak i zdefiniowanych przez użytkownika. Na razie nie zdefiniowaliśmy żadnych usług. Funkcja [config] modułu aplikacji jest wykonywana przed instancjonowaniem jakiejkolwiek usługi. Można ją wykorzystać do zdefiniowania informacji konfiguracyjnych usług, które będą następnie instancjonowane. W tym przypadku funkcja [config] zostanie wykorzystana do zdefiniowania zinternacjonalizowanych komunikatów aplikacji;
- wiersz 5: parametrem funkcji [config] jest tablica ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)], gdzie Oi jest znanym obiektem dostarczonym przez Angular. W tym przypadku obiekt [$translateProvider] jest dostarczany przez moduł [pascalprecht.translate]. [function] to funkcja wykonywana w celu skonfigurowania aplikacji;
- wiersze 7–14: funkcja [$translateProvider.translations] przyjmuje dwa parametry:
- pierwszym parametrem jest klucz języka. Można wpisać dowolną wartość. W tym przypadku wpisano „fr” dla tłumaczeń na język francuski (wiersz 7) oraz „en” dla tłumaczeń na język angielski (wiersz 16),
- drugi to lista tłumaczeń w postaci słownika {'cle1':'msg1', 'cle2':'msg2', ...};
- wiersze 7–14: komunikaty w języku francuskim;
- wiersze 16–23: komunikaty w języku angielskim;
- wiersz 25: metoda [preferredLanguage] ustala domyślny język. Jej parametrem jest jeden z argumentów użytych jako pierwszy parametr funkcji [$translateProvider.translations], a więc w tym przypadku albo „fr” (wiersz 7), albo „en” (wiersz 16);
- należy zauważyć, że istnieją trzy rodzaje komunikatów:
- komunikaty bez parametrów ani elementów HTML (wiersze 9, 11, 12, ...),
- komunikaty z elementami HTML (wiersze 8, 10, ...),
- komunikaty z parametrami (wiersze 10, 19);
Teraz duplikujemy [app-11.html] do [app-12.html] i wprowadzamy następujące zmiany:
<div class="container">
<!-- pierwszy tekst zawierający elementy HTML -->
<h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
<!-- drugi tekst z parametrami -->
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
<!-- trzeci tekst przetłumaczony przez kontroler -->
<h3 class="alert alert-danger">{{msg2}}</h3>
<pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- kalendarz-->
<div class="col-md-4">
<h4>{{'msg_calendrier'|translate}}</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
<!-- języki -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
{{'msg_langues'|translate}}<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
- tłumaczenia występują w wierszach 3, 5, 9, 13, 23;
- można wyróżnić trzy składnie:
- składnia [translate={{'msg_key'}}] (wiersz 3), gdzie [msg_key] jest jednym z kluczy słownika tłumaczeniowego. Ta składnia nadaje się do komunikatów z elementami HTML lub bez nich, ale nie do tych z parametrami;
- składnia [translate={{'msg_key'}} translate-values={{dictionnaire]}}] (wiersz 5), nadaje się do komunikatów z elementami HTML lub bez nich oraz z parametrami;
- składnia [{{'msg_key'|translate}}] (wiersze 9, 13, 23) jest odpowiednia dla komunikatów bez parametrów i bez elementów HTML;
Przyjrzyjmy się różnym komunikatom w tym widoku:
Gabinet lekarski <br/>Les Médecins Associés | The Associated Doctors | |
Kalendarz | Kalendarz | |
Języki | Języki | |
Wybrany dzień: | Wybrany dzień: |
Przyjrzyjmy się teraz wierszowi 5:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Należy zauważyć, że [msg.text] i [msg.model] nie są ujęte w apostrofy. Nie są to ciągi znaków, lecz elementy szablonu:
- msg.text: określa klucz skonfigurowanej wiadomości, który ma zostać użyty;
- msg.model: jest słownikiem dostarczającym wartości parametrów;
Nazwy pól [text, model] mogą być dowolne. W kontrolerze [rdvMedecinsCtrl] widoku obiekt [msg] jest zdefiniowany w następujący sposób:

- wiersz 245: definicja obiektu [msg];
- wiersz 245: pole [text] ma wartość klucza [msg_agenda], który jest powiązany z dwiema wartościami:
- Dziennik {{tytuł}} {{imię}} {{nazwisko}}<br/>{{dzień}} w słowniku francuskim;
- Dziennik {{tytuł}} {{imię}} {{nazwisko}}<br/> z dnia {{dzień}} w słowniku angielskim;
Komunikat do wyświetlenia ma zatem cztery parametry [titre, prenom, nom, jour];
- wiersz 245: pole [model] to słownik przypisujący wartości tym czterem parametrom. Pojawia się trudność w przypadku parametru [jour]. Chcemy wyświetlić pełną nazwę dnia. Różni się ona w zależności od tego, czy jest to język francuski, czy angielski. Wykorzystujemy zatem filtr [date], który został już użyty w widoku w postaci {{ dzień | data:'fullDate'}}. W kodzie JavaScript można użyć dowolnego filtra w postaci $filter('filter')(wartość, dodatki), gdzie $filter jest predefiniowanym obiektem Angulara, a 'filter' to nazwa filtra;
- wiersze 33–34: predefiniowany obiekt $filter jest przekazywany jako parametr do kontrolera, co pozwala na jego wykorzystanie w wierszu 245;
Wróćmy do innego wiersza wyświetlanego widoku:
<!-- trzeci tekst przetłumaczony przez kontroler -->
<h3 class="alert alert-danger">{{msg2}}</h3>
Wszystkie poprzednie tłumaczenia zostały wykonane w widoku za pomocą atrybutów modułu [pascalprecht.translate]. Można również zdecydować się na wykonanie tego tłumaczenia po stronie serwera. Tak właśnie zrobiono w tym przypadku. W kontrolerze (wiersz 247 na powyższym zrzucie ekranu) znajduje się następujący kod:
$scope.msg2 = $filter('translate')('msg_meteo');
Używamy tej samej składni, co w przypadku filtru „date”, ponieważ „translate” również jest filtrem. Żądamy tutaj komunikatu o kluczu „msg_meteo”.
Przyjrzyjmy się mechanizmowi zmiany języków. Widzieliśmy już, że funkcja konfiguracyjna [config] modułu [rdvmedecins] wyznaczyła język francuski jako domyślny (wiersz 9 poniżej):
// konfiguracja i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// komunikaty w języku francuskim
$translateProvider.translations("fr", {...});
// komunikaty w języku angielskim
$translateProvider.translations("en", {...});
// język domyślny
$translateProvider.preferredLanguage("fr");
}]);
Przypomnijmy również, że domyślną lokalizacją był również język francuski. W kodzie inicjalizacyjnym kontrolera [rdvmedecins] zapisano:
// ustawiamy lokalizację na francuski
angular.copy(locales['fr'], $locale);
- wiersz 2: [locales] to słownik, który stworzyliśmy;
Nie ma żadnego związku między internacjonalizacją komunikatów realizowaną przez moduł [pascalprecht.translate] a lokalizacją dat, którą wdrożyliśmy. Ta ostatnia wykorzystuje zmienną $locale, która nie jest używana przez moduł [pascalprecht.translate]. Są to dwa procesy, które nie mają ze sobą żadnego związku.
Czas teraz przyjrzeć się, co się dzieje, gdy użytkownik zmienia język:

- wiersz 251: podczas zmiany języka wywoływana jest funkcja [setLang] z jednym z dwóch parametrów ['fr','en'];
- wiersze 252–257: zostały już wyjaśnione – zmieniają one zmienną [$locale] kalendarza. Nie ma to żadnego wpływu na język tłumaczeń;
- wiersz 259: zmienia się język tłumaczeń. Wykorzystuje się obiekt [$translate] dostarczony przez moduł [pascalprecht.translate]. W tym celu należy go wstrzyknąć do kontrolera:
// kontroler
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {
W wierszach 3 i 4 powyżej wstawia się obiekt $translate;
- parametr lang funkcji [$translate.use(lang)] musi przyjmować wartość jednego z kluczy użytych w konfiguracji jako pierwszy parametr funkcji [$translateProvider.translations], tj. albo „fr”, albo „en”. Tak właśnie jest;
- wiersz 261: ponownie obliczana jest wartość msg2. Dlaczego? W widoku, po zmianie języka dokonanej w wierszu 259, wszystkie obecne atrybuty [translate] zostaną ponownie obliczone. Nie dotyczy to jednak wyrażenia {{msg2}}, które nie posiada tego atrybutu. Dlatego jego nowa wartość jest obliczana w kontrolerze. Musi to nastąpić po zmianie języka w wierszu 259, aby do obliczenia [msg2] wykorzystano nowy język;
Jeśli na tym poprzestaniemy, zauważymy dwie nieprawidłowości:
![]() |
- w [1] dzień pozostał w języku francuskim, podczas gdy reszta widoku jest w języku angielskim;
- w [2] i [3] wybranym dniem jest 24 czerwca, podczas gdy w [1] dzień pozostaje ustalony na 20 czerwca;
Spróbujmy najpierw znaleźć wyjaśnienia, zanim zaczniemy szukać rozwiązań. Komunikat [1] jest generowany w kontrolerze za pomocą następującego kodu:
$scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};
i wyświetlany w widoku za pomocą następującego kodu:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Anomalia [1] (dzień pozostał w języku francuskim, podczas gdy reszta widoku jest w języku angielskim) wydaje się wskazywać, że chociaż atrybut [translate] jest ponownie oceniany podczas zmiany języka, to nie miało to miejsca w przypadku atrybutu [translate-values]. Można zatem wymusić tę ocenę w kontrolerze:
// ------------------- menedżer zdarzeń
// zmiana języka
$scope.setLang = function (lang) {
...
// aktualizacja msg2
$scope.msg2 = $filter('translate')('msg_meteo');
// oraz dzień msg
$scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};
Przy każdej zmianie języka wiersz 8 powyżej ponownie oblicza wyświetlany dzień. Rozwiązuje to wprawdzie pierwszy problem, ale nie drugi (dzień wyświetlany w komunikacie nie zmienia się po wybraniu innego dnia w kalendarzu). Przyczyna tego zachowania jest następująca. Komunikat jest wyświetlany w widoku za pomocą następującego kodu:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Wyświetlany widok V zmienia się tylko wtedy, gdy zmienia się jego szablon M. Jednak w tym przypadku wybór nowego dnia w kalendarzu wyzwala zdarzenie, które nie jest obsługiwane, co powoduje, że szablon [msg] nie ulega zmianie, a zatem widok również pozostaje niezmieniony. Zmodyfikujemy w widoku definicję kalendarza:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>
Powyżej wskazujemy, że kliknięcie w kalendarzu powinno być obsługiwane przez funkcję [$scope.calendarClick]. Wygląda ona następująco:

- wiersz 267: obsługa kliknięcia w kalendarzu;
- wiersz 269: wymuszamy aktualizację wyświetlanego dnia za pomocą komunikatu [msg];
3.7.4. Przykład 4: usługa konfiguracyjna
Wróćmy do architektury aplikacji Angular JS:
![]() |
Skupimy się tutaj na pojęciu usługi. Jest to pojęcie dość szerokie. O ile powyżej warstwa [DAO] jest wyraźnie usługą, o tyle każdy obiekt Angular może stać się usługą:
- usługa ma określoną składnię. Posiada nazwę, a Angular rozpoznaje ją właśnie dzięki tej nazwie;
- usługa może być wstrzykiwana przez Angular do kontrolerów i innych usług;
Niektóre z usług, które skonfigurujemy w module [rdvmedecins], będą wymagały konfiguracji. Ponieważ usługa może zostać wstrzyknięta do innej usługi, kuszące jest przeprowadzenie konfiguracji w usłudze, którą nazwiemy [config], a następnie wstrzyknięcie jej do usług i kontrolerów, które mają zostać skonfigurowane. Opiszemy teraz ten proces.
Duplikujemy [app-13.html] do [app-14.html] i wprowadzamy następujące zmiany:
<div class="container">
<!-- kontrola wiadomości oczekującej -->
<label>
<input type="checkbox" ng-model="waiting.visible">
<span>Voir le message d'attente</span>
</label>
<!-- komunikat oczekujący -->
<div class="alert alert-warning" ng-show="waiting.visible">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
...
</div>
...
<script type="text/javascript" src="rdvmedecins-02.js"></script>
- wiersze 3–6: pole wyboru, które kontroluje, czy ma być wyświetlany komunikat oczekiwania z wierszy 9–15. Wartość pola wyboru jest umieszczana w zmiennej [waiting.visible] modelu M widoku V. Wartość ta wynosi true, jeśli pole jest zaznaczone, a false w przeciwnym razie. Działa to w obie strony. Jeśli przypiszemy zmiennej [waiting.visible] wartość true, pole wyboru zostanie zaznaczone. Mamy do czynienia z dwukierunkową zależnością między widokiem V a jego modelem M;
- wiersze 9–15: komunikat oczekiwania z przyciskiem anulowania oczekiwania (wiersz 11);
- wiersz 9: komunikat jest widoczny tylko wtedy, gdy zmienna [waiting.visible] ma wartość true. Tak więc, gdy zaznaczymy pole wyboru w wierszu 4:
- wartość true jest przypisywana do zmiennej [waiting.visible] (ng-model, wiersz 4);
- ponieważ nastąpiła zmiana modelu M, widok V jest automatycznie ponownie obliczany. Komunikat oczekiwania zostanie wówczas wyświetlony (ng-show, wiersz 9);
- analogicznie, po odznaczeniu pola wyboru w wierszu 4: komunikat oczekiwania zostaje ukryty;
- wiersz 10: komunikat oczekiwania jest przedmiotem tłumaczenia (filtr translate);
- wiersz 11: po kliknięciu przycisku wykonywana jest metoda [waiting.cancel()] (atrybut ng-click);
- wiersz 12: tekst przycisku jest tłumaczony;
- wiersz 19: umieszczamy kod JavaScript aplikacji w nowym pliku JS [rdvmedecins-02], aby nie utracić już napisanego kodu, który należy teraz przeorganizować;
Wynik wygląda następująco:
![]() |
- w pliku [1], pole niezaznaczone;
- w [2], pole zaznaczone;
Skrypt [rdvmedecins-02] stanowi reorganizację skryptu [rdvmedecins]:

- wiersz 6: moduł [rdvmedecins] aplikacji;
- wiersze 9–10: funkcja konfiguracyjna aplikacji;
- wiersze 38–39: usługa [config];
- wiersze 283–284: kontroler [rdvMedecinsCtrl];
Wcześniej zdefiniowaliśmy w kontrolerze słownik locales={'fr':..., 'en': ...}, który liczył 200 wierszy. Słownik ten jest ewidentnie elementem konfiguracyjnym, dlatego przenosimy go do usługi [config] w wierszach 38–39. Usługa ta jest zdefiniowana w następujący sposób:

- wiersze 38–39: tworzona jest usługa z wykorzystaniem funkcji [factory] obiektu [angular.module]. Składnia tej funkcji jest taka sama jak w poprzednich przypadkach: factory('nom_service', ['O1','O2', ...., 'On', function (O1, O2, ..., On){...}]), gdzie Oi to nazwy obiektów znanych Angularowi (predefiniowanych lub utworzonych przez programistę), które Angular wstrzykuje jako parametry funkcji factory. Ponieważ w tym przypadku funkcja nie ma parametrów, zastosowano krótszą, również akceptowaną składnię: `factory('nom_service', function (){...})]`;
- wiersz 40: funkcja [factory] musi zaimplementować usługę za pomocą obiektu, który zwraca. To właśnie ten obiekt jest usługą. Dlatego funkcja ta nazywa się factory (fabryka tworzenia obiektów);
Zazwyczaj kod usługi ma postać:
Angular.module('nom_module')
.factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
// przygotowanie usługi
...
// zwracamy obiekt implementujący usługę
return {
// pola
...
// metody
...
}
});
- wiersz 6: zwracany jest obiekt JS, który może zawierać zarówno pola, jak i metody. To właśnie te ostatnie zapewniają działanie usługi;
W tym przypadku usługa [config] definiuje wyłącznie pola, a nie zawiera żadnych metod. Umieści się w niej wszystko, co można skonfigurować w aplikacji:
- wiersze 42–47: klucze komunikatów do przetłumaczenia;
- wiersze 59–62: identyfikatory URL aplikacji;
- wiersze 64–69: identyfikatory URL zdalnej usługi internetowej;
- wiersz 71: wywołanie HTTP do serwisu internetowego, który nie odpowiada, może trwać długo. W tym miejscu ustalamy na 1 sekundę maksymalny czas oczekiwania na odpowiedź serwisu internetowego. Po upływie tego czasu wywołanie HTTP kończy się niepowodzeniem i generowany jest wyjątek JS;
- wiersz 73: przed każdym wywołaniem serwera będziemy symulować oczekiwanie, którego czas trwania jest tutaj ustalony w milisekundach. Czas oczekiwania równy 0 oznacza brak oczekiwania. Aplikacja zostanie skonstruowana w taki sposób, aby użytkownik mógł anulować operację, którą uruchomił. Aby operacja mogła zostać anulowana, musi trwać co najmniej kilka sekund. Wykorzystamy to sztuczne oczekiwanie do symulacji długotrwałych operacji;
- wiersz 75: w trybie [debug=true] w bieżącym widoku wyświetlane są dodatkowe informacje. Domyślnie tryb ten jest włączony. W środowisku produkcyjnym ustawilibyśmy to pole na false;
- wiersze 77–278: słownik dla dwóch ustawień regionalnych „fr” i „en”. Wcześniej znajdował się on w kontrolerze [rdvMedecinsCtrl];
Wraz z wprowadzeniem tej usługi kontroler [rdvMedecinsCtrl] ulega następującym zmianom:

- wiersze 284–285: usługa [config] zostaje wstrzyknięta do kontrolera;
- wiersz 290: słownik [locales] znajduje się teraz w usłudze [config], a nie w kontrolerze;
- wiersz 294: obiekt [waiting], który kontroluje wyświetlanie komunikatu oczekiwania. Klucz komunikatu oczekiwania znajduje się w usłudze [config] (pole text). Domyślnie komunikat oczekiwania jest ukryty (pole visible). Pole cancel ma jako wartość nazwę funkcji z wiersza 316. Pole to jest zatem metodą lub funkcją;
- wiersz 316: funkcja [cancel] jest prywatna (nie zapisano $scope.cancel=function(){}). Wróćmy do kodu przycisku anulowania:
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
Gdy użytkownik kliknie przycisk anulowania, wywoływana jest metoda [$scope.waiting.cancel()]. Ostatecznie wykonywana jest prywatna funkcja cancel z linii 316. Ogranicza się ona do ukrycia komunikatu o oczekiwaniu poprzez ustawienie zmiennej modelu [waiting.visible] na wartość false (linia 318);
3.7.5. Przykład 5: programowanie asynchroniczne
Przedstawiamy teraz nową usługę wraz z nowym pojęciem – programowaniem asynchronicznym.
![]() |
Nasza aplikacja będzie miała trzy usługi:
- [config]: usługa konfiguracyjna, którą właśnie przedstawiliśmy;
- [utils]: usługa zawierająca metody pomocnicze. Przedstawimy dwie z nich;
- [dao]: usługa zapewniająca dostęp do serwisu internetowego służącego do umawiania spotkań. Przedstawimy ją wkrótce;
Napiszemy następującą aplikację:
![]() |
![]() |
- Chodzi o wyświetlenie banera [2] przez czas określony przez [1]. Oczekiwanie można anulować za pomocą [3].
Duplikujemy plik [app-01.html] w pliku [app-15.html] i modyfikujemy kod w następujący sposób:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<!-- komunikat oczekiwania -->
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
<!-- formularz -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<button class="btn btn-primary" ng-click="execute()">Exécuter</button>
</div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
</html>
- wiersz 11: atrybut [ng-cloak] uniemożliwia wyświetlenie pola, dopóki nie zostaną obliczone zawarte w nim wyrażenia Angular. Zapobiega to chwilowemu wyświetleniu pola przed oceną atrybutu [ng-show], który faktycznie spowoduje jego ukrycie;
- wiersz 22: dane wprowadzone przez użytkownika (czas oczekiwania) zostaną zapisane w modelu [waiting.time] (atrybut ng-model);
- wiersz 28: strona wykorzystuje nowy skrypt [rdvmedecins-03];
Skrypt [rdvmedecins-03] ma następującą treść:

- wiersz 6: moduł Angular obsługujący aplikację;
- wiersz 10: funkcja [config] używana do internacjonalizacji komunikatów;
- wiersz 41: usługa [config], którą opisaliśmy;
- wiersz 286: usługa [utils], którą zamierzamy stworzyć;
- wiersz 315: kontroler [rdvmedecinsCtrl], który zamierzamy stworzyć;
Dodajemy do funkcji [config] nowy klucz komunikatu (wiersze 6, 11):
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// komunikaty w języku francuskim
$translateProvider.translations("fr", {
...
'msg_waiting_time_text': „Czas oczekiwania: ”
});
// komunikaty w języku angielskim
$translateProvider.translations("en", {
...
'msg_waiting_time_text': „Czas oczekiwania:”
});
// język domyślny
$translateProvider.preferredLanguage("fr");
}]);
Dodajemy do usługi [config] nowy wiersz (wiersz 6) dla tego klucza komunikatu:
angular.module("rdvmedecins")
.factory('config', function () {
return {
// komunikaty do internacjonalizacji
...
waitingTimeText: 'msg_waiting_time_text',
Usługa [utils] zawiera dwie metody (wiersze 4, 12):
angular.module("rdvmedecins")
.factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
// wyświetlanie reprezentacji JSON obiektu
function debug(message, data) {
if (config.debug) {
var text = data ? message + " : " + angular.toJson(data) : message;
console.log(text);
}
}
// oczekiwanie
function waitForSomeTime(milliseconds) {
// oczekiwanie asynchroniczne w milisekundach
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// zwracamy zadanie
return task;
};
// instancja usługi
return {
debug: debug,
waitForSomeTime: waitForSomeTime
}
}]);
- wiersz 2: usługa nosi nazwę [utils] (pierwszy parametr). Jest ona zależna od trzech usług: dwóch predefiniowanych usług Angular – $timeout i $q – oraz usługi config. Usługa [$timeout] umożliwia wykonanie funkcji po upływie określonego czasu. Usługa [$q] umożliwia tworzenie zadań asynchronicznych;
- wiersz 4: funkcja lokalna [debug];
- wiersz 12: funkcja lokalna [waitForSomeTime];
- wiersze 23–26: instancja usługi [utils]. Jest to obiekt udostępniający dwie metody, te z wierszy 4 i 12. Należy zauważyć, że pola obiektu mogą nosić dowolne nazwy. Dla zachowania spójności nadano im nazwy funkcji, do których się odnoszą;
- wiersze 4–9: metoda [debug] wyświetla na konsoli komunikat [message] oraz ewentualnie reprezentację JSON obiektu [data]. Umożliwia to wyświetlanie obiektów o dowolnej złożoności;
- wiersze 12–20: metoda [waitForSomeTime] tworzy zadanie asynchroniczne trwające [milliseconds] milisekund;
- wiersz 14: utworzenie zadania za pomocą predefiniowanego obiektu [$q] (https://docs.angularjs.org/api/ng/service/$q). Poniżej znajduje się obiekt API zadania o nazwie [deferred] w dokumentacji Angular:

- zadanie asynchroniczne [task] jest tworzone przez instrukcję [$q.defer()];
- zadanie to jest kończone za pomocą jednej z dwóch metod:
- [task.resolve(value)]: która pomyślnie kończy zadanie i zwraca wartość [value] do tych, którzy oczekują na zakończenie zadania;
- [task.reject(value)]: która kończy zadanie niepowodzeniem i zwraca wartość [value] do podmiotów oczekujących na zakończenie zadania;
Zadanie [task] może regularnie przekazywać informacje tym, którzy oczekują na jego zakończenie:
- [task.notify(value)]: wysyła wartość [value] do podmiotów oczekujących na zakończenie zadania. Zadanie kontynuuje swoje wykonywanie;
Ci, którzy chcą czekać na zakończenie zadania, korzystają z pola [promise] tego zadania:
Obiekt [promise] ma następujący obiekt API (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Aby obsłużyć zarówno pomyślne, jak i nieudane wykonanie zadania, należy napisać:
- wiersz 1: pobieramy obietnicę zadania;
- wiersz 2: definiujemy funkcje do wykonania w przypadku powodzenia lub niepowodzenia. Można pominąć funkcję na wypadek niepowodzenia. Funkcja [successCallback] zostanie wykonana dopiero po pomyślnym zakończeniu zadania [task] (z wynikiem [task.resolve()]). Funkcja [errorCallBack] zostanie wykonana dopiero po zakończeniu zadania [task], jeśli zadanie [task.reject()] zakończy się niepowodzeniem.
- wiersz 3: definiujemy funkcję, która ma zostać wykonana po zakończeniu jednej z dwóch poprzednich funkcji. Umieszczamy tutaj kod wspólny dla obu funkcji [successCallback, errorCallBack].
Wróćmy do kodu funkcji [waitForSomeTime]:
// oczekiwanie
function waitForSomeTime(milliseconds) {
// oczekiwanie asynchroniczne w milisekundach
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// zwracamy zadanie
return task;
};
- wiersz 4: tworzone jest zadanie;
- wiersze 5–7: obiekt [$timeout] pozwala zdefiniować funkcję (pierwszy parametr), która zostanie wykonana po upływie określonego czasu wyrażonego w milisekundach (drugi parametr). W tym przypadku drugim parametrem funkcji [$timeout] jest parametr metody (wiersz 1);
- wiersz 6: po upływie czasu [milliseconds] zadanie zostaje pomyślnie zakończone;
- wiersz 9: zwracane jest zadanie [task]. Należy tutaj pamiętać, że wiersz 9 jest wykonywany natychmiast po zdefiniowaniu obiektu [$timeout]. Nie czeka się na upływ terminu [milliseconds]. Kod z wierszy 2–10 jest zatem wykonywany w dwóch różnych momentach:
- po raz pierwszy podczas definiowania obiektu [$timeout];
- po raz drugi, gdy upłynie czas oczekiwania [milliseconds];
Mamy tu do czynienia z funkcją asynchroniczną: jej wynik uzyskuje się w momencie późniejszym niż moment jej wykonania.
Kod kontrolera korzystającego z usługi [config] wygląda następująco:
// kontroler
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
function ($scope, utils, config, $filter) {
// ------------------- inicjalizacja modelu
// komunikat oczekiwania
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
// zadanie oczekujące
var task;
// logi
utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
utils.debug("locales['fr']=", config.locales['fr']);
// wykonanie akcji
$scope.execute = function () {
// log
utils.debug('début', new Date());
// wyświetlanie komunikatu oczekiwania
$scope.waiting.visible = true;
// symulowane oczekiwanie
task = utils.waitForSomeTime($scope.waiting.time);
// koniec oczekiwania
task.promise.then(function () {
// powodzenie
utils.debug('fin', new Date());
}, function () {
// niepowodzenie
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// koniec oczekiwania we wszystkich przypadkach
$scope.waiting.visible = false;
});
};
// anulowanie oczekiwania
function cancel() {
// zadanie zakończone
task.reject();
}
}]);
- wiersz 3: kontroler korzysta z usługi [config];
- wiersz 7: do obiektu [$scope.waiting] dodano pole [time]. Obiekt [$scope.waiting.time] otrzymuje wartość czasu oczekiwania ustalonego przez użytkownika;
- wiersz 8: klucz komunikatu o oczekiwaniu wyświetlanego przez widok jest umieszczany w szablonie [$scope.waitingTimeText]. Ogólnie rzecz biorąc, wszystko, co jest wyświetlane przez widok V, musi być umieszczone w obiekcie [$scope];
- wiersz 10: zmienna lokalna. Nie jest ona udostępniana widokowi V;
- wiersze 12–13: użycie metody [debug] usługi [config]. Na konsoli otrzymujemy następujący wynik:
W wierszu 2 otrzymujemy oznaczenie JSON obiektu locales['fr'].
- wiersz 16: metoda wykonywana po kliknięciu przez użytkownika przycisku [Executer];
- wiersz 18: wyświetla godzinę rozpoczęcia wykonywania metody;
- wiersz 22: uruchamia się zadanie [waitForSomeTime]. Nie czeka się na jego zakończenie. Wykonanie jest kontynuowane od następnego wiersza 24;
- wiersze 24–30: definiuje się funkcje, które mają zostać wykonane po pomyślnym zakończeniu zadania (wiersz 26) oraz w przypadku wystąpienia błędu (wiersz 29);
- wiersz 26: wyświetla godzinę zakończenia wykonywania metody;
- wiersz 29: wyświetla informację, że operacja została anulowana. Dzieje się tak wyłącznie wtedy, gdy użytkownik kliknie przycisk [Annuler]. Instrukcja w wierszu 41 zatrzymuje wówczas zadanie asynchroniczne z kodem błędu;
- wiersze 31–34: definiuje się funkcję, która ma zostać wykonana po zakończeniu jednej z dwóch poprzednich funkcji;
Ważne jest, aby zrozumieć sekwencje wykonywania tego kodu. W przypadku, gdy użytkownik ustawi opóźnienie na 3 sekundy i nie anuluje oczekiwania:
- po kliknięciu przycisku [Exécuter] uruchamia się funkcja [$scope.execute]. Wiersze 16–34 są wykonywane bez oczekiwania na upływ 3 sekund. Po zakończeniu tego wykonania widok V jest synchronizowany z modelem M. Wyświetlany jest komunikat o oczekiwaniu (ng-show=$scope.waiting.visible=true, wiersz 20), a formularz jest ukrywany (ng-hide=$scope.waiting.visible=true, wiersz 20);
- od tego momentu użytkownik może ponownie wchodzić w interakcję z widokiem. Może w szczególności kliknąć przycisk [Annuler];
- jeśli tego nie zrobi, po upływie 3 sekund uruchamia się funkcja [$timeout] (patrz wiersze 5–7 poniżej):
// oczekiwanie
function waitForSomeTime(milliseconds) {
// oczekiwanie asynchroniczne trwające milisekund milisekund
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// zwracamy zadanie
return task;
};
- a więc po upływie 3 sekund kod zostanie wykonany. Kod ten kończy zadanie [task] z kodem sukcesu (resolve). Spowoduje to uruchomienie wszystkich kodów, które czekały na to zakończenie (wiersz 4 poniżej):
// symulowane oczekiwanie
task = utils.waitForSomeTime($scope.waiting.time);
// koniec oczekiwania
task.promise.then(function () {
// powodzenie
utils.debug('fin', new Date());
}, function () {
// niepowodzenie
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// koniec oczekiwania w każdym przypadku
$scope.waiting.visible = false;
});
- Zostanie zatem wykonana linia 6 powyżej (pomyślne zakończenie). Następnie wykonane zostaną linie 11–14. Po wykonaniu tego kodu powracamy do widoku V, który zostanie zsynchronizowany z modelem M. Komunikat oczekiwania zostaje ukryty (ng-show=$scope.waiting.visible=false, wiersz 13), a formularz zostanie wyświetlony (ng-hide=$scope.waiting.visible=false, wiersz 13);
Wyświetlane ekrany wyglądają wówczas następująco:
Jak widać powyżej, między początkiem a końcem oczekiwania upływa 3 sekundy (06:01–05:58). Jeśli natomiast użytkownik anuluje oczekiwanie przed upływem 3 sekund, pojawia się następujący komunikat:
Na koniec należy pamiętać, że w dowolnym momencie istnieje tylko jeden wątek wykonawczy, zwany wątkiem UI (interfejs użytkownika). Zakończenie zadania asynchronicznego jest sygnalizowane przez zdarzenie, dokładnie tak samo jak kliknięcie przycisku. Zdarzenie to nie jest przetwarzane natychmiast. Zostaje ono umieszczone w kolejce zdarzeń oczekujących na wykonanie. Gdy nadejdzie jego kolej, zostaje przetworzone. Przetwarzanie to odbywa się w wątku UI, dlatego w tym czasie interfejs jest zawieszony. Nie reaguje on na działania użytkownika. Z tego powodu ważne jest, aby przetwarzanie zdarzenia przebiegało szybko. Ponieważ każde zdarzenie jest przetwarzane przez wątek UI, nigdy nie trzeba rozwiązywać problemów związanych z synchronizacją między wątkami działającymi jednocześnie. W każdym momencie działa tylko wątek UI.
3.7.6. Przykład 6: usługi HTTP
Przedstawiamy teraz usługę [dao], która komunikuje się z serwerem WWW:
![]() |
3.7.6.1. Widok V
![]() |
Napiszemy formularz służący do wywołania listy lekarzy:

Duplikujemy [app-01.html] do [app-16.html], a następnie modyfikujemy go w następujący sposób:
<div class="container" ng-cloak="">
<h1>Rdvmedecins - v1</h1>
<!-- komunikat o oczekiwaniu -->
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
<!-- żądanie -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<div class="form-group">
<label for="urlServer">{{urlServerLabel | translate}}</label>
<input type="text" id="urlServer" ng-model="server.url"/>
</div>
<div class="form-group">
<label for="login">{{loginLabel | translate}}</label>
<input type="text" id="login" ng-model="server.login"/>
</div>
<div class="form-group">
<label for="password">{{passwordLabel | translate}}</label>
<input type="password" id="password" ng-model="server.password"/>
</div>
<button class="btn btn-primary" ng-click="execute()">{{medecins.title|translate:medecins.model}}</button>
</div>
<!-- lista lekarzy -->
<div class="alert alert-success" ng-show="medecins.show">
{{medecins.title|translate:medecins.model}}
<ul>
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
</ul>
</div>
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
{{errors.title|translate:errors.model}}
<ul>
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
</ul>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-04.js"></script>
- wiersze 13–31: implementują formularz. Nie jest on widoczny, gdy wyświetlany jest komunikat o oczekiwaniu (ng-hide="waiting.visible"). Należy zauważyć, że cztery wprowadzane wartości są zapisywane w (atrybutach ng-model) [waiting.time (ligne 16), server.url (ligne 20), server.login (ligne 24), server.password (ligne 28)];
- wiersze 34–39: wyświetlają listę lekarzy. Lista ta nie zawsze jest widoczna (ng-show="medecins.show").
- wiersz 35: alternatywa dla już znanej składni <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}">;
- wiersz 36: lista nieuporządkowana;
- wiersz 37: lista lekarzy znajduje się w szablonie [medecins.data]. Dyrektywa Angular [ng-repeat] umożliwia przeglądanie listy. Składnia ng-repeat="medecin in medecins.data" powoduje, że tag <li> jest powtarzany dla każdego elementu listy [medecins.data]. Bieżący element listy nosi nazwę [medecin];
- wiersz 37: dla każdego elementu <li> zapisuje się tytuł, imię i nazwisko bieżącego lekarza, wskazanego przez zmienną [medecin];
- wiersze 42–47: wyświetlają listę błędów. Lista ta nie zawsze jest widoczna (ng-show="errors.show"). Wyświetlanie tej listy odbywa się według tego samego schematu, co wyświetlanie listy lekarzy. Ogólnie rzecz biorąc, do wyświetlania listy obiektów używa się dyrektywy Angular [ng-repeat];
- wiersz 51: kod JavaScript znajduje się teraz w pliku [rdvmedecins-04]
3.7.6.2. Kontroler C i model M
![]() |
Kod JavaScript zmienia się w następujący sposób:

- wiersze 6–9: moduł [rdvmedecins] deklaruje zależność od modułu [base64] dostarczanego przez bibliotekę [angular-base64], która jest jedną z zależności projektu. Moduł ten służy do kodowania w Base64 ciągu znaków [login:password] wysyłanego do serwisu internetowego w celu uwierzytelnienia;
- wiersze 12–13: funkcja inicjalizacyjna zawierająca nasze komunikaty z lokalizacją międzynarodową. Pojawiają się nowe komunikaty. Nie będziemy ich już przedstawiać;
- wiersze 69–70: usługa [config], która konfiguruje naszą aplikację. Dodano do niej nowe klucze komunikatów. Nie będziemy ich już omawiać;
- wiersze 318–319: usługa [utils] zawierająca metody pomocnicze. Dodano do niej nowe metody. Omówimy je;
- wiersze 385–386: usługa [dao] odpowiedzialna za komunikację z serwisem internetowym. To właśnie na niej będziemy się skupiać;
- wiersze 467–468: kontroler C widoku V, który właśnie przedstawiliśmy. Omówimy go teraz, ponieważ to właśnie on pełni rolę dyrygenta, reagując na żądania użytkownika;
3.7.6.3. Kontroler C
Kod kontrolera wygląda następująco:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- inicjalizacja szablonu
// szablon
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
$scope.server = {url: undefined, login: undefined, password: undefined};
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
$scope.errors = {show: false, model: {}};
$scope.urlServerLabel = config.urlServerLabel;
$scope.loginLabel = config.loginLabel;
$scope.passwordLabel = config.passwordLabel;
// zadanie asynchroniczne
var task;
// wykonanie akcji
$scope.execute = function () {
// aktualizujemy UI
$scope.waiting.visible = true;
$scope.medecins.show = false;
$scope.errors.show = false;
// symulowane oczekiwanie
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// oczekiwanie
promise = promise.then(function () {
// żądanie listy lekarzy;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
// analizujemy wynik poprzedniego wywołania
promise.then(function (result) {
// wynik={err: 0, dane: [med1, med2, ...]}
// wynik={err: n, komunikaty: [msg1, msg2, ...]}
if (result.err == 0) {
// umieszczamy zebrane dane w modelu
$scope.medecins.data = result.data;
// aktualizuje się UI
$scope.medecins.show = true;
$scope.waiting.visible = false;
} else {
// wystąpiły błędy podczas pobierania listy lekarzy
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
// aktualizujemy plik UI
$scope.waiting.visible = false;
}
});
};
// oczekiwanie na anulowanie
function cancel() {
// zadanie zakończone
task.reject();
// aktualizujemy plik UI
$scope.waiting.visible = false;
$scope.medecins.show = false;
$scope.errors.show = false;
}
}
])
;
- wiersz 2: kontroler ma nową zależność od serwisu [dao];
- wiersze 6–13: model M widoku V jest inicjowany na potrzeby pierwszego wyświetlenia tego widoku;
- wiersz 8: [$scope.server] zostanie użyty do pobrania trzech z czterech informacji z formularza V, przy czym czwarta jest przechowywana w [$scope.waiting.time] (wiersz 6);
- wiersz 9: [$scope.medecins] zgromadzi informacje niezbędne do wyświetlenia listy lekarzy:
<!-- lista lekarzy -->
<div class="alert alert-success" ng-show="medecins.show">
{{medecins.title|translate:medecins.model}}
<ul>
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
</ul>
</div>
Atrybut [medecins.title] będzie tytułem banera. Jest on zdefiniowany w usłudze [config]. Atrybut [medecins.show] będzie kontrolował, czy baner ma być wyświetlany, czy nie (atrybut ng-show="medecins.show"). Atrybut [medecins.model] jest pustym słownikiem i pozostanie pusty. Służy on jedynie do zilustrowania zastosowania wariantu tłumaczenia użytego w wierszu 3. Niezdefiniowany jeszcze atrybut [medecins.data] będzie zawierał listę lekarzy (wiersz 5).
- wiersz 10: [$scope.errors] zgromadzi informacje niezbędne do wyświetlenia listy błędów:
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
{{errors.title|translate:errors.model}}
<ul>
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
</ul>
</div>
Atrybut [errors.title] będzie tytułem paska. Jest on zdefiniowany w usłudze [config]. Atrybut [errors.show] będzie kontrolował, czy pasek ma być wyświetlany (atrybut ng-show="errors.show"). Atrybut [errors.model] jest pustym słownikiem i pozostanie pusty. Służy on jedynie do zilustrowania zastosowania wariantu tłumaczenia użytego w wierszu 3. Atrybut [errors.messages], który będzie zawierał listę komunikatów o błędach do wyświetlenia (wiersz 5), nie został jeszcze zdefiniowany.
- wiersz 16: zadanie asynchroniczne. Kontroler uruchomi kolejno dwa zadania asynchroniczne. Odwołania do tych kolejnych zadań zostaną umieszczone w zmiennej [task]. Umożliwi to ich anulowanie (wiersz 55);
- wiersz 19: metoda wykonywana po kliknięciu przez użytkownika przycisku [Liste des médecins]:
<button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
- wiersze 21–23: interfejs graficzny zostaje zaktualizowany: wyświetlany jest komunikat o oczekiwaniu, a cała reszta jest ukryta;
- wiersz 25: tworzy się asynchroniczne zadanie oczekiwania. Sygnał (zadanie wykonane) zostanie odebrany po upływie czasu wprowadzonego przez użytkownika w formularzu;
- wiersz 26: pobieramy obietnicę zadania asynchronicznego. To właśnie z nią współpracuje program uruchamiający zadanie. Należy jednak posiadać odwołanie do samego zadania, aby móc je anulować (wiersz 55);
- wiersze 28–32: definiujemy zadanie do wykonania po zakończeniu oczekiwania;
- wiersz 30: wykorzystuje się metodę [dao.getData] do uruchomienia nowego zadania asynchronicznego. Przekazuje się jej niezbędne informacje:
- URL – korzeń serwisu internetowego [$scope.server.url], na przykład [http://localhost:8080];
- nazwę użytkownika [$scope.server.login] do uwierzytelnienia, na przykład [admin];
- hasło [$scope.server.password] do logowania, na przykład [admin];
- URL, który realizuje żądaną usługę [config.urlSvrMedecins], w tym przypadku [/getAllMedecins]. W sumie kompletny URL będzie brzmiał [http://localhost:8080/getAllMedecins];
Metoda [dao.getData] zwraca wynik, który może przybrać dwie postacie:
- (ciąg dalszy)
- {err: 0, data: [med1, med2, ...]}, gdzie [medi] jest obiektem reprezentującym lekarza (tytuł, imię, nazwisko),
- {err: n, messages: [msg1, msg2, ...]}, gdzie [msgi] to komunikat o błędzie, a n jest różne od 0;
- wiersz 31: zwracana jest obietnica zadania. W tym miejscu należy coś wyjaśnić. Mamy dwie obietnice:
- promise.then(): zwraca pierwszą obietnicę [promise1];
- return task.promise: zwraca drugą obietnicę [promise2];
- w rezultacie `promise=promise.then(...; return task.promise)` jest łańcuchem dwóch obietnic `[promise2.promise1]`. [promise1] zostanie oceniona dopiero wtedy, gdy zostanie uzyskana obietnica [promise2], tj. gdy zadanie [dao.getData] zostanie zakończone. Obietnica [promise1] nie zależy od żadnego zadania asynchronicznego. Zostanie więc zrealizowana natychmiast;
- wiersze 34–50: z powyższego wyjaśnienia wynika, że wiersze te zostaną wykonane dopiero po zakończeniu zadania [dao.getData]. Parametr [result] przekazany do funkcji w wierszu 34 jest tworzony przez metodę [dao.getData] i przekazywany do kodu wywołującego przez operację [task.resolve(result)], gdzie [result] ma następującą postać:
- {err: 0, data: [med1, med2, ...]}, gdzie [medi] jest obiektem reprezentującym lekarza (tytuł, imię, nazwisko),
- {err: n, messages: [msg1, msg2, ...]}, gdzie [msgi] jest komunikatem o błędzie, a n jest różne od 0;
- wiersz 37: sprawdzamy kod błędu [result.err];
- wiersze 38–42: jeśli nie ma błędu (result.err == 0), pobieramy listę lekarzy i wyświetlamy ją;
- wiersze 44–47: jeśli natomiast wystąpił błąd (result.err ≠ 0), pobieramy listę komunikatów o błędach i wyświetlamy ją;
- wiersze 53–56: komunikat oczekiwania wraz z przyciskiem anulowania jest widoczny tak długo, jak długo obie operacje asynchroniczne nie zostały zakończone. Zobaczmy, co się dzieje w zależności od momentu anulowania:
- należy przede wszystkim zrozumieć, że wiersze 19–50 są wykonywane w całości. Uruchomiono wówczas tylko jedno zadanie asynchroniczne, to z wiersza 25,
- po tym pierwszym wykonaniu widok V zostaje zaktualizowany, a zatem pasek oczekiwania wraz z przyciskiem anulowania jest widoczny. Jeśli użytkownik anuluje oczekiwanie, zanim zadanie z wiersza 25 zostanie zakończone, wykonywana jest metoda z wiersza 53, a zadanie zostaje anulowane z wynikiem niepowodzenia (wiersz 55);
- wiersze 56–59: interfejs zostaje zaktualizowany: ponownie wyświetlany jest formularz, a cała reszta zostaje ukryta,
- następnie następuje powrót do widoku V, a przeglądarka przetwarza kolejne zdarzenie. Ponieważ zadanie zostało zakończone, uzyskuje się obietnicę tego zadania, co generuje zdarzenie. Zostaje ono następnie przetworzone;
- następnie wykonywane są wiersze 28–32. Nie zdefiniowano funkcji na wypadek niepowodzenia, więc żaden kod nie jest wykonywany. Otrzymujemy nową obietnicę, tę samą, która zawsze jest zwracana przez [promise.then] i zawsze jest pobierana,
- po przetworzeniu zdarzenia następuje powrót do widoku V, a przeglądarka przetworzy kolejne zdarzenie. Ponieważ obietnica [promise] z wiersza 28 została przetworzona, obietnica z wiersza 34 zostanie zrealizowana, co spowoduje nowe zdarzenie. Zostaje ono następnie przetworzone;
- linie 34–49 zostaną następnie wykonane po kolei, ponieważ obietnica użyta w linii 34 została spełniona. Ponownie, ponieważ nie zdefiniowano funkcji na wypadek niepowodzenia, żaden kod nie jest wykonywany,
- w ten sposób dochodzimy do linii 50. Nie ma już oczekiwania na zadanie i wyświetlany jest nowy widok V;
- załóżmy teraz, że anulowanie następuje w trakcie wykonywania drugiego zadania asynchronicznego [dao.getData]. Powyższe rozumowanie można ponownie zastosować. Zakończenie zadania spowoduje wykonanie wierszy 34–50 przy zakończeniu zadania z niepowodzeniem. Wkrótce okaże się, że metoda [dao.getData] wykonuje asynchroniczne wywołanie HTTP do serwisu internetowego. Wywołanie to nie zostanie anulowane, ale jego wynik nie zostanie wykorzystany.
Ważne jest, aby zrozumieć tę ciągłą wymianę między wyświetlaniem widoku V a przetwarzaniem zdarzeń przeglądarki. Zdarzenia są wywoływane przez użytkownika (kliknięcie) lub przez operacje systemowe, takie jak zakończenie operacji asynchronicznej. Stan spoczynku przeglądarki to wyświetlanie widoku V. Z tego stanu spoczynku wyrywa ją zdarzenie, które ma miejsce i które następnie przetwarza. Gdy tylko zdarzenie zostanie przetworzone, przeglądarka powraca do stanu spoczynku. Widok V jest wówczas aktualizowany, jeśli przetworzone zdarzenie zmieniło jego model M. Przeglądarka zostaje wyrywana ze stanu spoczynku przez kolejne zdarzenie.
Wszystko odbywa się w jednym wątku. Dwa zdarzenia nigdy nie są przetwarzane jednocześnie. Ich wykonanie jest sekwencyjne. Przeglądarka przechodzi do następnego zdarzenia dopiero wtedy, gdy poprzednie zwolni jej miejsce, zazwyczaj dlatego, że zostało całkowicie przetworzone.
Pozostała nam jeszcze jedna kwestia do wyjaśnienia. Aby wyświetlić komunikaty o błędach, piszemy:
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
Lista komunikatów jest dostarczana przez metodę [utils.getErrors] zdefiniowaną w usłudze [utils]. Metoda ta wygląda następująco:
// analiza błędów w odpowiedzi serwera JSON
function getErrors(data) {
// dane {err:n, messages:[]}, err!=0
// błędy
var errors = [];
// kod błędu
var err = data.err;
switch (err) {
case 2 :
// brak uprawnień
errors.push('not_authorized');
break;
case 3 :
// dostęp zabroniony
errors.push('forbidden');
break;
case 4 :
// błąd lokalny
errors.push('not_http_error');
break;
case 6 :
// nie znaleziono dokumentu
errors.push('not_found');
break;
default :
// inne przypadki
errors = data.messages;
break;
}
// jeśli nie ma komunikatu, wyświetla się komunikat
if (! errors || errors.length == 0) {
errors=['error_unknown'];
}
// wyświetla się lista błędów
return errors;
}
- wiersze 2–3: otrzymany parametr [data] jest obiektem posiadającym dwa atrybuty:
- [err]: kod błędu;
- [messages]: lista komunikatów;
- wiersz 5: utworzymy tablicę komunikatów o błędach. Komunikaty te są zinternacjonalizowane. Z tego powodu do tablicy nie umieszczamy samych komunikatów, lecz ich klucze internacjonalizacji, z wyjątkiem wiersza 27. W tym przypadku wykorzystujemy atrybut [messages] parametru [data]. Komunikaty te są prawdziwymi komunikatami, a nie kluczami komunikatów. Widok V będzie je jednak traktował jako klucze komunikatów, których wówczas nie uda się znaleźć. W takim przypadku moduł [translate] wyświetla klucz komunikatu, którego nie znalazł, a więc w tym przypadku prawdziwy komunikat. Jest to pożądany wynik;
- wiersze 32–34: obsługują przypadek, w którym wartość w wierszu 27 modułu [data.messages] wynosi null. Dzieje się tak w przypadku napisanej usługi internetowej. Należało uniknąć tego przypadku.
3.7.6.4. Usługa [dao]
![]() |
Usługa [dao] zapewnia wymianę danych z usługą internetową HTTP / JSON. Jej kod wygląda następująco:
angular.module("rdvmedecins")
.factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
function ($http, $q, config, $base64, utils) {
// logi
utils.debug("[dao] init");
// ----------------------------------metody prywatne
// pobieranie danych z serwisu internetowego
function getData(serverUrl, username, password, urlAction, info) {
// operacja asynchroniczna
var task = $q.defer();
// adres URL żądania HTTP
var url = serverUrl + urlAction;
// uwierzytelnianie podstawowe
var basic = "Basic " + $base64.encode(username + ":" + password);
// odpowiedź
var réponse;
// wszystkie żądania HTTP muszą być uwierzytelnione
var headers = $http.defaults.headers.common;
headers.Authorization = basic;
// wysyłamy żądanie HTTP
var promise;
if (info) {
promise = $http.post(url, info, {timeout: config.timeout});
} else {
promise = $http.get(url, {timeout: config.timeout});
}
promise.then(success, failure);
// zwracamy samo zadanie, aby można było je anulować
return task;
// powodzenie
function success(response) {
// response.data={status:0, data:[med1, med2, ...]} lub {status:x, data:[msg1, msg2, ...]
utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
// odpowiedź
var payLoad = response.data;
réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
// zwracamy odpowiedź
task.resolve(réponse);
}
// błąd
function failure(response) {
utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
// analizujemy status
var status = response.status;
var error;
switch (status) {
case 401 :
// nieautoryzowany
error = 2;
break;
case 403:
// zabronione
error = 3;
break;
case 404:
// nie znaleziono
error = 6;
break;
case 0:
// błąd lokalny
error = 4;
break;
default:
// coś innego
error = 5;
}
// zwracana jest odpowiedź
task.resolve({err: error, messages: [response.statusText]});
}
}
// --------------------- instancja usługi [dao]
return {
getData: getData
}
}]);
- wiersze 77–79: usługa posiada tylko jedno pole: metodę [getData], która pozwala uzyskać informacje z serwisu internetowego / JSON;
- wiersz 2: pojawia się zależność [$http], z którą jeszcze się nie spotkaliśmy. Jest to predefiniowana usługa Angulara, która umożliwia komunikację HTTP z jednostką zdalną;
- wiersz 6: wpis w dzienniku pozwalający sprawdzić, w którym momencie cyklu życia aplikacji kod jest wykonywany;
- wiersz 10: metoda [getData] przyjmuje pięć parametrów:
- [serverUrl]: katalog główny serwisu internetowego (http://localhost:8080);
- [urlAction]: adres URL konkretnej żądanej usługi (/getAllMedecins);
- [username]: nazwa użytkownika;
- [password]: hasło użytkownika;
- [info]: obiekt zawierający dodatkowe informacje, gdy parametr URL żądanej usługi jest wywoływany za pośrednictwem operacji POST. W przypadku URL (/getAllMedecins) ten parametr nie został przekazany. Jest to zatem [undefined];
- wiersz 12: tworzone jest zadanie asynchroniczne;
- wiersz 14: URL realizuje żądaną usługę (http://localhost:8080/getAllMedecins);
- wiersz 16: uwierzytelnianie odbywa się poprzez wysłanie następującego nagłówka:
gdzie [code] to kod Base64 ciągu znaków [username:password];
Wiersz 16 tworzy część [Basic code] nagłówka HTTP;
- wiersz 18: odpowiedź serwisu internetowego;
- wiersz 20: nagłówki HTTP wysyłane domyślnie przez Angular w żądaniu HTTP są zdefiniowane w obiekcie [$http.defaults.headers.common]. Nagłówek [Authorization:Basic code] nie znajduje się wśród nich;
- wiersz 21: dodajemy go do nagłówków HTTP, które mają być wysyłane systematycznie. Po lewej stronie przypisania znajduje się nagłówek [Authorization], który należy zainicjować, a po prawej stronie wartość nagłówka, w tym przypadku wartość zdefiniowana w wierszu 16. Jeśli więc napiszemy:
Angular wyśle nagłówek HTTP:
- wiersz 23: metody serwisu [$http] zwracają obietnice. Zostaną one zapisane w zmiennej [promise];
- wiersz 27: ponieważ w tym miejscu parametr [info] ma wartość [undefined], wykonywany jest wiersz 27. URL (http://localhost:8080/getAllMedecins) jest wywoływane za pomocą GET. Aby nie czekać zbyt długo, ustala się maksymalny czas oczekiwania (timeout) na otrzymanie odpowiedzi z serwera. Domyślnie czas ten wynosi jedną sekundę;
- wiersz 29: definiujemy dwie metody, które mają zostać wykonane po otrzymaniu obietnicy:
- [success]: zdefiniowana w wierszu 34, jest to metoda, która ma zostać wykonana, gdy obietnica zostanie spełniona w wyniku pomyślnego zakończenia zadania;
- [failure]: zdefiniowana w wierszu 45, jest to metoda, która ma zostać wykonana, gdy obietnica zostanie uzyskana w przypadku niepowodzenia zadania;
- obie metody (a właściwie funkcje) są zdefiniowane wewnątrz funkcji [getData]. Jest to możliwe w JavaScript. Zmienne zdefiniowane w [getData] są dostępne w obu funkcjach wewnętrznych [success, failure];
- wiersz 31: zwracane jest zadanie utworzone w wierszu 12. Należy tu pamiętać o kodzie wywołującym:
promise = promise.then(function () {
// żądanie listy lekarzy;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
W wierszu 3 powyżej faktycznie pobierane jest zadanie.
- wiersz 34: funkcja [success] jest wykonywana później, gdy wywołanie HTTP zakończy się pomyślnie. Pojęcie „pomyślnego zakończenia” wiąże się z pierwszym wierszem odpowiedzi HTTP. Ma ona następującą postać:
Kod to trzycyfrowy tekst wskazujący, czy wywołanie zakończyło się powodzeniem, czy nie. Ogólnie rzecz biorąc, kody 2xx i 3xx oznaczają powodzenie, a pozostałe – niepowodzenie. Tekst to krótkie wyjaśnienie. Oto dwie możliwe odpowiedzi: jedna w przypadku powodzenia, druga w przypadku niepowodzenia:
- wiersz 36: na konsoli wyświetlana jest odpowiedź serwera. W przypadku błędu [404 Not Found] otrzymujemy coś w rodzaju:
[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}
W tej odpowiedzi wykorzystamy tylko pola [data], [status] i [statusText].
- wiersz 38: pobieramy z odpowiedzi pole [data]. Będzie ono miało jedną z następujących postaci:
- {status: 0, data: [med1, med2, ...]}, gdzie [medi] jest obiektem reprezentującym lekarza (tytuł, imię, nazwisko),
- {status: n, data: [msg1, msg2, ...]}, gdzie [msgi] to komunikat o błędzie, a n jest różne od 0;
![]() |

- wiersz 39: tworzymy odpowiedź {0,data} lub {n,messages}. Pierwsza odpowiedź zawiera lekarzy w polu [data]. Druga sygnalizuje błąd, który wystąpił po stronie serwera. Serwer obsłużył ten błąd, wygenerował kod błędu w polu [err] oraz listę komunikatów o błędach w polu [data]. W obu przypadkach zwraca kod HTTP 200, wskazujący, że zlecenie HTTP zostało w pełni przetworzone. Dlatego oba przypadki są obsługiwane w tej samej funkcji [success];
- wiersz 41: zadanie zostało zakończone ([task.resolve]) i zwracana jest jedna z dwóch odpowiedzi:
- {err: 0, data: [med1, med2, ...]}, gdzie [medi] jest obiektem reprezentującym lekarza (tytuł, imię, nazwisko),
- {err: n, messages: [msg1, msg2, ...]}, gdzie [msgi] to komunikat o błędzie, a n jest różne od 0;
Należy powiązać ten kod ze sposobem, w jaki ta odpowiedź jest pobierana w kodzie wywołującym kontrolera:
// analizujemy wynik poprzedniego wywołania
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// wynik={err: n, komunikaty: [msg1, msg2, ...]}
...
}
Odpowiedź o wartości [task.resolve(réponse)] znajduje się powyżej w zmiennej [result].
- wiersz 45: funkcja [failure], gdy zadanie asynchroniczne kończy się niepowodzeniem. Istnieją dwa możliwe przypadki:
- serwer sygnalizuje tę awarię, zwracając kod inny niż 2xx lub 3xx,
- Angular anuluje wywołanie HTTP. W takim przypadku nie dochodzi do wywołania. Występuje wyjątek Angular, ale serwer nie zwraca kodu błędu HTTP. Dzieje się tak na przykład w przypadku podania nieprawidłowego URL, którego nie można wywołać;
- wiersz 46: wyświetlamy odpowiedź w konsoli;
- wiersz 48: przypominamy sobie, że odpowiedź serwera ma postać:
{"data":"...","status":404,"config":{...},"statusText":"Not Found"}
W wierszu 48 pobieramy powyższy atrybut [status];
- wiersze 50–70: na podstawie kodu błędu HTTP generujemy nowy kod błędu, aby ukryć przed kodami wywołującymi charakter metody HTTP, czyli [dao.getData]. Można sprawdzić, że w kontrolerze korzystającym z tej metody nic nie wskazuje na to, że w metodzie występuje wywołanie o nazwie HTTP;
- wiersz 51: błąd [401] oznacza niepowodzenie uwierzytelnienia (na przykład nieprawidłowe hasło),
- wiersz 55: błąd [403] oznacza nieautoryzowane wywołanie. Użytkownik poprawnie się uwierzytelnił, ale nie posiada wystarczających uprawnień, aby zażądać operacji URL, o którą prosił. Sytuacja ta dotyczy użytkownika [user / user]. Użytkownik ten faktycznie istnieje w bazie danych, ale nie ma uprawnień do korzystania z aplikacji. Tylko użytkownik [admin / admin] posiada to uprawnienie;
- wiersz 59: błąd [404] wynika z braku znalezionego użytkownika URL. Błąd ten może mieć kilka przyczyn:
- użytkownik popełnił błąd podczas wpisywania identyfikatora URL usługi;
- serwis internetowy nie został uruchomiony;
- usługa internetowa nie odpowiedziała wystarczająco szybko (domyślny limit czasu wynosi jedną sekundę);
- wiersz 63: kod błędu HTTP 0 nie istnieje. Mamy do czynienia z sytuacją, w której Angular nie wykonał żądanego wywołania HTTP, ponieważ wartość URL wprowadzona przez użytkownika jest nieprawidłowa i nie można jej wywołać. W dalszej części napotkamy inne przypadki, w których Angular nie wykona żądanego wywołania HTTP;
- wiersz 72: zadanie kończy się pomyślnie (task.resolve) poprzez zwrócenie odpowiedzi typu {err, messages}, gdzie tablica [messages] składa się wyłącznie z jednego komunikatu [response.statusText]. W przypadku, gdy Angular nie wykonał żądanego wywołania HTTP, otrzymamy pusty ciąg znaków;
Teraz, gdy mamy już zarówno ogólny, jak i szczegółowy obraz aplikacji, możemy rozpocząć testy.
3.7.6.5. Testowanie aplikacji – 1
Zacznijmy od poprawnych danych wejściowych:

![]() |
- w polu [1] wpisujemy 0, aby uniknąć oczekiwania;
- w przypadku [2] pojawia się komunikat o błędzie, mimo że dane są poprawne. Nie przedstawiliśmy różnych komunikatów o błędach. Komunikat wyświetlony w przypadku [2] jest komunikatem ogólnym powiązanym z błędem 0, który odpowiada wyjątkowi w Angularze. Angular napotkał problem, który uniemożliwił mu wykonanie wywołania HTTP. W takich przypadkach należy sprawdzić logi konsoli JavaScript. Można to zrobić na dwa sposoby:
- wpisać [F12] w przeglądarce Chrome;
- skorzystać z konsoli WebStorm;
W konsoli Webstormu znajdziemy różne komunikaty, w tym ten:
- wiersz 1: Angular zgłasza błąd, do którego jeszcze wrócimy;
- wiersz 2: log metody [dao.getData]. Znajdujemy w nim interesujące informacje:
- [status] ma wartość 0, co oznacza, że nie nastąpiło wywołanie metody HTTP. W związku z tym [statusText] jest puste,
- [url] odpowiada [http://localhost:8080/getAllMedecins], co jest poprawne;
- nagłówek HTTP uwierzytelniający [Authorization":"Basic YWRtaW46YWRtaW4=] jest również poprawny;
No to dlaczego to nie zadziałało? Kluczowym wyrażeniem w logach jest [No 'Access-Control-Allow-Origin' header is present]. Aby to zrozumieć, trzeba przedstawić długie wyjaśnienie. Zacznijmy od powrotu do ogólnej architektury aplikacji klient-serwer:

- strony HTML / CSS / JS aplikacji Angular pochodzą z serwera [1];
- w [2] usługa [dao] wysyła żądanie do innego serwera, serwera [2]. Cóż, jest to zabronione przez przeglądarkę, w której uruchomiona jest aplikacja Angular, ponieważ stanowi to lukę w zabezpieczeniach. Aplikacja może wysyłać zapytania wyłącznie do serwera, z którego pochodzi, tj. do serwera [1];
W rzeczywistości nieprawdziwe jest stwierdzenie, że przeglądarka zabrania aplikacji Angular wysyłania zapytania do serwera [2]. Aplikacja wysyła do niego zapytanie, aby dowiedzieć się, czy serwer zezwala na wysyłanie zapytań przez klienta spoza własnej domeny. Technikę tę nazywa się CORS (Cross-Origin Resource Sharing). Serwer [2] wyraża zgodę, wysyłając określone nagłówki HTTP. To właśnie dlatego, że nasz serwer [2] nie wysłał tych nagłówków, przeglądarka odmówiła wykonania wywołania HTTP, o które poprosiła aplikacja.
Przejdźmy teraz do szczegółów. Przyjrzyjmy się komunikacji sieciowej, która miała miejsce podczas wywołania HTTP. W tym celu w przeglądarce Chrome otwieramy narzędzia programisty i wybieramy kartę [Network], aby wyświetlić komunikację sieciową:
![]() |
- w [1] wybieramy zakładkę [network];
- w przypadku [2] żądamy listy lekarzy;
W zakładce [network] otrzymujemy następujące informacje:
![]() |
- w zakładce [1] – informacje wysłane do serwera;
- w [2] – odpowiedź serwera;
W [1] widać, że przeglądarka wysłała żądanie HTTP [OPTIONS] dotyczące żądanej strony URL. [OPTIONS] jest jednym z możliwych poleceń HTTP, obok bardziej znanych [GET] i [POST]. Pozwala ono na uzyskanie informacji od serwera, w szczególności na temat obsługiwanych przez niego opcji HTTP, stąd nazwa polecenia. Serwer odpowiada w formacie [2]. Aby wskazać, że akceptuje żądania od klientów spoza swojej domeny, musi zwrócić specjalny nagłówek o nazwie [Access-Control-Allow-Origin]. I właśnie dlatego, że nie odesłał tego nagłówka, Angular nie wykonał żądanego wywołania HTTP i zwrócił błąd:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. W żądanym zasobie nie występuje nagłówek „Access-Control-Allow-Origin”. W związku z tym źródło „http://localhost:63342” nie ma uprawnień dostępu.
Musimy zatem zmodyfikować nasz serwer, aby wysyłał oczekiwany nagłówek HTTP.
3.7.6.6. Modyfikacja serwera WWW / JSON
Wracamy do programu Eclipse. Aby zachować dotychczasowe zmiany, duplikujemy aktualną wersję serwera WWW / JSON [rdvmedecins-webapi-v2] do [rdvmedecins-webapi-v3] [1]:
![]() |
Wprowadzamy pierwszą zmianę w pliku [ApplicationModel], który jest jednym z elementów konfiguracji serwisu internetowego:
package rdvmedecins.web.models;
...
@Component
public class ApplicationModel implements IMetier {
// warstwa [métier]
@Autowired
private IMetier métier;
// dane pochodzące z warstwy [métier]
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// dane konfiguracyjne
private boolean CORSneeded = true;
...
public boolean isCORSneeded() {
return CORSneeded;
}
}
- wiersz 17: tworzymy zmienną logiczną, która określa, czy akceptujemy klientów spoza domeny serwera;
- wiersze 21–23: metoda uzyskiwania dostępu do tej informacji;
Następnie tworzymy nowy kontroler Spring o nazwie MVC [3]:
![]() |
Klasa [RdvMedecinsCorsController] wygląda następująco:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// wysyłanie opcji do klienta
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// ustalanie nagłówka CORS
response.addHeader("Access-Control-Allow-Origin", "*");
}
}
// lista lekarzy
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
}
- wiersze 28–31: definiują kontroler dla URL [/getAllMedecins], gdy jest on wywoływany za pomocą polecenia HTTP [OPTIONS];
- wiersz 29: metoda [getAllMedecins] przyjmuje jako parametr obiekt [HttpServletResponse], który zostanie wysłany do klienta, który złożył żądanie. Obiekt ten jest wstrzykiwany przez Spring;
- wiersz 30: przetwarzanie żądania przekazujemy do prywatnej metody z wierszy 19–25;
- wiersze 15–16: obiekt [ApplicationModel] jest wstrzykiwany;
- wiersze 20–23: jeśli serwer jest skonfigurowany tak, aby akceptować klientów spoza swojej domeny, wówczas wysyłany jest nagłówek HTTP:
Access-Control-Allow-Origin: *
co oznacza, że serwer akceptuje klientów z dowolnej domeny (*).
Jesteśmy teraz gotowi do przeprowadzenia nowych testów. Uruchamiamy nową wersję serwisu internetowego i okazuje się, że problem nadal występuje. Nic się nie zmieniło. Jeśli w wierszu 30 powyżej umieścimy wyświetlanie konsoli, to nigdy nie zostanie ono wyświetlone, co wskazuje, że metoda [getAllMedecins] z wiersza 29 nigdy nie jest wywoływana.
Po kilku poszukiwaniach okazuje się, że Spring MVC samodzielnie przetwarza polecenia HTTP i [OPTIONS], stosując domyślną obsługę. W związku z tym to zawsze Spring odpowiada, a nigdy metoda [getAllMedecins] z linii 29. To domyślne zachowanie Springa MVC można zmienić. Wprowadzamy nową klasę konfiguracyjną, aby skonfigurować nowe zachowanie:
![]() |
Nowa klasa konfiguracyjna [WebConfig] wygląda następująco:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
// konfiguracja serwletu dispatcherservlet dla nagłówków CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
- wiersz 8: klasa jest klasą konfiguracyjną Springa. Deklaruje ona bean’y, które zostaną umieszczone w kontekście Springa;
- wiersz 12: bean [dispatcherServlet] służy do zdefiniowania serwletu obsługującego żądania klientów. Jest on typu [DispatcherServlet]. Serwlet ten jest zazwyczaj tworzony domyślnie. Jeśli jednak utworzymy go samodzielnie, możemy go skonfigurować;
- wiersz 14: tworzymy instancję typu [DispatcherServlet];
- wiersz 15: nakazujemy serwletowi przekazywanie do aplikacji poleceń HTTP oraz [OPTIONS];
- wiersz 16: ustawniamy serwlet zgodnie z powyższą konfiguracją;
Pozostaje nam jeszcze zmodyfikować klasę [AppConfig]:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- wiersz 11: importowana jest nowa klasa konfiguracyjna [WebConfig];
3.7.6.7. Testy aplikacji – 2
Uruchamiamy nową wersję serwisu internetowego / JSON i próbujemy uzyskać listę lekarzy za pomocą naszego klienta Angular. Sprawdzamy komunikację sieciową w zakładce [Network]:
![]() |
- w [1] widać, że nagłówek HTTP [Access-Control-Allow-Origin: *] jest teraz obecny w odpowiedzi serwera. A jednak nadal to nie działa. Sprawdzamy w pliku [2] logi konsoli. Znajdujemy tam następujący wpis:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Pole nagłówka żądania „Authorization” nie jest dozwolone przez Access-Control-Allow-Headers
Widać, że przeglądarka oczekuje nowego nagłówka HTTP [Access-Control-Allow-Headers], który poinformowałby ją, że mamy prawo wysłać jej nagłówek uwierzytelniający:
To może być dobry znak. Być może Angular chciał wysłać polecenie HTTP GET. Ponieważ jednak towarzyszy mu nagłówek uwierzytelniający, pyta, czy serwer go akceptuje.
Modyfikujemy nasz serwer WWW / JSON, aby wysyłał ten nagłówek. Klasa [RdvMedecinsCorsController] zmienia się w następujący sposób:
// wysyłanie opcji do klienta
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// ustawiamy nagłówek CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// zezwalamy na nagłówek [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
- w wierszach 6–7 dodano brakujący nagłówek.
Uruchamiamy ponownie serwer i ponownie żądamy listy lekarzy za pomocą klienta Angular:
![]() |
Tym razem wszystko działa poprawnie. Logi konsoli pokazują odpowiedź otrzymaną przez metodę [dao.getData]:
[dao] getData[/getAllMedecins] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
Widać, że:
- serwer zwrócił kod błędu [status=200] wraz z komunikatem [statusText=OK]. Dlatego znajdujemy się w funkcji [success];
- serwer zwrócił obiekt [data] z dwoma polami:
- [status]: (nie należy mylić z kodem błędu HTTP [status]). W tym przypadku [status=0] oznacza, że URL i [/getAllMedecins] zostały przetworzone bez błędów;
- [data]: zawierający listę lekarzy JSON;
Przyjrzyjmy się teraz innym interesującym przypadkom:
Popełniono błąd w identyfikatorach [login, password]:
![]() |
Logujemy się pod identyfikatorem [user / user], który nie ma dostępu do aplikacji (dostęp ma tylko [admin]):
![]() |
Tym razem błąd nie dotyczy już identyfikatora [Erreur d'authentification], ale [Accès refusé].
3.7.7. Przykład 7: lista klientów
Wracamy do poprzedniej aplikacji, aby tym razem wyświetlić listę klientów w rozwijanej liście typu [Bootstrap select] (patrz punkt 3.6.6).
3.7.7.1. Widok V
Początkowy widok będzie wyglądał następująco:
![]() |
Aby uzyskać widok V, powielamy kod [app-16.html] do pliku [app-17.html] i modyfikujemy go w następujący sposób:
<div class="container" >
<h1>Rdvmedecins - v1</h1>
<!-- komunikat o oczekiwaniu -->
<div class="alert alert-warning" ng-show="waiting.visible" >
...
</div>
<!-- żądanie -->
<div class="alert alert-info" ng-hide="waiting.visible" >
...
<button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
</div>
<!-- lista klientów -->
<div class="row" style="margin-top: 20px" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
- wiersze 5–7: pasek oczekiwania pozostaje bez zmian;
- wiersze 10–13: formularz pozostaje bez zmian, z wyjątkiem nazwy przycisku (wiersz 12);
- wiersze 28–30: pasek błędów pozostaje bez zmian;
- wiersze 16–25: klienci są wyświetlani na liście rozwijanej, której styl jest określony przez komponent [Bootstrap-selectpicker] (atrybuty data-style, class, wiersz 19);
- wiersz 20: użyto dyrektywy [ng-repeat] do wygenerowania różnych opcji listy rozwijanej. Należy zauważyć, że nazwa opcji ma typ [Mme Julienne Tatou], a wartość opcji ma typ [100], gdzie 100 to identyfikator (id) wyświetlanego klienta;
- wiersz 34: kod JavaScript zostaje przeniesiony do nowego pliku o nazwie [rdvmedecins-05];
3.7.7.2. Kontroler C i model M
Kod JavaScript z pliku [rdvmedecins-05] uzyskano poprzez skopiowanie pliku [rdvmedecins-04]:

Prawie nic się nie zmienia, z wyjątkiem kontrolera, który został teraz dostosowany do dostarczania listy klientów:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- inicjalizacja szablonu
// szablon
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
$scope.server = {url: undefined, login: undefined, password: undefined};
$scope.clients = {title: config.listClients, show: false, model: {}};
$scope.errors = {show: false, model: {}};
$scope.urlServerLabel = config.urlServerLabel;
$scope.loginLabel = config.loginLabel;
$scope.passwordLabel = config.passwordLabel;
// zadanie asynchroniczne
var task;
// wykonanie akcji
$scope.execute = function () {
// aktualizujemy UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// symulowane oczekiwanie
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// oczekiwanie
promise = promise.then(function () {
// żądamy listy klientów;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
return task.promise;
});
// analizujemy wynik poprzedniego wywołania
promise.then(function (result) {
// wynik={err: 0, dane: [client1, client2, ...]}
// wynik={err: n, komunikaty: [msg1, msg2, ...]}
if (result.err == 0) {
// wprowadzamy zebrane dane do modelu
$scope.clients.data = result.data;
// aktualizuje się UI
$scope.clients.show = true;
$scope.waiting.visible = false;
// stylujemy listę rozwijaną
$('.selectpicker').selectpicker();
} else {
// wystąpiły błędy podczas pobierania listy klientów
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// aktualizujemy plik UI
$scope.waiting.visible = false;
}
});
};
// oczekiwanie na anulowanie
function cancel() {
// zadanie zakończone
task.reject();
// aktualizowany jest plik UI
$scope.waiting.visible = false;
$scope.clients.show = false;
$scope.errors.show = false;
}
}
])
;
- w kontrolerze zmienia się bardzo niewiele. Wcześniej dostarczał on listę lekarzy. Teraz dostarcza listę klientów;
- wiersz 9: [$scope.clients] będzie szablonem paska klientów w widoku V;
- wiersz 30: obecnie używane są szablony URL i [/getAllClients];
- wiersze 35–36: dwie formy odpowiedzi zwracanej przez metodę [dao.getData]. Teraz mamy klientów zamiast lekarzy;
- wiersz 44: dość rzadka instrukcja w kodzie Angular. Bezpośrednio manipulujemy obiektem DOM (Document Object Model). W tym miejscu chcemy zastosować metodę [selectpicker] (będącej częścią [bootstrap-select.min.js]) do elementów DOM, które mają klasę [selectpicker] [$('.selectpicker')]. Jest tylko jeden taki element – lista rozwijana:
<select data-style="btn-primary" class="selectpicker" select-enable="">
....
</select>
W punkcie 3.6.6 wykazano, że powodowało to stylizację listy rozwijanej w następujący sposób:
![]() | ![]() |
Podobnie jak w przypadku lekarzy, musimy również zmodyfikować usługę internetową.
3.7.7.3. Modyfikacja serwisu internetowego – 1
![]() |
Klasa [RdvMedecinsController] została wzbogacona o nową metodę:
package rdvmedecins.web.controllers;
...
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// wysyłanie opcji do klienta
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// ustawianie nagłówka CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// zezwalamy na nagłówek [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
}
// lista lekarzy
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
// lista klientów
@RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
public void getAllClients(HttpServletResponse response) {
sendOptions(response);
}
}
- wiersze 29–32: metoda [getAllClients] będzie obsługiwać żądania HTTP i [OPTIONS], które zostaną jej przesłane przez przeglądarkę;
3.7.7.4. Testy aplikacji – 1
Jesteśmy teraz gotowi do przeprowadzenia testu. Uruchamiamy serwer WWW, a następnie wprowadzamy prawidłowe wartości w formularzu Angular. Otrzymujemy następującą odpowiedź:

Ten komunikat o błędzie pojawia się, gdy Angular nie zdołał wykonać żądanego zapytania HTTP. Przyczyny tego błędu należy wówczas poszukać w logach konsoli. Znajduje się tam następujący komunikat:
XMLHttpRequest cannot load http://localhost:8080/getAllClients. W żądanym zasobie nie występuje nagłówek „Access-Control-Allow-Origin”. W związku z tym źródło „http://localhost:63342” nie ma uprawnień dostępu.
Problem, który wydawał się już rozwiązany. Przyjrzyjmy się zatem przepływowi danych w sieci:

Widać, że operacja [getAllClients] z metodą HTTP [OPTIONS]zakończyła się pomyślnie, ale operacja [getAllClients] z metodą HTTP [GET] została anulowana. Odpowiedź na żądanie [OPTIONS] brzmiała następująco:

Nagłówki HTTP z CORS są na swoim miejscu. Przyjrzyjmy się teraz wymianie danych HTTP podczas GET:

Żądanie HTTP wydaje się poprawne. Widoczny jest w szczególności nagłówek uwierzytelniający.
Oprócz poprzedniego komunikatu o błędzie w logach konsoli znajduje się następujący komunikat:
[dao] getData[/getAllClients] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}
Jest to wpis, który metoda [dao.getData] generuje automatycznie po otrzymaniu odpowiedzi na swoje żądanie HTTP. Można zauważyć dwie rzeczy:
- [status=0]: oznacza to, że to Angular anulował żądanie HTTP;
- [method=GET]: oznacza to, że anulowano żądanie GET;
W połączeniu z pierwszym komunikatem oznacza to, że również w przypadku żądania GET Angular oczekuje tutaj nagłówków CORS. Obecnie jednak nasza usługa internetowa wysyła je tylko dla żądania HTTP i [OPTIONS]. Bardzo dziwne jest, że ten błąd pojawia się teraz, a nie w przypadku listy lekarzy. Nie mam na to żadnego wyjaśnienia.
Należy zatem ponownie zmodyfikować serwis internetowy.
3.7.7.5. Modyfikacja serwisu internetowego – 2
![]() |
Metody [GET] i [POST] są obsługiwane w klasie [RdvMedecinsController]. Musimy ją zmodyfikować, aby te metody wysyłały nagłówki CORS. Robimy to w następujący sposób:
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// lista klientów
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients(HttpServletResponse response) {
// nagłówki CORS
rdvMedecinsCorsController.getAllClients(response);
// stan aplikacji
if (messages != null) {
return new Reponse(-1, messages);
}
// lista klientów
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
...
- wiersz 8: chcemy ponownie wykorzystać kod, który umieściliśmy w kontrolerze [RdvMedecinsCorsController]. Wstawiamy go więc tutaj;
- wiersz 14: metoda obsługująca żądanie [GET /getAllClients]. Wprowadzamy dwie zmiany:
- wiersz 14: wstrzykujemy obiekt [HttpServletResponse] do parametrów metody,
- wiersz 16: wykorzystujemy metody klasy [RdvMedecinsCorsController], aby umieścić w tym obiekcie nagłówki CORS;
3.7.7.6. Testy aplikacji – 2
Uruchamiamy nową wersję serwisu internetowego i ponownie żądamy listy klientów. Otrzymujemy następującą odpowiedź:
![]() |
- w przypadku [1] otrzymujemy odpowiedź, ale jest ona pusta [2];
- w przypadku [3]: komunikacja sieciowa przebiegła pomyślnie;
W logach konsoli metoda [dao.getData] wyświetliła otrzymaną odpowiedź:
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
Zatem metoda pomyślnie otrzymała listę klientów. Po sprawdzeniu kodu pojawia się podejrzenie, że przyczyną jest następująca instrukcja, której nie znamy zbyt dobrze:
// stylizowanie listy rozwijanej
$('.selectpicker').selectpicker();
Wykomentowaliśmy wiersz 2 i spróbowaliśmy ponownie. Otrzymaliśmy następującą odpowiedź:
![]() |
Zlokalizowaliśmy więc problem. Problemem jest zastosowanie metody [selectpicker] do listy rozwijanej. Gdy przyjrzymy się kodowi źródłowemu błędnej strony, widzimy następujący fragment:
![]() |
- okazuje się, że w [1] lista rozwijana wraz z jej elementami rzeczywiście istnieje, ale nie jest wyświetlana [style='display:none'];
- w [2] widoczny jest przycisk z [bootstrap select]. Elementy listy rozwijanej powinny pojawić się na liście <ul role='menu'>. Nie ma ich tam, więc mamy pustą listę. Wygląda na to, że w momencie zastosowania metody [selectpicker] do listy rozwijanej jej zawartość była pusta;
Przeglądając internet w poszukiwaniu rozwiązania, znaleźliśmy to. Zastępujemy kod:
// stylizowanie listy rozwijanej
$('.selectpicker').selectpicker();
na następujący:
// stylizowanie listy rozwijanej
$timeout(function(){
$('.selectpicker').selectpicker();
});
Styl [bootstrap-select] jest stosowany za pośrednictwem funkcji [$timeout]. Spotkaliśmy się już z tą funkcją, która pozwala na wykonanie funkcji po upływie określonego czasu. W tym przypadku brak czasu oczekiwania oznacza czas zerowy. Poprzednie linie umieszczają zdarzenie w kolejce zdarzeń przeglądarki. Gdy przetwarzanie bieżącego zdarzenia (kliknięcie przycisku [Liste des clients]) dobiegnie końca, zostanie wyświetlony widok V. Zaraz potem przeglądarka sprawdzi swoją listę zdarzeń. Ze względu na zerowy czas oczekiwania zdarzenie [$timeout] znajdzie się na początku listy i zostanie przetworzone. Styl [bootstrap-select] zostanie wówczas zastosowany do wypełnionej listy rozwijanej. Zobaczmy wynik:
![]() |
Jeśli ponownie przyjrzymy się kodowi źródłowemu wyświetlanej strony, zobaczymy następujące elementy:
![]() |
Przycisk [bootstrap-select], który wcześniej był pusty, zawiera teraz listę klientów.
3.7.7.7. Korzystanie z dyrektywy
W kontrolerze C widoku V znaleźliśmy następujący kod:
// stylizowanie listy rozwijanej
$('.selectpicker').selectpicker();
Mamy tu do czynienia z obiektem DOM. Wielu programistów Angular ma awersję do manipulowania obiektem DOM w kodzie kontrolera. Ich zdaniem powinno się to odbywać w dyrektywie. Dyrektywę Angulara można postrzegać jako rozszerzenie języka HTML. Dzięki temu możliwe jest tworzenie nowych elementów lub atrybutów HTML. Spójrzmy na pierwszy przykład:
Tworzymy następujący plik JS [selectEnable]:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
var selectpicker = $('.selectpicker');
selectpicker.selectpicker();
});
}
};
}]);
- dyrektywa jest zgodna ze składnią kontrolera, do której już się przyzwyczailiśmy:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)
Dyrektywa należy do modułu [rvmedecins]. Jest to funkcja przyjmująca dwa parametry:
- (ciąg dalszy)
- pierwszym z nich jest nazwa dyrektywy [selectEnable];
- drugim jest tablica ['obj1','obj2',..., function(obj1, obj2,...)], w której [obj] to obiekty, które mają zostać wstrzyknięte do funkcji. W tym przypadku jedynym wstrzykniętym obiektem jest obiekt predefiniowany [$timeout];
- funkcja [directive] zwraca obiekt, który może posiadać różne atrybuty. W tym przypadku jedynym atrybutem jest atrybut [link] (wiersz 3). Jego wartością jest tutaj funkcja przyjmująca trzy parametry:
- scope: szablon widoku, w którym używana jest dyrektywa;
- element: element widoku, obiekt, na który odnosi się dyrektywa;
- attrs: atrybuty tego elementu;
Weźmy przykład. Dyrektywa [selectEnable] mogłaby zostać użyta w następującym kontekście:
W powyższym przykładzie atrybut [select-enable] stosuje dyrektywę [selectEnable] do elementu HTML <div>. Dyrektywę [doSomething] można zastosować do dowolnego elementu HTML poprzez dodanie do niego atrybutu [do-something]. Należy zwrócić uwagę na zmianę zapisu między nazwą dyrektywy a powiązanym z nią atrybutem. Zapis zmienia się z [camelCase] na [camel-case].
Dyrektywa [selectEnable] mogłaby być również wykorzystana w następujący sposób:
W tym przypadku dyrektywa [doSomething] jest stosowana w postaci znacznika HTML <do-something>.
Wróćmy do zapisu
oraz do trzech parametrów funkcji [link] dyrektywy [scope, element, attrs]:
- scope: to szablon widoku, w którym znajduje się element <div>;
- element: to sam element <div>;
- attrs: to tablica atrybutów elementu <div>. Atrybuty te mogą służyć do przekazywania informacji do dyrektywy. W powyższym przykładzie należy wpisać attrs['selectEnable'], aby uzyskać informację [data]. Należy zwrócić uwagę na zmianę zapisu [selectEnable] w celu określenia atrybutu [select-enable];
Wróćmy do kodu dyrektywy:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
$('.selectpicker').selectpicker();
});
}
};
}]);
- wiersze 14–16: znajdujemy tu kod, który wcześniej umieściliśmy w kontrolerze. Jest on wykonywany po napotkaniu dyrektywy [select-enable] (w postaci elementu lub atrybutu) podczas wyświetlania widoku V.
Aby wdrożyć tę dyrektywę, kopiujemy plik [app-17.html] do pliku [app-17B.html] i modyfikujemy go w następujący sposób:
<select data-style="btn-primary" class="selectpicker" select-enable="">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
- wiersz 1: stosujemy dyrektywę [selectEnable] do elementu HTML [select]. Ponieważ nie ma żadnych informacji do przekazania do dyrektywy, wpisujemy po prostu [select-enable=""];
Modyfikujemy również kontroler, duplikując plik JS [rdvmedecins-05.js] do [rdvmedecins-05B.js], a następnie odwołujemy się do nowego pliku JS w pliku dyrektywy [app-17B.html] oraz pliku dyrektywy [selectEnable.js]. Nie wolno zapomnieć o tym ostatnim punkcie. Jeśli plik dyrektywy nie istnieje, atrybut [select-enable=""] nie będzie obsługiwany, ale Angular nie zgłosi żadnego błędu.
<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>
W pliku JS [rdvmedecins-05B.js] usuwamy z kontrolera następujące wiersze:
// stylizowanie listy rozwijanej
$timeout(function(){
$('.selectpicker').selectpicker();
});
ponieważ operacja ta jest teraz wykonywana przez dyrektywę.
3.7.7.8. Testowanie aplikacji – 3
Podczas testowania nowej aplikacji [app-17B.html] uzyskujemy następujący wynik:
![]() |
- w przypadku [1] otrzymujemy pustą listę.
W logach konsoli wyświetla się następująca informacja:
- wiersz 1: inicjalizacja usługi [dao];
- wiersz 2: przy pierwszym wyświetleniu widoku V wykonywana jest dyrektywa [selectEnable];
- wiersz 3: ten wiersz pojawia się, gdy użytkownik kliknie przycisk [Liste des clients]. Można wówczas zauważyć, że dyrektywa [selectEnable] nie jest wykonywana po raz drugi. Ostatecznie została ona wykonana, gdy lista klientów była pusta, w związku z czym otrzymujemy pustą listę rozwijaną;
Innymi słowy, operacja:
$('.selectpicker').selectpicker();
nie została przeprowadzona w odpowiednim momencie. Problem ten można próbować rozwiązać na różne sposoby. Po wielu nieudanych testach okazuje się, że powyższa operacja musi zostać przeprowadzona tylko raz i wyłącznie wtedy, gdy lista rozwijana została wypełniona. Aby uzyskać ten wynik, przepisujemy tag <select> w następujący sposób:
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
W wierszu 1 tag <select> jest generowany tylko wtedy, gdy istnieje [clients.data]. Nie ma to miejsca podczas pierwszego wyświetlenia widoku V. Tag <select> nie zostanie zatem wygenerowany, a dyrektywa [selectEnable] nie zostanie oceniona. Gdy użytkownik kliknie przycisk [Liste des clients], [clients.data] uzyska nową wartość w szablonie M. Ponieważ szablon M uległ zmianie, tag <select> zostanie ponownie oceniony i w tym momencie wygenerowany. Dyrektywa [selectEnable] zostanie zatem również oceniona. W momencie jej oceny wiersze 2–4 tagu <select> nie zostały jeszcze ocenione. Mamy więc pustą listę klientów. Jeśli zapiszemy dyrektywę [selectEnable] w następujący sposób:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$('.selectpicker').selectpicker();
}
}
}]);
wiersz 5 zostanie wykonany z pustą listą, w wyniku czego na ekranie pojawi się pusta lista rozwijana. Należy zatem zapisać:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$timeout(function () {
$('.selectpicker').selectpicker();
})
}
}
}]);
, aby uzyskać oczekiwany wynik. Ze względu na dyrektywę [$timeout] w wierszu 5, wiersz 6 zostanie wykonany dopiero po pełnej ocenie widoku V, a więc w momencie, gdy tag <select> będzie zawierał wszystkie swoje elementy.
3.7.8. Przykład 8: kalendarz lekarza
Przedstawiamy teraz aplikację wyświetlającą kalendarz lekarza.
3.7.8.1. Widok V aplikacji
Przedstawimy następujący formularz:
![]() |
- w [1] wysyłamy zapytanie o kalendarz pani PELISSIER [2] na dzień 25 czerwca 2014 r. [3];
Otrzymujemy następujący wynik: [4]:
![]() |
Przeanalizujemy oba widoki osobno.
3.7.8.2. Formularz
Duplikujemy plik [app-17.html] jako [app-18.html], a następnie modyfikujemy kod w następujący sposób:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- komunikat oczekiwania -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- żądanie -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="row" style="margin-bottom: 20px">
<div class="col-md-3">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
<div class="col-md-3">
<h2 translate="{{calendar.title}}"></h2>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
class="well well-sm"></datepicker>
</div>
</div>
</div>
<button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
</div>
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- kalendarz -->
<div id="agenda" ng-show="agenda.show">
...
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
- wiersze 5–7: komunikat oczekiwania pozostaje bez zmian;
- wiersze 12–19: lista lekarzy typu [bootstrap select];
- wiersze 20–26: kalendarz typu [ui-bootstrap], który już przedstawiliśmy. Należy zauważyć, że wybrany dzień jest umieszczany w szablonie typu [calendar.jour] (atrybut ng-model);
- wiersz 28: przycisk wywołujący kalendarz;
- wiersze 32–34: lista błędów pozostaje bez zmian;
- wiersze 37–39: kalendarz, który przedstawimy w dalszej części;
- wiersz 42: kod JS jest przenoszony do pliku [rdvmedecins-06.js] poprzez skopiowanie pliku [rdvmedecins-05.js];
3.7.8.3. Kontroler C
Kod aplikacji JS przyjmuje następującą postać:

Zmiany będą miały wpływ wyłącznie na usługę [utils] oraz kontroler [rdvMedecinsCtrl].
Kontroler [rdvMedecinsCtrl] przyjmuje następującą postać:
// kontroler
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- inicjalizacja szablonu
// szablon
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', hasło: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins};
$scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
$scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
// stylizujemy listę rozwijaną
$timeout(function () {
$('.selectpicker').selectpicker();
});
// francuska lokalizacja dla kalendarza
angular.copy(config.locales['fr'], $locale);
...
}
])
;
- wiersz 7: ustawia się czas oczekiwania wynoszący 3 sekundy przed wykonaniem wywołania HTTP;
- wiersz 8: na stałe ustalono elementy niezbędne do nawiązania połączenia z HTTP;
- wiersze 10–17: lista lekarzy jest stała;
- wiersz 18: szablon [agenda] konfiguruje wyświetlanie kalendarza w widoku;
- wiersz 19: szablon [calendar] konfiguruje wyświetlanie kalendarza w widoku. Ustalamy datę minimalną [minDate] na dzisiejszy dzień, a datę bieżącą również na dzisiejszy dzień;
- wiersze 21–23: lista rozwijana jest stylizowana przy użyciu metody omówionej wcześniej;
- wiersz 25: ustawiamy lokalizację aplikacji na „fr”. Domyślnie jest to „en”;
Metoda wykonywana podczas wywołania kalendarza jest następująca:
// wykonanie akcji
$scope.execute = function () {
// dane z formularza
var idMedecin = $('.selectpicker').selectpicker('val');
// weryfikacja
utils.debug("[homeCtrl] idMedecin", idMedecin);
utils.debug("[homeCtrl] jour", $scope.calendar.jour);
// ustawianie formatu daty na yyyy-MM-dd
var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
// aktualizacja widoku
$scope.waiting.visible = true;
$scope.errors.show = false;
$scope.agenda.show = false;
...
};
- wiersz 4: pobieramy atrybut [value] wybranego lekarza. Ponownie wykorzystujemy tutaj metodę [selectpicker], która pochodzi z pliku [bootstrap-select.min.js]. Należy pamiętać o formie opcji listy rozwijanej:
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
Wartość (atrybut value) opcji to zatem identyfikator lekarza [id].
- wiersz 11: wpisujemy dzień wybrany przez użytkownika w formacie [aaaa-mm-jj], który jest formatem daty oczekiwanym przez serwer WWW;
- wiersze 13–15: po zakończeniu działania metody [execute] zostanie wyświetlony pasek oczekiwania, a cała reszta zostanie ukryta;
Kod jest kontynuowany w następujący sposób:
// symulowane oczekiwanie
var task = utils.waitForSomeTime($scope.waiting.time);
// zapytanie o harmonogram wizyt lekarza
var promise = task.promise.then(function () {
// ścieżka do pliku URL
var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
// żądanie harmonogramu
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// zwracane jest potwierdzenie zakończenia zadania
return task.promise;
});
// analiza wyniku wywołania usługi [dao]
promise.then(function (result) {
// koniec oczekiwania
$scope.waiting.visible = false;
// błąd?
if (result.err == 0) {
// przygotowuje się szablon kalendarza
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// formatowanie wyświetlania harmonogramów
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
// tworzy się zdarzenie w celu stylizacji tabeli po wyświetleniu widoku
$timeout(function () {
$("#creneaux").footable();
});
} else {
// wystąpiły błędy podczas pobierania kalendarza
$scope.errors = {
title: config.getAgendaErrors,
messages: utils.getErrors(result),
show: true
};
}
- wiersz 2: asynchroniczne zadanie oczekiwania trwające 3 sekundy;
- wiersze 5–10: kod, który zostanie wykonany po zakończeniu oczekiwania;
- wiersz 6: tworzymy URL, które wywołuje [/getAgendaMedecinJour/1/2014-06-25];
- wiersz 8: wywoływana jest funkcja URL. Rozpoczyna się zadanie asynchroniczne;
- wiersz 10: zwracana jest obietnica tego zadania asynchronicznego;
- wiersze 14–38: kod, który zostanie wykonany, gdy wywołanie HTTP zwróci swoją odpowiedź;
- wiersz 13: [result] to odpowiedź wysłana przez metodę [dao.getData]. Należy tutaj pamiętać o formacie odpowiedzi serwera WWW:
![]() |
Parametr [result.data] w wierszu 19 jest atrybutem [data] [1] podanym powyżej. Ten atrybut zawiera z kolei atrybut [creneauxMedecin] [2], o którym mowa powyżej. Jest to tablica przedziałów czasowych, z których każdy zawiera dwie informacje:
- [rv]: forma JSON spotkania lub [null], jeśli w danym przedziale czasowym nie zaplanowano żadnego spotkania;
- [hDeb, mDeb, hFin, mFin]: informacje dotyczące godzin w danym przedziale czasowym;
Wróćmy do kodu kontrolera:
- wiersz 15: oczekiwanie zostało zakończone;
- wiersz 19: wypełniany jest szablon [$scope.agenda], który kontroluje wyświetlanie kalendarza;
- wiersz 20: kalendarz zostaje wyświetlony;
- wiersze 22–24: przeglądamy każdy z elementów C tablicy [creneauxMedecin], o której właśnie mówiliśmy;
- wiersz 23: każdy element C posiada atrybut [creneau], który określa przedział czasowy. Atrybut ten jest uzupełniony o atrybut [text], który będzie tekstową reprezentacją przedziału czasowego w postaci [10h20:10h40];
- wiersze 26–28: nadajemy tabeli HTML, używanej do wyświetlania przedziałów czasowych w kalendarzu, właściwości „responsive”. Pojęcie to omówiono w paragrafie 3.6.7;
![]() |
- wiersz 27: aby tabela była responsywna, należy zastosować do niej metodę [footable]. Mamy tu do czynienia z tą samą trudnością, co w przypadku komponentu [bootstrap-select]. Jeśli po prostu wpiszemy wiersz 17, zauważymy, że tabela nie jest responsywna. Problem ten rozwiązuje się w ten sam sposób za pomocą funkcji [$timeout] (wiersz 26);
- wiersze 31–34: sytuacja, w której wywołanie funkcji HTTP zakończyło się niepowodzeniem. Wyświetlane są wówczas komunikaty o błędach;
3.7.8.4. Wyświetlanie kalendarza
Wracamy teraz do kodu kalendarza w pliku [app-18.html]. Wygląda on następująco:
<!-- kalendarz -->
<div id="agenda" ng-show="agenda.show">
<!-- przypadek lekarza bez dostępnych terminów konsultacji -->
<h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
translate="agenda_medecinsanscreneaux"></h4>
<!-- kalendarz lekarza -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table creneaux-table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span translate="agenda_creneauhoraire"></span>
</th>
<th>
<span translate="agenda_client">Client</span>
</th>
<th data-hide="phone">
<span translate="agenda_action">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
<td>
<span
ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro status-suspended'">
{{creneauMedecin.creneau.text}}
</span>
</td>
<td>
<span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}} {{creneauMedecin.rv.client.nom}}</span>
</td>
<td>
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active">
</a>
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended">
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
- wiersze 4–5: przypominamy, że [agenda.data] to kalendarz, a [agenda.data.creneauxMedecin] to tablica obiektów typu [creneauMedecin]. Każdy element tego ostatniego typu posiada atrybut [creneauMedecin.creneau], który jest przedziałem czasowym. Każdy przedział czasowy zawiera dwa interesujące nas elementy:
- [creneauMedecin.creneau.rv], czyli ewentualny RV (rv!=null) przypisany do przedziału czasowego;
- [creneauMedecin.creneau.text], który jest tekstem [début:fin] dla danego przedziału czasowego;
- wiersz 4: wyświetla specjalny komunikat, jeśli lekarz nie ma dostępnych przedziałów czasowych. Jest to mało prawdopodobne, ale zdarza się, że nasza baza danych jest niekompletna i taki przypadek ma miejsce. Generowanie komunikatu HTML lub jego brak jest kontrolowane przez dyrektywę [ng-if];

Dyrektywa [ng-if] różni się od dyrektyw [ng-show, ng-hide]. Te ostatnie ograniczają się jedynie do ukrycia obszaru występującego w dokumencie. Jeśli zastosowano [ng-if='false'], wówczas obszar ten jest usuwany z dokumentu. Zastosowano ją tutaj w celach ilustracyjnych;
- wiersz 9: atrybut [id='creneaux'] jest istotny. To właśnie on jest używany w instrukcji:
$("#creneaux").footable();
- wiersze 10–22: wyświetlają nagłówki tabeli [1];
- wiersze 23–45: wyświetlają zawartość tabeli [2];
![]() |
- wiersz 24: przeglądamy tablicę [agenda.data.creneauxMedecin];
- wiersze 26–29: zapisujemy tekst [3]. Używamy dyrektywy [ng-class], która wygeneruje atrybut [class] elementu. W tym przypadku, jeśli mamy [creneauMedecin.rv==null], oznacza to, że termin jest wolny i nadajemy tekstowi zielone tło. W przeciwnym razie nadajemy mu czerwone tło;
- wiersz 32: wpisujemy nazwę klienta, dla którego zarezerwowano RV lub [4]. Jeśli jest to [rv==null], informacje te nie istnieją, ale Angular poprawnie obsługuje ten przypadek i nie zgłasza błędu;
- wiersze 34–39: wyświetlają jeden z dwóch przycisków: [Réserver] lub [Supprimer]. To właśnie istnienie lub brak spotkania decyduje o wyborze jednego lub drugiego przycisku;
3.7.8.5. Modyfikacja serwera WWW
Podobnie jak w poprzednich przykładach, serwer WWW należy zmodyfikować tak, aby URL [/getAgendaMedecinJour] wysyłał nagłówki CORS:
![]() |
W klasie [RdvMedecinsCorsController] dodajemy nową metodę:
// kalendarz lekarza
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getAgendaMedecinJour(HttpServletResponse response) {
sendOptions(response);
}
Metoda ta wyśle nagłówki CORS dla żądań HTTP i [OPTIONS]. To samo należy zrobić dla żądania HTTP [GET] w klasie [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
// nagłówki CORS
rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}
3.7.8.6. Wykorzystanie dyrektyw
Podobnie jak poprzednio, przeniesiemy obsługę DOM do dyrektyw. Mamy dwie operacje dotyczące DOM:
- podczas pierwszego wyświetlenia widoku:
// stylizujemy listę rozwijaną
$timeout(function () {
$('.selectpicker').selectpicker();
});
- podczas wyświetlania kalendarza:
// tworzymy zdarzenie w celu stylizacji tabeli po wyświetleniu widoku
$timeout(function () {
$("#creneaux").footable();
});
W pierwszym przypadku wykorzystamy już przedstawioną dyrektywę [selectEnable]. W drugim przypadku tworzymy dyrektywę [footable] w poniższym pliku JS [footable.js]:
angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive footable");
$timeout(function () {
$("#creneaux").footable();
})
}
}
}]);
Stosujemy zatem tę samą technikę, co w przypadku dyrektywy [selectEnable].
Kod HTML [app-18.html] jest zduplikowany w [app-18B.html]. Następnie modyfikujemy go w następujący sposób:
<select data-style="btn-primary" class="selectpicker" select-enable="">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
- wiersz 1: stosuje się dyrektywę [selectEnable] (poprzez atrybut [select-enable]) do tagu <select> lekarzy;
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
<thead>
<tr>
- wiersz 3: stosuje się dyrektywę [footable] (poprzez atrybut [footable]) do tabeli HTML kalendarza;
<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- wytyczne -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- wiersze 3–4: odwołuje się do plików JS obu dyrektyw;
- wiersz 1: kod JS z pliku [app-18B.html] jest kodem JS z pliku [app-18.html], zduplikowanym w pliku [rdvmedecins-06B.js];
Plik [rdvmedecins-06B.js] jest identyczny z plikiem [rdvmedecins-06.js] z wyjątkiem dwóch szczegółów. Znikają wiersze dotyczące pliku DOM:
// stylizowanie listy rozwijanej
$timeout(function () {
$('.selectpicker').selectpicker();
});
// tworzy się zdarzenie w celu dostosowania stylu tabeli po wyświetleniu widoku
$timeout(function () {
$("#creneaux").footable();
});
W związku z tym uruchomienie aplikacji [app-18B.html] daje takie same wyniki jak uruchomienie aplikacji [app-18.html].
3.7.9. Przykład 9: tworzenie i anulowanie rezerwacji
Przedstawiamy teraz aplikację, która umożliwia tworzenie i anulowanie rezerwacji.
3.7.9.1. Widok V aplikacji
Przedstawimy następujący formularz:
![]() |
- w [1] będzie można dokonać rezerwacji. Rezerwacja zostanie dokonana dla losowego klienta;
- w [2] będzie można usunąć dokonane przez nas rezerwacje;
Duplikujemy plik [app-18.html] do pliku [app-19.html], a następnie modyfikujemy kod w następujący sposób:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- komunikat o oczekiwaniu -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- kalendarz -->
<div id="agenda" ng-show="agenda.show">
..
<!-- kalendarz lekarza -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
...
<tbody>
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
...
<td>
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active" ng-click="reserver(creneauMedecin.creneau.id)">
</a>
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-07.js"></script>
<script type="text/javascript" src="footable.js"></script>
- wiersze 5–7: komunikat o oczekiwaniu pochodzi z poprzedniej wersji;
- wiersze 10–12: komunikat o oczekiwaniu jest taki sam jak w poprzedniej wersji;
- wiersze 15–36: kalendarz jest taki sam jak w poprzedniej wersji, z wyjątkiem dwóch szczegółów:
- wiersz 26: kliknięcie przycisku [réserver] (atrybut ng-click) jest obsługiwane przez metodę [reserver] modelu M widoku V. Przekazujemy jej numer przedziału czasowego rezerwacji;
- wiersz 26: kliknięcie przycisku [supprimer] jest obsługiwane przez metodę [reserver] modelu M widoku V. Przekazywany jest do niej numer spotkania, które ma zostać usunięte;
- wiersz 39: kod JS, który obsługuje aplikację, znajduje się w pliku [rdvmedecins-07.js];
- wiersz 40: kod JS z dyrektywy [footable] zastosowanej w wierszu 20;
3.7.9.2. Kontroler C
Kod JS z pliku [rdvmedecins-07.js] jest najpierw uzyskiwany poprzez skopiowanie z pliku [rdvmedecins-06.js]. Następnie jest modyfikowany. Nadal występują tu typowe duże bloki kodu. Modyfikacje dotyczą głównie kontrolera:

Opiszemy kontroler C widoku V w kilku etapach.
3.7.9.3. Inicjalizacja kontrolera C
Kod inicjalizujący kontroler wygląda następująco:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- inicjalizacja szablonu
// szablon
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', hasło: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins
};
var médecin = $scope.medecins.data[0];
var clients = [
{id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
{id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
{id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
{id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
];
// francuski format daty
angular.copy(config.locales['fr'], $locale);
var today = new Date();
var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
var fullDay = $filter('date')(today, 'fullDate');
$scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre: médecin.titre, prenom: médecin.prenom, nom: médecin.nom, jour: fullDay}};
// ---------------------------------------------------------------- początkowy kalendarz
// globalne zadanie asynchroniczne
var task;
// żądanie kalendarza
getAgenda();
// ------------------------------------------------------------------ rezerwacja
$scope.reserver = function (creneauId) {
....
};
// ------------------------------------------------------------ usunięcie RV
$scope.supprimer = function (idRv) {
...
};
// pobieranie kalendarza
function getAgenda() {
...
}
// anulowanie w toku
function cancel() {
...
}
} ]);
- wiersz 6: konfiguracja komunikatu oczekiwania. Domyślnie będziemy czekać 3 sekundy przed wykonaniem wywołania HTTP;
- wiersz 7: informacje niezbędne do wywołań HTTP;
- wiersz 8: konfiguracja komunikatu o błędach;
- wiersze 9–17: stała lista lekarzy;
- wiersz 18: lekarz prywatny. Rezerwacje będą dotyczyły właśnie jego terminów;
- wiersze 19–24: stali klienci;
- linia 26: chcemy wprowadzać daty francuskie;
- wiersz 27: wizyty będą umawiane na dzisiejszą datę;
- wiersz 28: internetowy serwis rezerwacyjny oczekuje dat w formacie „rrrr-mm-dd”;
- wiersz 29: dzisiejsza data w formacie [jeudi 26 juin 2014];
- wiersz 30: konfiguracja kalendarza. Atrybut [model] przenosi parametry zwiastowanej wiadomości, która zostanie wyświetlona:
agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
- wiersz 35: zmienna globalna [task] reprezentuje w danym momencie wykonywane zadanie asynchroniczne;
- wiersz 37: żądany jest kalendarz początkowy;
To wszystko, co dzieje się podczas początkowego ładowania strony. Jeśli wszystko przebiega pomyślnie, widok wyświetla kalendarz na bieżący dzień pani PELISSIER.

3.7.9.4. Pobieranie kalendarza
Kalendarz jest pobierany za pomocą następującej metody [getAgenda]:
// pobranie kalendarza
function getAgenda() {
// ścieżka serwisu URL
var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
// zapytanie o kalendarz
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// komunikat o oczekiwaniu
$scope.waiting.visible = true;
// analiza wyniku wywołania usługi [dao]
task.promise.then(function (result) {
// koniec oczekiwania
$scope.waiting.visible = false;
// błąd?
if (result.err == 0) {
// przygotowywanie szablonu kalendarza
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// formatowanie wyświetlania harmonogramów
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
} else {
// wystąpiły błędy podczas pobierania kalendarza
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
}
});
}
Jest to ten sam kod, który analizowaliśmy w poprzedniej aplikacji. Wprowadzono dwie zmiany:
- nie ma symulowanego oczekiwania przed wywołaniem HTTP;
- wiersz 4: wykorzystuje się lekarza utworzonego podczas inicjalizacji kontrolera oraz sformatowaną datę, która została skonstruowana;
Kod ten został wyodrębniony do funkcji, ponieważ jest on również wykorzystywany przez funkcje [reserver] i [supprimer].
3.7.9.5. Rezerwacja terminu
![]() | ![]() |
Należy pamiętać, że klienci są wybierani losowo.
Kod rezerwacji jest następujący:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// tworzy się RV z losowym klientem w przedziale czasowym oznaczonym przez [id]
var idClient = clients[Math.floor(Math.random() * clients.length)].id;
utils.debug("réservation du créneau pour le client", idClient);
// symulowane oczekiwanie
$scope.waiting.visible = true;
var task = utils.waitForSomeTime($scope.waiting.time);
// dodano przedział czasowy
var promise = task.promise.then(function () {
// ścieżka do pliku URL usługi
var path = config.urlSvrResaAdd;
// dane do przesłania do usługi
var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
// uruchamianie zadania asynchronicznego
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// zwracamy obietnicę zakończenia zadania
return task.promise;
});
// analiza wyniku zadania
promise = promise.then(function (result) {
if (result.err != 0) {
// wystąpiły błędy podczas weryfikacji rv
$scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
} else {
// żądanie nowego harmonogramu
getAgenda();
}
});
};
- wiersz 1: przypominamy, że parametrem funkcji [reserver] jest numer przedziału czasowego (atrybut id);
- wiersz 4: klient jest wybierany losowo z listy klientów zdefiniowanej na stałe w kodzie inicjalizacyjnym. Zapisujemy jego identyfikator [id];
- wiersze 7–8: 3-sekundowe oczekiwanie;
- wiersze 11–18: te wiersze są wykonywane dopiero po upływie 3 sekund;
- wiersz 12: identyfikator URL usługi rezerwacyjnej [/ajouterRv]. Ten identyfikator URL różni się od tych, z którymi mieliśmy do tej pory do czynienia. W usłudze internetowej jest on zdefiniowany w następujący sposób:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
- (ciąg dalszy)
- wiersz 1: URL nie ma parametrów i jest wywoływany wraz z POST;
- wiersz 2: parametry są przesyłane w postaci obiektu JSON. Obiekt ten zostanie deserializowany do parametru [post] (@RequestBody);
Przykład takiego obiektu POST omówiono w punkcie 2.12.2:
![]() |
- w [0], URL z serwisu internetowego;
- w [1] zastosowano metodę POST;
- w [2] tekst JSON informacji przekazanych do serwisu internetowego w postaci {dzień, idClient, idCreneau};
- w [3] klient informuje serwis internetowy, że przesyła mu informacje JSON;
Wróćmy do kodu JS funkcji [reserver]:
- wiersz 14: tworzymy wartość do wysłania w postaci obiektu JS. Angular zserializuje ją do postaci JSON podczas wysyłania;
- wiersz 16: wykonywane jest wywołanie funkcji HTTP. Wartość do wysłania jest ostatnim parametrem funkcji [dao.getData]. Gdy ten parametr jest obecny, funkcja [dao.getData] generuje POST zamiast GET (zobacz kod w paragrafie 3.7.6.4);
- wiersz 18: zwracana jest obietnica wywołania funkcji HTTP;
- wiersze 23–29: są wykonywane dopiero po zwróceniu odpowiedzi przez wywołanie HTTP;
- wiersz 23: parametr [result] ma postać [err,data] lub [err,messages], gdzie [err] jest kodem błędu;
- wiersze 23–26: jeśli wystąpiły błędy, wyświetla się komunikat o błędzie;
- wiersz 28: jeśli rezerwacja przebiegła pomyślnie, ponownie wyświetla się nowy kalendarz;
3.7.9.6. Modyfikacja serwera
![]() |
W klasie [RdvMedecinsCorsController] dodajemy następującą metodę:
// wysyłanie opcji do klienta
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// ustalono nagłówek CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// autoryzowano nagłówek [authorization]
response.addHeader("Access-Control-Allow-Headers", "authorization");
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
public void ajouterRv(HttpServletResponse response) {
sendOptions(response);
}
Dodanie nastąpiło w wierszach 10–13. Nagłówki w wierszach 2–8 zostaną wysłane dla klasy URL [/ajouterRv] (wiersz 10) oraz metody HTTP [OPTIONS] (wiersz 10).
Czy klasa [RdvMedecinsController] została zmodyfikowana w następujący sposób:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
// nagłówki CORS
rdvMedecinsCorsController.ajouterRv(response);
...
W przypadku metody [POST] (wiersz 1) oraz metod URL i [/ajouterRv] (wiersz 1) wywoływana jest metoda, którą właśnie dodaliśmy w [RdvMedecinsCorsController] (wiersz 4), zwracając tym samym te same nagłówki HTTP, co w przypadku metod HTTP i [OPTIONS].
3.7.9.7. Tests
Przeprowadźmy pierwszy test, w którym rezerwujemy dowolny termin:
![]() |
Jak zawsze w takich przypadkach, należy sprawdzić logi konsoli:
[dao] getData[/ajouterRv] error réponse : {"data":"","status":0,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/ajouterRv","data":{"jour":"2014-06-30","idCreneau":1,"idClient":4},"headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4=","Content-Type":"application/json;charset=utf-8"}},"statusText":""}
Metoda [dao.getData] zakończyła się niepowodzeniem z kodem [status=0], co oznacza, że to Angular anulował żądanie. Przyczynę błędu można znaleźć w logach:
XMLHttpRequest cannot load http://localhost:8080/ajouterRv. Pole nagłówka żądania Content-Type nie jest dozwolone przez Access-Control-Allow-Headers.
Jeśli przyjrzymy się komunikacji sieciowej, widzimy następujące dane:
![]() |
- w [1] i [2]: wystąpiło tylko jedno żądanie HTTP, żądanie [OPTIONS];
- w [3] klient Angular żąda dwóch uprawnień:
- uprawnienie do wysłania nagłówków HTTP i [accept, authorization, content-type];
- na wysłanie polecenia POST;
- w przypadku [4]: serwer autoryzuje nagłówek [authorization]. Przypomnijmy, że po stronie serwera to my sami wysyłamy tę autoryzację;
Nowością jest zatem to, że podczas operacji POST klient Angular żąda od serwera dodatkowych uprawnień. Należy zatem zmodyfikować serwer, aby je mu przyznał:
![]() |
W klasie [RdvMedecinsCorsController] modyfikujemy metodę prywatną, która generuje nagłówki HTTP wysyłane dla poleceń OPTIONS, GET oraz POST:
// wysyłanie opcji do klienta
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// ustawiamy nagłówek CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// zezwalamy na niektóre nagłówki
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// zezwalamy na POST
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
- wiersz 7: dodano uprawnienie dla nagłówków HTTP i [accept, content-type];
- wiersz 9: dodano uprawnienie dla metody POST;
Po ponownym uruchomieniu serwera przeprowadzamy test ponownie:
![]() |
Tym razem rezerwacja się powiodła.
3.7.9.8. Usunięcie terminu
![]() | ![]() |
Kod funkcji [supprimer] jest następujący:
$scope.supprimer = function (idRv) {
utils.debug("suppression rv n°", idRv);
// symulowane oczekiwanie
$scope.waiting.visible = true;
task = utils.waitForSomeTime($scope.waiting.time);
// dodaje się przedział czasowy
var promise = task.promise.then(function () {
// ścieżka do serwisu URL
var path = config.urlSvrResaRemove;
// dane do przesłania do usługi
var post = {idRv: idRv};
// uruchamianie zadania asynchronicznego
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// zwracamy obietnicę zakończenia zadania
return task.promise;
});
// analiza wyniku zadania
promise = promise.then(function (result) {
if (result.err != 0) {
// wystąpiły błędy podczas usuwania rv
$scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
// aktualizowanie pliku UI
$scope.waiting.visible = false;
} else {
// żądany jest nowy kalendarz
getAgenda();
}
});
};
- wiersz 1: należy pamiętać, że parametrem funkcji jest numer spotkania, które ma zostać usunięte. Mamy tu kod bardzo podobny do tego z rezerwacji. Omówimy tylko różnice;
- wiersz 9: funkcja URL serwisu ma tutaj postać [/supprimerRV], a dostęp do niej uzyskuje się również poprzez POST:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
Parametr wysłany jest tutaj ponownie przekazywany w postaci JSON. W punkcie 2.12.17 pokazaliśmy charakter ręcznie utworzonego kodu POST:
![]() |
- w [1], URL z serwisu internetowego;
- na [2], zastosowano metodę POST;
- w [3] tekst JSON informacji przekazanych do serwisu internetowego w postaci {idRv};
- w [4] klient informuje serwis internetowy, że przesyła mu informacje JSON;
Wróćmy do kodu JS funkcji [supprimer]:
- wiersz 11: tworzymy obiekt wysyłany. Angular automatycznie zserializuje go do postaci JSON;
Pozostała część kodu jest analogiczna do kodu rezerwacji.
3.7.9.9. Modyfikacje po stronie serwera
Po stronie serwera wprowadzamy następujące zmiany:
![]() |
W klasie [RdvMedecinsCorsController] dodajemy następującą metodę:
// wysyłanie opcji do klienta
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// ustawiono nagłówek CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// zezwala się na niektóre nagłówki
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// zezwala się na POST
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
...
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(HttpServletResponse response) {
sendOptions(response);
}
Dodajemy ją w wierszach 13–16. Nagłówki w wierszach 2–10 zostaną wysłane do metod URL i [/supprimerRv] (wiersz 13) oraz HTTP i [OPTIONS] (wiersz 13).
Klasa [RdvMedecinsController] została natomiast zmodyfikowana w następujący sposób:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
// nagłówki CORS
rdvMedecinsCorsController.supprimerRv(response);
...
W przypadku metody [POST] (wiersz 1) oraz metod URL i [/supprimerRv] (wiersz 1) wywoływana jest metoda, którą właśnie dodaliśmy w [RdvMedecinsCorsController] (wiersz 4), zwracając tym samym te same nagłówki HTTP, co w przypadku metod HTTP i [OPTIONS].
3.7.10. Przykład 10: tworzenie i anulowanie rezerwacji – 2
Przedstawiamy teraz tę samą aplikację, co poprzednio, ale zamiast dokonywać rezerwacji dla losowego klienta, klient ten zostanie wybrany z listy rozwijanej.
3.7.10.1. Widok V aplikacji
Przedstawimy następujący formularz:
![]() |
Klientów będzie można wybierać w [1].
Kod jest podobny do tego z poprzedniej aplikacji, dlatego przedstawimy tylko główne różnice.
Duplikujemy plik [app-19.html] do pliku [app-20.html], a następnie tworzymy kod listy rozwijanej klientów [1]:
<!-- lista klientów -->
<div class="alert alert-info">
<h3>{{agenda.title|translate:agenda.model}}</h3>
<div class="row" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
- wiersze 8–12: lista rozwijana zostanie zaimplementowana za pomocą komponentu [bootstrap-select];
- wiersz 1: dyrektywa [selectEnable] jest stosowana za pomocą atrybutu [select-enable];
- wiersz 1: tag <select> jest generowany tylko wtedy, gdy istnieje [clients.data] (# null, undefined). Jest to ważna kwestia, wyjaśniona w paragrafie 3.7.7.8;
Ponadto importujemy nowe pliki JS:
<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- wytyczne -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- wiersz 1: plik [rdvmedecins-08.js] powstaje poprzez skopiowanie pliku [rdvmedecins-0.js];
- wiersze 3–4: importujemy pliki z obu dyrektyw;
3.7.10.2. Kontroler C
Kod kontrolera C zmienia się w następujący sposób:
// kontroler
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- inicjalizacja szablonu
...
// klienci
$scope.clients = {title: config.listClients, show: false, model: {}};
//------------------------------------------- inicjalizacja widoku
// globalne zadanie asynchroniczne
var task;
// pobieramy klientów, a następnie kalendarz
getClients().then(function () {
getAgenda();
});
...
// wykonanie akcji
function getClients() {
....
};
} ]);
- wiersz 8: obiekt [$scope.clients] konfiguruje listę rozwijaną klientów w widoku V;
- wiersze 14–16: w trybie asynchronicznym najpierw pobierana jest lista klientów, a po jej uzyskaniu – kalendarz pani PELISSIER na dzisiejszy dzień. Zastosowana tutaj składnia działa tylko dlatego, że funkcja [getClients] zwraca obietnicę (promise);
Metoda [getClients] pobiera listę klientów:
function getClients() {
// aktualizujemy UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// pobieranie listy klientów;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analizujemy wynik poprzedniego wywołania
promise = promise.then(function (result) {
// wynik={err: 0, dane: [client1, client2, ...]}
// wynik={err: n, komunikaty: [msg1, msg2, ...]}
if (result.err == 0) {
// wprowadzamy zebrane dane do modelu
$scope.clients.data = result.data;
// aktualizuje się UI
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// wystąpiły błędy podczas pobierania listy klientów
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// aktualizuje się plik UI
$scope.waiting.visible = false;
}
});
// spełniono obietnicę
return promise;
};
Jest to kod, z którym już się spotkaliśmy i który omówiliśmy. Ważnym elementem, na który należy zwrócić uwagę, jest wiersz 31:
- wiersz 27: zwracamy obietnicę z wiersza 10, czyli ostatnią obietnicę uzyskaną w kodzie. Obietnica ta zostanie uzyskana dopiero wtedy, gdy wywołanie HTTP zwróci swoją odpowiedź;
Metoda [reserver] ulega niewielkiej zmianie:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// tworzy się plik RV dla wybranego klienta
var idClient = $(".selectpicker").selectpicker('val');
...
});
- wiersz 4: nie rezerwujemy już miejsca dla losowego klienta, lecz dla klienta wybranego z listy klientów.
3.7.11. Przykład 11: dyrektywa [selectEnable2]
Ten przykład dotyczy dyrektyw.
3.7.11.1. Widok V
Aplikacja wyświetla następujący widok:
![]() |
3.7.11.2. Kod HTML widoku
Kod HTML widoku [app-21.html] wygląda następująco:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- komunikat o oczekiwaniu -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- lista klientów -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- lista lekarzy -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-09.js"></script>
<!-- wytyczne -->
<script type="text/javascript" src="selectEnable2.js"></script>
- wiersze 19–23: lista rozwijana klientów;
- wiersz 19: stosuje się dyrektywę [selectEnable2] (atrybut [select-enable2]);
- wiersz 19: tylko wtedy, gdy [clients.data] nie jest puste;
- wiersz 19: lista rozwijana jest identyfikowana przez atrybut [id="selectpickerClients"];
- wiersze 33–37: lista rozwijana lekarzy;
- wiersz 33: stosuje się dyrektywę [selectEnable2] (atrybut [select-enable2]);
- wiersz 33: tylko wtedy, gdy [medecins.data] nie jest puste;
- wiersz 33: lista rozwijana jest identyfikowana przez atrybut [id="selectpickerMedecins"];
- wiersz 43: importuje się nowy plik JS [rdvmedecins-09.js];
- wiersz 45: importuje się plik JS z nowej dyrektywy;
3.7.11.3. Dyrektywa [selectEnable2]
Kod dyrektywy [selectEnable2] jest następujący:
angular.module("rdvmedecins").directive('selectEnable2', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable2 attrs", attrs);
$timeout(function () {
$('#' + attrs['id']).selectpicker();
})
}
}
}]);
- wiersz 4: wyświetla się wartość parametru [attrs], aby wyjaśnić działanie kodu. Okazuje się, że attrs['id']='selectpickerClients' dla listy klientów;
- wiersz 6: aby zlokalizować w DOM element z [id='x'], wpisujemy [$('#x')]. Aby więc zlokalizować listę klientów, należy wpisać [$('#selectpickerClients')]. Osiąga się to za pomocą składni [$('#' + attrs['id'])];
Dyrektywa [selectEnable2] wykorzystuje zatem informacje zawarte w jednym z atrybutów elementu HTML, do którego jest zastosowana.
3.7.11.4. Kontroler C
Kontroler C znajduje się w pliku JS [rdvmedecins-09.js] i ma następującą strukturę:
// kontroler
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- inicjalizacja szablonu
// komunikat oczekiwania
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// dane logowania
$scope.server = {url: 'http://localhost:8080', login: 'admin', hasło: 'admin'};
// błędy
$scope.errors = {show: false, model: {}};
// lekarze
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// klienci
$scope.clients = {title: config.listClients, show: false, model: {}};
// globalne zadanie asynchroniczne
var task;
// ---------------------------------------------------- inicjalizacja widoku
// aktualizujemy UI
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.medecins.show = false;
$scope.errors.show = false;
// pobieramy klientów, a następnie lekarzy
getClients().then(function () {
getMedecins();
});
// lista klientów
function getClients() {
...
}
// lista lekarzy
function getMedecins() {
...
}
// anulowanie oczekujących
function cancel() {
...
}
} ]);
- wiersze 26–28: najpierw pobierane są dane klientów, a następnie lekarzy;
3.7.11.5. Testy
Przetestuj tę nową wersję.
3.7.12. Przykład 12: dyrektywa [list]
Wracamy do tego samego przykładu, co poprzednio, ale chcemy uprościć kod HTML, wykorzystując dyrektywę. Obecnie mamy bowiem następujący kod HTML:
<!-- lista klientów -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- lista lekarzy -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
Wiersze 14–26 są identyczne jak wiersze 1–13. Dotyczą one lekarzy zamiast klientów. Chcielibyśmy móc zapisać to w następujący sposób:
<!-- lista klientów -->
<list model="clients" ng-if="clients.show"></list>
<!-- lista lekarzy -->
<list model="medecins" ng-if="medecins.show"></list>
Kod ten wymaga zastosowania nowej dyrektywy [list], którą teraz utworzymy.
3.7.12.1. Dyrektywa [list]
Dyrektywa [list] znajduje się w pliku JS [list.js]. Jej kod wygląda następująco:
angular.module("rdvmedecins")
.directive("list", ['utils', '$timeout', function (utils, $timeout) {
// instancja zwróconej dyrektywy
return {
// element HTML
restrict: "E",
// adres URL fragmentu
templateUrl: "list.html",
// zakres unikalny dla każdej instancji dyrektywy
scope: true,
// funkcja łącząca z dokumentem
link: function (scope, element, attrs) {
utils.debug("directive list attrs", attrs);
scope.model = scope[attrs['model']];
utils.debug("directive list model", scope.model);
$timeout(function () {
$('#' + scope.model.id).selectpicker();
})
}
}
}]);
- wiersz 2: definiuje dyrektywę o nazwie „list”;
- wiersz 6: atrybut [restrict] określa sposoby wykorzystania dyrektywy. [restrict: "E"] oznacza, że dyrektywa [list] może być używana jako element HTML <list ...>...</list>. [restrict: "A"] oznacza, że dyrektywa [list] może być używana jako atrybut, na przykład <div ... list='...'>. [restrict: "AE"] oznacza, że dyrektywa [list] może być używana zarówno jako atrybut, jak i jako element;
- wiersz 8: atrybut [templateUrl] określa nazwę fragmentu HTML, który ma zostać użyty przy napotkaniu znacznika. Fragment ten będzie treścią znacznika;
- wiersz 10: atrybut [scope] określa zakres szablonu dyrektywy. [scope: true] oznacza, że dwa elementy typu <list> będą miały każdy swój własny szablon. Domyślnie (zakres niezinicjalizowany) dzielą one te same szablony;
- wiersz 12: funkcja [link], z której korzystaliśmy już kilkakrotnie;
Aby zrozumieć powyższy kod, należy pamiętać o przeznaczeniu tej dyrektywy:
<!-- lista klientów -->
<list model="clients" ng-if="clients.show"></list>
<!-- lista lekarzy -->
<list model="medecins" ng-if="medecins.show"></list>
Dyrektywa [list] jest wykorzystywana jako element HTML <list>. Element ten posiada dwa atrybuty:
- [model]: którego wartością będzie element modelu M widoku V, w którym znajduje się dyrektywa [list]. Element ten będzie zasilał model dyrektywy;
- [ng-if]: który sprawi, że kod HTML dyrektywy nie zostanie wygenerowany, jeśli nie ma nic do wyświetlenia;
Wróćmy do kodu funkcji [link] dyrektywy:
link: function (scope, element, attrs) {
utils.debug("directive list attrs", attrs);
scope.model = scope[attrs['model']];
utils.debug("directive list model", scope.model);
$timeout(function () {
$('#' + scope.model.id).selectpicker();
})
}
Połączmy ten kod JS z kodem HTML, który wykorzystuje tę dyrektywę:
<list model="clients" ng-if="clients.show"></list>
- wiersz 3: attrs['model'] ma tutaj wartość „clients”;
- wiersz 3: scope[attrs['model']] ma wartość scope['clients'] i reprezentuje zatem [$scope.clients], czyli pole [clients] w modelu widoku. Pole to będzie miało wartość {id: '...', data: [client1, client2, ...], show: ..., title: '...'};
- wiersz 3: dodajemy pole [model] do modelu dyrektywy. Dyrektywa ta odziedziczyła model widoku, w którym się znajduje. Należy zatem uniknąć kolizji z ewentualnym polem [model], które widok mógłby również posiadać. W tym przypadku nie dojdzie do kolizji;
- wiersz 4: wyświetlamy [scope.model], aby lepiej zrozumieć kod;
- wiersze 5–7: ponownie pojawia się kod, z którym już się spotkaliśmy. Różnica polega na tym, że wcześniej identyfikator id komponentu był umieszczony w atrybucie attrs['id']. Tutaj zostanie umieszczony w [scope.model.id];
Teraz przyjrzyjmy się kodowi HTML wygenerowanemu przez dyrektywę. Ze względu na atrybut [templateUrl: "list.html"] dyrektywy należy go odszukać w pliku [list.html]:
<!-- lista klientów lub lekarzy -->
<div class="alert alert-info" ng-show="model.show">
<div class="row">
<div class="col-md-4">
<h2 translate="{{model.title}}"></h2>
<select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
<option ng-repeat="element in model.data" value="{{element.id}}">
{{element.titre}} {{element.prenom}} {{element.nom}}
</option>
</select>
</div>
</div>
</div>
- Pierwszą rzeczą, o której należy pamiętać podczas analizy tego kodu, jest to, że dyrektywa utworzyła obiekt [scope.model] o postaci [{id :'...', data:[client1, client2, ...], show : ..., title :'...'}]. Ten obiekt [model] (scope jest domyślnie zawarty w kodzie HTML) jest wykorzystywany przez kod HTML zawarty w dyrektywie;
- wiersz 2: użycie [model.show] do wyświetlenia/ukrycia widoku wygenerowanego przez dyrektywę;
- wiersz 5: użycie kodu [model.title] w celu nadania tytułu;
- wiersz 6: użycie [model.id] w celu nadania identyfikatora tagowi <select>. Ten identyfikator id jest wykorzystywany przez kod JS z dyrektywy;
- wiersz 6: użycie [model.data] do wygenerowania elementu <select> tylko wtedy, gdy istnieją dane do wyświetlenia;
- wiersze 7–9: użycie kodu [model.data] do wygenerowania elementów listy rozwijanej;
3.7.12.2. Kod HTML
Kod HTML aplikacji [app-22.html] wygląda następująco:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- komunikat oczekiwania -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- lista klientów -->
<list model="clients" ng-if="clients.show"></list>
<!-- lista lekarzy -->
<list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- wytyczne -->
<script type="text/javascript" src="list.js"></script>
- wiersz 22: nie należy zapomnieć o uwzględnieniu kodu JS z dyrektywy;
3.7.12.3. Kontroler C
Kontroler C zmienia się w bardzo niewielkim stopniu:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- inicjalizacja szablonu
...
// lekarze
$scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
// klienci
$scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
- w wierszach 7 i 9 dodajemy atrybut [id] do szablonów lekarzy i klientów;
3.7.12.4. Testy
Testy dają takie same wyniki jak w poprzednim przykładzie.
3.7.13. Przykład 13: aktualizacja szablonu za pomocą dyrektywy
Pozostajemy przy analizie dyrektyw i korzystamy z przykładu listy rozwijanej. Chcemy tutaj zbadać zachowanie dyrektywy [list] w przypadku zmiany zawartości listy rozwijanej.
3.7.13.1. Widoki V
Dostępne są następujące widoki:
![]() |
- w [1] po raz pierwszy wywołujemy listę klientów;
![]() |
- w widoku [2] po raz drugi żąda się listy klientów. Ta druga lista jest następnie sumowana z pierwszą listą [3]. W tym przykładzie chcemy przeanalizować aktualizację komponentu [Bootstrap select].
3.7.13.2. Strona HTML
Strona HTML [app-23.html] powstaje poprzez skopiowanie strony [app-22.html], a następnie jest modyfikowana w następujący sposób:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- komunikat o oczekiwaniu -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- przycisk -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- lista klientów -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- dyrektywy -->
<script type="text/javascript" src="list2.js"></script>
Zmiany w stosunku do poprzedniej wersji aplikacji są następujące:
- wiersze 15–17: dodano przycisk;
- wiersz 20: zastosowanie nowej dyrektywy [list2];
- wiersz 23: użycie nowego pliku JS;
- wiersz 25: import pliku JS z dyrektywy [list2];
3.7.13.3. Dyrektywa [list2]
Dyrektywa [list2] w pliku [list2.js] ma następującą treść:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instancja zwróconej dyrektywy
return {
// element HTML
restrict: "E",
// adres URL fragmentu
templateUrl: "list.html",
// zakres unikalny dla każdej instancji dyrektywy
scope: true,
// funkcja powiązania z dokumentem
link: function (scope, element, attrs) {
utils.debug('directive list2');
scope.model = scope[attrs['model']];
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
}
}
}]);
Jedyna różnica w stosunku do dyrektywy [list] dotyczy wiersza 16: za pomocą metody [selectpicker('refresh')] żądamy od komponentu [Bootstrap-select] odświeżenia się. Chodzi o to, że za każdym razem, gdy użytkownik zażąda nowej listy klientów, lista rozwijana zostanie odświeżona. To nie zadziała, ale taka jest podstawowa idea.
3.7.13.4. Kontroler C
Kontroler znajduje się w pliku [rdvmedecins-11.js], utworzonym poprzez skopiowanie pliku [rdvmedecins-10.js]:
// klienci
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
// lista klientów
$scope.getClients = function getClients() {
// aktualizujemy UI
$scope.waiting.visible = true;
$scope.errors.show = false;
// żądamy listy klientów;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analizujemy wynik poprzedniego wywołania
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// wynik={err: n, komunikaty: [msg1, msg2, ...]}
if (result.err == 0) {
// umieszczamy pobrane dane w nowym szablonie, aby wymusić odświeżenie widoku
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
// aktualizujemy UI
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// wystąpiły błędy podczas pobierania listy klientów
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// aktualizujemy UI
$scope.waiting.visible = false;
}
});
}
- wiersz 1: aby umożliwić łączenie tablic w [clients.data], obiekt ten jest inicjowany pustą tablicą;
- wiersz 18: łączymy nową listę klientów z listami już znajdującymi się w tablicy [clients.data];
Wcześniej napisaliśmy:
Teraz piszemy:
Aby zrozumieć ten kod, należy przypomnieć sobie, w jaki sposób szablon M jest wykorzystywany w widoku V w przypadku dyrektywy [list2]:
<!-- lista klientów -->
<list2 model="clients" ng-if="clients.show"></list2>
Szablonem używanym przez dyrektywę [list2] jest [clients]. Zostanie ona ponownie oceniona w widoku V tylko wtedy, gdy zmieni się [clients] w szablonie M tego widoku. Pierwszym pomysłem na modyfikację jest wpisanie:
aby uwzględnić fakt, że nowa lista klientów musi zostać dodana do poprzednich. W ten sposób modyfikujemy [clients.data], ale nie [clients]. Nie znam tajników języka JavaScript, ale nie byłoby zaskoczeniem, gdyby [clients] był wskaźnikiem, podobnie jak [clients.data]. Wskaźnik [clients] nie zmienia się po zmianie wskaźnika [clients.data]. Dyrektywa [list2] nie jest zatem ponownie oceniana. Tak właśnie wygląda sytuacja podczas debugowania aplikacji (F12 w przeglądarce Chrome).
Wpisując:
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
upewniamy się, że [$scope.clients] rzeczywiście otrzymuje nową wartość. Wskaźnik [$scope.clients] wskazuje na nowy obiekt. Dyrektywa [list2] powinna zatem zostać ponownie oceniona. Jednak nie uzyskujemy pożądanego wyniku. Przyjrzyjmy się zrzutom ekranu po dwukrotnym wywołaniu listy klientów:
![]() |
- w [1] mamy tylko cztery elementy zamiast ośmiu;
- w [2] te cztery elementy znajdują się w [select], ale ten ostatni jest ukryty (style='display: none');
![]() |
- w [3] te cztery elementy znajdują się w innej strukturze HTML i to właśnie tę strukturę widzi użytkownik po kliknięciu listy rozwijanej;
Wreszcie logi konsoli zawierają następujące informacje:
- wiersz 1: uruchomiono instancję usługi [dao];
- wiersz 2: usługa [dao] pobiera pierwszą listę klientów;
- wiersz 3: wykonywana jest dyrektywa [list2];
- wiersz 4: usługa [dao] pobiera drugą listę klientów;
Wyświetlenie wiersza 2 wynika z następującego kodu w dyrektywie:
link: function (scope, element, attrs) {
utils.debug('directive list2');
...
}
Przyjrzyjmy się cyklowi życia dyrektywy [list2]:
- między wierszami 1 i 2 nie jest ona aktywna, mimo że widok został wyświetlony po raz pierwszy. Wynika to z jej atrybutu [ng-if="clients.show"] w widoku V:
<list2 model="clients" ng-if="clients.show"></list2>
- wiersz 3: po uzyskaniu pierwszej listy lekarzy wartość [clients.show] zmienia się na true i dyrektywa zostaje aktywowana;
- po uzyskaniu drugiej listy klientów widać, że kod dyrektywy [list2] nie jest wywoływany. Dlatego nie wyświetla się druga lista;
Aby rozwiązać ten problem, modyfikujemy dyrektywę [list2] w następujący sposób:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instancja zwróconej dyrektywy
return {
// element HTML
restrict: "E",
// adres URL fragmentu
templateUrl: "list.html",
// zakres unikalny dla każdej instancji dyrektywy
scope: true,
// funkcja powiązania z dokumentem
link: function (scope, element, attrs) {
// za każdym razem, gdy zmienia się attrs["model"], musi również zmienić się szablon dyrektywy
scope.$watch(attrs["model"], function (newValue) {
utils.debug("directive list2 newValue", newValue);
// aktualizujemy szablon dyrektywy
scope.model = newValue;
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
});
}
}
}]);
- wiersz 14: funkcja [scope.$watch] pozwala obserwować wartość modelu. Jej składnia to [scope.$watch('var'), f], gdzie [var] jest identyfikatorem zmiennej modelu, a f – funkcją, która ma zostać wykonana, gdy zmienna ta zmieni swoją wartość. W tym przypadku chcemy monitorować zmienną [clients]. Należy więc wpisać [scope.$watch('clients')]. Ponieważ mamy attrs['model']='clients', wpisujemy [scope.$watch(attrs["model"], function (newValue)] ;
- wiersz 14: drugim parametrem funkcji [scope.$watch] jest funkcja, która ma zostać wykonana, gdy obserwowana zmienna zmieni swoją wartość. Parametr [newValue] to nowa wartość zmiennej, a więc w naszym przypadku nowa wartość zmiennej [clients] z modelu;
- wiersz 17: ta nowa wartość jest przypisywana do pola [model] w modelu dyrektywy;
Po wprowadzeniu tej zmiany logi ulegają zmianie:
![]() |
Powyżej widać, że po uzyskaniu drugiej listy klientów dyrektywa [list2] została ponownie wykonana, co potwierdza wynik [2].
3.7.14. Przykład 14: dyrektywy [waiting] i [errors]
Wróćmy do kodu HTML z poprzedniej aplikacji:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- komunikat o oczekiwaniu -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- lista błędów -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- przycisk -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- lista klientów -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
- wiersze 5–7: komunikat oczekiwania;
- wiersze 10–12: komunikat o błędzie;
Postanawiamy umieścić kody HTML tych dwóch komunikatów w dyrektywach.
3.7.14.1. Nowy kod HTML
Nowy kod HTML [app-24.html] ma następującą postać:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- komunikat o oczekiwaniu -->
<waiting model="waiting"></waiting>
<!-- lista błędów -->
<errors model="errors"></errors>
<!-- przycisk -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- lista klientów -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- dyrektywy -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
- wiersz 5: dyrektywa dotycząca komunikatu oczekiwania;
- wiersz 8: dyrektywa dotycząca komunikatu o błędach;
- wiersz 19: nowy plik JS powiązany z aplikacją;
- wiersze 21–23: pliki JS dla trzech dyrektyw;
3.7.14.2. Dyrektywa [waiting]
Kod JS dyrektywy [waiting] znajduje się w następującym pliku [waiting.js]:
angular.module("rdvmedecins")
.directive("waiting", ['utils', function (utils) {
// instancja zwróconej dyrektywy
return {
// element HTML
restrict: "E",
// adres URL fragmentu
templateUrl: "waiting.html",
// zakres unikalny dla każdej instancji dyrektywy
scope: true,
// funkcja powiązania z dokumentem
link: function (scope, element, attrs) {
// za każdym razem, gdy zmienia się atrybut ["model"], szablon strony również musi ulec zmianie
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[waiting] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Kod ten opiera się na tej samej logice, co kod dyrektywy [list2], którą już omówiliśmy.
W wierszu 8 znajduje się odwołanie do następującego pliku [waiting.html]:
<div class="alert alert-warning" ng-show="model.show">
<h1>{{ model.title.text | translate:model.title.values}}
<button class="btn btn-primary pull-right" ng-click="model.cancel()">{{'cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
W kodzie JS aplikacji szablon [$scope.waiting] tego kodu HTML zostanie zdefiniowany w następujący sposób:
// komunikat oczekiwania
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};
3.7.14.3. Dyrektywa [errors]
Kod JS z dyrektywy [errors] znajduje się w następującym pliku [errors.js]:
angular.module("rdvmedecins")
.directive("errors", ['utils', function (utils) {
// instancja zwróconej dyrektywy
return {
// element HTML
restrict: "E",
// adres URL fragmentu
templateUrl: "errors.html",
// zakres unikalny dla każdej instancji dyrektywy
scope: true,
// funkcja powiązania z dokumentem
link: function (scope, element, attrs) {
// za każdym razem, gdy zmienia się atrybut ["model"], szablon strony również musi ulec zmianie
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[errors] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Kod ten opiera się na tej samej logice, co kod dyrektywy [list2], który został już omówiony.
W wierszu 8 znajduje się odwołanie do następującego pliku [errors.html]:
<div class="alert alert-danger" ng-show="model.show">
{{model.title.text|translate:model.title.values}}
<ul>
<li ng-repeat="message in model.messages">{{message|translate}}</li>
</ul>
</div>
W kodzie JS aplikacji szablon [$scope.errors] tego kodu HTML zostanie zdefiniowany w następujący sposób:
// wystąpiły błędy podczas pobierania listy klientów
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};
3.7.15. Przykład 15: nawigacja
Do tej pory korzystaliśmy z aplikacji jednostronicowych. W tym przykładzie zajmiemy się aplikacjami wielostronicowymi oraz nawigacją między nimi.
3.7.15.1. Widoki V aplikacji
![]() |
- w [1], URL widoku nr 1;
- w [2] – jego zawartość;
- w [3] przechodzi się do strony 2;
- w pliku [4] – widok nr 2;
- w [5] przechodzi się do strony 3;
![]() |
- w [6] – widok nr 3;
- w [7] przechodzi się do strony 1;
- po wpisaniu [8] powraca się do widoku nr 1;
3.7.15.2. Organizacja kodu
Rozpoczynamy nową organizację kodu:
![]() |
- widoki aplikacji zostaną umieszczone w folderze [views];
- moduł aplikacji zostanie umieszczony w folderze [modules];
- kontrolery aplikacji zostaną umieszczone w folderze [controllers];
Podobnie w wersji ostatecznej:
- usługi zostaną umieszczone w folderze [services];
- dyrektywy zostaną umieszczone w folderze [directives];
3.7.15.3. Kontener widoków
Widoki z folderu [views] będą wyświetlane w następującym kontenerze [app-25.html]:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container" ng-controller="mainCtrl">
<!-- pasek nawigacyjny -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- aktualny widok -->
<ng-view></ng-view>
</div>
...
<!-- moduł -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- kontrolery -->
<script type="text/javascript" src="controllers/mainController.js"></script>
<script type="text/javascript" src="controllers/page1Controller.js"></script>
<script type="text/javascript" src="controllers/page2Controller.js"></script>
<script type="text/javascript" src="controllers/page3Controller.js"></script>
</body>
</html>
- wiersz 7: treść kontenera jest kontrolowana przez plik [mainCtrl];
- wiersz 9: dyrektywa [ng-include] umożliwia dołączenie zewnętrznego pliku HTML, w tym przypadku paska nawigacyjnego;
- wiersz 12: różne widoki wyświetlane przez kontener są umieszczone wewnątrz dyrektywy [ng-view]. W rezultacie otrzymujemy kontener, który wyświetla:
- zawsze ten sam pasek nawigacyjny (wiersz 9);
- różne widoki w wierszu 12;
- wiersze 16–22: importujemy pliki JS z modułu aplikacji [rdvmedecins-13.js] oraz z jego kontrolerów;
3.7.15.4. Moduł aplikacji
Plik [rdvmedecins-13.js] definiuje moduł aplikacji oraz routing między widokami:
// --------------------- moduł Angular
angular.module("rdvmedecins", [ 'ngRoute' ]);
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ routing
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
$routeProvider.when("/page2",
{
templateUrl: "views/page2.html",
controller: 'page2Ctrl'
});
$routeProvider.when("/page3",
{
templateUrl: "views/page3.html",
controller: 'page3Ctrl'
});
$routeProvider.otherwise(
{
redirectTo: "/page1"
});
}]);
- wiersz 1: definiuje się moduł [rdvmedecins]. Jest on zależny od modułu [ngRoute] dostarczanego przez bibliotekę [angular-route.min.js]. To właśnie ten moduł umożliwia routing zdefiniowany w wierszach 6–24;
- wiersz 4: definiuje funkcję [config] modułu [rdvmedecins]. Przypominamy, że funkcja ta jest wykonywana przed instancjonowaniem jakiejkolwiek usługi. Jest to funkcja konfiguracyjna modułu. W tym przypadku konfigurowane jest jej przekierowanie. Odbywa się to za pomocą obiektu [$routeProvider] dostarczanego przez moduł [ngRoute];
- wiersze 6–10: definiują widok, który ma zostać wyświetlony, gdy użytkownik zażąda URL [/page1]. Jest to routing wewnętrzny w ramach aplikacji. URL to w rzeczywistości [/rdvmedecins-angular-v1/app-21.html#/page1]. Widać, że nadal wykorzystywany jest element URL z kontenera [/rdvmedecins-angular-v1/app-21.html], ale z dodatkową informacją umieszczoną za znakiem #. To właśnie tę dodatkową informację obsługuje routing Angulara;
- wiersz 8: wskazuje fragment HTML, który ma zostać wstawiony do dyrektywy [ng-view] kontenera:
- wiersz 9: określa nazwę kontrolera tego fragmentu;
- wiersze 11–15: definiują widok, który ma zostać wyświetlony, gdy użytkownik zażąda fragmentu URL [/page2];
- wiersze 16–20: definiują widok, który ma zostać wyświetlony, gdy użytkownik zażąda URL [/page3];
- wiersze 21–24: definiują trasę, którą należy zastosować, gdy żądany URL nie jest jednym z trzech poprzednich (w przeciwnym razie, wiersz 21);
- wiersz 23: przekierowanie do URL [/page1], a więc do widoku zdefiniowanego w wierszach 6–10;
3.7.15.5. Kontroler kontenera widoków
Widzieliśmy, że kontener widoków deklarował kontroler:
<div class="container" ng-controller="mainCtrl">
Kontroler [mainCtrl] jest zdefiniowany w pliku [mainController.js]:
// kontroler
angular.module("rdvmedecins")
.controller('mainCtrl', ['$scope', '$location',
function ($scope, $location) {
// szablony stron
$scope.page1 = {};
$scope.page2 = {};
$scope.page3 = {};
// szablon globalny
var main = $scope.main = {};
main.text = "[Modèle global]";
// metody udostępniane widokowi
main.showPage1 = function () {
$location.path("/page1");
};
main.showPage2 = function () {
$location.path("/page2");
};
main.showPage3 = function () {
$location.path("/page3");
}
}]);
- wiersz 3: kontroler [mainCtrl] potrzebuje obiektu [$location] dostarczanego przez moduł routingu [ngRoute]. Obiekt ten umożliwia zmianę widoku (wiersze 16, 19, 22);
Wróćmy do kodu kontenera:
<div class="container" ng-controller="mainCtrl">
<!-- pasek nawigacyjny -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- bieżący widok -->
<ng-view></ng-view>
</div>
- kontroler [mainCtrl] tworzy model obszaru 1-7;
- widok zawarty w wierszu 6 również posiada kontroler. Na przykład widok [page1] ma kontroler [page1Ctrl]. Ten ostatni tworzy model obszaru wyświetlanego w wierszu 6. W tym obszarze mamy zatem dwa modele:
- model tworzony przez kontroler [mainCtrl];
- model utworzony przez kontroler [page1Ctrl];
Istnieje dziedziczenie szablonów. W widoku wyświetlonym w wierszu 6 widoczne są oba szablony kontrolerów: [mainCtrl] i [pagexCtrl]. Jeśli dwie zmienne w tych szablonach mają tę samą nazwę, jedna z nich zasłoni drugą. Aby uniknąć tej kolizji nazw, tworzymy cztery szablony o czterech różnych nazwach:
kontener | mainCtrl | ręka | 11 |
strona 1 | page1Ctrl | strona 1 | 7 |
strona 2 | page2Ctrl | strona 2 | 8 |
strona 3 | page3Ctrl | strona 3 | 9 |
- wiersz 12: definiuje element [text] w modelu [main];
Wiersze 7–11 mają bardzo szczególne znaczenie: definiują one element [$scope] kontrolera [mainCtrl], a w nim tworzą cztery zmienne [main, page1, page2, page3]. Te cztery zmienne zostaną wykorzystane jako odpowiednie szablony kontenera oraz trzech widoków, które będą się w nim kolejno pojawiać.
3.7.15.6. Pasek nawigacyjny
Pasek nawigacyjny jest zdefiniowany w kontenerze w następujący sposób:
<div class="container" ng-controller="mainCtrl">
<!-- pasek nawigacyjny -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- aktualny widok -->
<ng-view></ng-view>
</div>
Pasek nawigacyjny jest zdefiniowany w wierszu 3. Oznacza to, że rozpoznaje on wyłącznie szablon [main]. Jego kod wygląda następująco:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span ng-click="main.showPage1()">Page 1</span>
</a>
</li>
<li class="active">
<a href="">
<span ng-click="main.showPage2()">Page 2</span>
</a>
</li>
<li class="active">
<a href="">
<span ng-click="main.showPage3()">Page 3</span>
</a>
</li>
</ul>
</div>
</div>
</div>
- w wierszach 16, 21, 26 wykorzystywane są metody z modelu [main];
- w wierszu 16: kliknięcie linku [Page1] spowoduje uruchomienie metody [$scope.main.showPage1]. Jest ona zdefiniowana w kontrolerze [mainCtrl] w następujący sposób:
// szablon globalny
var main = $scope.main = {};
main.text = "[Modèle global]";
// metody udostępnione w widoku
main.showPage1 = function () {
$location.path("/page1");
};
- wiersz 6: z powyższego kodu wynika, że metoda [main.showPage1] to w rzeczywistości metoda [$scope.main.showPage1]. To właśnie ta metoda zostanie wykonana;
- wiersz 7: zmieniamy metodę URL w aplikacji na [/page1]. Wróćmy do trasowania, które zostało zdefiniowane w module głównym:
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
widać, że fragment [views/page1.html] zostanie wstawiony do kontenera, a jego kontrolerem jest [page1Ctrl].
3.7.15.7. Widok [/page1] i jego kontroler
Fragment [views/page1.html] wygląda następująco:
<h1>Page 1</h1>
<div class="alert alert-info">
<ul>
<li>Modèle global : {{main.text}}</li>
<li>Modèle local : {{page1.text}}</li>
</ul>
</div>
Przypomnijmy, że w widoku umieszczonym w kontenerze widoczny jest szablon [main]. Właśnie to chcemy sprawdzić w wierszu 4. Ponadto kontroler [page1Ctrl] fragmentu [views/page1.html] definiuje szablon [page1]. To właśnie on jest używany w wierszu 5.
Kod kontrolera [page1Ctrl] jest następujący:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope',
function ($scope) {
// szablon strony 1
var page1=$scope.page1;
page1.text="[Modèle local dans page 1]";
}]);
- wiersz 2: wstawiony tutaj kontroler [$scope] nie jest pusty. Ponieważ kontroler [page1Ctrl] kontroluje obszar wstawiony do kontenera kontrolowanego przez [mainCtrl], [$scope] z wiersza 2 zawiera elementy [$scope] zdefiniowanego przez kontroler [mainCtrl]. Ważne jest, aby to zrozumieć. [$scope], zdefiniowany przez kontroler [mainCtrl], zawiera następujące elementy: [main, page1, page2, page3]. Oznacza to, że mamy dostęp do modeli wszystkich widoków. Niekoniecznie jest to pożądane, ale w tym przypadku tak właśnie jest. W ostatecznej wersji klienta Angular wykorzystamy tę cechę do przechowywania w modelu [main] informacji, które muszą być współdzielone między widokami. Będzie to koncepcja analogiczna do koncepcji „sesji” po stronie serwera;
- wiersz 6: pobieramy z [$scope] model [page1] ze strony 1, a następnie pracujemy z nim (wiersz 7). Otrzymujemy wówczas następujący widok:
![]() |
Widoki [/page2] i [/page3] są zbudowane na tym samym szablonie co widok [/page1] (patrz zrzuty ekranu na stronie 240).
3.7.15.8. Kontrola nawigacji
Chcemy teraz kontrolować nawigację w następujący sposób: [page1 --> page2 --> page3 --> page1]. Jeśli więc użytkownik znajduje się na stronie 1 [/page1] i wpisze w przeglądarce adres URL lub [/page3], to nawigacja ta nie powinna zostać zaakceptowana i należy pozostać na stronie 1.
Aby uzyskać ten efekt, modyfikujemy kontrolery stron w następujący sposób:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope', '$location',
function ($scope, $location) {
// czy nawigacja jest dozwolona?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// powrót do ostatniej strony URL
$location.path(main.lastUrl);
return;
}
// zapisujemy identyfikator URL strony
main.lastUrl = '/page1';
// szablon strony
var page1 = $scope.page1;
page1.text = "[Modèle local dans page 1]";
}]);
- wiersz 12: gdy strona zostanie wyświetlona, zapiszemy jej identyfikator URL w szablonie [main.lastUrl]. Wykorzystujemy tutaj koncepcję, o której mówiliśmy wcześniej: użycie szablonu [main] do przechowywania informacji wspólnych dla wszystkich widoków. W tym przypadku jest to ostatni wywołany szablon URL;
- kod w wierszach 4–12 jest powielony i dostosowany do trzech widoków. Znajdujemy się tutaj w widoku [/page1];
- wiersz 5: pobieramy szablon [main];
- wiersz 6: jeśli szablon [main.lastUrl] istnieje i różni się od [/page3], to nawigacja jest zabroniona (ostatni odwiedzony szablon URL istnieje i nie jest to /page3);
- wiersz 8: w takim przypadku powracamy do ostatniej odwiedzonej strony o nazwie URL;
Spróbujmy:
![]() |
- w [1] jesteśmy na stronie 1 i wpisujemy URL ze strony 3 jako [2];
- w przypadku [3] nawigacja nie doszła do skutku i powrócono do URL na stronie 1;
3.7.16. Wnioski
Omówiliśmy wszystkie przypadki użycia, z którymi spotkamy się w ostatecznej wersji klienta Angular. Podczas prezentacji tej wersji będziemy skupiać się bardziej na funkcjonalnościach aplikacji niż na szczegółach jej implementacji. W odniesieniu do tych ostatnich ograniczymy się do odwołania do przykładu ilustrującego omawiany przypadek użycia.
3.8. Ostateczna wersja klienta Angular
3.8.1. Struktura projektu
Ostateczny projekt wygląda następująco:
![]() |
![]() |
- w pliku [1] znajduje się cały projekt. [app.html] to strona główna aplikacji;
- w pliku [2] znajdują się kontrolery;
- w pliku [3] znajdują się dyrektywy;
- w pliku [4] znajdują się usługi oraz moduł Angular [main.js] aplikacji;
- w pliku [5] – różne widoki, które są wstawiane do strony głównej [app.html];
3.8.2. Zależności projektu
Zależności projektu są następujące:
![]() |
Rola tych różnych elementów została wyjaśniona w punkcie 3.4 na stronie 134.
3.8.3. Strona wzorcowa [app.html]
Strona wzorcowa wygląda następująco:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tahé">
<!-- CSS -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
<link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!-- kontroler [appCtrl], szablon [app] -->
<body ng-controller="appCtrl">
<div class="container">
...
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- moduły -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- usługi -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- dyrektywy -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- kontrolery -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
- wiersz 18: należy zauważyć, że [appCtrl] jest kontrolerem strony głównej;
- wiersze 19–21: treść strony głównej;
Treść ta wygląda następująco:
<div class="container">
<!-- paski nawigacyjne -->
<ng-include src="'views/navbar-start.html'" ng-show="app.navbarstart.show"></ng-include>
<ng-include src="'views/navbar-run.html'" ng-show="app.navbarrun.show"></ng-include>
<!-- jumbotron -->
<ng-include src="'views/jumbotron.html'"></ng-include>
<!-- tytuł strony -->
<div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
translate-values="{{app.titre.model}}"></div>
<!-- błędy na stronie -->
<errors model="app.errors" ng-show="app.errors.show"></errors>
<!-- komunikat o oczekiwaniu -->
<waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
<!-- aktualny widok -->
<ng-view></ng-view>
<!-- debugowanie -->
<debug model="app" ng-show="app.debug.on"></debug>
</div>
Niezależnie od wyświetlanego widoku, zawsze będzie on zawierał następujące elementy:
- wiersze 3–4: pasek sterowania. Dwa paski w wierszach 3 i 4 wykluczają się wzajemnie;
![]()
![]()
- wiersz 6: logo / tekst aplikacji:

- wiersz 8: tytuł

- wiersz 11: komunikat o błędzie:

- wiersz 13: komunikat o oczekiwaniu:

- wiersz 17: informacja debugowania:

Wszystkie powyższe elementy są kontrolowane przez dyrektywę [ng-show / ng-hide], która sprawia, że nawet jeśli są one obecne, niekoniecznie są widoczne.
3.8.4. Widoki aplikacji
W kodzie strony głównej mamy:
<div class="container">
...
<!-- bieżący widok -->
<ng-view></ng-view>
...
</div>
Wiersz 4 zawiera różne widoki aplikacji. Są one zdefiniowane w module [main.js]:

Rola konfiguracji poszczególnych tras została wyjaśniona w paragrafie 3.7.15.4, strona 242.
Widok [login.html] jest pusty, tzn. nie dodaje żadnych elementów do tych już obecnych na stronie głównej.
Widok [home.html] dodaje do strony głównej następujący element:

Widok [agenda.html] dodaje następujący element do strony szablonu:

Widok [resa.html] dodaje następujący element do strony szablonu:

3.8.5. Funkcje aplikacji
Widoki klienta Angular zostały już przedstawione w punkcie 1.3.3 na stronie 7. Aby ułatwić lekturę tego nowego rozdziału, przytaczamy je tutaj ponownie. Pierwszy widok jest następujący:
![]() |
- w [6], strona startowa aplikacji. Jest to aplikacja do umawiania wizyt u lekarzy;
- w [7] – pole wyboru, które pozwala włączyć lub wyłączyć tryb [debug]. Ten ostatni charakteryzuje się obecnością ramki [8], która wyświetla szablon bieżącego widoku;
- w [9] – sztuczny czas oczekiwania w milisekundach. Domyślnie wynosi 0 (brak oczekiwania). Jeśli N jest wartością tego czasu oczekiwania, każda akcja użytkownika zostanie wykonana po upływie czasu oczekiwania wynoszącego N milisekund. Pozwala to na obserwację sposobu zarządzania oczekiwaniem zaimplementowanego przez aplikację;
- w [10] – identyfikator serwera Spring 4; zgodnie z powyższym jest to [http://localhost:8080];
- w [11] i [12] – identyfikator i hasło osoby, która chce korzystać z aplikacji. Istnieją dwaj użytkownicy: admin/admin (login/hasło) z rolą (ADMIN) oraz user/user z rolą (USER). Tylko rola ADMIN ma uprawnienia do korzystania z aplikacji. Rola USER służy wyłącznie do pokazania, jaką odpowiedź zwraca serwer w tym przypadku użycia;
- w [13] przycisk umożliwiający połączenie z serwerem;
- w [14] – język aplikacji. Dostępne są dwa języki: domyślnie francuski oraz angielski.
![]() |
- w pliku [1] logujemy się;
![]() |
- po zalogowaniu można wybrać lekarza, u którego chce się umówić na wizytę [2], oraz dzień wizyty [3];
- w [4] prosimy o wyświetlenie terminarza wybranego lekarza na wybrany dzień;
![]() |
- po wyświetleniu terminarza lekarza można zarezerwować termin [5];
![]() |
- w [6] wybiera się pacjenta na wizytę i potwierdza ten wybór w [7];
![]() |
Po zatwierdzeniu wizyty następuje automatyczny powrót do kalendarza, w którym nowa wizyta jest już zapisana. Wizytę tę można później usunąć za pomocą kodu [7].
Opisano już główne funkcje. Są one proste. Funkcje, które nie zostały opisane, to opcje nawigacyjne służące do powrotu do poprzedniego widoku. Na koniec omówimy zarządzanie językiem:
![]() |
- w [1] przechodzi się z języka francuskiego na angielski;
![]() |
- na [2], widok przechodzi na język angielski, łącznie z kalendarzem;
3.8.6. Moduł [main.js]
Moduł [main.js] definiuje moduł Angular, który będzie sterował aplikacją:
![]() |
- wiersz 4: moduł nosi nazwę [rdvmedecins];
- wiersz 5: moduł [ngRoute] służy do routingu modułów URL;
- wiersz 6: moduł [translate] służy do internacjonalizacji tekstów;
- wiersz 7: moduł [base64] służy do kodowania ciągu znaków „login:password” w formacie Base64;
- wiersz 8: moduł [ngLocale] służy do internacjonalizacji kalendarza;
- wiersz 9: moduł [ui.bootstrap] służy do obsługi kalendarza;
- wiersz 12: konfiguracja tras;
- wiersz 40: internacjonalizacja komunikatów;
3.8.7. Kontroler strony głównej
Przypomnijmy kod HTML strony głównej [app.html]:
<body ng-controller="appCtrl">
<div class="container">
...
W wierszu 1 cała treść (body) strony głównej jest kontrolowana przez kontroler [appCtrl]. Ze względu na swoje położenie jest to główny kontroler aplikacji. Jak wyjaśniono w paragrafie 3.7.15, model utworzony przez ten kontroler jest dziedziczony przez wszystkie widoki, które zostaną wstawione do strony głównej.
Jego kod wygląda następująco:
angular.module("rdvmedecins")
.controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
function ($scope, config, utils, $location, $locale) {
// debug
utils.debug("[app] init");
// ----------------------------------------inicjalizacja strony
// szablony # stron
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// szablon bieżącej strony
var app = $scope.app;
...
// ---------------------------------- metody
// anulowanie bieżącego zadania
app.cancel = function () {
...
};
// wylogowanie
app.deconnecter = function () {
...
};
// ten kod musi pozostać w tym miejscu, ponieważ odwołuje się do poprzedzającej go funkcji [cancel]
app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
}])
;
Wiersze 10–14 definiują pięć szablonów wykorzystywanych w aplikacji:
app.html | appCtrl | |
login.html | loginCtrl | |
home.html | homeCtrl | |
resa.html | resaCtrl | |
agenda.html | agendaCtrl |
Ważne jest, aby zrozumieć, że obiekt [$scope], będący szablonem kontrolera strony głównej, jest dziedziczony przez wszystkie widoki i kontrolery. W ten sposób kontroler [loginCtrl] ma dostęp do elementów [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda]. Innymi słowy, kontroler ma dostęp do modeli innych kontrolerów. Analizowana aplikacja starannie unika korzystania z tej możliwości. Na przykład kontroler [loginCtrl] pracuje wyłącznie z dwoma modelami:
- własnym [$scope.login];
- oraz model kontrolera nadrzędnego [$scope.app];
To samo dotyczy wszystkich pozostałych kontrolerów. Model [$scope.app] będzie wykorzystywany jako pamięć współdzielona między różnymi kontrolerami. Gdy kontroler C1 będzie musiał przesłać informacje do kontrolera C2, postępuje się w następujący sposób:
W [C1]:
W kontrolerze [C2]:
W obu przypadkach $scope jest dziedziczone z kontrolera [appCtrl] i jest zatem identyczne (jest to wskaźnik) w [C1] oraz [C2]. Obiekt [$scope.app], który służy jako pamięć współdzielona między kontrolerami, będzie często nazywany w komentarzach session, na wzór sesji stosowanej w klasycznych aplikacjach internetowych, która oznacza pamięć współdzieloną między kolejnymi żądaniami HTTP.
Wróćmy do kodu kontrolera [appCtrl]:
// szablony # stron
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// szablon bieżącej strony
var app = $scope.app;
// [app.debug] i [utils.verbose] muszą być zawsze zsynchronizowane
app.debug = utils.verbose;
app.debug.on = config.debug;
// na razie brak tytułu strony
app.titre = {show: false};
// brak pasków nawigacyjnych
app.navbarrun = {show: false};
app.navbarstart = {show: false};
// brak błędów
app.errors = {show: false};
// domyślna lokalizacja
angular.copy(config.locales['fr'], $locale);
// bieżący widok
app.view = {url: undefined, model: {}, done: false};
// bieżące zadanie
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
- wiersz 8: [$scope.app] będzie szablonem strony głównej. Będzie to również pamięć współdzielona między różnymi kontrolerami. Zamiast wszędzie wpisywać [$scope.app.champ=value], wskaźnik [$scope.app] jest przypisany do zmiennej [app], a następnie wpisujemy [app.champ=value]. Należy po prostu pamiętać, że [app] to szablon wyświetlany na stronie głównej;
- wiersz 11: [app.debug.on] to zmienna logiczna, która steruje trybem debug aplikacji. Domyślnie ma wartość true. Jego wartość jest powiązana z polem wyboru [debug] na paskach nawigacyjnych;
- wiersz 15: [app.navbarrun.show] steruje wyświetlaniem następującego paska nawigacyjnego:
![]()
- wiersz 16: [app.navbarstart.show] steruje wyświetlaniem następującego paska nawigacyjnego:
![]()
- wiersz 18: [app.errors] to szablon paska błędów;

- wiersz 22: [app.view] będzie zawierał informacje o bieżącym widoku, czyli tym, który jest aktualnie wyświetlany przez tag [ng-view] na stronie głównej. Zapiszemy tam następujące informacje:
- [url]: URL bieżącego widoku, na przykład [/agenda];
- [model]: szablon bieżącego widoku, na przykład [$scope.agenda];
- [done]: w odniesieniu do vrai oznacza, że bieżący widok zakończył swoją pracę i następuje przejście do innego widoku;
Te informacje służą do sterowania nawigacją.
- wiersz 24: uruchamia zadanie asynchroniczne, symulowane oczekiwanie. Zadanie asynchroniczne jest odwołane przez dwa wskaźniki: [app.view.model.task.action] i [app.task];
W kontrolerze [appCtrl] wyodrębniono dwie metody:
// anulowanie bieżącego zadania
app.cancel = function () {
...
};
// wylogowanie
app.deconnecter = function () {
...
};
- wiersz 2: funkcja [app.cancel] służy do anulowania bieżącego zadania, dla którego aktualnie wyświetlany jest komunikat o oczekiwaniu. Wszystkie widoki wyświetlają ten komunikat, dlatego anulowanie zadania nastąpi właśnie tutaj;
- wiersz 7: funkcja [app.deconnecter] przenosi użytkownika z powrotem na stronę uwierzytelniania. Wszystkie widoki, z wyjątkiem widoku [/login], oferują tę możliwość;
Funkcja [app.deconnecter] działa w następujący sposób:
// wylogowanie
app.deconnecter = function () {
// powrót do strony logowania
$location.path(config.urlLogin);
};
- wiersz 4: powrót do strony logowania URL [/login];
3.8.8. Zarządzanie zadaniami asynchronicznymi
W naszej aplikacji w danym momencie będzie wykonywane tylko jedno zadanie asynchroniczne. Możliwe jest jednak uruchomienie kilku zadań jednocześnie. Na przykład po uruchomieniu aplikacja wysyła do serwisu internetowego żądanie o listę lekarzy, a następnie o listę klientów za pomocą dwóch kolejnych żądań HTTP. To samo można by osiągnąć za pomocą dwóch równoczesnych żądań HTTP. Angular oferuje narzędzia do takiego zarządzania. W tym przypadku nie zdecydowaliśmy się na takie rozwiązanie.
Bieżące zadanie jest anulowane za pomocą następującego kodu w kontrolerze [appCtrl]:
// anulowanie bieżącego zadania
app.cancel = function () {
utils.debug("[app] cancel task");
// anulowanie zadania asynchronicznego bieżącego widoku
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
...
};
- wiersz 5: zadanie jest wyszukiwane w [app.view.model.task]. Dlatego wszystkie kontrolery powinny zadbać o to, by ich zadania asynchroniczne były odwołane do tego obiektu;
- wiersz 6: w celu wskazania, że zadanie zostało zakończone;
- wiersz 7: w celu zakończenia zadania z niepowodzeniem. Notacja ta różni się od tej użytej w omawianych przykładach Angular:
- w przykładach obiekt [task] był obiektem [$q.defer()], który można było zakończyć;
- w ostatecznej wersji obiekt [task] jest obiektem zawierającym pola [action, isFinished], gdzie [action] to obiekt [$q.defer()], którymożna zakończyć, a [isFinished] jest wartością logiczną wskazującą, że akcja została zakończona;
Przyjrzyjmy się cyklowi życia obiektu [task] na przykładzie. Po uruchomieniu, po kontrolerze [appCtrl], kontrolę przejmuje kontroler [loginCtrl], aby wyświetlić widok [views/login.html]. Jego kod inicjalizacyjny wygląda następująco:
// pobieranie modelu nadrzędnego
var login = $scope.login;
var app = $scope.app;
// bieżący widok
app.view = {url: config.urlLogin, model: login, done: false};
W wierszu 5 mamy [model=login]. Oznacza to, że modyfikując obiekt [login], modyfikujemy obiekt [app.view.model], a tym samym [$scope.app.view.model]. Gdy w kontrolerze [loginCtrl] chcemy wykonać symulowane oczekiwanie, wpisujemy:
// symulowane oczekiwanie
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
Dodając pole [task] do obiektu [login], zostało ono zatem dodane do obiektu [$scope.app.view.model]. Jeśli użytkownik anuluje oczekiwanie, kod w obiekcie [appCtrl.cancel]:
// model bieżącej strony
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
prawidłowo zakończy symulowane oczekiwanie (wiersze 4–6).
3.8.9. Kontrola nawigacji
W aplikacji stosowane są następujące reguły nawigacji:
dowolny | tak | |
/login | tak, jeśli kontroler [loginCtrl] poinformował, że zakończył swoją pracę | |
/home | tak | |
/kalendarz | tak | |
/strona główna | tak, jeśli kontroler [homeCtrl] zgłosił zakończenie pracy | |
/resa | tak | |
/kalendarz | tak | |
/kalendarz | tak, jeśli kontroler [homeCtrl] zgłosił zakończenie pracy | |
/resa | tak |
Zostało to zaimplementowane za pomocą następującego kodu:
Dla [agendaCtrl]:

- wiersze 11–20: implementacja reguły nawigacji;
- wiersz 26: nowy widok bieżący;
Dla [resaCtrl]:

- wiersze 12–20: implementacja reguły nawigacji:
- wiersz 27: nowy widok bieżący;
Dla [loginCtrl]:

- nie ma tu żadnej kontroli nawigacji, ponieważ reguła mówi, że do URL [/login] można przejść z dowolnego miejsca. Jeśli więc użytkownik wpisze ten adres URL w przeglądarce, zadziała to niezależnie od aktualnego widoku;
- wiersz 16: nowy widok bieżący;
Kod kontrolera [homeCtrl] podano w paragrafie 3.8.7.
Wreszcie, w przypadku reguły takiej jak:
/home | tak, jeśli kontroler [homeCtrl] zgłosił zakończenie pracy |
oto przykład kodu, który powoduje przejście z URL [/home] do URL [/agenda]:
![]() |
Powyżej znajdujemy się w metodzie [afficherAgenda] kontrolera [homeCtrl]. Użytkownik zażądał harmonogramu wizyt lekarza.
- wiersz 107: obietnica zadania HTTP;
- wiersz 109: zmienna [app] została zainicjowana wartością [$scope.app]. Ten ostatni obiekt, jak już widzieliśmy, jest wykorzystywany jako szablon widoku [app.html]. Ten szablon [$scope.app] służy również do przechowywania informacji, które mają być współdzielone między widokami;
- wiersz 111: analizowany jest kod błędu zwrócony przez zadanie;
- wiersz 113: wynik [result.data] jest umieszczany w szablonie [app];
- wiersz 116: kontroler [homeCtrl] przekaże kontrolę kontrolerowi [agendaCtrl]. Informuje go, że zakończył swoją pracę, podając kod z wiersza 115. Kod ten zostanie wykorzystany przez kontroler [agendaCtrl] w następujący sposób:

- wiersz 11: pobierany jest obiekt [$scope.app.view];
- wiersz 15: przetwarzanie pola [$scope.app.view.done] zainicjowanego przez [homeCtrl];
3.8.10. Usługi
![]() |
Usługi [config, utils, dao] to te, które zostały już opisane podczas prezentacji Angular:
- usługa [config] została przedstawiona w punkcie 3.7.4;
- usługa [utils] została przedstawiona w punkcie 3.7.5;
- usługa [dao] została przedstawiona w punkcie 3.7.6;
Dla przypomnienia przedstawiamy strukturę tych usług:
Usługa [config]
![]() |
- w [1]: widać, że kod ma około 250 wierszy. Istotą tego kodu jest wyodrębnienie kluczy z wiadomości zinternacjonalizowanych [2]. Unika się umieszczania tych kluczy na stałe w kodzie;
Usługa [utils]
![]() |
- wiersz 8: nie spotkaliśmy się jeszcze ze zmienną [verbose]. Kontroluje ona funkcję [debug] w następujący sposób:
![]() |
- wiersze 22–25: funkcja [utils.debug] nie wykonuje żadnej operacji, jeśli wartość [verbose.on] wynosi false. Ta zmienna jest powiązana ze zmienną kontrolera [appCtrl]:
![]() |
- wiersz 21: [app.debug] przyjmuje wartość wskaźnika [utils.verbose]. Zatem każda zmiana dokonana w [app.debug] zostanie również wprowadzona w [utils.verbose];
- wiersz 22: początkowa wartość [app.debug.on] jest pobierana z pliku konfiguracyjnego. Domyślnie jest to wartość true.. Wartość ta może ulec zmianie w miarę upływu czasu. Użytkownik ma bowiem możliwość jej zmiany w paskach nawigacyjnych:
![]() |
- wiersz 45: pole wyboru (type=checkbox) pozwala zmienić wartość [app.debug.on] (atrybut ng-model);
Usługa [dao]
![]() |
3.8.11. Dyrektywy
![]() |
Dyrektywy [errors, footable, list, waiting] to te, które zostały już opisane podczas prezentacji Angulara:
- dyrektywa [footable] została wprowadzona w paragrafie 3.7.8.6;
- dyrektywa [list] została wprowadzona w paragrafie 3.7.12;
- dyrektywy [errors] i [waiting] zostały wprowadzone w paragrafie 3.7.14;
Nie natrafiliśmy na dyrektywę [debug]. Brzmi ona następująco:
![]() |
Plik [debug.html], o którym mowa w wierszu 11, ma następującą treść:
- wiersz 2: dyrektywa [debug] wyświetla swój szablon w formacie JSON w pasku Bootstrap (wiersz 1);
Dyrektywa ta jest używana wyłącznie w szablonie głównym [app.html]:
![]() |
- Dyrektywa [debug] jest używana w wierszu 35. Wyświetla ona zatem formę JSON szablonu [$scope.app] w trybie debugowania (atrybut ng-show). Daje to następujące wyniki:
![]() |
Zrozumienie tego wymaga dobrej znajomości kodu, ale gdy już się ją opanuje, powyższe informacje stają się przydatne podczas debugowania. W tym miejscu zaznaczono elementy wyświetlanego szablonu [$scope.app]. Przypominamy, że [$scope.app] to pamięć współdzielona przez kontrolery;
- [waitingBeforeTask]: symulowany czas oczekiwania przed każdym żądaniem HTTP;
- [debug]: tryb debugowania – musi wynosić true, jeśli ten pasek jest wyświetlany;
- [navbarrun]: wartość logiczna kontrolująca wyświetlanie następującego paska nawigacyjnego:
![]()
- [navbarstart]: wartość logiczna kontrolująca wyświetlanie następującego paska nawigacyjnego:
![]()
- [errors]: szablon dyrektywy [errors];
- [view]: zawiera informacje o aktualnie wyświetlanym widoku;
- [waiting]: szablon dyrektywy [waiting];
- [serverUrl, username, password]: informacje dotyczące logowania do serwisu internetowego;
- [medecins]: szablon dla dyrektywy [list] stosowanej w odniesieniu do lekarzy;
- [clients]: to samo dla klientów;
- [menu]: kontroluje wyświetlane opcje menu. Są one zdefiniowane w [navbar-run.html]:

Opcje menu znajdują się w wierszach 16, 23, 29 i 36.
- [formattedJour]: wybrany dzień w kalendarzu w formacie „rrrr-mm-dd”;
- [agenda]: kalendarz lekarza. Znajdują się w nim wolne terminy (rv==null) oraz zarezerwowane. W przypadku tych ostatnich podane jest imię i nazwisko klienta, który dokonał rezerwacji;
- [selectedCreneau]: przedział czasowy wybrany do dokonania rezerwacji;
3.8.12. Kontroler [loginCtrl]
![]() |
Kontroler [loginCtrl] jest powiązany z widokiem [views/login.html], który w połączeniu ze stroną główną generuje następującą stronę:

Kontroler [loginCtrl] wygląda następująco:

- wiersz 13: [login] będzie szablonem bieżącego widoku;
- wiersz 14: [app] to pamięć współdzielona między kontrolerami;
- wiersz 16: [app.view] jest wypełniane danymi z bieżącego widoku;
Ten kod inicjalizacyjny znajdzie się w każdym kontrolerze. Dla kontrolera C1 widoku V1, którego szablonem jest M1, otrzymamy następujący kod inicjalizacyjny:
- wiersz 18: być może pamiętamy, że [appCtrl] uruchomił symulowane oczekiwanie, do którego odwołuje się obiekt [app.task.action]. Wykorzystujemy [promise] z tego zadania, aby oczekiwać na jego zakończenie;
- wiersz 39: metoda [login.setLang] obsługuje zmianę języka;
- wiersz 47: metoda [login.authenticate] obsługuje uwierzytelnianie użytkownika;
Przyjrzyjmy się głównym etapom działania metody uwierzytelniania:

- wiersze 50–51: [app.waiting] to szablon paska oczekiwania;
- wiersz 53: [app.errors] to szablon paska komunikatów o błędach;
- wiersz 55: uruchamiane jest symulowane oczekiwanie. Obiekt [action, isFinished] jest odwołany przez [login.task], a zatem – ponieważ [app.view.model=login] – przez [app.view.model.task]. Przypominamy, że jest to warunek umożliwiający anulowanie zadania;
- wiersz 57: po zakończeniu symulowanego oczekiwania ładuje się lekarzy;
- wiersz 62: po uzyskaniu zapotrzebowania na lekarzy analizuje się to zapotrzebowanie. Jeśli lekarze zostali pozyskani, następuje żądanie klientów;
- wiersz 83: analizuje się otrzymaną odpowiedź i wyświetla się widok końcowy. Odbywa się to za pomocą następującego kodu:

- wiersz 87: zmienna logiczna [task.isFinished] jest ustawiana na true w następujących przypadkach:
- użytkownik anulował oczekiwanie;
- żądanie lekarzy zakończyło się błędem;
- wiersze 91–98: przypadek, w którym pojawili się klienci;
- wiersz 93: [app.clients] jest szablonem dyrektywy [list], która wyświetli klientów na liście rozwijanej;
- wiersze 97–98: przygotowujemy się do zmiany widoku (wiersz 98), ale wcześniej sygnalizujemy, że kontroler zakończył swoją pracę (wiersz 97). Przypominamy, że [$scope.app.view.done] służy do kontroli nawigacji;
Warto tutaj zwrócić uwagę, że dane lekarzy i klientów zostały zapisane w pamięci podręcznej przeglądarki. Nie będą one już pobierane z serwisu internetowego.
3.8.13. Kontroler [homeCtrl]
![]() |
Kontroler [homeCtrl] jest powiązany z widokiem [views/home.html], który w połączeniu ze stroną główną generuje następującą stronę:

Struktura kontrolera [homeCtrl] jest następująca:

- wiersze 12–20: jest to kontrola nawigacji. Posiadają ją wszystkie kontrolery z wyjątkiem [loginCtrl], ponieważ strona [/login.html] jest dostępna bez żadnych warunków;

- wiersze 25–28: znajdują się tu wiersze analogiczne do tych, które spotkaliśmy w kontrolerze [loginCtrl]. [home] jest zatem szablonem widoku powiązanego z kontrolerem;
- wiersz 33: atrybut, z którym jeszcze się nie spotkaliśmy. Jest to szablon paska tytułu widoku:
![]()
- wiersz 36: [home.datepicker] to szablon kalendarza;
- wiersz 38: [app.menu] to szablon menu paska nawigacyjnego. Znajdzie się tam opcja [Agenda]. To właśnie ona umożliwia wyświetlenie harmonogramu wizyt u lekarza;
Wreszcie kontroler posiada dwie metody:

Wyświetlanie harmonogramu (wiersz 51) zostało omówione w punkcie 3.7.8.
3.8.14. Kontroler [agendaCtrl]
![]() |
Kontroler [agendaCtrl] jest powiązany z widokiem [views/agenda.html], który w połączeniu ze stroną główną generuje następującą stronę:

Struktura kontrolera [agendaCtrl] jest następująca:

- wiersze 10–20 zapewniają kontrolę nawigacji;

- wiersze 23–26: [agenda] będzie szablonem widoku powiązanego z kontrolerem [agendaCtrl];
- wiersze 36–44: [app.titre] to szablon następującego paska tytułu:

- wiersz 46: menu będzie zawierało opcję [Home / Accueil]:
![]()
Metody kontrolera są następujące:

- wiersz 95: metoda [agenda.supprimer] została omówiona w punkcie 3.7.9;
Metoda [agenda.home] jest metodą czysto nawigacyjną:

Metoda [agenda.reserver] wygląda następująco:

- wiersz 73: parametrem funkcji [reserver] jest numer przedziału czasowego (id);
- wiersze 77–86: służą do odnalezienia przedziału czasowego o tym identyfikatorze;
- wiersz 82: znaleziony przedział czasowy jest umieszczany w pamięci współdzielonej [app]. Kontroler [resaCtrl], który przejmie kontrolę (wiersz 90), wykorzysta tę informację do wyświetlenia swojego paska tytułowego;
- wiersze 89–90: przejście do kontrolera [/resa.html];
3.8.15. Kontroler [resaCtrl]
![]() |
Kontroler [resaCtrl] jest powiązany z widokiem [views/resa.html], który w połączeniu ze stroną główną generuje następującą stronę:

Struktura kontrolera [resaCtrl] jest następująca:

- wiersze 12–20: sterowanie nawigacją;

- wiersze 24–27: [resa] będzie szablonem bieżącego widoku;
- wiersze 38–45: [app.titre] to szablon następującego paska tytułu:

- wiersz 47: wyświetlane są dwie opcje menu:
![]()
Metody kontrolera są następujące:

Metoda [resa.valider] została omówiona w punkcie 3.7.9.
3.8.16. Obsługa języków
Wszystkie kontrolery udostępniają następującą metodę [setLang]:

Mogłaby ona zostać wyodrębniona do kontrolera [appCtrl].




























































































































