3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

プロジェクトの新規作成

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

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

bulmaのスタイルを利用できるように, angular.jsonprojects.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>
3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?