もくじ

今回のゴール

前回までで、ng new コマンドで雛形アプリを作り、そのコードを読み解くことでAngularアプリがどのような仕組みで動作するのかを学んできました。

いよいよ今回からは実際のアプリケーション開発に入っていきます!

この連載では、簡単なTodoアプリを作っていきます。今回は雛形アプリに1つだけ新しくコンポーネントを追加して、その動作を確認するところまでです。

前回、Angularアプリがモジュールとコンポーネントの組み合わせで作られていることを学びましたが、今回はコンポーネントの中身がどのようになっているのかをより深く理解することを目指しましょう :muscle:

初めてのコンポーネント作成

早速コンポーネントを新規作成してみましょう。今回はTodoアプリのうち、Todoを編集するためのフォーム のコンポーネントを作成していきます。

ちなみに、ここから先は、作業によるコード差分を管理しやすいよう、gitなどを使ってコードをバージョン管理しながら手を動かしていくことをおすすめします :+1:

さて、ng コマンドには、ng generate という様々なファイルを自動生成するためのコマンドが用意されています。新しくコンポーネントを作成する際には ng generate component コマンドを使うのが便利です。

プロジェクトのルートディレクトリ(/path/to/angular-todo 直下)にいる状態で、以下のコマンドを実行してみてください。

$ ng generate component components/todo-form

実行すると、以下のようなディレクトリ構成で4つのファイルが生成されます。

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

前回学んだとおり、コンポーネントとは「HTMLテンプレート・スタイル・ロジックをひとまとめにしたUI部品」のことでした。

ng generate component コマンドで生成した4つのファイルのうち、todo-form.component.ts がコンポーネントのロジックを記述する本体のクラスファイルです。中身を覗いてみましょう。

import { Component, OnInit } from '@angular/core';

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

  constructor() { }

  ngOnInit() {
  }

}

TodoFormComponent クラスが定義されていますが、中身は特に何も処理が書かれていない状態になっています。

その上の @Component({}) で囲まれた部分を見てみると、前回コードリーディングしたルートコンポーネント(AppComponent)と同じように、

  selector: 'app-todo-form',
  templateUrl: './todo-form.component.html',
  styleUrls: ['./todo-form.component.scss']

といった記述がありますね。

どうやら templateUrl styleUrls によって、コンポーネントをレンダリングするためのHTMLテンプレートと、そのテンプレートに適用するスタイルを指定するようです :ok_hand:

そして selector: 'app-todo-form' ですが、ここは AppComponent においては selector: 'app-root' となっていた部分です。ここで指定した文字列が、このコンポーネントをビューに配置する際のタグ名になります。

つまり、別のコンポーネントのHTMLテンプレート内に <app-todo-form></app-todo-form> というタグを書くと、その箇所に TodoFormComponent を挿入することができるというわけです :ok_hand:

COLUMN

ここでは、ng generate component components/todo-form というコマンドで TodoFormComponent を生成しましたが、単に ng generate component todo-form とだけ書いても実行可能です。 作りたいコンポーネントの名前の前に、今回のように components/ などと格納箇所を示すディレクトリパス(src/app からの相対パス)を付け足すことで、任意の場所にコンポーネントのファイル群を生成することができるのです。

今回はより整理しやすいディレクトリ構成とするために、components/ 配下にコンポーネントのファイル群を格納するようにしました。

ng generate component コマンドで利用できるオプションなど、より詳しい情報は 公式のリファレンス をご参照ください。

モジュールへの登録

ところで、コンポーネントは作るだけではダメで、モジュールに登録することでそのモジュール内で利用できるようになる のでしたよね。今回作った TodoFormComponent も当然ながらモジュールに登録することが必要です。

雛形アプリの時点で git commit していた人は、この時点で git diff してみてください。src/app/app.module.ts に以下のような差分が出ているのではないでしょうか。

import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
+ import { TodoFormComponent } from './components/todo-form/todo-form.component';

@NgModule({
  declarations: [
-   AppComponent
+   AppComponent,
+   TodoFormComponent
  ],
  imports: [
    BrowserModule

そう、ng generate component コマンドを使ってコンポーネントを生成すると、自動でルートモジュールにコンポーネントを登録してくれる のです :+1:

これで、ルートモジュール内で TodoFormComponent を自由に使えるようになりました。

作ったコンポーネントをアプリに組み込んでみる

では、いよいよ TodoFormComponent をアプリに組み込んで表示させてみましょう。

src/app/app.component.html の中身を丸ごと削除して、<app-todo-form></app-todo-form> という一行だけが書かれた状態にしてみてください。

- <!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
- <!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
- <!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
- <!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
                :
                略
                :
- <!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * -->
- <!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
-
-
+ <app-todo-form></app-todo-form>

ng serve --open で以下のような画面が表示されていれば成功です :+1:

TodoFormComponent のHTMLテンプレートである src/app/todo-form/todo-form.compoentn.html の中身を覗いてみてください。

<p>todo-form works!</p>

となっていますね。

ルートコンポーネントのHTMLを <app-todo-form></app-todo-form> という一行だけにしたので、TodoFormComponent のHTMLだけが表示されている状態になったわけです :+1:

Todoオブジェクトの型を決めておく

さて、今後いくつかのコンポーネントを追加してTodoアプリを作り込んでいくわけですが、その際にやり取りするデータの型が決まっていると何かと楽になるので、今のうちにTodoオブジェクトの型を作っておきましょう。

$ mkdir src/app/models
$ touch src/app/models/todo.ts

src/app/models/todo.ts というファイルを作って、以下のようなコードを書いてください。

export interface Todo {
  name: string;
  isDone: boolean;
}

この、nameisDone という2つのプロパティを持った Todo インターフェースを使ってデータのやり取りをしていきます :+1:

Todoオブジェクトを画面から編集できるようにする

それではいよいよ、Todoオブジェクトを画面から編集できるようにしていきましょう :muscle:

コンポーネントクラスを修正

src/app/components/todo-form/todo-form.component.ts に以下のコードを追記してみてください。

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

@Component({
  selector: 'app-todo-form',
  templateUrl: './todo-form.component.html',
  styleUrls: ['./todo-form.component.scss']
})
export class TodoFormComponent implements OnInit {
+
+ public todo: Todo = {
+   name: '',
+   isDone: false,
+ };

  constructor() { }

  ngOnInit() {
  }

}

追記したコードの意味は以下のとおりです。

  • 先ほど作成した Todo インターフェースの定義ファイルをimportした上で
  • Todo 型のpublicなクラス変数 todo を宣言し
  • 初期値として { name: '', isDone: false } を代入している

まだTypeScriptの文法に戸惑いがあるかもしれませんが、やっていること自体はごく簡単ですね :+1:

HTMLテンプレートを修正

では続いて、画面を作り込んでいきましょう。

src/app/components/todo-form/todo-form.component.html を以下のように修正してください。

- <p>todo-form works!</p>
+ <input type="text" [(ngModel)]="todo.name">
+ <input type="checkbox" [(ngModel)]="todo.isDone">

<input> タグを使って、Todoの名前入力用のテキストフィールドと完了済みかどうかのチェックボックスを設置しています。

が、それぞれに何やら [(ngModel)]="todo.name" [(ngModel)]="todo.isDone" という記述がありますね :thinking:

実は、ngModel はAngularに標準で用意されているディレクティブ(ビューで利用できる命令の一種)の1つで、双方向バインディング を実現するもっとも基本的な手段になります :muscle:

双方向バインディングとは、コンポーネント本体とビューの間でデータを同期する仕組みのことで、この場合、画面上でテキストフィールドの値を書き換えたり、チェックボックスのON/OFFを切り替えたりするたびに、TodoFormComponent クラスのクラス変数 todonameisDone の値がリアルタイムで変更されます。

ちなみに、「双方向 」というだけあって、逆にクラス内で todo に何かを代入する処理を実行すると、画面側にもそれが反映されます。(今回は、{ name: '', isDone: false } を初期値として代入しているので、画面の初期表示は 名前:空欄 完了済:チェックなし となるはずです :+1:

ngModelを使うために、FormsModuleをインポート

先ほど、双方向バインディングを実現するためにHTMLテンプレート内で ngModel を使用しましたが、実はこの機能は ng new しただけの雛形アプリには含まれていません。

ngModel を利用するためには、Angular標準の FormsModule というモジュールを追加でインポート する必要があります :muscle:

Angularには「モジュール」という機構があり、必要に応じて複数のモジュールを組み合わせてアプリを構築できるようになっていることは前回ざっくりお伝えしたかと思います。「モジュールを組み合わせる」と表現しましたが、より具体的には、あるモジュールに他のモジュールをインポートする ことによってそれを実現します。

今回は、AppModuleFormsModule をインポートする ことで、AppModule 内で FormsModule が持っている ngModel という機能を使えるようにしておきます :+1:

具体的には、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';
import { TodoFormComponent } from './components/todo-form/todo-form.component';

@NgModule({
  declarations: [
    AppComponent,
    TodoFormComponent
  ],
  imports: [
    BrowserModule,
+   FormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

@NgModule({}) の中の imports: [] という配列に、FormsModule を追加しただけですね :+1:

動作確認

ここまでで、無事にTodoオブジェクトを画面から編集できるようになりました! :tada:

画面の状態はこんな感じになっているかと思います。テキストフィールドとチェックボックスで、todo 変数の中身を変更することができます。

…が、実際に変更できているのかどうかが見えないので、何の手応えもありませんね :scream:

というわけで、todo 変数の中身を画面に表示するようにして、実際に値が変更されていることを目視できるようにしてみましょう :muscle:

src/app/components/todo-form/todo-form.component.html に以下の一行を追記してみてください。

<input type="text" [(ngModel)]="todo.name">
<input type="checkbox" [(ngModel)]="todo.isDone">
+ {{ todo|json }}

{{ }} は、コンポーネントが持つクラス変数をビューに表示するためのAngularのテンプレート記法です。

単に {{ todo }} としてしまうと、オブジェクトである todo を文字列として表現できず [object Object] といった表示になってしまうため、ここでは Angular標準の json パイプ を使って、todo の中身をJSON形式で表現した文字列を画面に表示するようにしてあります。

この状態で画面を操作してみると、以下のように画面の入力に合わせて todo の中身が同期的に変更されていることが分かると思います。これが双方向バインディングです :raised_hands:

次回予告

というわけで、今回は実際のTodoアプリを作るにあたり、Todoを編集するためのコンポーネントを新たに作成し、中身を実装してきました。コンポーネントをアプリに組み込む手順や、クラスファイルとHTMLテンプレートの関係などが前回よりも深くご理解いただけたのではないかと思います。

次回はTodoアプリの開発をさらに進めるべく、Todo一覧画面の実装に取り組んでいきます。

お楽しみに! :raised_hands: