Goalist Developers Blog

はじめてのAngular TODOアプリをつくる② コンポーネントとサービス編

はじめてAngular2を触ってみた初心者のメモ書きです。

前回の投稿から、公式チュートリアルをチラ見しながらTODOアプリを作っています。
今回は、前回作ったappコンポーネントをサクサクと分割していきたいと思います。
チュートリアルで言うとこのあたりです。

Angular Docs

今回もAngular CLIの恩恵を享受していきます。

目次

コンポーネントを分割する

コンポーネントを作成(ng generate component)

ファイル名はケバブケースでコンポーネント作成します。(最初キャメルにしたらAngular CLIに直された)

ng g component task-detail

作成したコンポーネントをインポート

前回同様Taskクラスをインポートします。import { Task } from './task';
app.component.ts、task-detail.component.tsどちらにもインポート文が必要です。

親コンポーネントのプロパティをバインド

また子コンポーネントのプロパティに@Input()アノテーションを付けることで、
親コンポーネントの値がバインドされるようになります。
よくわかんないけど親側での変更が反映されるんだな~と思っておきます。

task-detail.component.ts

//ここでInputアノテーションが使えるようにInputをインポートしてます
import { Component, OnInit, Input } from '@angular/core';

import { Task } from './task';

@Component({
  selector: 'app-task-detail',
  templateUrl: './task-detail.component.html',
  styleUrls: ['./task-detail.component.scss']
})
export class TaskDetailComponent implements OnInit {
  @Input() task: Task;

  constructor() { }

  ngOnInit() {
  }

}

ディレクティブを置き換えてコンポーネント表示

task-detail.component.html

<div *ngIf="task">
  <h2>{{task.name}} details!</h2>
  <div><label>id: </label>{{task.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="task.name" placeholder="name"/>
  </div>
</div>

今まで↑のディレクティブが書いてあった部分を今作ったコンポーネントに置き換えます。
selectedTaskをtaskというプロパティ名で子コンポーネントに送ってバインドしています。

app.component.html

<app-task-detail [task]="selectedTask"></app-task-detail>

ヌゥ…なんでエラー

ERROR in C:/angularWork/my-new-app/src/app/task-detail/task-detail.component.ts (3,22): Cannot find module './task'.)

ERROR in ./src/app/task-detail/task-detail.component.ts
Module not found: Error: Can't resolve './task' in 'C:\angularWork\my-new-app\src\app\task-detail'
 @ ./src/app/task-detail/task-detail.component.ts 11:0-30
 @ ./src/app/app.module.ts
 @ ./src/main.ts
 @ multi webpack-dev-server/client?http://localhost:4200/ ./src/main.ts

appコンポーネントにも同じインポート文書いてるのにな?と思ったら
task-detail.component.tsはapp/task-detail/下にあったのでした。
import { Task } from './task';じゃなくて
import { Task } from '../task';でした。
変なところで時間使ってしまった。

これでコンポーネント分割できました~見た目は全く変わってませんが。

f:id:y-iio:20170410164651p:plain

サービスを使う

気を取り直して、サービスを作っていきます。
チュートリアルで言うとこのあたりです。

Angular Docs

サービスを作る(ng generate service)

新しいコマンドですが、いつものようにng g

ng g service task
  installing service
    create src\app\task.service.spec.ts
    create src\app\task.service.ts
    WARNING Service is generated but not provided, it must be provided to be used

not provided?ということは何かやらないと使えないのか…

app下にtask.service.tsが作られてました。
中身はこう。

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

@Injectable()
export class TaskService {

  constructor() { }

}

@Injectable()アノテーションは最初からつけておいてくれるんですね。

モックデータを移動する

今までapp.component.tsで宣言していた配列を別ファイルに分離させます。
Taskクラスも忘れずにインポート。

src/app/mock-tasks.ts

import { Task } from './task';

export const TASKS: Task[] = [
  { id: 11, name: '企画ロードマップ作成' },
  { id: 12, name: '山田さんにメール返信' },
  { id: 13, name: 'Angular2キャッチアップ' },
  { id: 14, name: 'ブログ更新' },
  { id: 15, name: '新卒技術研修' }
];

親コンポーネントのプロパティであるところのtasksは初期化しないように変更します。

app.component.ts

export class AppComponent {
  private title = 'todo app';
  private tasks: Task[];// ここです
  private selectedTask: Task;
}

task.service.tsでモックデータを受け取って、返却するようにします。

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

import { Task } from './task';
import { TASKS } from './mock-tasks';

@Injectable()
export class TaskService {
  public getTasks(): Task[] {
    return TASKS;
  }

  constructor() { }

}

TaskServiceをインポートする

作ったサービスをAppComponentで使えるようにしていきます。
いつものようにインポート文を加えて

import { TaskService } from './task.service';

コンストラクタでサービスインスタンスを取得します。newして呼び出してはいけないそうです。

export class AppComponent {
  private title = 'todo app';
  private tasks: Task[];
  private selectedTask: Task;

  constructor(private taskService: TaskService) {
  }

  public onSelect(task: Task): void {
    this.selectedTask = task;
  }
}

で保存したらエラー

f:id:y-iio:20170410202740p:plain

そういえばさっきnot providedって言われてました。
app.module.tsでprovidersを宣言しないといけないようです。

app.module.ts

@NgModule({
 ・・・
  providers: [TaskService], 
 ・・・
})

これでエラーが消えました。一安心。
サービスのgetTasks()を宣言しなおします。

app.component.ts

private getTasks(): void {
  this.tasks = this.taskService.getTasks();
}

ngOnInitというメソッドが、コンポーネントの生成後に呼ばれるそうなので、
ここでさっき作ったgetTasks()を呼んでもらいます。

app.component.ts

import { Component, OnInit } from '@angular/core'; // OnInitもインポート
・・・
export class AppComponent implements OnInit { // implementsでOnInitを使えるようにして
  private title = 'todo app';
  private tasks: Task[];
  private selectedTask: Task;

  constructor(private taskService: TaskService) { }

  private getTasks(): void {
    this.taskService.getTasks().then(tasks => this.tasks = tasks);
  }

  // ここで使う
  ngOnInit() {
    this.getTasks();
  }
}

これでモックデータを呼んでくることができるようになりました~
見た目は全く変わってませんが。

f:id:y-iio:20170410164440p:plain

非同期でサービスを使う

promise使って非同期でモックデータを持ってきてみます。

TaskServiceのgetTasks()メソッドでTasksを返していたところを、
Promise<Tasks
>を返すように変更します。

task.service.ts

public getTasks(): Promise<Task[]> {
  return Promise.resolve(TASKS);
}

tasks.component.ts

private getTasks(): void {
  this.taskService.getTasks().then(tasks => this.tasks = tasks);
}

アロー関数、初めて使いました。
thisを別名で宣言しなくて良いんですね。

これで非同期でデータを取ってくることができるようになりました~
見た目は全く変わってませんが。

f:id:y-iio:20170410164440p:plain

ソースコードまとめ

感想

なんだか大変だったわりに見た目は全く変わっていません。
何にもやってないみたいじゃないか、いやいや学びはありました。
次回でもう少しは体裁を整えられるのか、TODOアプリを作る③ ルーティング編、乞うご期待!