AngularJS

Kiedy nasza aplikacja zaczyna rosnąć dobrym pomysłem jest podział jej zawartości. Na zwykłych stronach robimy to zazwyczaj za pomocą różnych podstron natomiast bądź co bądź AngularJS jest to framework przeznaczony do tworzenia Single Page Applications, który obsługuje podstrony za pomocą techniki, która nazywa się routing.

routing oznacza ładowanie różnych widoków / szablonów w zależności od tego na jakim URL znajduje się użytkownik.

Dodatkowym problem SPA jest to że nie za dobrze współgrają z przeglądarkowym guzikiem wstecz lub dodawaniem takiej strony do ulubionych, a wynika to z tego, że jak sama nazwa wskazuje SPA jest przeznaczona i ładowana na pojedynczej stronie.

W takim razie jak przekonać AngularJS żeby udawał, że jedna strona to tak naprawdę cały rozbudowany serwis? AngularJS zapewnia nam specialny moduł ngRoute z usługą $route, która obserwuje aktualny URL naszej strony i sprawdza jak przekłada się on na stan naszej aplikacji. Dodatkowo ustawia ona odpowiednie URL’e w zależności od tego gdzie nasz użytkownik zawędruje.

Same zmiany stanu naszej aplikacji są obsługiwane poprzez hasztag.

Konfiguracja

Pierwszą rzeczą, którą musimy zrobić to dodać moduł ngRoute do zależności naszej aplikacji.

Moglibyśmy dodać zależność bezpośrednio do modułu acodemy-app i tam skonfigurować wszystkie ścieżki, które obsługuje nasza aplikacja. Jednak lepszym rozwiązaniem będzie przeniesienie tych informacji do modułów poszczególnych ścieżek. Dzięki temu, aby usunąć jakąś ścieżkę wystarczy usunąć odpowiedni moduł z zależności aplikacji.

Zacznijmy od ścieżki /search. Do modułu acodemy-app.routes.search dodajmy ngRoute.

angular.module 'acodemy-app.routes.search', [ 'ngRoute' 'acodemy-app.apis.spotify' 'acodemy-app.filters.duration' ] ... angular.module('acodemy-app.routes.search', [ 'ngRoute', 'acodemy-app.apis.spotify', 'acodemy-app.filters.duration' ]) ...

Następnie musimy zarejestrować ścieżkę /search. Przed “startem” aplikacji, zanim wszystkie usługi zostaną stworzone, odbywa się konfiguracja usług. Możemy zgłosić potrzebę skonfigurowania usługi $route wywołaniem metody module.config():

angular.module 'acodemy-app.routes.search', [ 'ngRoute' ... ] .config ($routeProvider) -> ... angular.module('acodemy-app.routes.search', [ 'ngRoute', ... ]) .config(function($routeProvider) { ... })

$routeProvider powazala nam skonfigurować wszystkie ścieżki wraz z ich szablonami oraz kontrolerami. Sam $routeProvider posiada dwie metody, w których określamy:

  • when - co jeśli url pasuje do ścieżki
  • otherwise - co się dzieje w każdym innym przypadku
... .config ($routeProvider) -> $routeProvider .when '/search', templateUrl: 'routes/search/search.html' controller: 'SearchRouteController' reloadOnSearch: false ... .config(function($routeProvider) { $routeProvider .when('/search', { templateUrl: 'routes/search/search.html', controller: 'SearchRouteController', reloadOnSearch: false }); })

ng-view

Gdy używamy routingu, template który powinien zostać zaserwowany dla podanej ścieżki zostanie włożone wewnątrz taga posiadającego dyrektywę ng-view

reloadOnSearch

Jeśli ustawimy reloadOnSearch na false, zmiany w parametrach URL’a (np. ?q=search) nie będą powodować przeładowania widoku. Zostanie jednak wysłane zdarzenie $routeUpdate, żeby poinformować kontroler o zmianie URL’a.

Zadanie dla Ciebie:

  • Dodaj ścieżkę /search i zaktualizuj SearchRouteController
  • Spraw, aby domyślną ścieżką było /search
  • Pytania?

Widoki z parametrami

Nasza aplikacja z powodzeniem obsługuje już różne podstrony, choć mamy ją tylko jedną. Dużo ciekawiej by było gdyby w naszych ścieżkach można było przekazywać dodatkowe parametry i dodać szczyptę dynamizmu do naszej strony. Tylko jak?

Metoda when w swoim pierwszym argumencie przyjmuje zmienną path, która może zawierać:

  • nazwane grupy zaczynające się od dwukropka np: :name, które zostaną dopasowane do końca tekstu lub slasha
  • nazwane grupy zaczynające się od dwukropka a kończące się gwiazdką np: :name* gdzie wszystkie znaki zostaną dopasowane
  • nazwane grupy zaczynające się od dwukropka a kończące się znakiem zapytania np: :name? gedzie dana grupa jest opcjonalna

Dla ścieżki /hi/:name wywołanie /hi/ala pobierze wartość ala do atrybutu name. Jak to wygląda w praktyce?

... $routeProvider .when '/some/url/:param', templateUrl: 'views/my_custom_view.html' controller: 'ParamCtrl' .when '/some/url/:param*/edit', templateUrl: 'views/my_custom_view.html' controller: 'WildcardParamCtrl' .when '/some/url/:param?', templateUrl: 'views/my_custom_view.html' controller: 'OptionalParamCtrl' ... ... $routeProvider .when('/some/url/:param', { templateUrl: 'views/my_custom_view.html', controller: 'ParamCtrl' }) .when('/some/url/:param*/edit', { templateUrl: 'views/my_custom_view.html', controller: 'WildcardParamCtrl' }) .when('/some/url/:param?', { templateUrl: 'views/my_custom_view.html', controller: 'OptionalParamCtrl' }); ...

Kontroler jak to odczytać?

Aby przechwycić parametry z URL’a posłużymy się usługą $routeParams.

# routes/album/album.js angular.module 'acodemy-app.routes.album', [ 'ngRoute' ... ] .config ($routeProvider) -> $routeProvider .when '/album/:id', template: 'routes/album/album.html', controller: 'AlbumRouteController' .controller('AlbumRouteController', ( $scope, $routeParams, SpotifyApi ) -> console.log($routeParams.id); // routes/album/album.js angular.module('acodemy-app.routes.album', [ 'ngRoute', ... ]) .config(function($routeProvider) { $routeProvider .when('/album/:id', { template: 'routes/album/album.html', controller: 'AlbumRouteController' }); }) .controller('AlbumRouteController', function( $scope, $routeParams, SpotifyApi ) { console.log($routeParams.id); });

Widok

Aby nasz użytkownik miał możliwość przejścia na naszą stronę z parametrem, trzeba dopisać w szablonie po prostu:

Click me!

ng-href

Warto sprawdzić czym się różni ng-href od href

Zadanie dla Ciebie:

  • Skonfiguruj ścieżkę /album/:id wyświetlającą szczegóły o danym albumie na podstawie szablonu routes/album/index.html
  • Zaktualizuj linki, tak aby korzystały z powyższych ścieżek
  • Dodaj przejście do strony wyszukiwania kiedy wpiszę się coś w polu wyszukiwania
  • Dodaj przejście do pustej strony wyszukiwania gdy kliknie się na logo aplikacji
  • Pytania?

Zadanie dodatkowe:

  • Skonfiguruj ścieżkę /artist/:id wyświetlającą szczegóły o danym artyście na podstawie szablonu routes/artist/index.html

API Spotify - albums, artists

GET https://api.spotify.com/v1/albums/
GET https://api.spotify.com/v1/artists/

Parametry

ids - lista identyfikatorów albumów, oddzielonych przecinkami

Przykład

GET https://api.spotify.com/v1/albums?ids=3B0PgLmgaW0gJth55ApWbw { "albums" : [ { "id" : "3B0PgLmgaW0gJth55ApWbw", "name" : "Transistor Original Soundtrack", "release_date" : "2014-05-20", ... "images" : [ { "width" : 640 "height" : 640, "url" : "https://i.scdn.co/ime/6776280c479cbd4a09c363e1208e4aa40cb79e93", }, ...], "artists" : [ { "id" : "0ZMWrgLff357yxLyEU77a1", "name" : "Darren Korb", ... }, ...], "tracks" : { "items" : [ { "artists" : [ { "id" : "0ZMWrgLff357yxLyEU77a1", "name" : "Darren Korb", ... }, ...], "duration_ms" : 201324, "id" : "4zmT3KiW5UVfzGSIkYbs0y", "name" : "Old Friends", "preview_url" : "https://p.scdn.co/mp3-priew/27d0aa224616b39d6469bc0e0bf27388e3cca973", ... }, ...], "limit" : 50, "next" : null, "offset" : 0, "previous" : null, "total" : 23 } }, ...] }

API Spotify - dodatkowe informacje o artyście

Albumy danego artysty
GET https://api.spotify.com/v1/artists/{id}/albums

Najpopularniejsze utwory artysty
GET https://api.spotify.com/v1/artists/{id}/top-tracks

Podobni artyści
GET https://api.spotify.com/v1/artists/{id}/related-artists

$q

Aby zagregować kilka zapytań, tak żeby obsłużyć je wszyskie jednym callbackiem, można posłużyć się modułem $q. Posiada on metodę $q.all(promises), która zwraca promise, który zostanie zrealizowany gdy wszystkie podane promises zostaną spełnione.

Efekt

W naszej strukturce powinno pojawić się kilka plików:

acodemy-spotify ├── bower.json ├── gulpfile.js ├── package.json └── src ├── index.html ├── app │   ├── app.css │   └── app.js ├── apis │   └── spotify.js ├── directives │ └── clearable-input │      └── clearable-input.js ├── webcomponents │ ├── clearable-input │   │   ├── clearable-input.html │   │   └── clearable-input.js │   └── play-button │   ├── play-button.html │   └── play-button.js ├── filters │   └── duration.js ├── navbar │   ├── navbar.css │   ├── navbar.html │   └── navbar.js └── routes ├── album │   ├── album.html │   └── album.js ├── artist │   ├── artist.html │   └── artist.js └── search ├── search.html └── search.js

Natomiast wizualnie szczegóły albumu/artysty powinny pezentować się tak:

Album efekt

Album efekt

$route

Mamy już podzieloną aplikację na trzy widoki. Warto w tym momencie wspomnieć o usłudze, która nazywa się $route. $route sam w sobie daje nam dostęp do tego co już skonfigurowaliśmy za pomocą $routeProvider oraz do wszystkich aktualnych parametrów powiązanych z aktualnie aktywnym URL’em.

Jeżeli dodamy jakikolwiek parametr (myCustomVariable) do konfiguracji $routeProvider w app.coffee/app.js:

... $routeProvider .when '/album/:id', templateUrl: 'routes/album/album.html' controller: 'AlbumRouteController' myCustomVariable: 'myCustomValue' ... ... $routeProvider .when('/album/:id', { templateUrl: 'routes/album/album.html', controller: 'AlbumRouteController', myCustomVariable: 'myCustomValue' }); ...

Mamy możliwość pobrania go w kontrolerze za pomocą usługi $route:

.controller 'AlbumRouteController', ($scope, $route) -> console.log $route.current.myCustomVariable ... .controller('AlbumRouteController', function($scope, $route) { console.log($route.current.myCustomVariable); ... });

Dodatkowo możemy pobrać parametry z URL’a poprzez current.params, np: http://localhost:9000/#/?myCustomVariable=myCustomValue

.controller 'AlbumRouteController', ($scope, $route) -> console.log $route.current.params.myCustomVariable ... .controller('AlbumRouteController', function($scope, $route) { console.log($route.current.params.myCustomVariable); ... })

Usługa $route umożliwia nam też dostanie się do naszych magicznych zmiennych takich jak id więc jeżeli odwiedzimy http://localhost:8888/#/album/3B0PgLmgaW0gJth55ApWbw

.controller 'AlbumRouteController', ($scope, $route) -> console.log $route.current.params.id ... .controller('AlbumRouteController', function($scope, $route) { console.log($route.current.params.id); ... });

Kolejnym sposobem na dotarcie do naszych zmiennych jest $route.current.pathParams

.controller 'AlbumRouteController', ($scope, $route) -> console.log $route.current.pathParams.id ... .controller('AlbumRouteController', function($scope, $route) { console.log($route.current.pathParams.id); ... });

params vs pathParams

Zmienna params przechowuje wszystkie zmienne z URL’a, te ze ścieżki jak id oraz te z query jak ?myCustomVariable. pathParams przechowuje jedynie zmienne takie jak id.

Ostatnią przydatną metodą w usłudze $route jest $route.reload(). W sytuacji kiedy chcemy odświeżyć pojedynczy widok, bez ładowania ponownie całej aplikacji wystarczy wywołać w kontrolerze $route.reload() a wszystko w danym widoku powróci do stanu początkowego.

HTML5

Jak już pewnie zauważyliście w URL’ach towarzyszy nam #, można się tego drania pozbyć poprzez drobne zmiany w konfiguracji naszej aplikacji za pomocą $locationProvidera.

angular.module 'acodemy-app', [...] .config ($locationProvider) -> $locationProvider.html5Mode true ... angular.module('acodemy-app', [...]) .config(function($locationProvider) { $locationProvider.html5Mode(true); ... });

Dodatkowo jeżeli chcemy aby wszysko działało poprawnie należy poprawić linki:

{{ album.name }}

Czy wszystko działa poprawnie?

Używając HTML5 Mode trzeba być świadomym że AngularJS wie jak obsługiwać nasze ścieżki / URL’e ale nasz serwer już nie. Nasz serwer HTTP założy, że skoro wywołujemy adres /questions/123456 to fizycznie gdzieś powinien istnieć plik lub folder o takiej nazwie.

Zadanie dla Ciebie:

  • Wypróbuj html5mode
  • Przywróć aplikację do stanu sprzed html5Mode

Przedyskutuj z mentorem różnice między $apply, $digest, $evalAsync. Zapytaj o różnicę w działaniu pomiędzy addEventListener'em, a ng-click

resolve

Już całkiem dobrze znamy usługę $routeProvider ale warto jeszcze wspomnieć o kilku jej właściwościach.

Gdy pobieramy dane z zewnętrznych usług mają one tendencję do zajmowania sporej ilości czasu. Kiedy AngularJS czeka na te dane nasz szablon może być widoczny pusty. Aby to zasymulować możemy użyć usługi $timeout w naszym kontrolerze:

.controller 'AlbumRouteController', ( $scope, $routeParams, $timeout, SpotifyApi ) -> $timeout -> SpotifyApi.getAlbum $routeParams.id .then (album) -> ... , 3000 .controller('AlbumRouteController', function( $scope, $routeParams, $timeout, SpotifyApi ) { $timeout(function { SpotifyApi.getAlbum($routeParams.id) .then(function(album) { ... }); }, 3000); })

Możemy temu zapobiec wymuszając na AngularJS wstrzymanie ładowania się widoku do momentu pobrania danych.

$routeProvider .when '/album/:id', templateUrl: 'routes/album/album.html' controller: 'AlbumRouteController' resolve: album: ($routeParams, SpotifyApi) -> SpotifyApi.getAlbum $routeParams.id # W kontrolerze możemy to odczytać tak: .controller 'AlbumRouteController', ($scope, album) -> $scope.album = album $routeProvider .when('/album/:id', { templateUrl: 'routes/album/album.html', controller: 'AlbumRouteController', resolve: { album: function($routeParams, SpotifyApi) { return SpotifyApi.getAlbum($routeParams.id); } } }); // W kontrolerze możemy to odczytać tak: .controller('AlbumRouteController', function($scope, album) { $scope.album = album; });

Teraz AngularJS poczeka z wyświetleniem widoku na załadowanie się danych.