もくじ

はじめに

カルテットコミュニケーションズ開発部ブログで Angular超入門 の連載をスタートしてから、かなりの間が空いてしまいました。🙇前回の投稿からすでに半年が経過してしまいましたが、ぼちぼち再開しようと思います。また、金本からのバトンタッチを受け、今回から松岡が執筆を担当します。

さて、前回の投稿 Todoアプリ開発開始!新しくコンポーネントを作ってみよう! では todo を入力するためのシンプルなコンポーネントを作成しました。

ブラウザにはこのような入力画面が表示されます。

ちょっと味気ないですね…。今回はこのコンポーネントをベースに todo をリスト管理できるようにしてみたいと思います!

まだチュートリアルを実践していない方は Hello Worldしてみよう! から試してみてくださいませ。10分足らずで終わるはずです。

今回のゴール

todo リストを表示し、追加ボタンでリストの要素を増やせるようにします。

前回作った TodoFormComponent に加え、今回は TodoListComponent を新たに追加します。この2つのコンポーネントの親である AppComponent を通して todo データをやり取りしてみましょう。

ちなみに本文中の「親」「子」とは、とあるコンポーネント(親)に別のコンポーネントを設置している(子)、という関係を指しています。

DOM のイベントを拾ってみよう!

ボタンを設置する

前回までのチュートリアルのソースコードをベースに TodoFormComponent に追加ボタンを設置してみましょう。

src/app/components/todo-form/todo-form.component.html

<input type="text" [(ngModel)]="todo.name">
<input type="checkbox" [(ngModel)]="todo.isDone">

+ <button (click)="clicked()">追加</button>

src/app/components/todo-form/todo-form.component.ts

@Component({
  selector: 'app-todo-form',
  ...
})
export class TodoFormComponent implements OnInit {
  ...
+  clicked() {
+    console.log(this.todo);
+  }

イベントバインディング

(click) のような書き方を イベントバインディング と呼びます。 (change) (focus) (blur) など () には DOM の標準イベント の名前が入ります。

(click)="clicked()" のように書くと イベント発火時にコンポーネントのメソッドを呼び出す 事ができます。

ユーザー入力イベントにバインドする(angular.jp)

これで、追加ボタンを押した時に任意の処理が実行できるように準備が整いました。

ng serve --open してブラウザで追加ボタンをクリックしてみてください。入力した todo の内容がデバッグコンソールに出力されるはずです。

親子関係のコンポーネントでデータを連携させてみよう!

子コンポーネントでイベント発火する

TodoFormComponent は親である AppComponent に「追加ボタンが押された」事を知らせる必要があります。

子コンポーネントから親コンポーネントに 何らかのタイミング を伝えるには @OutputEventEmitter を使います。

src/app/components/todo-form/todo-form.component.ts

- import { Component, OnInit } from '@angular/core';
+ import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Todo } from 'src/app/models/todo';

@Component({
  selector: 'app-todo-form',
  ...
})
export class TodoFormComponent implements OnInit {

+  @Output() addTodo = new EventEmitter<Todo>();

  public todo: Todo = {
    name: '',
    isDone: false,
  };

  constructor() { }

  ngOnInit(): void {
  }

  clicked() {
-    console.log(this.todo);
+    this.addTodo.emit(this.todo);
  }

}

@Output() / EventEmitter

カスタムイベントを発火させる仕組みです。 @Output() addTodo はこのコンポーネントが addTodo というカスタムイベントを持っている事を宣言 しています。emit() を呼び出すと カスタムイベントが発火 します。

emit() には、カスタムイベントのパラメータを渡す事ができます。

  • 数値 this.addTodo.emit(1)
  • 文字列 this.addTodo.emit("abc")
  • オブジェクト this.addTodo.emit({ id: 1 })

上記のソースコードでは this.addTodo.emit(this.todo) としているため、コンポーネントの持つ todo をパラメータとして渡している事になります。

親が子のイベントをリッスンする(angular.jp)

これで、TodoFormComponent が入力内容の todo を親コンポーネントに伝えられるようになりました。

親コンポーネントがイベントを拾う

次に親コンポーネントである AppComponent で「追加ボタンが押された」イベントを拾ってみましょう。

DOMの標準イベントをキャッチするのに (click)="method()" と書くように、カスタムイベントも (イベント名)="method()" という書き方をします。

src/app/app.component.html

- <app-todo-form></app-todo-form>
+ <app-todo-form (addTodo)="added($event)"></app-todo-form>

パラメータを受け取る時はイベントバインディングの引数に $event を指定します。

src/app/app.component.ts

import { Component } from '@angular/core';
+ import { Todo } from './models/todo';

@Component({
  selector: 'app-root',
  ...
})
export class AppComponent {
  title = 'angular-todo';

+  public appTodos: Todo[] = [];

+  added(todo: Todo) {
+    this.appTodos.push({
+      name: todo.name,
+      isDone: todo.isDone,
+    });
+  }
}

added() の引数名は、$event に縛られません。任意に変更できます。

これで、AppComponent が TodoFormComponent のイベントを拾って、受け取った todo をリストに追加するようになりました。

リスト表示用コンポーネントを作成

次に AppComponent が持っている todo のリストを画面に表示するために、専用のコンポーネントを作成します。

$ ng g c components/todo-list

ng generate component コマンドは頭文字を取ってこのように短くタイプする事もできます。

コマンドによって <todo-list> が追加されているはずです。

> $ tree src/app/components/
src/app/components/
├── todo-form
│   ├── ...
└── todo-list
    ├── todo-list.component.html
    ├── todo-list.component.scss
    ├── todo-list.component.spec.ts
    └── todo-list.component.ts

子コンポーネントが値を受け取る準備をする

TodoListComponent は親コンポーネントである AppComponent から todo のリストを受け取る必要があります。

子コンポーネントが 親コンポーネントから値を受け取る には @Input を使います。

src/app/components/todo-list/todo-list.component.ts

- import { Component, OnInit } from '@angular/core';
+ import { Component, Input, OnInit } from '@angular/core';
import { Todo } from 'src/app/models/todo';

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

+  @Input() todos: Todo[] = [];

  constructor() { }

  ngOnInit(): void {
  }

}

@Input()

データバインディングでデータを受け取る仕組みです。 @Input() todos のように宣言すると親コンポーネントから [todos]="値" のように値を受け渡しする事ができます。

これで TodoListComponent が todo のリストを受け取る準備ができました。何が渡されたのか分かるようにオブジェクトの中身をデバッグ出力しておきましょう。

src/app/components/todo-list/todo-list.component.html

- <p>todo-list works!</p>
+ <pre>{{todos|json}}</pre>

親コンポーネントから子コンポーネントに値を渡す

さて、ようやく連携の準備が整いました。AppComponent の HTML に リスト表示コンポーネントを設置 してみましょう。

src/app/app.component.html

<app-todo-form (addTodo)="added($event)"></app-todo-form>
+ <app-todo-list [todos]="appTodos"></app-todo-list>

AppComponent の動作:

  • (addTodo) イベントバインディングを通して TodoFormComponent から todo を受け取ります
  • 受け取った todo を変数 appTodos に追加します
  • [todos] データバインディングを通して TodoListComponent に appTodos を渡します

動作確認

ブラウザで確認してみてください。 TodoFormComponent で入力した内容が、親である AppComponent を通して TodoListComponent に伝わるはずです。

まとめ

DOM の標準イベントを拾う。

<button (name)="method()">

子コンポーネントのイベントを拾う。

class ChildComponent {
  @Output() myEvent = new EventEmitter<ParamType>();

  method() {
    const param: ParamType = {...};
    this.myEvent.emit(param);
  }
}
// 親
<child-component (myEvent)="method($event)">

子コンポーネントに値を渡す。

class ChildComponent {
  @Input() name: Type;
}
// 親
<child-component [name]="value">

次回予告

次回は、繰り返しを制御する *ngFor や分岐を制御する *ngIf を使って、ビューをカスタマイズしてみたいと思います。

お楽しみに! :raised_hands: