Nauki Ember 2.0 ciąg dalszy. Przejrzałem mnóstwo materiałów dot. tego framework’a, jednak jak wiadomo – najlepszym sposobem na naukę jest praktyka. Postawiłem sobie za cel przygotowanie prostej aplikacji w Ember 2.0., a ponieważ aplikację typu „TODO” są tym czym „Hello World” przy nauce języka, postanowiłem napisać taką właśnie aplikację. Poniżej opiszę kolejne kroki powstania tej aplikacji oraz problemy, na które się w tym czasie natknąłem.
Gotową aplikację można znaleźć tutaj a cały kod źródłowy na moim koncie github

Zacznijmy jednak od założeń projektu.

[wr_heading tag=”h3″ text_align=”inherit” heading_margin_top=”5″ heading_margin_bottom=”25″ font=”inherit” enable_underline=”yes” border_bottom_style=”solid” appearing_animation=”0″ disabled_el=”no” ]Założenia projektu[/wr_heading]

Główną funkcjonalnością aplikacji, powinna być możliwość dodawania nowych zadań do wykonania oraz możliwość zaznaczania zadań już wykonanych. Poniżej reszta założeń projektu:
– dodanie nowego zadania powinno odbywać się po wciśnięciu ‚Enter’ na klawiaturze oraz po kliknięciu przycisku ‚Add’
– aplikacja powinna zliczać ile jest dodanych wszystkich zadań, ile zadań zostało zakończonych oraz ile zadań pozostało do wykonania
– w aplikacji powinna być możliwość filtrowania zadań zakończonych oraz nowych
– każde zadanie można usunąć lub zaznaczyć jako ‚zakończone’
– każde zadanie można edytować poprzez podwójne kliknięcie na jego opis
– w aplikacji powinna być możliwość usunięcia zadań zakończonych lub wszystkich
– wszystkie zadania powinny być przechowywane w bazie lub lokalnym magazynie przeglądarki

Dodatkowo do części stricte front-endowej postanowiłem wykorzystać Semantic UI (zamiast oklepanego już Twitter Bootstrap :)) aby przy okazji nauczyć się czegoś nowego.
Aby ułatwić debugowanie aplikacji i jednocześnie zweryfikować co wykonuje Ember proponuje zainstalować dodatek Ember Inspector, który jest dostępny zarówno dla Firefox jak i Chrome.

[wr_heading tag=”h5″ text_align=”inherit” heading_margin_top=”25″ heading_margin_bottom=”25″ font=”inherit” enable_underline=”yes” border_bottom_style=”solid” appearing_animation=”0″ disabled_el=”no” ]Podział na komponenty[/wr_heading]

W ostatnim poście dot. Ember 2.0 opisałem, że obiekt ‚Controller’ zostanie całkowicie usunięty i w związku z tym całą aplikację podzieliłem na komponenty. Każdy komponent jest odpowiedzialny za inną część funkcjonalności. Komponenty będą komunikowały się ze sobą poprzez dyspozytor (dispatcher) – wywołanie akcji w jednym komponencie powoduje wywołanie odpowiedniej akcji w innym. Poniżej schemat wszystkich komponentów w aplikacji:

Schemat komponentów
Schemat komponentów

Opis każdego z komponentów:

new-task new-task Component
Komponent odpowiedzialny za dodanie nowego zadania.
manage-tasks manage-tasks Component
Komponent odpowiedzialny za zarządzane listą zadań. Umożliwia filtrowanie listy oraz usunięcia wszystkich lub tylko skończonych zadań
tasks-list tasks-list Component
Komponent odpowiedzialny za wyświetlanie listy zadań.
single-task single-task Component
Komponent odpowiedzialny za wyświetlenie pojedynczego zadania. Umożliwia zmianę statusu zadania (New/Done) lub jego usunięcie.
single-task-description single-task-description Component
Komponent odpowiedzialny za wyświetlenie opisu zadania oraz jego edycję po dwukrotnym kliknięciu.

[wr_heading tag=”h3″ text_align=”inherit” heading_margin_top=”5″ heading_margin_bottom=”25″ font=”inherit” enable_underline=”yes” border_bottom_style=”solid” appearing_animation=”0″ disabled_el=”no” ]Przygotowanie projektu[/wr_heading]

W ostatnim poście opisałem jak rozpocząć pracę w Ember 2.0 korzystając z narzędzie ember-cli (http://meandjs.com/2015/12/19/hi-ember-2-0/). W związku z tym pominę tę część i przejdę od razu do instalacji niezbędnych elementów do projektu.

[wr_heading tag=”h5″ text_align=”inherit” heading_margin_top=”25″ heading_margin_bottom=”25″ font=”inherit” enable_underline=”yes” border_bottom_style=”solid” appearing_animation=”0″ disabled_el=”no” ]Instalacja Semantic UI[/wr_heading]

Zgodnie z dokumentacją Semantic UI możemy go zainstalować bezpośrednio dla ember jako addon. W związku z tym w konsoli wpisujemy:

ember install semantic-ui-ember

Następnie uruchamiamy bibliotekę:

ember generate semantic-ui-ember

[wr_heading tag=”h5″ text_align=”inherit” heading_margin_top=”25″ heading_margin_bottom=”25″ font=”inherit” enable_underline=”yes” border_bottom_style=”solid” appearing_animation=”0″ disabled_el=”no” ] Ember Data i Local Storage Adapter[/wr_heading]

Ember Data jest biblioteką do zarządzania danymi w aplikacjach Ember 2.0. Biblioteka została tak zaprojektowana aby korzystała z różnego rodzaju mechanizmów przechowywania danych. Oznacza to, że radzi sobie tak samo dobrze zarówno z JSON Api, strumieniami WebSocket czy lokalnymi bazami danych, np. Local Storage. W przypadku zmiany mechanizmu przechowywania danych z aplikacji, wystarczy zmienić adapter, z którego korzysta Ember Data bez konieczności zmian w kodzie. W opisywanej aplikacji skorzystamy z lokalnej bazy Local Storage jaką zapewnia przeglądarka. W związku z tym stwórzmy Adapter dla Local Storage, z którego będzie korzystać Ember Data.

Najpierw wygenerujmy adapter dla aplikacji

ember generate adapter application

Domyślnie zostanie wygenerowany adapter dla mechanizmu REST. Musimy zatem dokonać zmian w pliku app/adapters/application.js aby Ember Data korzystał z Local Storage.

 

// app/adapters/application.js
// -----

import DS from 'ember-data';

export default DS.LSAdapter.extend({
  namespace: 'todo-app'
});

 

[wr_heading tag=”h5″ text_align=”inherit” heading_margin_top=”25″ heading_margin_bottom=”25″ font=”inherit” enable_underline=”yes” border_bottom_style=”solid” appearing_animation=”0″ disabled_el=”no” ] Przygotowanie modelu[/wr_heading]

Mamy już przygotowany adapter dla naszej aplikacji, przygotujmy zatem model, który będzie opisywał poszczególne zadania. Do wygenerowania modelu wykorzystamy oczywiście ember-cli. Podczas generowania modelu, mamy możliwość zdefiniowania właściwości oraz ich domyślnych typów. Dla naszego modelu pojedynczego zadania (task) potrzebujemy dwóch właściwości:
– description: właściwość typu ‚String’ przechowująca treść (opis) zadania
– isCompleted: właściwość typu boolowskiego (true/false) przechowująca status zadania. W przypadku zadania zakończonego właściwość ‚isCompleted’ będzie ustawiona na ‚true’.

Wpisujemy zatem w konsoli:

ember generate model task description:string isCompleted:boolean

[wr_heading tag=”h3″ text_align=”inherit” heading_margin_top=”5″ heading_margin_bottom=”25″ font=”inherit” enable_underline=”yes” border_bottom_style=”solid” appearing_animation=”0″ disabled_el=”no” ]Implementacja komponentów[/wr_heading]

[wr_heading tag=”h5″ text_align=”inherit” heading_margin_top=”25″ heading_margin_bottom=”25″ font=”inherit” enable_underline=”yes” border_bottom_style=”solid” appearing_animation=”0″ disabled_el=”no” ]Komponent ‚new-task’ – dodawanie nowego zadania[/wr_heading]

Zgodnie z założeniami w Ember 2.0, komponenty są samodzielnymi, niezależnymi obiektami. Aby zachować te założenia każdy komponent wygenerujemy w strukturze POD.

Czym jest POD?
Podczas generowania elementów przy użyciu ember-cli zostaje on wygenerowany domyślnie w odpowiadającym mu folderze. Oznacza to, że przy wygenerowaniu komponentu ‚new-task’, kod JS, w którym będzie kod komponentu zostanie wygenerowany w folderze app/components jako new-task.js, natomiast template (widok) tego komponentu w folderze app/templates/components jako new-task.hbs. Struktura POD pozwala na wygenerowanie całego elementu (plik JS, widok etc.) w jednym miejscu. Wygenerowanie komponentu w ten sposób pozwoli zachować charakter jego niezależności.

Wygenerujemy zatem komponent new-task. Uwaga: nazwy komponentów generowanych w Ember muszą składać się z minimum dwóch wyrazów, oraz każdy z nich musi być oddzielony myślnikiem (tzw. kebab-case).

ember generate component new-task --pod

Ember wygeneruje plik JS w folderze app/components/new-task/component.js oraz plik template w folderze app/component/new-task/template.hbs.

Przygotujmy teraz szablon naszego komponentu:

 

<!-- app/components/new-task/template.hbs -->
<!-- ----- -->

<div class="ui right labeled fluid action input">
  <div class="ui label">New task</div>
  {{input value=newTask type="text" class="form-control" placeholder="wpisz nowe zadanie ..." enter="addNewTask"}}
  <button {{action 'addNewTask' newTask}} class="ui green button" type="button">Add</button>
</div>

 

 
Pole ‚input’ wpisujemy korzystając {{ }} aby dane wpisane w to pole można było przekazać do metody dodania nowego zadania. Odpowiedzialny za to jest atrybut value pola ‚input’. W tym przypadku wartością pola value jest ‚newTask’, co oznacza, że pod tą nazwą zostanie przekazana każda wartość wpisana w pole ‚input’. Do elementu ‚button’ dodajemy akcję o nazwie ‚addNewTask’, która wywoła metodę addNewTask w komponencie po naciśnięciu przycisku. Aby do wywoływanej funkcji przekazać argument należy go dodać po nazwie akcji. W tym przypadku przekazujemy wartość wpisaną w pole ‚input’. Zgodnie z założeniami projektu, chcemy aby po naciśnięciu ‚Enter’ zadanie zostało również dodane do listy zadań. W związku z tym w polu ‚input’ dodajemy również akcję ‚addNewTask’ jako wartość parametru ‚enter’. W tym przypadku tekst wpisany w polu ‚input’ zostanie automatycznie przekazany do wywoływanej funkcji jako argument.

Czas na kod odpowiedzialny za logikę komponentu.

Zacznijmy od zależności. Ze względu na to, że komponenty są z założenia zamkniętymi obiektami, nie mają bezpośredniego dostępu do modelu oraz do magazynu Ember Data. Musimy zatem ‚wstrzyknąć’ service ‚store’ (magazyn Ember Data) do komponentu. Odpowiedzialny za to jest kod: store : Ember.inject.service('store'). Od tego momentu, magazyn Ember Data będzie dostępny dla komponentu pod właściwością store.

 

// app/components/new-task/component.js
// -----

import Ember from 'ember';

export default Ember.Component.extend({
  // DI
  store  : Ember.inject.service('store'),

});

 

 

Zaimplementujmy teraz naszą akcję dodawania nowego zadania. Wszystkie akcję w komponentach Ember należy dodać w obiekcie actions. Dodajmy ją zatem:

 

// app/components/new-task/component.js
// -----

import Ember from 'ember';

export default Ember.Component.extend({
  // DI
  store  : Ember.inject.service('store'),

  // events / actions
  actions : {
    addNewTask() {

    }
  }
});

 

 

Zgodnie z założeniami, po wywołaniu tej akcji do listy zadań powinno zostać dodane nowe zadanie. Musimy zatem do funkcji przekazać argument, który będzie zawierał tekst wpisany w polu ‚input’ komponentu, następnie musimy utworzyć nowy rekord w magazynie Ember Data i go zapisać. Dodatkowo, zabezpieczmy funkcję tak, aby rekord nie został dodany i zapisany w przypadku przekazania pustej wartości z pola ‚input’.

 

// app/components/new-task/component.js
// -----

import Ember from 'ember';

export default Ember.Component.extend({
  // DI
  store : Ember.inject.service('store'),

  // events / actions
  actions : {
    addNewTask(newTask) {
      // jeżeli długość wpisanego tekstu jest większa od 0
      if(newTask.length > 0) {
        // utwórz nowy rekord w modelu 'task' dla magazynu danych
        // i przypisz go do zmiennej
        let new_task = this.get('store').createRecord('task', {
          description : newTask,
          isCompleted : false
        });

        // zapisz nowy rekord
        new_task.save()
          .then(() => {
            // następnie wyczyść wartość z pola 'input'
            this.set('newTask', '');
          });
      }
    }
  }
});

 

 

Pierwszy komponent został utworzony. Musimy go teraz dodać do szablonu naszej aplikacji. Utwórzmy domyślną ścieżkę ‚index’ i dodajmy do niej nasz utworzony komponent.

ember generate route index

 

<!-- app/templates/index.hbs -->
<!-- ----- -->

<!-- new-task -->
{{new-task}}
<!-- /new-task -->

 

 

Dodatkowo do utworzonej ścieżki przekazujemy magazyn z Ember Data jako model.

 

// app/routes/index.js
// -----

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.store.findAll('task');
  }
});

 

 

Gotowe! 🙂 Otwórzmy przeglądarkę i wpiszmy adres podany podczas wywołania komendy: ember serve. Komponent dodawania nowego zadania wyświetla się w aplikacji. Sprawdźmy jednak, czy zadania dodają się do Ember Data. W tym celu odpalmy ‚Ember Inspector’ i przechodzimy do zakładki ‚Data’. Widzimy, że nasz model task jest widoczny. Po wpisaniu tekstu w polu ‚input’ i kliknięciu przycisku ‚Add’ lub ‚Enter’ na klawiaturze, zadanie zostaje dodane do magazynu danych Ember i tym samym do Local Storage.

 

ember-todo-first-example
Komponent new-task oraz uruchomiony Ember Inspector.

Utworzyliśmy na pierwszy w pełni funkcjonalny komponent w Ember 2.0. Wykorzystaliśmy Ember Data do przechowywania danych i adapter Local Storage aby zapisywać dane w lokalnym magazynie przeglądarki. Na tym kończę pierwszy wpis dotyczący stworzenia aplikacji „TODO”. W kolejnych wpisach zajmiemy się utworzeniem komponentu do wyświetlania listy zadań oraz do komponentu do zarządzania zadaniami.