Daj Się Poznać #7 Upgrade do AngularJS 1.6 - Components, ES6, Webpack


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 nam się ukończyć wstępny zarys działającej aplikacji. Tym razem zajmiemy się zmianą kodu do wersji AngularJS 1.6. Nasza aplikacja będzie przerobiona na najnowszy standard, pozwalający na łatwiejszą zmianę do Angulara (2/4) w razie potrzeby. Skorzystamy także z Babela, który pozwoli nam pisać kod w ES6, dzięki czemu zastosujemy sporo rzeczy, które poznaliśmy przy serii postów o ES6. 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 1a8c63fba51ef10ff143c719c30e8430c1759ab9

# Co się zmieni w stosunku do poprzedniego kodu?

Przede wszystkim skorzystamy tym razem z innego startera. NG6-starter ze skonfigurowanym gulpem, webpackiem, babelem do ES6, przygotowany do korzystania z SASS oraz testowania będzie naszym wyborem.

  • Dyrektywa zostanie zamieniona przez component
  • Class - Użyjemy klas z ES6 do tworzenia serwisów i komponentów. Więcej na ten temat możesz dowiedzieć się tutaj.
  • Webpack - zamiast bowera skorzystamy z webpacka.
  • arrow functions - w kodzie będziemy używali funkcji strzałkowych z ES6. Przeczytać na ten temat możesz tutaj.
  • let skorzystamy także z nowego słowa kluczowego, które poznaliśmy w ES6. Więcej: tutaj.

# GetJson service - zmiany

Serwisy tworzymy jako klasy z konstruktorem i metodami.

 
export default class GetJson {
  constructor($http) {
    this._$http = $http;
  }

  getData() {
    let request = {
      url: 'answers.JSON',
      method: 'GET',
    };
    return this._$http(request).then((res) => res.data);
  }
}

Tworzymy klasę GetJson gotową do eksportu i użycia w komponencie. W konstruktorze, który wywołuje się przy stworzeniu obiektu mamy wstrzykiwanie zależności, dzięki któremu możemy korzystać z funkcji $http. Metoda getData określa ścieżkę pliku i typ komunikacji. Jako wartość zwracaną mamy this._$http, z którego korzystamy tak jak przypisaliśmy to w konstruktorze. Funkcja zwraca dane z piku answers.JSON.

 
import angular from 'angular';

import productsList from './productsList';
import GetJson from './GetJson';

export default angular
  .module('app.services', [])
  .service({ productsList })
  .service({ GetJson });

W pliku services.js importujemy nasz serwis i rejestrujemy go do modułu app.services.

# Modele z danymi - zmiany

Wcześniej mieliśmy pliki macro.model.js oraz product.model.js, które były serwisami, do których zapisywaliśmy nasze dane. Tym razem stworzymy jeden serwis składający się z dwóch klas.

Tworzymy plik ProductsList.js zawierający dwie klasy.

 
class Macros {
  constructor(id, name, kcal, protein, carb, fat, img){
    this.id = id;
    this.name = name;
    this.kcal = kcal;
    this.protein = protein;
    this.carb = carb;
    this.fat = fat;
    this.img = img;
  }
}

Macros to pierwsza klasa odpowiadająca za macro.model.js. Nie będzie wykorzystywana poza tym plikiem, więc nie musimy jej eksportować. Nasza klasa tworzy nowe obiekty z podanych wartości.

Kolejną klasą w tym pliku będzie ProductsList czyli odpowiednik product.model.js.

 
export default class ProductsList {
  constructor() {
    this.list = [];
  }

  add(id, name, kcal, protein, carb, fat, img){
    const macros = new Macros(id, name, kcal, protein, carb, fat, img);
    this.list.push(macros);
  }

  getList(id){
    return id < this.list.length ? this.list[id] : false; 
  }
}

Eksportujemy klasę, ponieważ będziemy korzystać z niej w naszym komponencie. W konstruktorze tworzymy listę, w której będziemy przechowywać obiekty. Metoda add dodaje nasze dane do obiektu wytworzonego przez klasę Macros, a następnie dołącza go do tablicy this.list. Metoda getList wyszukuje obiekt o podanym id i go zwraca. Serwis rejestrujemy tak jak to było w przypadku GetJson.

# Directive - zmiana na Component - controller

Nasz plik macro.directive.js zostanie zastąpiony plikiem controller.js

 
export default class Quiz {
  /**
   * @param {ProductsList} productsList
   */
  constructor(productsList, GetJson) {
    "ngInject";
    this.productsList = productsList;
    this.GetJson = GetJson;

    this.id = 0;
    this.quizEnd = true;
    this.inProgress = false;
    this.Res = [];
    this.id = 0;
    this.name = "";
    this.kcal = "";
    this.protein;
    this.carb;
    this.fat;
    this.img = "";
    this.answerMode = false;

    this.init();
  }

Stworzyliśmy klasę Quiz, w którą wstrzykujemy productsList i GetJson. Nasz konstruktor ustawia wartości początkowe i wywołuje metodę this.init();.

init(){
  let answers = this.GetJson.getData();
  answers.then(data => {
    data.questions.forEach(answer => {
      this.productsList.add(answer.id,
                            answer.name,
                            answer.kcal,
                            answer.protein,
                            answer.carb,
                            answer.fat,
                            answer.img);
    });
  });
}

Funkcja init() zmieniła się nieznacznie w stosunku do naszego poprzedniego kodu. Odbieramy dane z GetJson i dodajemy je do serwisu productsList za pomocą metody add().

start() {
  this.id = 0;
  this.quizEnd = false;
  this.inProgress = true;
  this.getQuestion();
}
 getQuestion() {
  var q = this.productsList.getList(this.id);
  if(q) {
    this.name = q.name;
    this.kcal = q.kcal;
    this.protein = q.protein;
    this.carb = q.carb;
    this.fat = q.fat;
    this.img = q.img;
    this.answerMode = true;
  } else {
    this.quizEnd = true;
  }
}

nextQuestion() {
  this.id++;
  this.getQuestion();
}

reset() {
  this.inProgress = false;
  this.score = 0;
}

Funkcje start(), getQuestion(), nextQuestion(), reset() pozostają praktycznie bez zmian, poza różnicami w składni.

check(){
  let test = function(modelData, viewData){
    if(!viewData) return "";
    if(modelData == viewData) return "idealnie!";
    return modelData > viewData ? modelData - viewData + " za mało" : viewData - modelData + " za dużo";
  };
  this.Res = [];
  this.answerMode = false;
  this.Res[0] = test(this.kcal, this.macro1);
  this.Res[1] = test(this.protein, this.macro2);
  this.Res[2] = test(this.carb, this.macro3);
  this.Res[3] = test(this.fat, this.macro4);
}

Funkcja check() została tylko troszkę skrócona, a funkcja test została dołączona wewnątrz metody.

# Directive - zmiana na Component - pozostałe pliki

Nasz komponent musimy określić w pliku quiz.component.js. Restrict, bindings, template znamy już z dyrektywy.

import template from './mainFile.html';
import controller from './controller';

let quizComponent = {
  restrict: 'E',
  bindings: {},
  template,
  controller,
  controllerAs: 'c'
};

export default quizComponent;

Nowością jest controllerAs, który określa jak będziemy odwoływać się do zmiennych i metod, zamiast użycia $scope.

Używamy tym razem odwołania c.zmienna zamiast $scope.zmienna. Poza tymi zmianami, plik html naszej dyrektywy pozostaje taki sam:

<div class="container" ng-show="c.inProgress">
  <div class="row">
    <div class="col-md-12">

      <div ng-show="!c.quizEnd">
        <h2></h2>
        <img ng-src="/assets/">
        <p>Kcal:</p><input type="number" ng-model="c.macro1">
        <p ng-show="!c.answerMode"></p>
        <p>Białko:</p><input type="number" ng-model="c.macro2">
        <p ng-show="!c.answerMode"></p>
        <p>Węglowodany:</p><input type="number" ng-model="c.macro3">
        <p ng-show="!c.answerMode"></p>
        <p>Tłuszcz:</p><input type="number" ng-model="c.macro4">
        <p ng-show="!c.answerMode"></p>
        <button ng-click="c.check()" ng-show="c.answerMode">Submit</button>
      
        <div ng-show="!c.answerMode">
          <button ng-click="c.nextQuestion()">Next</button>
        </div>
      </div>

      <div ng-show="c.quizEnd">
        <button ng-click="c.reset()">Play again</button>
      </div>

    </div>
  </div>
</div>

<div class="container" ng-show="!c.inProgress">
  <div class="row">
    <button ng-click="c.start()">Start</button>
  </div>
</div>

Ostatnim plikiem jest components.js, który pozwala zarejestrować nasz komponent do kolejnego modułu app.components:

import angular from 'angular';

import quizComponent from './quiz.component';

export default angular
  .module('app.components', [
  ])
  .component('quiz', quizComponent);

Published: May 13 2017

blog comments powered by Disqus