Angular実践入門チュートリアル [2] 基礎編
Angular実践入門チュートリアル の基礎編です。
# 1. ビューを色々いじってみる
Angularの動作原理を大まかに理解できたところで、実際にコードを触っていきます。
まずはビュー(HTMLテンプレート)の基本機能を体験するために、 AppComponent
のビューを色々いじってみましょう。
# Hello, World!
してみる
AppComponent
のビューは src/app/app.component.html
に書かれていましたね。まずはこのファイルの中身を Hello, World!
と表示するだけの内容にしてみましょう。
<p>Hello, World!</p>
保存すると画面が自動でリロードされて
Hello, World!
と表示されますね。
# コンポーネントの変数をビューに表示してみる
では次に、コンポーネント本体が持っている変数の値をビューに表示してみましょう。
src/app/app.component.ts
と src/app/app.component.html
を以下のように変更します。
- title = 'angular-todo';
+ tasks = [
+ {title: '牛乳を買う', done: false},
+ {title: '可燃ゴミを出す', done: true},
+ {title: '銀行に行く', done: false},
+ ];
- <p>Hello, World!</p>
+ <ul>
+ <li>{{ tasks[0].title }} <span>{{ tasks[0].done }}</span></li>
+ <li>{{ tasks[1].title }} <span>{{ tasks[1].done }}</span></li>
+ <li>{{ tasks[2].title }} <span>{{ tasks[2].done }}</span></li>
+ </ul>
{{ コンポーネントのクラス変数 }}
で変数の値を出力することができます。
ちなみに、HTMLテンプレートから利用できるクラス変数は
public
なものに限られます。(アクセス修飾子を書かなければデフォルトでpublic
になります)
これで、画面には以下のようなリストが表示されているはずです。
牛乳を買う false
可燃ゴミを出す true
銀行に行く false
# *ngFor
による繰り返し処理を使ってみる
さて、このままだとビューの記述がタスクの個数に依存してしまっているので、繰り返し処理を使った書き方に変えてみましょう。
AngularのHTMLテンプレートで繰り返し処理をするには、 *ngFor
<ul>
- <li>{{ tasks[0].title }} <span>{{ tasks[0].done }}</span></li>
- <li>{{ tasks[1].title }} <span>{{ tasks[1].done }}</span></li>
- <li>{{ tasks[2].title }} <span>{{ tasks[2].done }}</span></li>
+ <li *ngFor="let task of tasks">{{ task.title }} <span>{{ task.done }}</span></li>
</ul>
繰り返し出力したいDOM要素に *ngFor="let 要素の変数名 of 配列の変数名"
と書くことで、そのDOM要素の内側で 要素の変数名
を使えるようになります。
コードを修正して、画面の表示が先ほどと変わっていなければOKです👌
# *ngIf
による条件分岐を使ってみる
さて、繰り返し処理を使ったので次は条件分岐を使ってみたいと思います。条件分岐には *ngIf
タスクが完了済みかどうかの true
false
をそのまま表示するのではなく、完了済みの場合にのみ [完了]
と表示するようにしてみましょう。
<ul>
- <li *ngFor="let task of tasks">{{ task.title }} <span>{{ task.done }}</span></li>
+ <li *ngFor="let task of tasks">
+ <span *ngIf="task.done">[完了]</span>
+ {{ task.title }}
+ </li>
</ul>
出力するかどうかを制御したいDOM要素に *ngIf="真偽値"
と書くことで、 真偽値
が true
の場合にのみDOM要素が出力されるようになります。
これで、画面の内容は以下のようなものになるはずです。
牛乳を買う
[完了] 可燃ゴミを出す
銀行に行く
# コンポーネントのスタイルを書いてみる
次はコンポーネントのスタイルを書いてみましょう。
先ほど *ngIf
を使って [完了]
と出力するようにしてみましたが、やっぱり完了済みのタスクはスタイルで判別できるようにしてみることにします。
<ul>
- <li *ngFor="let task of tasks">
- <span *ngIf="task.done">[完了]</span>
+ <li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
{{ task.title }}
</li>
</ul>
こんなふうに、完了済みの場合のみ <li>
要素に done
というクラスを付けておき、その上で src/app/app.component.scss
に以下のようなスタイルを書きます。
.done {
color: gray;
text-decoration: line-through;
}
可燃ゴミを出す
だけ字が薄くなって取り消し線が引かれた見た目になっていればOKです👌
# 2. 新しいタスクを追加できるようにしてみる
ビューを色々といじってみて、画面の作り方はなんとなくイメージできたかと思います。ここらで新しいタスクを追加する機能を実装してみましょう👍
# ngModel
を使って変数と入力欄をバインドする
ひとまず何も考えずに入力欄を追加してみましょう。
<ul>
<li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
{{ task.title }}
</li>
+ <li>
+ <input type="text">
+ </li>
</ul>
当然ながらこれだけでは入力しても何も起こりませんね。
Angularで画面からの入力をコンポーネントのロジックに渡すには、 <input>
要素とコンポーネントのクラス変数を紐付ける(バインドする) という作業が必要です。
そのために使うのが ngModel
まずは実際に ngModel
を使ったコードを見てみましょう。
export class AppComponent {
tasks = [
{title: '牛乳を買う', done: false},
{title: '可燃ゴミを出す', done: true},
{title: '銀行に行く', done: false},
];
+
+ newTaskTitle = '';
}
<li>
- <input type="text">
+ <input type="text" [(ngModel)]="newTaskTitle">
</li>
AppComponent
にクラス変数 newTaskTitle
を追加して、ビュー側では <input>
要素に [(ngModel)]="newTaskTitle"
という記述を足しました。
これで <input>
要素が newTaskTitle
と紐付きます💪(※ただしこの時点ではまだ動作しません)
ngModel
は、Angularにおいて 双方向データバインディング を実現するもっとも基本的な手段です。
双方向データバインディングとは、コンポーネント本体とビューの間でデータを同期する仕組みのことです。今回の例だと、画面上で <input>
の値を書き換えるたびに newTaskTitle
の値がリアルタイムで変更されることになります。
ちなみに、「双方向」というだけあって、逆にクラス内で newTaskTitle
に何かを代入する処理を実行すると、画面側にもそれが反映されます👍
例えば、今回は newTaskTitle = '';
と空文字列を初期値として代入しているので <input>
の値も空欄で初期化されますが、 newTaskTitle = 'test';
などと変更すれば <input>
の内容も初めから test
が入力されている状態になります。
# ngModel
を使うために、 FormsModule
をインポートする
さて、先ほどコンポーネントに ngModel
を使うコードを書き足しましたが、実はこの機能は ng new
しただけの雛形アプリには含まれていません。
ngModel
を利用するためには、Angular標準の FormsModule
Angularには「モジュール」という機構があり、必要に応じて複数のモジュールを組み合わせてアプリを構築できるようになっていることにはすでに触れましたね。「モジュールを組み合わせる」と表現していましたが、より具体的には、 あるモジュールに他のモジュールをインポートする ことによってそれを実現します。
今回は、 AppModule
に FormsModule
をインポートする ことで、 AppModule
内で FormsModule
が持っている ngModel
というディレクティブを使えるようにしておく必要がある、ということにになります。
具体的には、 src/app/app.module.ts
に以下のようなコードを追記すればOKです。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
+ import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
- BrowserModule
+ BrowserModule,
+ FormsModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
@NgModule({})
の中の imports: []
という配列に FormsModule
を追加しただけですね 👍
# ngModel
の振る舞いを確認してみる
ここまでで、無事に画面の入力欄から newTaskTitle
変数の値を操作できるようになっています。
と言われても、画面には入力欄そのものしか表示されていないので、変数の値が変化しているのかが分からないですね🤔
というわけで、とりあえず動作確認のために画面に newTaskTitle
の値を表示するようにしてみましょう。
<ul>
<li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
{{ task.title }}
</li>
<li>
<input type="text" [(ngModel)]="newTaskTitle">
</li>
</ul>
+
+ newTaskTitleの値: {{ newTaskTitle }}
これで画面を操作してみると、下図のように newTaskTitle
の値がリアルタイムで入力値と同期していることが分かります✨
# 入力した内容をタスクとして追加できるようにする
これで入力欄の用意はできました。
次は「追加」ボタンを設置して、クリックしたら新しいタスクをリストに追加するという処理を書いてみましょう💪
とりあえずボタンを追加してみます。
<ul>
<li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
{{ task.title }}
</li>
<li>
<input type="text" [(ngModel)]="newTaskTitle">
+ <button>追加</button>
</li>
</ul>
newTaskTitleの値: {{ newTaskTitle }}
このボタンをクリックしたときに何か処理を実行する、ということができればよさそうですね。
これは イベントバインディング
<ul>
<li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
{{ task.title }}
</li>
<li>
<input type="text" [(ngModel)]="newTaskTitle">
- <button>追加</button>
+ <button (click)="addTask()">追加</button>
</li>
</ul>
newTaskTitleの値: {{ newTaskTitle }}
このように、DOM要素に (click)="実行したい処理"
を書くことで、要素がクリックされたときに処理を呼び出すことができます。
(click)
以外にも (change)
や (keyup)
など DOMの標準イベント
ここでは、クリック時に addTask()
を実行するようにしました。なのでコンポーネント側に addTask()
クラスメソッドを定義しましょう。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
tasks = [
{title: '牛乳を買う', done: false},
{title: '可燃ゴミを出す', done: true},
{title: '銀行に行く', done: false},
];
newTaskTitle = '';
+
+ addTask() {
+ this.tasks.push({title: this.newTaskTitle, done: false});
+ this.newTaskTitle = '';
+ }
}
これで、「追加」ボタンをクリックしたら newTaskTitle
をタイトルとする新しいタスクが this.tasks
の末尾に追加されます👍
ビューからクラス変数やクラスメソッドにアクセスするときは newTaskTitle
や addTask()
のように書けばよかったのに対し、コンポーネントクラス内でアクセスする場合は this.tasks
this.newTasks
と this.
が必要なので注意しましょう。
動作を確認してみましょう。下図のようにタスクの追加ができるようになっていればOKです🙌
# 3. Todoアプリとして必要そうな機能を追加する
ngModel
を使った双方向データバインディングと (click)
のようなイベントバインディングを体験してきましたが、基本的なインタラクションはこの2つを使うだけでだいたい実装できます👍
というわけで、コンポーネント実装の基本的な流れが分かってきたところで、Todoアプリとして必要そうな機能をいくつか追加していくことにしましょう。
# タイトルなしのタスクは登録できないようにする
とりあえず、現状だとタイトルを空欄のまま「追加」を押せばタイトルなしのタスクが登録できてしまってよろしくないので、何か入力しないと「追加」ボタンを押せないように対応しておきましょう。
Angularが持っている フォームのバリデーション機能
<li>
- <input type="text" [(ngModel)]="newTaskTitle">
- <button (click)="addTask()">追加</button>
+ <input type="text" [(ngModel)]="newTaskTitle" #title="ngModel" required>
+ <button (click)="addTask()" [disabled]="title.invalid">追加</button>
</li>
コードをこのように修正することで、タイトル入力欄が空欄だと「追加」ボタンが押せないようになるのですが、ちょっと難解ですよね。一つずつ見ていきましょう。
まず、 <input type="text">
に #title="ngModel" required"
というコードが追記されています。
required
はただのHTML5の属性ですが、 #title="ngModel"
というのは初めて見る記述ですね。これは テンプレート参照変数#任意の名前
とマークすることで、他のDOM要素から 任意の名前
という変数名でそのDOM要素を参照できるようになるという代物です。
ここでは #title
とマークすることで title
というテンプレート参照変数を宣言し、さらにそのテンプレート参照変数に ngModel
自体を代入するということをしています。実はDOM要素がフォームコントロールの場合は、こうすることでそのテンプレート参照変数を通してフォームコントロールの状態(バリデーション結果など)にアクセスできるようになるのです。
これを理解した上で <button (click)="addTask()" [disabled]="title.invalid">追加</button>
を見てみると、意味が分かりそうですね。
title.invalid
の title
は、先ほどのテンプレート参照変数 title
です。 title
には ngModel
を代入してあったので、フォームコントロールのバリデーション結果が invalid
というプロパティから得られるようになっているわけですね。(バリデーションにエラーがあれば invalid
が true
になります)
そして、 [disabled]="真偽値"
によって、HTML5の disabled
属性を有効にするかどうかを 真偽値
の値に応じて切り替える、ということをしています。
まとめると、
- タイトル入力欄には
required
属性が付与されているので - ここが空欄だと
title.invalid
がtrue
になる - 「追加」ボタンは、
title.invalid
がtrue
の場合にdisabled
属性が付与されるので - タイトル入力欄が空欄だと「追加」ボタンが押せない
という実装になるわけですね👍
# タスクの完了・未完了を変更できるようにする
Todoアプリなら当然タスクの完了・未完了をチェックボックスで変更できるようにする必要があるでしょう。
これは、チェックボックス要素に ngModel
を適用するだけで簡単に実装できます👍
<li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
- {{ task.title }}
+ <label>
+ <input type="checkbox" [(ngModel)]="task.done">
+ {{ task.title }}
+ </label>
</li>
簡単ですね!
# タスクに期日を設定できるようにする
このままだとちょっと機能的に寂しいので、タスクごとに期日を設定できるようにしてみたいと思います。
まずはコンポーネントクラスを以下のように修正します。
export class AppComponent {
tasks = [
- {title: '牛乳を買う', done: false},
- {title: '可燃ゴミを出す', done: true},
- {title: '銀行に行く', done: false},
+ {title: '牛乳を買う', done: false, deadline: new Date('2021-01-01')},
+ {title: '可燃ゴミを出す', done: true, deadline: new Date('2021-01-02')},
+ {title: '銀行に行く', done: false, deadline: new Date('2021-01-03')},
];
- newTaskTitle = '';
+ newTask = {
+ title: '',
+ deadline: new Date(),
+ };
addTask() {
- this.tasks.push({title: this.newTaskTitle, done: false});
- this.newTaskTitle = '';
+ this.tasks.push({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
+ this.newTask = {
+ title: '',
+ deadline: new Date(),
+ };
}
}
タスクに deadline
というプロパティを追加して、画面の入力値を入れておく箱も分かりやすいようにオブジェクトにしました。
これに合わせてビューも修正します。
<ul>
<li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
<label>
<input type="checkbox" [(ngModel)]="task.done">
{{ task.title }}
+ (期日:{{ task.deadline|date:'yyyy/MM/dd' }})
</label>
</li>
<li>
- <input type="text" [(ngModel)]="newTaskTitle" #title="ngModel" required>
+ <input type="text" [(ngModel)]="newTask.title" #title="ngModel" required>
+ <input type="date" [(ngModel)]="newTask.deadline">
<button (click)="addTask()" [disabled]="title.invalid">追加</button>
</li>
</ul>
- newTaskTitleの値: {{ newTaskTitle }}
+ newTaskの値: {{ newTask|json }}
task.deadline|date:'yyyy/MM/dd'
や newTask|json
といった見慣れない記述が登場しましたね。
この |
に続く date
や json
は パイプ
標準でいくつかのパイプが提供されている
date
json
これで、下図のように期日付きのタスクを登録できるようになりました👍
# 期日超過しているタスクを強調表示するようにする
せっかく期日を設定できるようにしたので、期日超過しているタスクを強調表示するようにしてみましょう。
<label>
<input type="checkbox" [(ngModel)]="task.done">
{{ task.title }}
(期日:{{ task.deadline|date:'yyyy/MM/dd' }})
+ <span *ngIf="isOverdue(task)" class="overdue">期日超過</span>
</label>
.done {
color: gray;
text-decoration: line-through;
}
+
+ .overdue {
+ color: darkred;
+ }
このようにビューに 期日超過
と表示するための <span>
要素を追記して、その <span>
要素には *ngIf
で isOverdue(task)
の戻り値が true
のときにだけ表示されるよう設定します。
あとはその isOverdue()
メソッドをコンポーネントクラスに追加すればOKですね。
tasks = [
{title: '牛乳を買う', done: false, deadline: new Date('2021-01-01')},
- {title: '可燃ゴミを出す', done: true, deadline: new Date('2021-01-02')},
- {title: '銀行に行く', done: false, deadline: new Date('2021-01-03')},
+ {title: '可燃ゴミを出す', done: true, deadline: new Date('2020-01-02')},
+ {title: '銀行に行く', done: false, deadline: new Date('2020-01-03')},
];
// ...
addTask() {
this.tasks.push({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
this.newTask = {
title: '',
deadline: new Date(),
};
}
+
+ isOverdue(task) {
+ return !task.done && task.deadline < (new Date()).setHours(0, 0, 0, 0);
+ }
ついでに動作確認をしやすくするためにタスクの初期データのうち2つを期日超過状態( deadline
の値が2020年)に変更しました。
isOverdue()
メソッドの実装は、
- タスクが完了済みでなく
- タスクに設定されている期日が「今日の0時0分0秒0ミリ秒」よりも以前である
という条件で true
になる(期日超過と見なす)ようにしています。(参考: Date.prototype.setHours()
これで、下図のように、未完了かつ期日を超過しているタスクにのみ 期日超過
というラベルが表示されるようになりました。
だんだんTodoアプリっぽくなってきましたね!✨
# 4. コンポーネントを分けてみる
さて、ここまですべての処理を AppComponent
の中に書いてきました。
これぐらいの規模のアプリならこのような実装でもさほど問題なさそうですが、本格的なアプリ開発をする際には UI部品ごとにコンポーネントに分けて、コンポーネント同士を連携させながらアプリを組み上げていく ということが必要になってきます。
なのでここらでコンポーネントを分ける練習をしておきましょう💪
現状このアプリが持っている機能を分解して考えてみると、
- タスクリスト
- タスクリストの1行
- タスク追加フォーム
の3つぐらいに分けられそうです。ここではこの3つのコンポーネントに分けてみることにしましょう。
# TaskListComponent
を作る
まずタスクリストの実装を持つ TaskListComponent
を作って、 AppComponent
から分離してみましょう。
新しいコンポーネントを作る場合、手作業でファイルを作ってゼロからコードを書かなくても、 ng generate
プロジェクト直下で以下のコマンドを実行してみてください。
ng generate component TaskList
## 実は
## ng g c TaskList
## と略記することもできます
src/app/task-list/
配下に
task-list.component.ts
(コンポーネントクラス)task-list.component.html
(ビュー)task-list.component.scss
(スタイル)task-list.component.spec.ts
(テスト)
の4ファイルが生成され、さらに src/app/app.module.ts
が以下のように変更されていると思います。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
+ import { TaskListComponent } from './task-list/task-list.component';
@NgModule({
declarations: [
AppComponent,
+ TaskListComponent
],
imports: [
BrowserModule,
FormsModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
コンポーネントの各種ファイルを生成して、 AppModule
への登録まで自動で行ってくれたわけですね。便利!
では、先ほどまで AppComponent
に書いていたコードを新たに生成された TaskListComponent
に移してみましょう。
AppComponent
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
- tasks = [
- {title: '牛乳を買う', done: false, deadline: new Date('2021-01-01')},
- {title: '可燃ゴミを出す', done: true, deadline: new Date('2020-01-02')},
- {title: '銀行に行く', done: false, deadline: new Date('2020-01-03')},
- ];
-
- newTask = {
- title: '',
- deadline: new Date(),
- };
-
- addTask() {
- this.tasks.push({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
- this.newTask = {
- title: '',
- deadline: new Date(),
- };
- }
-
- isOverdue(task) {
- return !task.done && task.deadline < (new Date()).setHours(0, 0, 0, 0);
- }
}
- <ul>
- <li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
- <label>
- <input type="checkbox" [(ngModel)]="task.done">
- {{ task.title }}
- (期日:{{ task.deadline|date:'yyyy/MM/dd' }})
- <span *ngIf="isOverdue(task)" class="overdue">期日超過</span>
- </label>
- </li>
- <li>
- <input type="text" [(ngModel)]="newTask.title">
- <input type="date" [(ngModel)]="newTask.deadline">
- <button (click)="addTask()">追加</button>
- </li>
- </ul>
-
- newTaskの値: {{ newTask|json }}
+ <app-task-list></app-task-list>
- .done {
- color: gray;
- text-decoration: line-through;
- }
-
- .overdue {
- color: darkred;
- }
TaskListComponent
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-task-list',
templateUrl: './task-list.component.html',
styleUrls: ['./task-list.component.scss']
})
export class TaskListComponent implements OnInit {
constructor() { }
+ tasks = [
+ {title: '牛乳を買う', done: false, deadline: new Date('2021-01-01')},
+ {title: '可燃ゴミを出す', done: true, deadline: new Date('2020-01-02')},
+ {title: '銀行に行く', done: false, deadline: new Date('2020-01-03')},
+ ];
+
+ newTask = {
+ title: '',
+ deadline: new Date(),
+ };
+
ngOnInit(): void {
}
+ addTask() {
+ this.tasks.push({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
+ this.newTask = {
+ title: '',
+ deadline: new Date(),
+ };
+ }
+
+ isOverdue(task) {
+ return !task.done && task.deadline < (new Date()).setHours(0, 0, 0, 0);
+ }
}
- <p>task-list works!</p>
+ <ul>
+ <li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
+ <label>
+ <input type="checkbox" [(ngModel)]="task.done">
+ {{ task.title }}
+ (期日:{{ task.deadline|date:'yyyy/MM/dd' }})
+ <span *ngIf="isOverdue(task)" class="overdue">期日超過</span>
+ </label>
+ </li>
+ <li>
+ <input type="text" [(ngModel)]="newTask.title">
+ <input type="date" [(ngModel)]="newTask.deadline">
+ <button (click)="addTask()">追加</button>
+ </li>
+ </ul>
+
+ newTaskの値: {{ newTask|json }}
+ .done {
+ color: gray;
+ text-decoration: line-through;
+ }
+
+ .overdue {
+ color: darkred;
+ }
ほとんどコピペしただけですが、唯一のポイントは src/app/app.component.html
に書いた
<app-task-list></app-task-list>
これです。
導入編のコードリーディングで、 AppComponent
の selector
に書かれている 'app-root'
に対応して index.html
内の <app-root></app-root>
の箇所に AppComponent
のレンダリング結果が挿入されるという関係を紐解いたことを覚えているでしょうか。
今回もまったく同じで、自動生成された TaskListComponent
の selector
のところには 'app-task-list'
と書かれています。つまり、このコンポーネントを別のビューに挿入したい場合は <app-task-list></app-task-list>
という要素として設置すればよいというわけです。
今回はもともと AppComponent
に書いていたHTMLを丸ごと TaskListComponent
に移動したので、 AppComponent
のビューには <app-task-list></app-task-list>
だけを書いておけば、そこに TaskListComponent
の中身が丸ごと展開される結果になります。
この時点で一度動作を確認してみてください。先ほどまでと何ら変わらない動作になっていれば正常にコンポーネントの分割ができている証拠です👍
# TaskListItemComponent
を作る
では、先ほどと同じように
ng generate component TaskListItem
で TaskListItemComponent
を作成して、 TaskListComponent
のコードから「タスクリストの1行」の実装に関する部分を TaskListItemComponent
に移してみましょう。
TaskListComponent
- isOverdue(task) {
- return !task.done && task.deadline < (new Date()).setHours(0, 0, 0, 0);
- }
<ul>
- <li *ngFor="let task of tasks" class="{{ task.done ? 'done' : '' }}">
- <label>
- <input type="checkbox" [(ngModel)]="task.done">
- {{ task.title }}
- (期日:{{ task.deadline|date:'yyyy/MM/dd' }})
- <span *ngIf="isOverdue(task)" class="overdue">期日超過</span>
- </label>
- </li>
+ <li *ngFor="let task of tasks">
+ <app-task-list-item [task]="task"></app-task-list-item>
+ </li>
<li>
<input type="text" [(ngModel)]="newTask.title">
<input type="date" [(ngModel)]="newTask.deadline">
<button (click)="addTask()">追加</button>
</li>
</ul>
newTaskの値: {{ newTask|json }}
- .done {
- color: gray;
- text-decoration: line-through;
- }
-
- .overdue {
- color: darkred;
- }
TaskListItemComponent
- import { Component, OnInit } from '@angular/core';
+ import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-task-list-item',
templateUrl: './task-list-item.component.html',
styleUrls: ['./task-list-item.component.scss']
})
export class TaskListItemComponent implements OnInit {
constructor() { }
+ @Input() task;
+
ngOnInit(): void {
}
+ isOverdue(task) {
+ return !task.done && task.deadline < (new Date()).setHours(0, 0, 0, 0);
+ }
}
- <p>task-list-item works!</p>
+ <label class="{{ task.done ? 'done' : '' }}">
+ <input type="checkbox" [(ngModel)]="task.done">
+ {{ task.title }}
+ (期日:{{ task.deadline|date:'yyyy/MM/dd' }})
+ <span *ngIf="isOverdue(task)" class="overdue">期日超過</span>
+ </label>
+ .done {
+ color: gray;
+ text-decoration: line-through;
+ }
+
+ .overdue {
+ color: darkred;
+ }
今回もほとんどコピペなのですが、 class="{{ task.done ? 'done' : '' }}"
を付加する対象のDOM要素を <li>
からその配下の <label>
に変更してあります。( <li>
までが TaskListComponent
の関心で、その中身を TaskListItemComponent
に移譲するという関係にしたかったので)
また、今回初めて見るコードが2つほど登場していました。
src/app/task-list.component.html
の<app-task-list-item [task]="task"></app-task-list-item>
src/app/task-list-item.component.ts
の@Input() task;
これらは2つでセットになっていて、
TaskListComponent
からTaskListItemComponent
のtask
クラス変数に対して、(自分のクラス変数である)task
を渡すTaskListItemComponent
はクラス変数task
を宣言し、これに@Input()
デコレーターを付けることで親コンポーネントからデータを受け取れるようにする
ということをしています。
この [相手の変数名]="自分のデータ"
で親コンポーネントから子コンポーネントへデータを受け渡す機能のことを、単に「データバインディング」、あるいは「双方向データバインディング」と対比して「単方向データバインディング」などと呼んだりします。
ちなみに、ここまでで
単方向データバインディング
イベントバインディング
双方向データバインディング
の3種類のバインディングが登場しましたが、それぞれビューにおける記法は[]
()
[()]
となっていました。初めて[(ngModel)]
を見たときは 「何だこの難解な記法は!」 と思ったと思うのですが😅、こうして3種類出揃ってみると[]
と()
の両方の性質を兼ね備えている(双方向)という意味があったのだと分かりますね。これら3種類のバインディングの対比は、こちらのドキュメント
に記載されている下表が分かりやすいので参考までに貼っておきます。
# TaskFormComponent
を作る
最後も同様に
ng generate component TaskForm
で TaskFormComponent
を作成して、 TaskListComponent
のコードから「タスク追加フォーム」の実装に関する部分を TaskFormComponent
に移してみましょう。
TaskListComponent
export class TaskListComponent implements OnInit {
constructor() { }
tasks = [
{title: '牛乳を買う', done: false, deadline: new Date('2021-01-01')},
{title: '可燃ゴミを出す', done: true, deadline: new Date('2020-01-02')},
{title: '銀行に行く', done: false, deadline: new Date('2020-01-03')},
];
- newTask = {
- title: '',
- deadline: new Date(),
- };
-
ngOnInit(): void {
}
- addTask() {
- this.tasks.push({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
- this.newTask = {
- title: '',
- deadline: new Date(),
- };
- }
+ addTask(task) {
+ this.tasks.push(task);
+ }
}
<ul>
<li *ngFor="let task of tasks">
<app-task-list-item [task]="task"></app-task-list-item>
</li>
<li>
- <input type="text" [(ngModel)]="newTask.title">
- <input type="date" [(ngModel)]="newTask.deadline">
- <button (click)="addTask()">追加</button>
+ <app-task-form (addTask)="addTask($event)"></app-task-form>
</li>
</ul>
-
- newTaskの値: {{ newTask|json }}
TaskFormComponent
- import { Component, OnInit } from '@angular/core';
+ import { Component, EventEmitter, OnInit, Output } from '@angular/core';
@Component({
selector: 'app-task-form',
templateUrl: './task-form.component.html',
styleUrls: ['./task-form.component.scss']
})
export class TaskFormComponent implements OnInit {
constructor() { }
+ @Output() addTask = new EventEmitter();
+
+ newTask = {
+ title: '',
+ deadline: new Date(),
+ };
+
ngOnInit(): void {
}
+ submit() {
+ this.addTask.emit({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
+ this.newTask = {
+ title: '',
+ deadline: new Date(),
+ };
+ }
}
- <p>task-form works!</p>
+ <input type="text" [(ngModel)]="newTask.title">
+ <input type="date" [(ngModel)]="newTask.deadline">
+ <button (click)="submit()">追加</button>
+ <br>newTaskの値: {{ newTask|json }}
最後はまたちょっと難しいコードが色々登場しましたね。 解説していきます。
今回初めて見るコードは以下の箇所かと思います。
<app-task-form (addTask)="addTask($event)"></app-task-form>
@Output() addTask = new EventEmitter();
this.addTask.emit({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
一見難しそうに見えますが、
<app-task-form (addTask)="addTask($event)"></app-task-form>
このコードはもともとの
<button (click)="addTask()">追加</button>
ととてもよく似ていますよね。もともとが「 click
イベントの発火に合わせて addTask()
を実行する」という処理だったのが、「 addTask
イベントの発火に合わせて addTask($event)
を実行する」に変わっているだけです。
つまり、 TaskFormComponent
が addTask
というカスタムイベントを持っていて、「追加」ボタンがクリックされたときにそのイベントを発火してくれるようになっているのです。( $event
については後述します)
TaskFormComponent
側でそのカスタムイベントを作成しているのが、
@Output() addTask = new EventEmitter();
このコードです。
EventEmitter
クラスのインスタンスを代入したクラス変数 addTask
を宣言しており、これがカスタムイベントの発火装置になります。これに @Output()
デコレーター
そして最後に
this.addTask.emit({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
このコードが、実際にイベントを発火しています。 EventEmitter
クラスの emit()
メソッドを呼ぶことでイベントを発火させ、その際に {title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)}
というオブジェクトをイベントにパラメータとして添付しています。
この添付したパラメータは、親コンポーネント側で $event
として受け取ることができます。
なので、
<app-task-form (addTask)="addTask($event)"></app-task-form>
このコードでタスクをリストに追加するという操作が可能だったわけです。
# 5. 型安全なコードにする
今さらですが、AngularではTypeScriptを使ってコードを書きます。(Angular自体もTypeScriptで書かれています)
せっかくTypeScriptを採用しているのに、ここまでに書いてきたコードでは 型注釈
というわけで、ここらでTypeScriptの強みを生かした型安全なコードにグレードアップさせておきましょう💪
# Task
インターフェースを定義する
数値や文字列などのプリミティブ型だけでなく、自分で定義したインターフェースも型として利用できます。今回のアプリでは「タスク」が重要な構造を持っているので、これをインターフェースとして定義しておくことにしましょう。
src/models/task.ts
というファイルを新しく作って、以下のような内容を書いてください。
export interface Task {
title: string;
done: boolean;
deadline: Date;
}
# 既存のコードに型注釈を付ける
これで Task
型が定義できたので、既存のコードに型注釈を付けていきましょう。
src/app/task-list/task-list.component.ts
import { Component, OnInit } from '@angular/core';
+ import { Task } from '../../models/task';
// ...
- tasks = [
+ tasks: Task[] = [
{title: '牛乳を買う', done: false, deadline: new Date('2021-01-01')},
{title: '可燃ゴミを出す', done: true, deadline: new Date('2020-01-02')},
{title: '銀行に行く', done: false, deadline: new Date('2020-01-03')},
];
// ...
- addTask(task) {
+ addTask(task: Task): void {
this.tasks.push(task);
}
src/app/task-list-item/task-list-item.component.ts
import { Component, Input, OnInit } from '@angular/core';
+ import { Task } from '../../models/task';
// ...
- @Input() task;
+ @Input() task: Task;
// ...
- isOverdue(task) {
- return !task.done && task.deadline < (new Date()).setHours(0, 0, 0, 0);
+ isOverdue(task: Task): boolean {
+ return !task.done && task.deadline.getTime() < (new Date()).setHours(0, 0, 0, 0);
}
src/app/task-form/task-form.component.ts
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
+ import { Task } from '../../models/task';
// ...
- @Output() addTask = new EventEmitter();
+ @Output() addTask = new EventEmitter<Task>();
// ..
- submit(): {
+ submit(): void {
差し当たりこんなところでしょうか。
型注釈を付け加える以外に、一箇所
isOverdue(task: Task): boolean {
return !task.done && task.deadline.getTime() < (new Date()).setHours(0, 0, 0, 0);
}
この部分のコードについて以下のように修正を加えました。
- task.deadline < (new Date()).setHours(0, 0, 0, 0);
+ task.deadline.getTime() < (new Date()).setHours(0, 0, 0, 0);
元のコードだと Date
型と数値型(Date.prototype.setHours()
# 期日なしのタスクも登録できるようにインターフェースを修正する
現状だと、期日入力欄を空欄のままタスクを追加すると「現在日時」が設定されるようになっています。これは仕様として微妙なので、期日なしのタスクも登録できるようにしましょう。
まずはインターフェースを修正しましょう。 deadline
プロパティに ?
を付けてnullableにします。
export interface Task {
title: string;
done: boolean;
- deadline: Date;
+ deadline?: Date;
}
その上で、期日が入力されなかったときは現在日時ではなく null
をセットするように、また期日に null
が入っていることを考慮するように、コードを修正します。
src/app/task-list-item/task-list-item.component.ts
isOverdue(task: Task): boolean {
- return !task.done && task.deadline.getTime() < (new Date()).setHours(0, 0, 0, 0);
+ return !task.done && task.deadline && task.deadline.getTime() < (new Date()).setHours(0, 0, 0, 0);
}
src/app/task-list-item/task-list-item.component.html
<label class="{{ task.done ? 'done' : '' }}">
<input type="checkbox" [(ngModel)]="task.done">
{{ task.title }}
- (期日:{{ task.deadline|date:'yyyy/MM/dd' }})
+ <span *ngIf="task.deadline">(期日:{{ task.deadline|date:'yyyy/MM/dd' }})</span>
<span *ngIf="isOverdue(task)" class="overdue">期日超過</span>
</label>
src/app/task-form/task-form.component.ts
newTask = {
title: '',
- deadline: new Date(),
+ deadline: null,
};
// ...
submit(): void {
- this.addTask.emit({title: this.newTask.title, done: false, deadline: new Date(this.newTask.deadline)});
+ this.addTask.emit({
+ title: this.newTask.title,
+ done: false,
+ deadline: this.newTask.deadline ? new Date(this.newTask.deadline) : null,
+ });
this.newTask = {
title: '',
- deadline: new Date(),
+ deadline: null,
};
}
これで、下図のとおり期日なしのタスクも登録できるようになりました👍
型注釈を付けておけば、このようなデータ構造の変更も安心して行うことができますね。
# 次の章へ
- 【1】導入編
- 【2】基礎編
- 【3】UIフレームワーク編
- 【4】Firebase編
このチュートリアルが役に立ったと思っていただけた方は、こちらのトップページ をSNS等でシェアしていただけるととても嬉しいです!😆