TypeScript
angular
angular-cli

Angularで簡易todoアプリを作ってみる

はじめに

オープンストリーム Advent Calendar 2017 最終日の記事になります。

入社一年目の初案件で Angular + Cordova の iOSアプリ作成に携わりました。その時のトレーニング期間で簡単な TODOアプリを作りましたので、復習もかねて 再度AngularでTodoアプリを作成していこうと思います。
(あまり技術的な話はできませんので気休め程度に読み流してください。。)

Angular-Cli 導入

Angular-Cli をインストールします。
Angular-Cli は公式のコマンドラインツールであり、これ一つで簡単に開発環境を用意することができます。
案件のときは Mac でしたが今回は自分の WindowsPC で作成してしていきます。
windows での導入は以下のリンクを参考に導入しました。

Windows は導入の手間がかかりますね。。。Mac ほしい

雛形作成

コマンドを叩いて Angular の 雛形アプリを作成します。

$ ng new simple-todo-app --style=scss --routing

このコマンドを叩くことで Angular-Cli が必要なファイルを作成してくれます。簡単ですね。
今回はオプションで スタイルの記述方法を scss に指定し、予め RouterModule( SPA でゴリゴリページ遷移するための公式モジュール)を用意するようにしています。
ngnewapp.png

アプリを動かす場合は以下のコマンドを叩きます。 ブラウザで localhost:4200 で確認できます

$ ng serve 

angularserve.png

ngx-bootstrap 導入

レイアウトを bootstrap をつかって構築していきたいので、
bootstrap の機能を ディレクテイブで利用できる ngx-bootstrap を導入します。

todo モジュール作成

これから作成する todo 機能をもたせるための module を作っていきます。

  • まずは、angular-cli で todo 用の module、component の雛形を作成します。
// --routing オプションで routing.module.ts も同時に生成してくれます
$ ng generate module todo --routing
$ ng generate component todo/todo

上記のコマンドを叩くことでsrc/app/todo以下のファイルを自動で生成してくれます。
component を生成する場合、最も近い階層にある module.ts に 生成した component を自動で追記してくれます

todomodule.png

  • app.module.tstodo.module.ts を追加します
app.module.ts
import { TodoModule } from './todo/todo.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    TodoModule   //<-- imports に追記
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

ルーティング設定

todoComponent の内容を表示できるようにルーティングを設定します。

todo-routing.module.ts の route を以下のように定義します。これで /todo で todoComponent に遷移できます。

todo-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { TodoComponent } from './todo/todo.component';

// routes に path と component のペアを定義する
const routes: Routes = [
  { path: 'todo', component: TodoComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class TodoRoutingModule { }

todo 機能実装

ベースが出来たので todo機能を実装していきます。

  • 今回はサーバー側は用意せずクライアント側でデータを保持するようにします。
    • サーバ側も構築してみたいですが、それは後々やりたいと思います。多分
  • 一つのアイテムに 「タイトル」「詳細」「日時」 を入力できるようにします。
  • todoリストに対して、「完了/未完了」 の状態の切り替え および「削除」ができるようにします。

ベースは作成しているので、あとは todo.module.ts, todo.component.ts, todo.component.ts の3つのファイルにゴリゴリ追記していきます。

todo.module.ts

todo.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';

import { CollapseModule } from 'ngx-bootstrap/collapse';
import { AccordionModule } from 'ngx-bootstrap/accordion';

import { TodoRoutingModule } from './todo-routing.module';
import { TodoComponent } from './todo/todo.component';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,             
    ReactiveFormsModule,
    CollapseModule.forRoot(),
    AccordionModule.forRoot(),
    TodoRoutingModule
  ],
  declarations: [TodoComponent]
})
export class TodoModule { }

imports に FormModule ~ AccordionModule.forRoot() の4行を追加しただけです。

module , component はそれぞれ imports,declarations に、追加することで、その module 内で追加した module,component の機能を利用することが出来ます。

todo.component.ts

todo.component.ts
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';

interface TodoItem {
  title: string;
  description: string;
  isComplete: boolean;
  date: any;
}

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

  // 詳細が表示されているか
  public hasDetail = false;

  // アイテムリスト
  public itemList: Array<TodoItem> = new Array<TodoItem>();

  // 入力フォーム
  public todoForm: FormGroup;

  constructor(
    protected formBuilder: FormBuilder
  ) { }

  ngOnInit() {
    this.createForm();
  }

  createForm(): void {
    // Form の作成と初期値設定をします。
    this.todoForm = this.formBuilder.group({
      title: ['',
        [
          Validators.required
        ]
      ],
      description: [''],
      date: [''],
      isComplete: [false]
    });
  }

  // todoItem を 保存します
  onSaveTodoItem(): void {
    const item: TodoItem = {
      title: this.todoForm.get('title').value,
      isComplete: false,
      description: null,
      date: null
    };

    if (this.hasDetail) {
      item.description = this.todoForm.get('description').value;
      item.date = this.todoForm.get('date').value;
    }

    this.itemList.push(item);
    this.clearForm();
    console.log(this.itemList);
  }

  // フォームの値をリセット
  clearForm(): void {
    this.todoForm.reset();
  }

  // 指定した要素を削除
  onDeleteItem(index: number): void {
    this.itemList.splice(index, 1);
  }
}

component.ts は主にロジックを書きますが、今回は機能が単純ですので
コードも短く済みました。

createForm()では ReactiveFormModule の機能を利用して、入力フォームを
一元管理するためのFormGroupオブジェクトを生成しています。

FormGroupは入力された値、バリデーション状態の取得や フォーム項目の追加・削除 などができ、非常に便利なオブジェクトです。

todo.component.html

todo.component.html
<h1>Todo App</h1>
<form [formGroup]="todoForm">
  <div class="form-group registItemArea">
    <div class="input-group">
      <input id="title" class="form-control" placeholder="Title" formControlName="title" />
      <span class="input-group-btn">
        <button class="btn btn-default" type="button" (click)="hasDetail = !hasDetail">詳細</button>
      </span>
    </div>
  </div>


  <div class="card card-block card-header" [collapse]="!hasDetail">
    <div class="form-group">
      <label for="description">
        <strong>詳細:</strong>
      </label>
      <textarea id="text" class="form-control" formControlName="description"></textarea>
    </div>

    <div class="form-group">
      <label for="date">
        <strong>日付:</strong>
      </label>
      <input type="date" class="form-control" formControlName="date" />
    </div>
  </div>

</form>
<div class=" row ">
  <div class="col-sm-2 ">
    <button class="createBtn btn btn-success " [disabled]="this.todoForm.invalid" (click)="onSaveTodoItem()">作成</button>
  </div>
</div>
<hr>
<div class="row itemListArea ">
  <accordion class="col-sm-12">
    <accordion-group *ngFor="let item of itemList; let i=index;">
      <div accordion-heading [class.isComplete]="item.isComplete">
        {{item.title}}
      </div>
      <div>
        <strong>詳細:</strong>
        <pre>{{item.description ? item.description : "-"}}</pre>
      </div>
      <div>
        <strong>日付:</strong>
        <span>{{item.date ? item.date : "-"}}</span>
      </div>
      <div>
        <button class="btn btn-success" (click)="item.isComplete = !item.isComplete ">{{item.isComplete ? "未完了に戻す" : "完了にする"}}</button>
        <button class="btn btn-danger" (click)="onDeleteItem(i)">削除</button>
      </div>
    </accordion-group>
  </accordion>
</div>

久々に angular で html を書きましたが、テンプレートシンタックスが非常に便利ですね。
画面表示に関わる簡単な処理は html で記述できるので非常に楽でした

今回使ったテンプレートシンタックスは以下の通りです。

構文 詳細
{{ 式 }} 式の結果を文字列として出力できます。
*ngFor=”let 一時変数 of 式 ” HTML要素を繰り返し生成します
[class.クラス名]=”式” 式の値の真偽値によって指定クラスの追加、削除を行います
(イベント名) = ”式” 指定したイベントが発生した時、指定した式が呼び出されます
[プロパティ名] = ”式” html要素の指定したプロパティを式の値にセットします

作成したtodo アプリ

稚拙な出来ですみません。。
angular-todo-app.gif

まとめ

  • angular やっぱり楽しい
  • angular-cli で大抵のことはやってくれるので環境構築が楽でした。(実際の案件ではいろいろ設定いじる必要があるとは思いますが。。。)
    • また、誰が作っても基本的に構成が同じになるので分かりやすいですね。
  • 実装はロジックよりも bootstrap でレイアウト整えるのに時間がかかりました。。

今後について

  • サーバ側も用意して連携させたいです。
    • それに合わせて CURD 処理は service させる。今回はべた書きだったので><
      • DIが容易なのも angular の良いところだと思います。
  • 編集など、機能もいろいろ追加したい

後々に実装してこれも記事に出来れば、、と思います。

最後まで読んでいただきありがとうございました。