albatrosary's blog

UI/UXとエンタープライズシステム

Angular 2の新しいNgModuleを使ってTodosを作ってみました

Angular 2 RC5から導入されるNgModuleを使ってTodosを作ってみます。NgModuleの導入により@Componentで定義されていたものをNgModuleでまとめて定義することが可能となるようです。詳しくは @laco 氏のこちらを見て頂けると良いと思います。

ng2-info.github.io

変わるところとしては次のようなところのようです:

  • bootstrap関数の呼び出し
  • @Componentのdirectives
  • @Componentのpipes

Todosを作ると少なくともコンポーネントが4つは作ると思います。

  • 元となるコンポーネント: todos.ts
  • 入力のコンポーネント: todos.input.ts
  • 一覧のコンポーネント: todos.body.ts
  • 詳細機能用のコンポーネント: todos.detail.ts

このコンポーネント郡をひとつにまとめるための機能がNgModuleかなといった感じです。ベストプラクティスは今度色々と出てくるとは思います。

具体的に見ていきます。

元となるコンポーネント: todos.ts

todos.tstodos.input.tstodos.body.tsを利用しています。RC4までだとdirectivesを定義し、コンポーネント呼び出すように記述していましたが、これをNgModuleに記載します。したがってtodos.tsからコンポーネントの宣言は削除されます。

before:

@Component({
  selector: 'my-app',
  template: `
    <h2>Todos</h2>
    <todos-input></todos-input>
    <todos-body></todos-body>
  `,
  directives: [
    TodosInputComponent,
    TodosBodyComponent
  ],
  providers: [TodoStore]
})

after:

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

import {TodosInputComponent} from './todos.input';
import {TodosBodyComponent} from './todos.body';

import {TodoStore} from './shared/todo.store';

@Component({
  selector: 'my-app',
  template: `
    <h2>Todos</h2>
    <todos-input></todos-input>
    <todos-body></todos-body>
  `,
  providers: [TodoStore]
})
export class TodosComponent {}

入力のコンポーネント: todos.input.ts

入力コンポーネントは、関連するコンポーネントを持ちあわせていませんでした、しかし、NgFormを利用していたためその記述をprovidersにしていましたがこれが無くなります。

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

import {TodoStore} from './shared/todo.store';
import {Todo} from './shared/todo';

@Component({
  selector: 'todos-input',
  template: `
    <form (ngSubmit)="onSubmit()" #todoForm="ngForm">
      <input [(ngModel)]="todo.title" name="title" required placeholder="title">
      <textarea [(ngModel)]="todo.desc" name="desc" required placeholder="desc"></textarea>
      <button type=submit [disabled]="!todoForm.form.valid">登録</button>
    </form>
  `,
  styles: [`
    input {
      width: 100%;
    }
    textarea {
      width: 100%;
      height: 7em;
    }
  `]
})
export class TodosInputComponent
  implements OnInit {

  private todo: Todo;
  
  constructor (
    private todoStore: TodoStore
  ) {}

  ngOnInit(): void {
    this.todo = new Todo;
  }

  public onSubmit(): void {
    this.todoStore.add(this.todo);
    this.todo = new Todo;
  }
}

一覧のコンポーネント: todos.body.ts

todos.body.tstodos.detail.tsを利用していましたのでprovidersで宣言していましたがこれが無くなります。

before

@Component({
  selector: 'todos-body',
  template: `
    <todos-detail
      *ngFor="let todo of todos; let i = index"
      [list-no]="i"
      [todo-data]="todo"
      (on-delete)="onDelete(i)">
    </todos-detail>
  `,
  directives: [TodosDetailComponent]
})

after

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

import {TodoStore} from './shared/todo.store';
import {Todo} from './shared/todo';

@Component({
  selector: 'todos-body',
  template: `
    <todos-detail
      *ngFor="let todo of todos; let i = index"
      [list-no]="i"
      [todo-data]="todo"
      (on-delete)="onDelete(i)">
    </todos-detail>
  `
})
export class TodosBodyComponent
  implements OnInit {

  private todos: Todo[];

  constructor (
    private todoStore: TodoStore
  ) {}

  public ngOnInit () {
    this.todos = this.todoStore.list;
  }

  public onDelete(index: number): void {
    this.todoStore.delete(index);
  }
}

詳細機能用のコンポーネント: todos.detail.ts

todos.detail.tsは変化なく次の通りです

import {Component, Input, Output, EventEmitter} from '@angular/core';

import {Todo} from './shared/todo';

@Component({
  selector: 'todos-detail',
  template: `
    <div ngClass="list-no">{{listNo+1}}</div>
    <div>
      <p>{{todo.title}}</p>
      <pre>{{todo.desc}}</pre>
      <button (click)="onClick($event)">削除</button>
    </div>
    `,
  styles: [`
    :host {
      display: block;
      border:#4e5d5f solid 2px;
      margin: 5px 0 5px 0;
      padding: 5px 0 5px 0;
      width: 100%;
      min-height: 112px;
      overflow : hidden;
    }
    .list-no {
      text-align: center;
      font-size: 2rem;
      margin: 5px 5px 5px 5px;
      width: 100px;
      height: 100px;
      background-color: #4e5d5f;
      color: #ffffff;
    }
    div {
      float: left;
    }
    button {
      background-color: #e8345a;
    }
  `]
})
export class TodosDetailComponent {
  @Input('list-no')
  private listNo: number;

  @Input('todo-data')
  private todo: Todo;

  @Output('on-delete')
  private onDelete = new EventEmitter();
  
  public onClick($event: any): void {
    this.onDelete.emit($event);
  }
}

肝心のNgModule

NgModuleは次のようになります。モジュールで利用するコンポーネントを列記し、その中でもベースとなるエントリーコンポーネント、ブートストラップするコンポーネントを定義します。ブートストラップする方法が以前とは異なります。

todos.module.ts

import {NgModule, ApplicationRef} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {TodosComponent} from './todos';
import {TodosInputComponent} from './todos.input';
import {TodosBodyComponent} from './todos.body';
import {TodosDetailComponent} from './todos.detail';

@NgModule({
    imports: [BrowserModule, CommonModule, FormsModule, HttpModule],
    declarations: [TodosComponent, TodosInputComponent, TodosBodyComponent, TodosDetailComponent],
    entryComponents: [TodosComponent],
    bootstrap: [TodosComponent]
})
export class AppModule {
  constructor(appRef: ApplicationRef) {
    appRef.bootstrap(TodosComponent);
  }
}

ブートストラップは少し変わって次のようになるようです。

main.ts

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

import {AppModule} from '../components';

platformBrowserDynamic().bootstrapModule(AppModule);

最後に

ざっくりNgModuleを利用してみました。調べるともう少し違う書き方になるかも知れませんが雰囲気は味わえるかなと思います。今回作ったコードはこちらです:

GitHub - albatrosary/Angular2Todos

尚、今回はNightlyを使ってコードを書いています。Nightlyのインストール方法は下記を御覧ください:

How to Use Angular 2 Nightly Builds | <output type="laco">