Angular2とangular-cliでTODOを作る
angularコード: 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は
- コンポーネント
- Structual directives (DOM elementを追加したり削除したりする)
- Attribute directives (Dom elementの見た目や挙動を変える)
の3種類ある。
TODOを登録する
登録する用のコンポーネントを作成する。
$ ng g component todo-form
今度は@Outputで出力する方。
onCreateTodoでEventEmitterのnextに次の状態を渡してイベントを発火させる。
import { 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
サービスクラスとテストができる。
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で提供されている。
import { 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はコンポーネントのプロパティが初期化されたあと一度だけ呼ばれる。
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>"]);
}
}
テストは気が向いたら書く。