Angularの状態管理ライブラリ "ngxs" の紹介

## ngxsとは
ngxsとは，Angularでreduxを利用するためのライブラリです．
従来の ngrx などのライブラリでもreduxを利用することが出来ましたが，ngxsでは従来のライブラリが抱えていた「大量のボイラープレートを書く必要がある」という問題点を解決しています．

本記事では，簡単なサンプルアプリケーションを実装することで，その利用方法や，従来のライブラリとの違いを紹介します．

なお，今回実装するアプリケーションの完全なコードは，GitHubで見ることが出来ます．
https://github.com/musou1500/ngxs-todo-example

## 実装するアプリケーションの概要

以下の3つの機能を持つ，シンプルなTODOアプリケーションを実装します．

* タスクの一覧表示
* タスクの追加
* タスクの完了

![Screenshot from 2018-08-21 07-09-28.png](https://qiita-image-store.s3.amazonaws.com/0/28859/980463fe-13c5-6de9-ec64-db428fd17bc2.png)


## プロジェクトの新規作成
プロジェクトの新規作成と，利用するライブラリ一式のインストールを済ませておきます．
今回はngxsに加え，CSSフレームワークの bulma を利用します．

```bash
$ ng new try-ngxs && cd try-ngxs
$ npm i -S bulma @ngxs/store
```

bulmaのスタイルを利用できるように， `angular.json` の `projects.try-ngxs.architect.build.styles` を以下のように編集しておきます．

```angular.json
"styles": [
  "src/styles.css",
  "node_modules/bulma/css/bulma.css"
],
```

## 必要なコンポーネントを実装する
ここで実装するコンポーネントは，reduxには関与しない，いわゆる presentational component と呼ばれるコンポーネントです．
今回は，以下の2つのコンポーネントを実装します．

* タスクリスト
* タスクの追加フォーム

### タスクリスト
タスクリストは以下のようになります．
それぞれのタスクのタイトルとチェックボックスを表示し，チェックされると，チェックされたタスクのIDを持ったイベントを発火します．

```src/app/components/task-list.ts
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { Task } from "../models";

@Component({
  selector: "task-list",
  templateUrl: "./task-list.html"
})
export class TaskList {
  @Input()
  tasks: Task[] = [];

  @Output()
  done: EventEmitter<number> = new EventEmitter();
}
```

```src/app/components/task-list.html
<ul>
  <li *ngFor="let task of tasks">
    <label class="checkbox">
      <input type="checkbox" [disabled]="task.done" (change)="done.emit(task.id)" [checked]="task.done">
      {{ task.title }}
    </label>
  </li>
</ul>
```

### タスクの追加フォーム
タスクの追加フォームは以下のようになります．
入力フォームと追加ボタンを持ち，追加ボタンが押されると，入力されたタスクのタイトルを持ったイベントを発火します．

``` src/app/components/task-form.ts
import { Component, Output, EventEmitter } from "@angular/core";

@Component({
  selector: "task-form",
  templateUrl: "./task-form.html"
})
export class TaskForm {
  title: string;

  @Output()
  submit: EventEmitter<string> = new EventEmitter();

  clearAndSubmit() {
    this.title = '';
    this.submit.emit(this.title);
  }
}
```

``` src/app/components/task-form.html
<div class="field has-addons">
  <p class="control">
    <input [(ngModel)]="title" class="input" type="text" placeholder="task title" required>
  </p>
  <p class="control">
    <button class="button is-primary" (click)="clearAndSubmit()" [disabled]="!nameField.valid">
      add
    </button>
  </p>
</div>
```

## TodoServiceクラスを実装する
このままではタスクの取得や追加が実装できないので，タスク一覧を保持し，追加や完了を行う `TodoService` クラスを実装します．

```src/app/services/todo-service.ts
import { Task } from "../models";

export class TodoService {
  private mockData: Task[] = [
    {
      id: 1,
      title: "Learn ngrx",
      done: false
    },
    {
      id: 2,
      title: "Learn Dart",
      done: false
    },
    {
      id: 3,
      title: "move ionic v1 into ionic v2",
      done: false
    }
  ];

  async findAll() {
    return [...this.mockData];
  }

  async add(title: string) {
    const taskToAdd = {
      title,
      done: false,
      id: this.mockData[this.mockData.length - 1].id + 1
    };
    this.mockData.push(taskToAdd);
    return taskToAdd;
  }

  async done(taskId: number) {
    for (let task of this.mockData) {
      if (task.id === taskId) {
        task.done = true;
        return;
      }
    }
  }

  async remove(taskId: number) {
    const taskIdx = this.mockData.findIndex(task => task.id === taskId);
    if (taskIdx > -1) {
      this.mockData.splice(taskIdx, 1);
    }
  }
}
```

## アクションを実装する
従来のライブラリでも同様ですが，ngxsでは，発火させたアクションに反応し，アプリケーションの状態を変化させる．という流れを踏みます．
サンプルアプリケーションの仕様を考慮すると，

* タスクの読み込み
* タスクの追加
* タスクの完了

という3つのアクションに対応し，状態を変化させます．
ngxsでは，アクションはシンプルなJavaScriptのクラスで実装できます．
アクションにデータを持たせたい場合，単にプロパティとして持たせます．

```src/app/app-state.ts
export class FindAllTasks {
  static readonly type = "[Tasks] FindAll";
}

export class AddTask {
  static readonly type = "[Tasks] AddTask";
  constructor(public title: string) {}
}

export class DoneTask {
  static readonly type = "[Tasks] DoneTask";
  constructor(public taskId: number) {}
}

```


## 状態を定義する
次に，アプリケーションの状態を定義します．
アクションに対応する処理などは，このクラスに実装していくことになります．

``` src/app/app-state.ts
import { State } from "@ngxs/store";
import { Task } from "./models";

export interface TasksStateModel {
  tasks: Task[];
}

@State<TasksStateModel>({
  name: "tasks",
  defaults: {
    tasks: []
  }
})
export class TasksState {}
```

## アクションに対応する処理を実装する

``` src/app/app-state.ts
import { State } from "@ngxs/store";
import { Task } from "./models";

export interface TasksStateModel {
  tasks: Task[];
}

@State<TasksStateModel>({
  name: "tasks",
  defaults: {
    tasks: []
  }
})
export class TasksState {}
```


## コンポーネントを表示させる
実装したコンポーネントを表示させてみます．
`src/app/app.component.ts` を以下のように書き換えてください．

```src/app/app.component.ts
import { State, Action, StateContext, Selector } from "@ngxs/store";
import { Task } from "./models";
import { TodoService } from "./services/todo-service";

@State<TasksStateModel>({
  name: "tasks",
  defaults: {
    tasks: []
  }
})
export class TasksState {
  constructor(public todoService: TodoService) {}
  @Selector()
  static getTasks(state: TasksStateModel) {
    return state.tasks;
  }

  @Action(FindAllTasks)
  async findAllTasks(ctx: StateContext<TasksStateModel>) {
    const tasks = await this.todoService.findAll();
    ctx.patchState({ tasks });
  }

  @Action(DoneTask)
  async doneTask(ctx: StateContext<TasksStateModel>, doneTaskAction: DoneTask) {
    await this.todoService.done(doneTaskAction.taskId);
    ctx.patchState({
      tasks: ctx.getState().tasks.map(t => t.id === doneTaskAction.taskId ? { ...t, done: true } : t)
    });
  }

  @Action(AddTask)
  async addTask(ctx: StateContext<TasksStateModel>, addTaskAction: AddTask) {
    const task = await this.todoService.add(addTaskAction.title);
    console.log(task, ctx.getState().tasks);
    ctx.patchState({
      tasks: [
        ...ctx.getState().tasks,
        task
      ]
    });
  }

}

```

```src/app/app.component.html
<div class="container">
  <section class="section">
    <task-form (submit)="add($event)"></task-form>
    <task-list
      [tasks]="tasks"
      (done)="done($event)"
      *ngIf="tasks$ | async as tasks"></task-list>
  </section>
</div>
```