ngxsとは
ngxsとは,Angularでreduxを利用するためのライブラリです.
従来の ngrx などのライブラリでもreduxを利用することが出来ましたが,ngxsでは従来のライブラリが抱えていた「大量のボイラープレートを書く必要がある」という問題点を解決しています.
本記事では,簡単なサンプルアプリケーションを実装することで,その利用方法や,従来のライブラリとの違いを紹介します.
なお,今回実装するアプリケーションの完全なコードは,GitHubで見ることが出来ます.
https://github.com/musou1500/ngxs-todo-example
実装するアプリケーションの概要
以下の3つの機能を持つ,シンプルなTODOアプリケーションを実装します.
- タスクの一覧表示
- タスクの追加
- タスクの完了
プロジェクトの新規作成
プロジェクトの新規作成と,利用するライブラリ一式のインストールを済ませておきます.
今回はngxsに加え,CSSフレームワークの bulma を利用します.
$ ng new try-ngxs && cd try-ngxs
$ npm i -S bulma @ngxs/store
bulmaのスタイルを利用できるように, angular.json
の projects.try-ngxs.architect.build.styles
を以下のように編集しておきます.
"styles": [
"src/styles.css",
"node_modules/bulma/css/bulma.css"
],
必要なコンポーネントを実装する
ここで実装するコンポーネントは,reduxには関与しない,いわゆる presentational component と呼ばれるコンポーネントです.
今回は,以下の2つのコンポーネントを実装します.
- タスクリスト
- タスクの追加フォーム
タスクリスト
タスクリストは以下のようになります.
それぞれのタスクのタイトルとチェックボックスを表示し,チェックされると,チェックされたタスクのIDを持ったイベントを発火します.
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();
}
<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>
タスクの追加フォーム
タスクの追加フォームは以下のようになります.
入力フォームと追加ボタンを持ち,追加ボタンが押されると,入力されたタスクのタイトルを持ったイベントを発火します.
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);
}
}
<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
クラスを実装します.
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のクラスで実装できます.
アクションにデータを持たせたい場合,単にプロパティとして持たせます.
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) {}
}
状態を定義する
次に,アプリケーションの状態を定義します.
アクションに対応する処理などは,このクラスに実装していくことになります.
import { State } from "@ngxs/store";
import { Task } from "./models";
export interface TasksStateModel {
tasks: Task[];
}
@State<TasksStateModel>({
name: "tasks",
defaults: {
tasks: []
}
})
export class TasksState {}
アクションに対応する処理を実装する
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
を以下のように書き換えてください.
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
]
});
}
}
<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>