中安拓也のブログ

中安拓也がプログラミングについて書くブログ

【Angular】Akita学習(2) - TODOアプリ作成

f:id:l08084:20201010181001p:plain

はじめに

前回の記事に引き続き、状態管理ライブラリのAkitaについて学習していきます。今回は、AkitaのEntityStoreを使用して、TODOアプリを作成します。

環境

  • Angular: 8.2.14
  • @datorama/akita: 5.2.4

$ ng versionの実行結果

Angular CLI: 8.3.29
Node: 12.13.1
OS: darwin x64
Angular: 8.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.803.29
@angular-devkit/build-angular     0.803.29
@angular-devkit/build-optimizer   0.803.29
@angular-devkit/build-webpack     0.803.29
@angular-devkit/core              8.3.29
@angular-devkit/schematics        8.3.29
@angular/cli                      8.3.29
@ngtools/webpack                  8.3.29
@schematics/angular               8.3.29
@schematics/update                0.803.29
rxjs                              6.4.0
typescript                        3.5.3
webpack                           4.39.2

TODOアプリ作成

データベースのテーブルのようなデータ構造を持っているEntityStoreを使用して、TODOアプリを作成します。

EntityStoreには、通常のStoreと違ってselectAlladdremoveなどのメソッドが用意されています。

Model

まずは、Modelから作成していきます。Modelはデータベースでいうところのテーブルの構造にあたります。

todo.model.ts

import { ID } from '@datorama/akita';

export interface Todo {
  id: ID;
  title: string;
}

export function createTodo(params: Partial<Todo>) {
  return {} as Todo;
}

EntityStore

TODOの状態を保持するStoreです。今回はEntityStoreを採用しています。

todo.store.ts

import { Injectable } from '@angular/core';
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
import { Todo } from './todo.model';

export interface TodoState extends EntityState<Todo> {}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'todo' })
export class TodoStore extends EntityStore<TodoState> {
  constructor() {
    super();
  }
}

Query

TODOの取得機能を提供しているQueryです。

todo.query.ts

import { Injectable } from '@angular/core';
import { QueryEntity } from '@datorama/akita';
import { TodoState, TodoStore } from './todo.store';

@Injectable({ providedIn: 'root' })
export class TodoQuery extends QueryEntity<TodoState> {
  constructor(protected store: TodoStore) {
    super(store);
  }
}

Service

addTodoメソッドとremoveTodoメソッドでTODOの追加と削除の機能を提供しています。

todo.service.ts

import { Injectable } from '@angular/core';
import { guid, ID } from '@datorama/akita';
import { TodoStore } from './todo.store';
@Injectable({ providedIn: 'root' })
export class TodoService {
  constructor(private store: TodoStore) {}

  /**
   * TODOを追加する
   *
   * @param {string} title
   * @memberof TodoService
   */
  addTodo(title: string) {
    this.store.add({
      id: guid(),
      title,
    });
  }

  /**
   * TODOを削除する
   *
   * @param {ID} id
   * @memberof TodoService
   */
  removeTodo(id: ID) {
    this.store.remove(id);
  }
}

Component

TODO画面のコンポーネントクラスです。QueryのselectAllメソッドでTODOの全量を取得し、Serviceを経由してTODOの追加と削除を行っています。

todo.component.ts

import { TodoState } from './state/todo.store';
import { Component, OnInit } from '@angular/core';
import { getEntityType, ID } from '@datorama/akita';
import { Observable } from 'rxjs';
import { TodoService } from './state/todo.service';
import { TodoQuery } from './state/todo.query';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.scss'],
})
export class TodoComponent implements OnInit {
  readonly allTodo$: Observable<getEntityType<TodoState>[]>;
  todoFormGroup: FormGroup;
  titleControl: FormControl;

  constructor(
    private service: TodoService,
    private query: TodoQuery,
    private fb: FormBuilder
  ) {
    this.allTodo$ = this.query.selectAll();
    this.todoFormGroup = this.fb.group({
      title: ['', []],
    });
    this.titleControl = this.todoFormGroup.get('title') as FormControl;
  }

  ngOnInit() {}

  /**
   * TODOを追加する
   *
   * @memberof TodoComponent
   */
  addTodo() {
    this.service.addTodo(this.titleControl.value);
  }

  /**
   * TODOを削除する
   *
   * @param {ID} id
   * @memberof TodoComponent
   */
  removeTodo(id: ID) {
    this.service.removeTodo(id);
  }
}

todo.component.html

<div class="todo-area">
  <form [formGroup]="todoFormGroup">
    <mat-form-field>
      <input matInput placeholder="TODOタイトル" formControlName="title" />
    </mat-form-field>
  </form>
  <button (click)="addTodo()" mat-raised-button color="primary">
    Add TODO
  </button>
</div>
<ul>
  <li *ngFor="let todo of allTodo$ | async">
    {{ todo.title }}
    <button (click)="removeTodo(todo.id)" mat-button color="warn">Remove</button>
  </li>
</ul>

実際にTODOアプリを動作させる

上記のコードを動作させると、下記のようなアプリになります。Add TODOボタンを押下するとTODOが追加され、Removeボタンを押下すると、TODOが削除されます。

f:id:l08084:20201010185612p:plain
TODOアプリ

参考サイト

リストレンダリング — Vue.js

TypeScript特有の組み込み型関数 - log.pocka.io

Akita | Reactive State Management

Netanel Basal – Datorama Engineering

Angular向け状態管理ライブラリAkitaの紹介 - Qiita

Angularのシンプルな状態管理ライブラリ Akita について - Qiita

Akita🐶でがんばる状態管理 - Qiita

素朴な Angular 向けの type safe で immutable な Flux Store - Qiita