Goalist Developers Blog

はじめてのAngular TODOアプリをつくる③ ルーティング編

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

公式チュートリアルをチラ見しながらTODOアプリを作っています。
今回は第三回のルーティング編です。
前回に引き続き、appコンポーネントをドコドコ分割していきます。

やりたいこと

  • ダッシュボードを作る
  • ダッシュボード、タスク一覧、タスク詳細のビューを切り替える
  • それぞれのビューにURLパスを割り振る
  • TODOアプリとしての体裁を整える

チュートリアルで言うとこのあたりです。

Angular Docs

今回もAngular CLIの恩恵にあずかってまいります。

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

新しい子コンポーネントとしてTasksComponentを作ります。

ng g component tasks

で、今までappコンポーネントにそのまま書いてたタスク一覧と詳細部分を子側に切り取って貼っちゃいます。

app.component.ts

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

export class AppComponent {
  private title = 'todo app';

}

app.component.html

<h1>{{title}}</h1>
<app-tasks></app-tasks>

だいぶすっきりしました。

ルーティングを追加する

クライアント側でルーティングまでやってしまうんですね~

index.htmlのbase hrefを変更

<base href="/">にする、ということでしたが、
Angular CLIでプロジェクト作ったのでデフォルトでそうなってました。

app.module.tsにRouterModuleをインポート

で、パスと呼び出すコンポーネントを宣言。

app.module.ts

import { RouterModule } from '@angular/router';
・・・
@NgModule({
  ・・・
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot([
      {
        path: 'list',
        component: TasksComponent
      }
    ])
  ],
  ・・・
})

リンクを作る

さっき宣言したパスへのリンクを貼ります。
呼び出されたコンポーネントが<router-outlet></router-outlet>に表示されます。

app.component.html

<h1>{{title}}</h1>
<a routerLink="/list">task list</a>
<router-outlet></router-outlet>

トップページはこうなって

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

/listでタスク一覧が表示されるようになりました。

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

ダッシュボードを作る

コンポーネント追加

いつものようにng g componentでコンポーネント追加します。

ng g component dashboard

ルーティング追加

先ほど同様、app.module.tsにルーティングを追加します。
初期表示時にリダイレクトさせたいのでリダイレクトルートも追加。

app.module.ts

RouterModule.forRoot([
  {
    path: 'list',
    component: TasksComponent
  },
  {
    path: 'dashboard',
    component: DashboardComponent
  },
  {
    path: '',
    redirectTo: '/dashboard',
    pathMatch: 'full'
  },
])

これで初期表示(/にアクセス)時に/dashboardがリダイレクト表示されるようになりました。

ナビゲーションを作る

app.component.htmlにもリンクを貼り、ナビゲーション完成です。

app.component.html

<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/list">Task List</a>
</nav>
<router-outlet></router-outlet>

ダッシュボードの中身を作る

チュートリアルだとダッシュボードに表示されるHeroはid若い順に5人という謎な感じですから、
このTODOアプリでは重要なタスクだけ表示してみようとします。

Taskクラスに重要度を追加して、
ついでにTODOアプリとしての体裁のために完了フラグも追加して、

task.ts

export class Task {
  public id: number;
  public name: string;
  public importance: string;
  public isDone: boolean = false;
}

モックデータにも重要度、完了フラグを付けます。

mock-tasks.ts

import { Task } from './task';

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

タスク詳細画面で重要度を編集できるようにして…
(もっと良いやり方ありそうですがとりあえずこれで許して)

task-detail.component.html

<div>
  <label>importance: </label>
  <select [(ngModel)]="task.importance">
    <!-- options = ['low', 'mid', 'high'] -->
    <option *ngFor="let option of options" [value]="option">{{option}}</option>
  </select>
</div>

ダッシュボードは重要度の高いタスクのみフィルタして表示します。

dashboard.component.ts

export class DashboardComponent implements OnInit {
  private tasks: Task[];

  constructor(private taskService: TaskService) { }

  private getTasks(): void {
    this.taskService.getTasks().then(retTasks => {
      this.tasks = retTasks.filter(function(retTask) {
        return retTask.importance === 'high';
      });
    });
  }

  ngOnInit() {
    this.getTasks();
  }
}

dashboard.component.html

<h3>Important Tasks</h3>
<div class="grid grid-pad">
  <a *ngFor="let task of tasks" [routerLink]="['/detail', task.id]" class="col-1-4">
    <div class="module task">
      <h4>{{task.name}}</h4>
    </div>
  </a>
</div>

CSSもチュートリアルからパクってきて適当に入れました。

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

それらしくなってきました。

TODOアプリっぽくする

TODOアプリとしての体裁のために完了フラグをねじこんだのですが、
とりあえずtask-detailから変更できるようにしてみました。

task-detail.component.html

<h2>
 {{task.name}}
  <span *ngIf="!task.isDone">details</span>
  <span *ngIf="task.isDone">is DONE!!!</span>
</h2>
・・・
<button class="btn-primary" (click)="done()">done!!!</button>

task-detail.component.ts

public done(): void {
  this.task.isDone = true;
}

サービスで完了したものはフィルタします。

task.service.ts

public getTasks(): Promise<Task[]> {
  return Promise.resolve(TASKS.filter(function(task) {
     return !task.isDone;
  }));
}

これで完了ボタンを押すと

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

リストで非表示になります。

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

タスク詳細ビューもルーティング

ルーティング追加

先ほど同様、app.module.tsにルーティングを追加します。
:idでパラメータ送れるみたいな?

app.module.ts

{
  path: 'detail/:id',
  component: TaskDetailComponent
}

サービスにメソッド追加

ID指定でタスクひとつを返却するメソッドを追加します。

task.service.ts

public getTask(id: number): Promise<Task> {
  return this.getTasks().then(tasks => tasks.find(task => task.id === id));
}

タスク詳細でパラメータ取得できるようにする

どんどこimportしていきます。

task-detail.component.html

import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; // 追加
import { Location } from '@angular/common'; // 追加
import 'rxjs/add/operator/switchMap'; // 追加

import { Task } from '../task';
import { TaskService } from '../task.service'; // 追加

const OPTIONS: string[] = ['low', 'mid', 'high'];

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

  // コンストラクタで呼び出し
  constructor(
    private taskService: TaskService,
    private route: ActivatedRoute,
    private location: Location
  ) { }

  // コンポーネント初期表示時に実行
  ngOnInit(): void {
    // パラメータの値を取得して、タスク詳細を呼び出す
    this.route.params
      .switchMap((params: Params) => this.taskService.getTask(+params['id']))
      .subscribe(task => this.task = task);
  }

}

ちょっとこの部分

this.route.params
  .switchMap((params: Params) => this.taskService.getTask(+params['id']))
  .subscribe(task => this.task = task);

のわかんなさですが、非同期でgetTaskしてきて?
subscribeのときに結果を代入してる?

ブラウザバック実装

詳細ビューにはダッシュボードか一覧に戻れるように戻るボタンを付けてあげます。

メソッドは

public goBack(): void {
  this.location.back();
}

ダッシュボードからも一覧からも詳細が見られるようになりました~

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

ルーターを別モジュールにまとめる

いつものng generateでモジュールも作れます。

ng g module app-routing

AppRoutingモジュールの中身はapp.module.tsから取ってきてこんなかんじに

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';

import { DashboardComponent } from '../dashboard/dashboard.component';
import { TaskDetailComponent } from '../task-detail/task-detail.component';
import { TasksComponent } from '../tasks/tasks.component';

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'list', component: TasksComponent },
  { path: 'detail/:id', component: TaskDetailComponent },
];

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forRoot(routes)
  ],
  exports: [ RouterModule ],
  declarations: []
})
export class AppRoutingModule { }

これをAppModule側にインポートして、
importsにAppRoutingModuleを追加してあげれば分離完了です。
大変な道のりでした。

後は全体的にCSSをば調整して…こんな感じになりました。

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

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

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

感想

ちょっと本当に今回は長くて大変でした。
チュートリアルに沿っているだけで、モジュール?コンポーネント?ハハハ
まあいつか分かればいいです。

次回感動の最終回、TODOアプリ④ HTTP編を刮目して見よ!
GW明けに投稿予定です。