W ostatnich dniach trafilem na ciekawy artykuł Modeling Data and State in Your AngularJS Application dotyczący modelu w frameworku AngularJS.
Ponieważ założeniami AngularJS jest koncepcja MVM, toteż najczęściej dane (modele) są przechowywane w kontrolerze. Nie jest to złe podejście, szczególnie dobre w małych aplikacjach. Jeśli jednak tworzymy duży system, który będzie się szybko rozrastał, to może się nam przydać nieco inne spojrzenie na model.
Za cytowanym powyżej artykułem, można zamknąć nasz model jako serwis, dzięki czemu będzie istniała możliwość wykorzystania raz pobranych danych w kilku kontrolerach. Nie będzie też problemu z przekazywaniem danych między kontrolerami na różnych podstronach (różny routing).
Nasza aplikacja będzie miała za zadnie pobrać z zewnętrznego źródła listę książek i wyświetlić ją. Po wskazaniu wybranej pozycji powinien pojawić się szczegółowy jej opis. Pomysł nie jest zbyt wygórowany, ale nie chodzi nam tu o tworzenie skomplikowanej aplikacji, tylko o pokazanie pewnego mechanizmu. Zatem zaczynajmy.
Dane
Nasze dane są zapisane w pliku data.json, jednak nic nie stoi na przeszkodzie by zrobić to samo na bazie danych.
[ { "title": "Drużyna pierścienia", "author": "J.R.R Tolkien", "src" : "/bundles/examplesangular/img/books/druzyna_pierscienia.jpg", "description": "Lorem mattis in tempor et dignissim! Et nisi porta, nec lectus? Dis egestas sit tempor? Et tortor urna? Enim mid ac velit nascetur in sed mauris dictumst est, pellentesque. Sagittis! Elementum dictumst pid lundium placerat. Auctor! Pulvinar ut mid nec rhoncus risus placerat et porttitor porttitor dis tristique magna rhoncus! Cum, ut tortor sed, penatibus? Tristique mid turpis nec et? Augue nisi aliquet! Nunc? Elit cursus, arcu egestas in lundium pid elit adipiscing enim tempor ut elementum ac adipiscing, quis, velit dis mauris cum dapibus urna in! Habitasse tincidunt platea auctor et, ut, porttitor, augue a lorem lundium a, a? Nunc natoque platea risus auctor, amet? Aliquam placerat! Scelerisque aliquam, tristique penatibus elit hac rhoncus nisi natoque amet elit amet." }, { "title": "Dwie wieże", "author": "J.R.R Tolkien", "src" : "/bundles/examplesangular/img/books/dwie_wieze.jpg", "description": "Et rhoncus proin! Rhoncus ut sociis porttitor. Natoque, augue, ac aliquet, nec pulvinar et nunc auctor placerat scelerisque nunc rhoncus in! Tincidunt tincidunt? Tincidunt integer tempor sit urna auctor habitasse sit pellentesque tincidunt, in tempor! Nunc. Tincidunt natoque lectus, integer mattis! Odio mauris dis, porttitor eros proin non a tortor, ultrices augue augue risus duis porta facilisis risus hac, adipiscing augue. Augue placerat. Ultricies tincidunt odio pid nec. Lectus habitasse nec eu. Hac nec, hac aenean cras dolor placerat a adipiscing porta nunc nisi placerat enim! Facilisis, habitasse, ultricies, eu sagittis lacus et magnis augue! Lundium nec et, arcu sociis elementum lorem egestas ultrices ac lundium vel et proin elit sociis placerat auctor dolor, et, odio montes magna cum." }, { "title": "Powrót króla", "author": "J.R.R Tolkien", "src" : "/bundles/examplesangular/img/books/powrot_krola.jpg", "description": "Et ac tincidunt sagittis dolor vel enim ridiculus odio eros! Pellentesque duis. Montes placerat lectus, ac, massa proin, tempor nunc! Cursus ridiculus turpis sit magnis, duis! Est a, augue sit? Facilisis duis. Egestas, odio tincidunt pid, et platea, scelerisque enim, arcu scelerisque! Montes nec! Turpis risus duis nec enim habitasse? Egestas hac purus cum tincidunt! Hac facilisis diam risus lorem rhoncus. Sit ac lundium! In porta, mauris facilisis tincidunt et, in adipiscing, ac, magnis augue, eu, turpis augue. Vel aenean scelerisque arcu, pulvinar montes porta eu est. Tincidunt porttitor phasellus! Placerat, sociis nunc augue scelerisque? Pid, et platea a nisi! Integer. Mus lectus auctor, elementum, facilisis massa porttitor enim cras! Ultricies ac, ac lacus? Amet cras magna odio vel aliquet." } ]
Model
Poniższy listing pokazuje zawartość pliku model.js. Zawiera on listę książek (books), a także zestaw potrzebnych funkcji.
var booksListModel = app.service("booksListModel", ['$rootScope', '$resource', function($rootScope, $resource) { /** * Czy lista książek została załadowana * @type {boolean} */ this.isLoaded = false; /** * Lista książek * @type {Array} */ this.books = []; /** * Obecnie zaznaczona (wybrana) książka * @type {null} */ this.selectedBook = null; /** * * @param book */ this.selectBook = function(book) { if(this.books.indexOf(book) > -1) { this.selectedBook = book; $rootScope.$broadcast('booksModel::selectedBookBroadcast', book); } }; /** * Uchwyt do komunikacji z serwerem, może zawierać dodatkowe funkcje * zapisujące bądź modyfikujące listę * * @type {*} */ this.connection = $resource('/bundles/examplesangular/js/example-model/data.json',{}, { query: { method: "GET" ,isArray: true } }); /** * * @returns {boolean} */ this.isSelectedFirst = function(){ return (this.selectedBook == this.books[0]); } /** * * @returns {boolean} */ this.isSelectedLast = function(){ return (this.selectedBook == this.books[(this.books.length - 1)]); } /** * * @returns {*} */ this.getNext = function(){ if(!this.isSelectedLast()) { var index = this.books.indexOf(this.selectedBook); return this.books[++index]; } return false; } /** * * @returns {*} */ this.getPrev = function(){ if(!this.isSelectedFirst()) { var index = this.books.indexOf(this.selectedBook); return this.books[--index]; } return false; } /** * * @param book * @returns {boolean} */ this.isSelectedBook = function(book) { return book === this.selectedBook; }; /** * Ładuje dane na podstawie connection * * @param force * @returns {*} */ this.loadData = function(force){ if(!this.isLoaded || force) { this.books = this.connection.query(); this.isLoaded = true; } return this.books; } }]);
Większość funkcji jest na tyle prosta, że nie trzeba ich opisywać, poza dwiema:
- loadData – ładuje dane i zapmietuje ten fakt (każde następne wywołanie nie powoduje kolejnych pobrań danych z serwera), przyjmuje parametr force, który wymusza ponwne pobranie danych z serwera
- selectBook – zapamiętuje w zmiennej selectedBook zaznaczoną pozycję, a dodatkowo rozpropagowuje informację o tym fakcie do $rootScope’a, dzięki czemu wszystkie kontrolery, mogą przechwycić tą informację i odpowiednio zareagować
Aplikacja i kontrolery
Kolejny plik to app.js, główny plik aplikacji, zawiera konfigurację routingu i dwa kontrolery:
- BooksListCtrl – odpowiedzialny za wyświetlanie listy książek
- BookPropertiesCtrl – odpowiedzialny za przegladanie właściwości wybranej książki
var app = angular.module('modelExample', ['ngResource']). config(function ($routeProvider) { $routeProvider. when('/', { controller: 'BooksListCtrl', templateUrl: '/examples/angular/partials/example-model/list.html' ,resolve: { books: function(booksListModel){ return booksListModel.loadData(false); } } }). when('/properties', { controller: 'BookPropertiesCtrl', templateUrl: '/examples/angular/partials/example-model/properties.html' ,resolve: { books: function(booksListModel){ return booksListModel.loadData(false); } } }). otherwise({ redirectTo: '/' }); }); app.controller("BooksListCtrl", ['$scope', '$location', 'booksListModel', function($scope, $location, booksListModel) { $scope.model = booksListModel; $scope.showProperties = function(book){ $scope.model.selectBook(book); $location.path('/properties'); } }]); app.controller("BookPropertiesCtrl", ['$scope', '$location', 'booksListModel', function($scope, $location, booksListModel) { var model = booksListModel; $scope.selectedBook = booksListModel.selectedBook; $scope.$on('booksModel::selectedBookBroadcast', function(event) { $scope.selectedBook = booksListModel.selectedBook; }); $scope.showNext = function(){ var book = model.getNext(); if(book) { model.selectBook(book); } } $scope.showPrev = function(){ var book = model.getPrev(); if(book) { model.selectBook(book); } } $scope.goList = function(){ $location.path('/'); } $scope.disablePrevButton = function(){ return (!$scope.selectedBook || model.isSelectedFirst()); } $scope.disableNextButton = function(){ return (!$scope.selectedBook || model.isSelectedLast()); } }]);
W obydwu kontrolerach uzyty jest model booksListModel dzieki czemu można w obydwu kontrolerach korzytsać z tej samej listy książek. Kontorler BookPropertiesCtrl przechwytuje informacje o zmianie zaznaczonej książki w modelu i aktualizuje swoją wewnętrzną zmienną, która jest wykorzystywana przy wyświetlaniu szczegółów książki:
$scope.selectedBook = booksListModel.selectedBook; $scope.$on('booksModel::selectedBookBroadcast', function(event) { $scope.selectedBook = booksListModel.selectedBook; });
Widoki
Do pełni szczęścia potrzebujemy jeszcze dwóch widoków: dla listy książek i ich szczegółów
<!-- list.html -->
<div ng-controller="BooksListCtrl" >
<h4>Ilość książek na półce: {{ model.books.length }}</h4>
<table class="table table-bordered table-hover">
<tr>
<th>Lp.</th>
<th>Tytuł</th>
<th>Okładka</th>
</tr>
<tr ng-repeat="book in model.books" ng-click="showProperties(book)" style="cursor: pointer;">
<td>{{$index + 1}}</td>
<td>{{book.title}}</td>
<td><img ng-src="{{book.src}}" alt="{{book.title}}" style="width: 100px;"/></td>
</tr>
</table>
</div>
<!-- properties.html -->
<div ng-controller="BookPropertiesCtrl">
<div class="text-center" >
<div class="btn-group">
<button class="btn" ng-click="showPrev()" ng-class="{'disabled': disablePrevButton()}">Poprzedni</button>
<button class="btn btn-info" ng-click="goList()" >Powrót do listy</button>
<button class="btn" ng-click="showNext()" ng-class="{'disabled': disableNextButton()}">Następny</button>
</div>
</div>
<div style="margin: 20px;" ng-show="!selectedBook">
<div class="alert alert-error">
<h4>Uwaga!</h4>
Nie wybrano książki, powróć do listy książek
</div>
</div>
<div style="margin: 20px;" ng-show="selectedBook">
<div style="float: left; margin-right: 10px;">
<img ng-src="{{selectedBook.src}}" alt="{{selectedBook.title}}" style="width: 200px;"/>
</div>
<ul class="unstyled">
<li><b>Tytuł:</b> {{selectedBook.title}}</li>
<li><b>Autor:</b> {{selectedBook.author}}</li>
<li><b>Opis:</b> {{selectedBook.description}}</li>
</ul>
</div>
</div>
W widoku szczegółów wybranej książki oprócz przycisku Powrót do listy pojawiły się także przyciski Poprzednia i Następna, które umożliwiają zmianę wybranej książki.
Podsumowanie
Mam nadzieję, że mój przykład jest zrozumiały i pokaże Wam jak można skorzystać z modelu. Patrząc obecnie na niektóre moje projekty myślę, że to rozwiązanie w kilku z nich napewno znalazłoby zastosowanie. Takie podejście pozwala na zmniejszenie ilośći zapytań do serwera, a dzięki temu przyspiesza działanie aplikacji.
Dla tych którzy chcieliby zobaczyć opisywany przykład w praktyce zapraszam na tę stronę.