TypeScript + Angular

で作る Todo アプリ

About me

  • 桐生 達嗣(きりゅう たつし)
  •  
  • Technical Consulting Engineer
  • Front-End Developer
  • Social

作るもの

  • ToDoリスト表示
  • ToDoの
    • 編集
      • チェックONで完了済み
      • ダブルクリックで編集
      • Enterで確定
    • 削除
      • ゴミ箱ボタンクリック
        で削除
    • 追加
      • Enterで追加

作るもの

GitHubに雛形あります。

https://github.com/tkiryu/todo-app-seed

 

完成版

https://github.com/tkiryu/todo-app-complete

用意するもの

  • Visual Studio Code
  • ツール
    • Node.js  - v8.x.x 推奨
    • npm - v5.x.x 推奨

下準備

  • Angular CLI による Angular プロジェクトの作成
  • 動作確認
npm i -g @angular/cli
ng -v
cd <working directory>
ng new todo-app --style=scss
cd todo-app
ng serve -o

Angular 概念と基本

Angular の概念

  • Component と Module
  • Container component と Presentational component

Component と Module

  • Component = 部品
  • Module = 機能のカタマリ
    • Root Module = アプリケーション起動に必須
    • Feature Module = アプリケーションを機能ごとに分割、サードパーティ製のモジュール

Root Module

Feature Module

Feature Module

Component

Component

Component

Component

Component

Component

Container Component Presentational Component

  • Container Component はデータ・状態を扱う
  • Presentational Component は渡されたデータを表示

Container

Component

Presentational Component

Data

Flows Down

Event

Emits Up

Service

Angular の基本

  • Component
  • Binding
    • Interpolation & expression
    • Property binding
    • Event binding
  • Template reference
  • Custom Property and Event
    • @Input
    • @Output & EventEmitter
  • Directive
    • ngIf
    • ngFor

Component

XXXComponent

xxx.component.ts

xxx.component.html

xxx.component.scss

Component

// app.component.ts
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'Hello, app';
}
<!-- app.component.html -->
<h1>{{title}}</h1>

Interpolation & expression

<!-- Interpolation -->
<h1>{{ title }}</h1>

<!-- Expression -->
<span>{{ 1 + 1 }}</span>

{{ variable or expression }}

export class AppComponent {
  title = 'Hello, app';
}

Property binding

<input
    [value]="val"
    [disabled]="isDisabled">
<img [src]="url">

[property]="variable"

export class AppComponent {
  val = 'foo';
  isDisabled = true;
  url = 'path/to/image.png';
}

Event binding

<input (change)="onChange($event)">

<button (click)="onClick($event)">
    save
</button>

(event)="eventHander"

export class AppComponent {
  onChange(event) { console.log(event) }
  onClick(event) { console.log(event) }
}

Template reference

<input (change)="onChange(foo.value)" #foo>

<button (click)="onClick(foo.value)">
    save
</button>

#refName

export class AppComponent {
  onChange(value) { console.log(value) }
  onClick(value) { console.log(value) }
}

Custom Property - @Input

<foo [bar]="'baz'"></foo> // <h1>baz</h1>
@Component({
  selector: 'foo',
  templateUrl: './foo.component.html',
  styleUrls: ['./foo.component.scss']
})
export class FooComponent {
  @Input() bar: string;
}
<h1>{{ bar }}</h1>

Definition

Usage

Custom Event - @Output

@Component({
  selector: 'child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.scss']
})
export class ChildComponent {
  @Output() edit = new EventEmitter<string>();
  onChange(value: string) {
    this.edit.emit(value); // value = hoge
  }
}
<input (change)="onChange(input.value)" #input>

Definition

Custom Event - @Output

@Component({
  selector: 'parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.scss']
})
export class ParentComponent {
  handleEdit(event: string) {
    console.log(event); // hoge
  }
}
<child (edit)="handleEdit($event)"></child>

Usage

Custom Property - @Input

<foo bar="baz"></foo> // <h1>baz</h1>
@Component({
  selector: 'foo',
  templateUrl: './foo.component.html',
  styleUrls: ['./foo.component.scss']
})
export class FooComponent {
  @Input() bar: string;
}
<h1>{{ bar }}</h1>

NgIf

@Component({
  selector: 'foo',
  templateUrl: './foo.component.html',
  styleUrls: ['./foo.component.scss']
})
export class FooComponent {
  isError = true;
}
<input required>
<div *ngIf="isError">
  <p> This field is required </p>
</div>

NgFor

export class FooComponent {
  list = [
    { title: 'item1' },
    { title: 'item2' },
    { title: 'item3' }
  ];
}
<ul>
  <li *ngFor="let item of list">
    {{ item.title }}
  </li>
</ul>

ToDoアプリ実装

ToDoアプリの構成

AppModule

TodoComponent

TodoItemComponent

TodoItemComponent

TodoItemComponent

ToDoアプリ

  • ToDoリスト表示
  • ToDoの
    • 編集
      • チェックONで完了済み
      • ダブルクリックで編集
      • Enterで確定
      • ゴミ箱ボタンクリック
        で削除
    • 追加
      • Enterで追加

サードパーティモジュール追加

npm i igniteui-angular hammerjs
npm i @types/hammerjs -D

Terminal

Ignite UI for Angular

非商用利用であれば、無償で使えます!

サードパーティモジュール追加

import {
  IgxCheckboxModule,
  IgxButtonModule,
  IgxIconModule,
  IgxNavbarModule
} from 'igniteui-angular/main';
・・・
@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    IgxCheckboxModule,
    IgxButtonModule,
    IgxIconModule,
    IgxNavbarModule
    ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.module.ts

TodoComponent 生成

ng g component todo
ng g interface todo/todo-item

Terminal

export interface TodoItem {
  id: number;
  title: string;
  isDone: boolean;
}

todo-item.ts

TodoComponent 生成

import { Component, OnInit } from '@angular/core';
import { TodoItem } from './todo-item';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.scss']
})
export class TodoComponent implements OnInit {
  todoList: TodoItem[];
  ngOnInit() {
    this.todoList = [
      { id: 1, title: 'ToDo 1', isDone: false },
      { id: 2, title: 'ToDo 2', isDone: true },
      { id: 3, title: 'ToDo 3', isDone: false },
    ];
  }
}

todo.component.ts

TodoComponent 生成

<div *ngFor="let todo of todoList">
  {{ todo | json }}
</div>

todo.component.html

<app-todo></app-todo>

app.component.html

TodoComponent 生成

<div *ngFor="let todo of todoList" class="todoItem">

  <igx-checkbox class="todoItem__check"
    [checked]="todo.isDone">
  </igx-checkbox>

  <span class="todoItem__title">
    {{ todo.title }}
  </span>

  <button class="todoItem__remove"
    igxButton="icon">
    <igx-icon name="delete"></igx-icon>
  </button>

</div>

todo.component.html

TodoItemComponent 生成

ng g component todo/todo-item

Terminal

<app-todo-item *ngFor="let todo of todoList">
</app-todo-item>

todo.component.html

TodoItemComponent 生成

import { Component, OnInit, Input } from '@angular/core';
import { TodoItem } from '../todo-item';

@Component({
  selector: 'app-todo-item',
  templateUrl: './todo-item.component.html',
  styleUrls: ['./todo-item.component.scss']
})
export class TodoItemComponent implements OnInit {
  @Input() todo: TodoItem;
  ngOnInit() {
  }
}

todo-item.component.ts

TodoItemComponent 生成

<div class="todoItem">

  <igx-checkbox class="todoItem__check"
    [checked]="todo.isDone">
  </igx-checkbox>

  <span class="todoItem__title">
    {{ todo.title }}
  </span>

  <button class="todoItem__remove" igxButton="icon">
    <igx-icon name="delete"></igx-icon>
  </button>

</div>

todo-item.component.html

TodoItemComponent

<div class="todo__list">
  <app-todo-item *ngFor="let todo of todoList"
    [todo]="todo">
  </app-todo-item>
</div>

todo.component.ts

ToDoアプリ

  • ToDoリスト表示
  • ToDoの
    • 編集
      • チェックONで完了
      • ダブルクリックで開始
      • Enterで確定
      • ゴミ箱ボタンクリック
        で削除
    • 追加
      • Enterで追加

ToDo の編集 - チェックON

<div class="todoItem">

  <igx-checkbox class="todoItem__check"
    [checked]="todo.isDone"
    (change)="onCheckChange(check.checked)" #check>
  </igx-checkbox>

  <span class="todoItem__title">
    {{ todo.title }}
  </span>

  <button class="todoItem__remove" igxButton="icon">
    <igx-icon name="delete"></igx-icon>
  </button>

</div>
<div>{{ todo | json }}</div>

todo-item.component.html

ToDo の編集 - チェックON

import { Component, OnInit, Input } from '@angular/core';
import { TodoItem } from '../todo-item';

@Component({
  selector: 'app-todo-item',
  templateUrl: './todo-item.component.html',
  styleUrls: ['./todo-item.component.scss']
})
export class TodoItemComponent implements OnInit {
  @Input() todo: TodoItem;
  ngOnInit() {
  }
  onCheckChange(event: boolean) {
    this.todo.isDone = event;
  }
}

todo-item.component.ts

ToDoアプリ

  • ToDoリスト表示
  • ToDoの
    • 編集
      • チェックONで完了
      • ダブルクリックで開始
      • Enterで確定
      • ゴミ箱ボタンクリック
        で削除
    • 追加
      • Enterで追加

ToDo の編集 - 編集開始

・・・
  <span *ngIf="!isEditing"
    class="todoItem__title"
    (dblclick)="startEdit()">
    {{ todo.title }}
  </span>

  <input *ngIf="isEditing"
    type="text"
    class="todoItem__editor"
    [value]="todo.title">
・・・

todo-item.component.html

ToDo の編集 - 編集開始

・・・
export class TodoItemComponent implements OnInit {
  isEditing = false;
  @Input() todo: TodoItem;
  ngOnInit() {
  }
  onCheckChange(event: boolean) {
    this.todo.isDone = event;
  }
  startEdit() {
    this.isEditing = true;
  }
}

todo-item.component.ts

ToDoアプリ

  • ToDoリスト表示
  • ToDoの
    • 編集
      • チェックONで完了
      • ダブルクリックで開始
      • Enterで確定
      • ゴミ箱ボタンクリック
        で削除
    • 追加
      • Enterで追加

ToDo の編集 - Enter確定

・・・
  <span *ngIf="!isEditing"
    class="todoItem__title"
    (dblclick)="startEdit()">
    {{ todo.title }}
  </span>

  <input *ngIf="isEditing"
    type="text"
    class="todoItem__editor"
    [value]="todo.title"
    (input)="onTitleChange(input.value)"
    (keydown)="endEdit($event)"
    #input>
・・・

todo-item.component.html

ToDo の編集 - Enter確定

・・・
export class TodoItemComponent implements OnInit {
  isEditing = false;
  @Input() todo: TodoItem;
・・・
  onTitleChange(value: string) {
    this.todo.title = value;
  }
  endEdit(event: KeyboardEvent) {
    if (event.keyCode !== 13) {
      return;
    }
    this.isEditing = false;
  }
}

todo-item.component.ts

ToDo の編集

todoListが意図せず更新されている

ToDo の編集

・・・
export class TodoItemComponent implements OnInit {
  isEditing = false;
  @Input() todo: TodoItem;
  ngOnInit() {
    this.todo = {...this.todo};
  }
  onCheckChange(event: boolean) {
    this.todo.isDone = event;
  }
  startEdit() {
    this.isEditing = true;
  }
  onTitleChange(value: string) {
    this.todo.title = value;
  }
  endEdit(event: KeyboardEvent) {
    if (event.keyCode !== 13) {
      return;
    }
    this.isEditing = false;
  }
}

todo-item.component.ts

ToDo の編集

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { TodoItem } from '../todo-item';

@Component({
  selector: 'app-todo-item',
  templateUrl: './todo-item.component.html',
  styleUrls: ['./todo-item.component.scss']
})
export class TodoItemComponent implements OnInit {
  isEditing = false;
  @Input() todo: TodoItem;
  @Output() edit = new EventEmitter<TodoItem>();
  ngOnInit() {
    this.todo = {...this.todo};
  }
  onCheckChange(event: boolean) {
    this.todo.isDone = event;
    this.edit.emit(this.todo);
  }
  startEdit() {
    this.isEditing = true;
  }
  onTitleChange(value: string) {
    this.todo.title = value;
  }
  endEdit(event: KeyboardEvent) {
    if (event.keyCode !== 13) {
      return;
    }
    this.edit.emit(this.todo);
    this.isEditing = false;
  }
}

todo-item.component.ts

ToDo の編集

・・・
export class TodoItemComponent implements OnInit {
・・・
  onCheckChange(event: boolean) {
    this.todo.isDone = event;
    this.edit.emit(this.todo);
  }
・・・
  endEdit(event: KeyboardEvent) {
    if (event.keyCode !== 13) {
      return;
    }
    this.edit.emit(this.todo);
    this.isEditing = false;
  }
}

todo-item.component.ts

ToDo の編集

export class TodoComponent implements OnInit {
・・・
  handleEdit(event: TodoItem) {
    this.todoList = this.todoList.map((todo: TodoItem) => {
      if (todo.id === event.id) {
        todo = {...todo, ...event};
      }
      return todo;
    });
  }
}

todo.component.ts

  <app-todo-item *ngFor="let todo of todoList"
    [todo]="todo"
    (edit)="handleEdit($event)">
  </app-todo-item>

todo.component.html

ToDoアプリ

  • ToDoリスト表示
  • ToDoの
    • 編集
      • チェックONで完了済み
      • ダブルクリックで開始
      • Enterで確定
    • ゴミ箱ボタンクリック
      で削除
    • 追加
      • Enterで追加

ToDo の削除

・・・
  <button
    class="todoItem__remove"
    igxButton="icon"
    (click)="onRemove()">
    <igx-icon name="delete"></igx-icon>
  </button>
・・・

todo-item.component.html

ToDo の削除

・・・
export class TodoItemComponent implements OnInit {
・・・
  @Output() remove = new EventEmitter<TodoItem>();
・・・
  onRemove() {
    this.remove.emit(this.todo);
  }
}

todo-item.component.ts

ToDo の削除

export class TodoComponent implements OnInit {
・・・
  handleRemove(event: TodoItem) {
    this.todoList = this.todoList.filter((todo: TodoItem) => {
      return todo.id !== event.id;
    });
  }
}

todo.component.ts

  <app-todo-item *ngFor="let todo of todoList"
    [item]="todo"
    (edit)="handleEdit($event)"
    (remove)="handleRemove($event)">
  </app-todo-item>

todo.component.html

ToDoアプリ

  • ToDoリスト表示
  • ToDoの
    • 編集
      • チェックONで完了済み
      • ダブルクリックで開始
      • Enterで確定
    • ゴミ箱ボタンクリック
      で削除
    • 追加
      • Enterで追加

ToDo の追加

ng g component todo/add-todo-item

Terminal

ToDo の追加

<div class="addTodoItem">

  <igx-icon class="addTodoItem__icon" name="add"></igx-icon>

  <input
    type="text"
    class="addTodoItem__editor"
    placeholder="ToDo の追加"
    [value]="title"
    (input)="onTitleChange(input.value)"
    (keydown)="endEdit($event)"
    #input>

 </div>

add-todo-item.component.html

ToDo の追加

import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { TodoItem } from '../todo-item';

@Component({
  selector: 'app-add-todo-item',
  templateUrl: './add-todo-item.component.html',
  styleUrls: ['./add-todo-item.component.scss']
})
export class AddTodoItemComponent {
  title = '';
  @Output() add = new EventEmitter<TodoItem>();
  onTitleChange(event: string) {
    this.title = event;
  }
  endEdit(event: KeyboardEvent) {
    if (event.keyCode !== 13) {
      return;
    }
    if (!this.title) {
      return;
    }
    this.add.emit({ id: Date.now(), title: this.title, isDone: false });
    this.title = '';
  }
}

add-todo.component.ts

ToDo の追加

<div class="todo__list">
  <app-todo-item *ngFor="let todo of todoList"
    [item]="todo"
    (edit)="handleEdit($event)"
    (remove)="handleRemove($event)">
  </app-todo-item>
</div>
<app-add-todo-item class="todo__add"
  (add)="handleAdd($event)">
</app-add-todo-item>

todo.component.html

export class TodoComponent implements OnInit {
  todoList: TodoItem[];
・・・
  handleAdd(event: TodoItem) {
    this.todoList = [...this.todoList, event];
  }
}

todo.component.ts

最後に、ヘッダーナビの追加

<igx-navbar class="todo__navbar"
  title="ToDo"
  actionButtonIcon="menu">
</igx-navbar>
<div class="todo__list">
  <app-todo-item *ngFor="let todo of todoList"
    [item]="todo"
    (edit)="handleEdit($event)"
    (remove)="handleRemove($event)">
  </app-todo-item>
</div>
<app-add-todo-item class="todo__add"
  (add)="handleAdd($event)">
</app-add-todo-item>

todo.component.html

完成!

TypeScript + Angular で ToDo アプリ

By Tatsushi Kiryu

TypeScript + Angular で ToDo アプリ

  • 7,438