TypeScript + Angular
で作る Todo アプリ
About me
作るもの
- ToDoリスト表示
- ToDoの
- 編集
- チェックONで完了済み
- ダブルクリックで編集
- Enterで確定
- 削除
- ゴミ箱ボタンクリック
で削除
- ゴミ箱ボタンクリック
- 追加
- Enterで追加
- 編集
作るもの
GitHubに雛形あります。
https://github.com/tkiryu/todo-app-seed
完成版
https://github.com/tkiryu/todo-app-complete
用意するもの
- Visual Studio Code
- Extension : Angular Essentials
- ツール
- 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
非商用利用であれば、無償で使えます!
サードパーティモジュール追加
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,463