Angular2とangular-cliでTODOを作る

(2016-12-05)

コード: https://github.com/sambaiz/angular2-todo

動いているところ

アプリケーションの作成と立ち上げ

angular-cliをインストールしてサーバーを立ち上げるまで。

$ npm install angular-cli -g
$ ng -v
angular-cli: 1.0.0-beta.21
node: 5.12.0
os: darwin x64

$ ng new mytodo
$ cd mytodo
$ ng server

http://localhost:4200/

新しいコンポーネントを作る

新しいコンポーネントを作る。

$ ng g component todo-list

これでtodo-listディレクトリにコンポーネントクラスとテンプレートとCSS、テストとindexが出力される。

また、app.module.ts(BootstrapするRoot Module)にも追加されている。 NgModuleのdeclartionsなどに入っているものは、各Componentで指定しなくても使えるようになる。

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

import { AppComponent } from './app.component';
import { TodoListComponent } from './todo-list/todo-list.component';

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

なので、この状態でAppComponentのtemplateに追加するだけでTodoListComponentが表示される。 このapp-todo-listというのはコンポーネントのselectorと対応している。

<h1>
  {{title}}
</h1>
<app-todo-list></app-todo-list>
@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent {
...

TODOリストを表示する

TODOリストを入力として受け取って表示するコンポーネントを作る。

@Inputで入力を指定する。

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

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

  @Input() todos: string[] = [];

  constructor() { }

  ngOnInit() {
  }
}

渡すときは[@Inputの変数名]="値"。分かりやすいように変数名を変えてみた。

<app-todo-list [todos]="todos_"></app-todo-list>
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app works!';
  todos_ = ["朝起きる", "昼ご飯を食べる", "夜ご飯を食べる", "寝る"]
}

todosは*ngForでループさせて表示させる。

<ul>
  <li
    *ngFor="let todo of todos"
  >
  {{todo}}
  </li>
</ul>

ちなみに、出力する/しないを制御する*ngIfもあって、 これらの頭に付いている *はStructural directivesの糖衣構文

Directiveは

の3種類ある。

TODOを登録する

登録する用のコンポーネントを作成する。

$ ng g component todo-form

今度は@Outputで出力する方。 onCreateTodoでEventEmitterのnextに次の状態を渡してイベントを発火させる。

iimport { Component, OnInit, Output, EventEmitter } from '@angular/core';

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

  @Output() createTodo = new EventEmitter();

  newTodo = "";

  constructor() { }

  ngOnInit() {
  }

  onCreateTodo() {
    if(this.newTodo !== "") this.createTodo.next(this.newTodo);
    this.newTodo = "";
  }
}

フォームでは(ngSubmit)でonsubmitイベント時にonCreateTodoが呼ばれるようにしている。 また、[(ngModel)]="newTodo"でnewTodoを Two-way bindingして フォームと変数の値を同期させる。これにはFormsModuleが必要で、既にRoot Moduleに含まれている。

<div>
  <form (ngSubmit)="onCreateTodo()">
    <input
      type="text"
      name="todo"
      [(ngModel)]="newTodo"
    >
    <button
      type="submit"
    >
    登録
    </button>
  </form>
</div>

@Outputはこんな感じでハンドリングする。

<app-todo-form (createTodo)="onCreateTodo($event)"></app-todo-form>

登録イベントが起きたらtodos_に追加していく。これでリストの方も更新される。

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app works!';
  todos_ = ["朝起きる", "昼ご飯を食べる", "夜ご飯を食べる", "寝る"]

  onCreateTodo(todo) {
    this.todos_.push(todo);
  }
}

APIを叩くサービスを作る

データを保存して取得する簡易的なAPIを用意した。

var express = require('express')
var bodyParser = require('body-parser')
var app = express()
app.use(bodyParser());

data = []

app.get('/', function (req, res) {
  res.send(data)
})

app.post('/', function (req, res) {
  data.push(req.body)
  res.send(req.body)
})

app.listen(3000, function () {
  console.log('listening on port 3000')
})
$ mkdir services
$ ng g service services/todo

サービスクラスとテストができる。

HTTP Client - ts - GUIDE

Angularで用意されているHTTPクライアントはRxJSのObservableな値を返す。 これを扱うためにimport 'rxjs/Rx'するとサイズがとても大きくなってしまうので必要なものだけをimportする。

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

コンストラクタのhttpはAngularによって providerからDIされる。 Root Moduleのprovidersは空になっているが、HttpModuleで提供されている。

iimport { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import { Observable }     from 'rxjs/Observable';

@Injectable()
export class TodoService {
  private apiUrl = 'http://localhost:3000';

  constructor(private http: Http) { }

  getTodos (): Observable<string[]> {
    return this.http.get(this.apiUrl)
                    .map(this.extractData)
                    .catch(this.handleError);
  }

  addTodo (todo: string): Observable<string> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    return this.http.post(this.apiUrl, { todo }, options)
                    .map(this.extractData)
                    .catch(this.handleError);
  }

  private extractData(res: Response) {
    let body = res.json();
    return body || { };
  }

  private handleError (error: Response | any) {
    // In a real world app, we might use a remote logging infrastructure
    let errMsg: string;
    if (error instanceof Response) {
      const body = error.json() || '';
      const err = body.error || JSON.stringify(body);
      errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
    } else {
      errMsg = error.message ? error.message : error.toString();
    }
    console.error(errMsg);
    return Observable.throw(errMsg);
  }
}

このサービスがDIされるようにprovidersに追加する。

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

import { AppComponent } from './app.component';
import { TodoListComponent } from './todo-list/todo-list.component';
import { TodoFormComponent } from './todo-form/todo-form.component';

import { TodoService } from './services/todo.service';

// RxJS
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

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

サービスを使うようにする。ngOnInitはコンポーネントのプロパティが初期化されたあと一度だけ呼ばれる。

Lifecycle Hooks - ts - GUIDE

import { Component, OnInit } from '@angular/core';
import { TodoService } from './services/todo.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app works!';
  todos_: string[] = []

  constructor(private todoService: TodoService) { }

  ngOnInit() {
    this.todoService.getTodos().subscribe(
                       todos => this.todos_ = todos.map((t) => t["todo"]),
                       error => this.todos_ = ["<error>"]);
  }

  onCreateTodo(todo: string) {
    this.todoService.addTodo(todo).subscribe(
                       todo => this.todos_.push(todo["todo"]),
                       error => this.todos_ = ["<error>"]);
  }
}

テストは気が向いたら書く。