albatrosary's blog

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

Angular2.RCの終了もそろそろかな?

Angular2がGW中にRCに成り、それは良かったのだがRCから破壊的な変更がいくつもありいい加減にして欲しいなという雰囲気になってきたところ最後のRC5がそろそろリリースされますね!「Angular Weekly Meeting Notes http://g.co/ng/weekly-notes」の内容がこちらです。

Angular Weekly Meeting Notes (http://g.co/ng/weekly-notes)

August 8

Core Status  - RC 5
Top SHA in master addresses remaining NgModules implementation.
Caretaker is gatekeeper until we release.
Ames validation and internal tests pass
No blocking docs issues
NgModules is complete in master. This is the last piece of the core API.
Plan for remaining 2.0 releases after RC5 --
post-RC5: No further breaking changes in Router, Forms, and I18N before 2.1.
post-RC5: No further breaking changes in Core before 2.1. New features in animations, I18N, and Forms only. Bugfixes OK.
RC6: removes all deprecated APIs (planned: at least 1 week from RC5) and we assess I18N and Forms API and Animations for final.
We'll assess RC6 readiness at next week's meeting.
CLI - need to determine what will be done on timeline for final.
Interns

って、目をこすったのがRC6のリリース。これは新しく追加されるNgModuleへの移行期間を設定した感のあるRCのようです、「at least 1 week from RC5」という話なのでそうなのかと。そしてCoreモジュールはRC5完成と言ってます。

長旅だったRCもそろそろ作業を終えstableに行くかなという空気がようやく作られてきた感じがします。と言ってもAngular2への過去の経験上完全には安心できないというのもこれまたあったりなかったり。

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">