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!

Jeśli pobrałeś już moje repozytorium z Githuba, co opisałem w poprzednim poście to możesz teraz przeskoczyć do kolejnego commita, który pokaże Ci kod z tego posta:
$ 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:

aplikacja po wduszeniu 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:

aplikacja po wduszeniu 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ć.

Published: May 04 2017

blog comments powered by Disqus