🎃

Angular で作る todo list

2022/03/03に公開

Angular の環境構築

Angular の環境構築には、 ng コマンドを利用するのが便利です。

npm install -g @angular/cli

ng コマンドが利用可能になったら、 ng new でアプリケーションのプロジェクトを作成可能です。

ng new my-app

いくつか選択肢が表示されますが、よくわからない場合には以下のような形で作成しておくのがおすすめです。

? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS   [ https://sass-lang.com/docum
entation/syntax#scss

画面の作成

とりあえず画面を作成していくために、CSS のセットアップを始めましょう。
今回は tailwind を利用するため、以下のページの通りでサイトのセットアップを進めましょう。

https://tailwindcss.com/docs/guides/angular

今回は画面内でフォーム要素を取り扱うため、
以下のサイトで配布されている、tailwindの フォームプラグインを利用します。
https://github.com/tailwindlabs/tailwindcss-forms

インストール時にエラーが出る場合は、
以下のような形でバージョンを指定すると良いでしょう。

npm install -D @tailwindcss/forms@0.3

レイアウトの作成

まずはアプリケーションの基本の画面を作成してみましょう。
src/app/app.component.html

<div class="w-96 mx-auto">
    <div class="border-b-2 p-2 ">
    <span class="font-bold text-gray-400">
      Angular Todolist
    </span>
    </div>
    <div class="px-2">
        <router-outlet></router-outlet>
    </div>
</div>

画面の要素が正しく中央寄せで、ヘッダがレイアウトされていれば tailwind の設定はバッチリです。

トップページの追加

アプリケーションでは様々なページを用意するため、まずはトップのページをルートに追加しましょう。

Angular ではページはコンポーネントを用いて実装します。
コンポーネントファイルは、ng generate コマンドで自動生成することができます。

ng generate component top

上記のコマンドを実行すると src/app フォルダに top フォルダが生成され、
コンポーネントのファイル一式が自動生成されます。

またコンポーネント利用に必要なモジュールへの登録も自動的に app.module.ts に追加されます。

今回 ページ要素のコンポーネントは views フォルダにまとめたいので、
src/app/views フォルダを作成し、生成された top フォルダをそちらに移動させてください。
app.module.ts のパスも合わせて書き換えるようお願いします。

ルートへの登録は、app-routing.module.ts に記述します。
空で用意されている routes 変数に以下のように ルート定義を追加します。

//import ...
import {TopComponent} from "./views/top/top.component";
// ...
const routes: Routes = [
  {path: "", component: TopComponent},
];

ルートが登録できたら、top.component.htmlを編集してページを実装します。

<div class="my-4">
  ようこそ、タスク管理ツールへ!
</div>

<div class="my-4 py-2 text-right">
  <a routerLink="/form" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
    タスクの追加
  </a>
</div>

<div class="divide-y">
  <div class="p-2">
    牛乳を買いに行く
  </div>
  <div class="p-2">
    牛乳を買いに行く
  </div>
  <div class="p-2">
    牛乳を買いに行く
  </div>
</div>

トップページにアクセスして画面が表示されればOKです!

登録画面を作る

同様にして、 form コンポーネントを作成して、ルートに追加していきましょう。

ルートの定義は以下のようになるはずです。

import {TopComponent} from "./views/top/top.component";
import {FormComponent} from "./views/form/form.component";

const routes: Routes = [
  {path: "", component: TopComponent},
  {path: "form", component: FormComponent}
];

form.component.html は以下のようになります。

<div class="my-4">
  新しいタスクを登録しましょう。
</div>

<div>
  <div class="mb-2">
    <label for="" class="block">タイトル</label>
    <div>
      <input type="text" class="w-full" name="" id="">
    </div>
  </div>

  <div class="mb-2">
    <label for="" class="block">期日</label>
    <div>
      <input type="date" class="w-full" name="" id="">
    </div>
  </div>

  <div class="mb-2">
    <label for="" class="block">優先度</label>
    <div>
      <select name="" class="w-full">
        <option value="">High</option>
        <option value="">Normal</option>
        <option value="">Low</option>
      </select>
    </div>
  </div>

</div>

<div class="my-4 py-2 ">
  <a routerLink="/confirm" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
    タスクの追加!!
  </a>
</div>

todolist の動きを実装する

Angular におけるコンポーネントは、あくまで画面の描画ロジックに集中させるべきであり、
アプリケーション内のデータ保持に関しては一般的に サービスと呼ばれるコードが利用されます。

サービスも ng generate で以下のように作成することができます。

ng generate service service/todo 

作成された src/app/service/todo.service.ts は以下のように定義して、
デフォルトのタスクを3つ登録しておきましょう。

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class TodoService {

  todolist = [
    {title:"牛乳を買いに行く",expire_at:"2022/01/05",priority:"high"},
    {title:"本を読み切る",expire_at:"2022/01/05",priority:"high"},
    {title:"部屋を片付ける",expire_at:"2022/01/05",priority:"normal"},
  ]

  constructor() { }

  add() {
    this.todolist.push({
      title: "新しいタスク", expire_at: "2022/01/05", priority: "high"
    })
  }
}

サービスからデータを読み込む

コンポーネントで このサービスを利用するため、
src/app/views/top/top.component.ts を以下のように書き換えます。

export class TopComponent implements OnInit {

  constructor(public todos: TodoService) {  }
  // ...
}

top.component.html のリスト部分を以下のように ngFor を利用した記述に切り替えると、
画面内にサービス内で定義したデータが表示されます。

<div class="divide-y">
  <div class="p-2" *ngFor="let todo of todos.todolist">
    {{todo.title}}
  </div>
</div>

サービスにデータを書き込む

次は、フォームからデータ登録できるようにしてみましょう。

フォーム側のコンポーネントに以下のように add 関数を追加して、
ダミーデータをサービスに追加できるようにします。

import {Router} from "@angular/router";
//...
export class FormComponent implements OnInit {

  constructor(
    private todos:TodoService,
    private router: Router
  ) { }
  // ...  
  add(){
    this.todos.add()
    this.router.navigateByUrl("/")
  }
}

form.component.html では以下のようにタスクの追加ボタンに add 関数を割り当てます。

<div class="my-4 py-2 ">
  <a (click)="add()" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
    タスクの追加
  </a>
</div>

ここまででフォームの「タスクの追加」をクリックすると、
トップ画面でデータが追加できるようになったのが確認できるでしょう。

データの処理をコンポーネントから分離しておくことは設計上とても重要です。
このような形でサービスを構築しておけば、あとから Firebase や Rest API に接続する際にも
コンポーネントに手を入れずにデータ処理を実装することができます。

フォームのバリデーション

データの流れが確認できたので、
バリデーションを追加して実際にフォームのデータを追加していきましょう。

Angular におけるバリデーションは以下の公式資料にその概要が記されています。
https://angular.jp/guide/form-validation

Angular ではテンプレート駆動とリアクティブフォームとの書き方が用意されていますが、
ここではリアクティブフォームの書き方で実装を進めていきます。
リアクティブフォームの実装の概要については以下の資料も確認しておきましょう。
https://angular.jp/guide/reactive-forms

リアクティブフォームの実装は、コンポーネントからスタートします。

FormGroup や FromControl を利用して、
src/app/views/form/form.component.ts は以下のようになります。

import { Component, OnInit } from '@angular/core';
import {TodoService} from "../../service/todo.service";
import {Router} from "@angular/router";
import {FormGroup, FormControl, Validators, AbstractControl} from '@angular/forms';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.scss']
})
export class FormComponent implements OnInit {

  todoForm = new FormGroup({
    title: new FormControl('',[
      Validators.required,
      Validators.maxLength(20),
    ]),
    expire_at: new FormControl('',[
      Validators.required
    ]),
    priority: new FormControl(null,[
      Validators.required
    ]),
  });

  get title() { return this.todoForm.get("title") as AbstractControl }
  get expire_at() { return this.todoForm.get("expire_at") as AbstractControl}
  get priority() { return this.todoForm.get("priority") as AbstractControl}

  constructor(
    private todos:TodoService,
    private router: Router
  ) { }

  ngOnInit(): void {
  }

  add(){
    Object.values(this.todoForm.controls).forEach((ctrl)=>{
      ctrl.markAsDirty()
    })
    if(this.todoForm.status === "INVALID"){
      alert("フォームを入力して下さい")
      return
    }
    this.todos.add(this.todoForm.value)
    this.router.navigateByUrl("/")
  }
}

対応する html ファイルは以下のように実装できます。

<div class="my-4">
  新しいタスクを登録しましょう。
</div>

<div>
  <form [formGroup]="todoForm">
    <div class="mb-2">
      <label class="block">タイトル</label>
      <div>
        <input type="text" class="w-full" name="" formControlName="title">
      </div>
      <div class="text-red-600" *ngIf="title.invalid && title.dirty">
        <div *ngIf="title.hasError('required')">必須項目です。</div>
        <div *ngIf="title.hasError('maxlength')">20文字以内で入力してください。</div>
      </div>
    </div>

    <div class="mb-2">
      <label class="block">期日</label>
      <div>
        <input type="date" class="w-full" name="" formControlName="expire_at">
      </div>
      <div class="text-red-600" *ngIf="expire_at.invalid && expire_at.dirty">
        <div *ngIf="expire_at.hasError('required')">必須項目です。</div>
      </div>
    </div>

    <div class="mb-2">
      <label class="block">優先度</label>
      <div>
        <select name="" class="w-full" formControlName="priority">
          <option [value]="null">選択してください</option>
          <option value="high">High</option>
          <option value="normal">Normal</option>
          <option value="low">Low</option>
        </select>
        <div class="text-red-600" *ngIf="priority.invalid && priority.dirty">
          <div *ngIf="priority.hasError('required')">必須項目です。</div>
        </div>
      </div>
    </div>
  </form>

</div>

<div class="my-4 py-2 ">
  <a (click)="add()" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
    タスクの追加
  </a>
</div>

Discussion