Daj Się Poznać #6 Dyrektywa w AngularJS - część 2


W poprzednich częściach tworzyliśmy aplikację AngularJS Mistrz Makro, która jest quizem, pozwalającym na zgadywanie makroskładników produktu przedstawionego na zdjęciu. Udało się nam stworzyć modele, pozwalające na zarządzanie danymi aplikacji. Odebraliśmy dane z pliku JSON i wyświetliliśmy je w konsoli. W poprzedniej części zaczęliśmy budować główny mechanizm napędzający aplikacje, czyli dyrektywę. Stworzyliśmy jej część składającą się z pliku HTML. Tym razem znów zajmiemy się dyrektywą i utworzymy plik JavaScript, który sprawi, że nasz quiz zacznie działać tak jak powinien. Zaczynajmy!
$ git checkout 28c29bb2082ce088dc554c5c73cd01c02163d8c9
# Zmiany w pliku HTML dyrektywy
W stosunku do poprzedniego postu, w pliku HTML dyrektywy dodaliśmy tylko paragrafy, które wyświetlają wyniki, oraz dodatkowy stan inProgress
. Tak prezentuje się cały kod:
<div class="container" ng-show="inProgress">
<div class="row">
<div class="col-md-12">
<div ng-show="!quizEnd">
<h2></h2>
<img ng-src="/app/assets/">
<p>Kcal:</p><input type="number" ng-model="macro1">
<p ng-show="!answerMode"></p>
<p>Białko:</p><input type="number" ng-model="macro2">
<p ng-show="!answerMode"></p>
<p>Węglowodany:</p><input type="number" ng-model="macro3">
<p ng-show="!answerMode"></p>
<p>Tłuszcz:</p><input type="number" ng-model="macro4">
<p ng-show="!answerMode"></p>
<button ng-click="check()" ng-show="answerMode">Submit</button>
<div ng-show="!answerMode">
<button ng-click="nextQuestion()">Next</button>
</div>
</div>
<div ng-show="quizEnd">
<button ng-click="reset()">Play again</button>
</div>
</div>
</div>
</div>
<div class="container" ng-show="!inProgress">
<div class="row">
<button ng-click="start()">Start</button>
</div>
</div>
Po każdym inpucie dodaliśmy paragraf, który wyświetla się jeśli nie jesteśmy w stanie answerMode
, czyli w momencie kiedy wpisaliśmy wartość do inputów i wdusiliśmy przycisk sprawdzający. Wyświetlimy tam wyniki danego pytania.
<p ng-show="!answerMode"></p>
Stan inProgress
znajduje się w dwóch głównych containerach i pozwala na wyświetlenie quizu dopiero po wduszeniu przycisku start
.
# Tworzymy plik JS Dyrektywy
Na początku, dyrektywę tworzymy tak samo jak każdy plik AngularJS, czyli wstawiamy IIFE, podłączamy nasz główny moduł i tworzymy dyrektywę, którą nazwałem "quiz".
(function() {
'use strict';
angular
.module('quizProject')
.directive('quiz', quiz);
Następnie tworzymy funkcje zgodnie z dokumentacją dyrektywy:
function quiz(ProductModel) {
return {
restrict: 'AE',
scope: {},
templateUrl: 'app/main/directive/html/macro.directive.html',
link: function(scope, elem, attrs) {
Restrict pozwala na ograniczenie w jaki sposób będziemy mogli wywoływać naszą dyrektywę. Do wyboru mamy cztery sposoby - A
, C
, E
, M
, jednak domyślnie jest to AE
i właśnie tak będzie w naszej aplikacji. Jednak czym się różnią i w jaki sposób ją wywołują?
A = <div quiz></div> <!-- jako Atrybut-->
C = <div class="quiz"></div> <!-- jako Klasa -->
E = <quiz></quiz> <!-- jako Element-->
M = <!--directive:quiz --> <!-- jako Komentarz-->
Scope pozwala na stworzenie wyizolowanego zasięgu zmiennych i pobranie w razie potrzeby wartości przy wywołaniu dyrektywy. Nam jednak nie będzie to potrzebne. Przykład takiego użycia:
<div quiz name="ABC"></div>
scope: {
MyQuestionName: '@name'
},
TemplateUrl przechowuje link do pliku HTML dyrektywy. Link jest funkcją, która wykonuje się przy wywołaniu dyrektywy.
Teraz możemy wewnątrz funkcji link tworzyć funkcje.
Pierwszą funkcja, którą stworzymy będzie start
, która wywołuje się po wduszeniu przycisku "start", po włączeniu aplikacji. Ustawia wszystkie stany, tak, aby mogły się pojawić kolejne elementy w widoku.
scope.start = function() {
scope.id = 0;
scope.quizEnd = false;
scope.inProgress = true;
scope.getQuestion();
};
Po wywołaniu funkcji id wyświetlanego elementu ustawia się na zero. Dzięki zmiennej quizEnd
wiemy że quiz się rozpoczął i nie zakończył. Zmienna inProgress
wyświetla na ekranie quiz. Następnie wywołujemy zmienną getQuestion()
.
scope.getQuestion = function() {
var q = ProductModel.getQuestion(scope.id);
if(q) {
scope.name = q.name;
scope.kcal = q.kcal;
scope.protein = q.protein;
scope.carb = q.carb;
scope.fat = q.fat;
scope.img = q.img;
scope.answerMode = true;
} else {
scope.quizEnd = true;
}
};
Funkcja getQuestion
odbiera z ProductModel
dane, wybrane przez aktualną wartość zmiennej scope.id
, poprzez funkcję:
var q = ProductModel.getQuestion(scope.id);
A następnie przypisuje pobrane dane do odpowiadających im zmiennych. Zmieniamy stan answerMode
, dzięki czemu pojawiają się produkt i inputy, które będziemy mogli wypełnić. Jeśli nasza funkcja nie znajdzie odpowiednich danych, to zakończy quiz zmienną quizEnd
. Tak prezentuje się aplikacja po wduszeniu przycisku start:
Możemy teraz wpisać wartości numeryczne w pola, jednak nie są one walidowane w żaden sposób i prawdopodobnie pozostawię użytkownikowi wybór jakie pola wypełnić.
Po wduszeniu przycisku Submit, wywoła się funkcja check()
, która wygląda w taki sposób:
scope.check = function(){
scope.Res = [];
scope.answerMode = false;
scope.Res[0] = test(scope.kcal, scope.macro1);
scope.Res[1] = test(scope.protein, scope.macro2);
scope.Res[2] = test(scope.carb, scope.macro3);
scope.Res[3] = test(scope.fat, scope.macro4);
};
Funkcja check()
zmienia stan answerMode
na false, aby określić w aplikacji, że user już zatwierdził swoją odpowiedź. następnie tworzymy tablicę, w której przechowamy wyniki porównujące wartość podaną przez użytkownika z wartością w naszej aplikacji.
Funkcja test:
var test = function(modelData, viewData){
if(modelData > viewData) return modelData - viewData + " za mało";
if(modelData < viewData) return viewData - modelData + " za dużo";
if(modelData == viewData) return "idealnie!";
};
Funkcja test
, jest funkcją nie przypisaną do scope
, ponieważ nie będziemy wywoływać jej w widoku aplikacji, tylko bezpośrednio w dyrektywie. Funkcja ta przyjmuje dwa argumenty - wartość z inputa, oraz odpowiadającą wartość z aplikacji. Po porównaniu tych dwóch wartości, zwracana jest różnica wraz z opisem czy wartość była za mała czy za duża. W przypadku trafienia poprawnej wartości, funkcja zwraca string "idealnie!". Po przypisaniu tych wartości do tablicy scope.Res
, możemy wyświetlić ją w widoku aplikacji. Stanie się tak, ponieważ paragrafy, które wyświetlają tą tablice, wyświetlą się w momencie, kiedy zmienimy stan answerMode
na fałsz, co przed chwilą się wydarzyło. Tak wygląda nasza aplikacja, w momencie kiedy nie wpisaliśmy żadnej wartości i wdusiliśmy przycisk submit
:
Następna funkcja wywołuje się po wduszeniu klawisza next
, który wyświetla się po wykonaniu funkcji check()
. Nazywa się nextQuestion()
scope.nextQuestion = function() {
scope.id++;
scope.getQuestion();
}
Pozwala tylko na zwiększenie id, aby wyszukać kolejny produkt i znowu wywołuje funkcję qetQuestion()
scope.reset = function() {
scope.inProgress = false;
}
Ostatnią już funkcją tej dyrektywy jest scope.reset()
, które wywoływany jest przez przycisk play again
, pojawiający się w momencie kiedy w bazie zabraknie pytań. Funkcja zmienia stan inProgress
, dzięki czemu wracamy do przycisku start
.
# Cały kod dyrektywy
Tak prezentuje się cały kod JavaScript dyrektywy, którą zapisałem jako macro.directive.js
:
(function() {
'use strict';
angular
.module('quizProject')
.directive('quiz', quiz);
function quiz(ProductModel) {
return {
restrict: 'AE',
scope: {},
templateUrl: 'app/main/directive/html/macro.directive.html',
link: function(scope, elem, attrs) {
scope.start = function() {
scope.id = 0;
scope.quizEnd = false;
scope.inProgress = true;
scope.getQuestion();
};
scope.check = function(){
scope.Res = [];
scope.answerMode = false;
scope.Res[0] = test(scope.kcal, scope.macro1);
scope.Res[1] = test(scope.protein, scope.macro2);
scope.Res[2] = test(scope.carb, scope.macro3);
scope.Res[3] = test(scope.fat, scope.macro4);
};
var test = function(modelData, viewData){
if(modelData > viewData) return modelData - viewData + " za mało";
if(modelData < viewData) return viewData - modelData + " za dużo";
if(modelData == viewData) return "idealnie!";
};
scope.getQuestion = function() {
var q = ProductModel.getQuestion(scope.id);
if(q) {
scope.name = q.name;
scope.kcal = q.kcal;
scope.protein = q.protein;
scope.carb = q.carb;
scope.fat = q.fat;
scope.img = q.img;
scope.answerMode = true;
} else {
scope.quizEnd = true;
}
};
scope.nextQuestion = function() {
scope.id++;
scope.getQuestion();
}
}
}
};
})();
Proszę się nie martwić surowym wyglądem aplikacji, na stylowanie przyjdzie jeszcze czas - w najbliższych częściach postów Daj się Poznać.