Structuring Apps With Componentsは、 https://angular.io/ の2番目に書かれているサンプルプログラムのことです。
簡単なTodoリストのサンプルプログラムで、実際に下記URLで動作を確認できます。
https://angular.io/resources/live-examples/homepage-todo/ts/plnkr.html
サイトでは、tsファイルの@Componentのtemplate節にテンプレートが直接書かれていますが、テンプレートを別にしたほうが、コンポーネントの見通しが良いかとも思ったので、tsファイルからtemplateを分離したものを、Githubに用意しました。
なお、この記事は、アプリケーション中重要だと考えられる部分について、適当に書いたものです。申し訳ありません…
アプリケーションを構成するコンポーネント
TodoAppコンポーネントが、main.tsのbootstrapによって実行されるルートコンポーネントです。TodoAppコンポーネントは、中にTodoListとTodoFormのコンポーネントを含んでいます。すなわち、3つのコンポーネントによってアプリケーションが構成されています。
各コンポーネントはexport classのところで、どのような変数、関数がテンプレートから参照できるかを規定しています。Todoリスト自体は、TodoAppのtodosという変数に結びついていますが、テンプレートに{{todos.length}}と書けば、Todoリスト全体の長さ(すなわち、Todoの要素数)を表示することが可能ですし、TodoListコンポーネントは、直接それが代入されています。
以下、小さいコンポーネントからソースを観察します。
TodoList
import {Component, Input} from 'angular2/core';
import {Todo} from './todo';
@Component({
selector: 'todo-list',
styles: [`
.done-true {
text-decoration: line-through;
color: grey;
}`
],
templateUrl: 'app/todo_list.html'
})
export class TodoList {
@Input() todos: Todo[];
}
<ul class="list-unstyled">
<li *ngFor="#todo of todos">
<input type="checkbox" [(ngModel)]="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul>
TodoAppコンポーネントから渡されたtodosをリストで表示しています。
*ngForでtodosからアイテムを1つずつ取り出し、それをli要素の中で展開しています。このコンポーネントの面白いところは、このあたりだけかなあ…と思います。
TodoForm
Todoリストにアイテムを追加するためのフォームを提供するコンポーネントです。
import {Component, Output, EventEmitter} from 'angular2/core';
import {Todo} from './todo';
@Component({
selector: 'todo-form',
templateUrl: 'app/todo_form.html'
})
export class TodoForm {
@Output() newTask = new EventEmitter<Todo>();
task: string = '';
addTodo() {
if (this.task) {
this.newTask.next({text:this.task, done:false});
}
this.task = '';
}
}
<form (ngSubmit)="addTodo()">
<input type="text" [(ngModel)]="task" size="30"
placeholder="add new todo here">
<input class="btn-primary" type="submit" value="add">
</form>
input要素の[(ngModel)]=taskで、TodoFormコンポーネントのtask変数とバインディングされます。
また、フォームをsubmitすれば、TodoFormコンポーネントのaddTodo()関数が呼ばれます。addTodo関数内では、task変数をチェックし、何らかの文字列が含まれている場合(task変数が空文字列ではない場合に限って)、this.newTask.nextで、イベントが発生したことを通知する感じです。
なお、 https://angular.io/docs/ts/latest/api/core/EventEmitter-class.html を見ても、あまりドキュメントが充実していません…。
また、next関数を使っていますが、 https://github.com/angular/angular/issues/4287 とか、d.tsファイルを見てみると、nextはdeprecatedとのことなので、代わりにemitを使うとよさげです。
addTodo() {
if (this.task) {
this.newTask.emit({text:this.task, done:false});
}
this.task = '';
}
TodoApp
import {Component} from 'angular2/core';
import {Todo} from './todo';
import {TodoList} from './todo_list';
import {TodoForm} from './todo_form';
@Component({
selector: 'todo-app',
templateUrl: 'app/todo_app.html',
styles:['a { cursor: pointer; cursor: hand; }'],
directives: [TodoList, TodoForm]
})
export class TodoApp {
todos: Todo[] = [
{text: 'learn angular', done: true},
{text: 'build an angular app', done: false}
];
get remaining() {
return this.todos.filter(todo => !todo.done).length;
}
archive(): void {
let oldTodos = this.todos;
this.todos = [];
oldTodos.forEach(todo => {
if (!todo.done) { this.todos.push(todo); }
});
}
addTask(task: Todo) {
this.todos.push(task);
}
}
<h2>Todo</h2>
<span>{{remaining}} of {{todos.length}} remaining</span>
[ <a (click)="archive()">archive</a> ]
<todo-list [todos]="todos"></todo-list>
<todo-form (newTask)="addTask($event)"></todo-form>
ここで重要なのはテンプレート側だと思われます。TodoListではTodoの配列をtodosという名前でInputするという書き方がされていたので、[todos]="todos"で渡しているわけですね。左辺の[todos]がTodoList側の変数名で、右辺の"todos"がTodoAppコンポーネントが持つtodosを指しています。
また、(newTask)="addTask($event)"によって、TodoFormではnewTaskのイベント発生通知により、TodoAppのaddTaskが実行されることで処理がつながります。$eventには、TodoFormの{text:this.task, done:false}が入ってくるので、あとは、TodoAppのtodosにpushされてリストが更新ということになります。
get remaining()という書き方がES5のgetterの書き方ですが、レガシーに
getRemaining(): Number {
return this.todos.filter(todo => !todo.done).length;
}
みたいに定義して、テンプレートから {{getRemaining()}} と呼び出しても、remaining()と同じ結果が得られます。
まとめ
コンポーネント間でのイベントのやり取りは、AngularJS(1.x)では$scopeを使っていたと思いますが、コンポーネントのソースを見ると、$scopeではなく、EventEmitterという仕組みを通じて実現していることがわかります。
これにより、コンポーネントの独立性が高まり、コンポーネントを開発する人は、どのような変数を外部から参照されるのか、どのようなイベントを通知すべきかということに集中できると思います。
Angular1.xと2.xではこのような違いはあるものの、基本的には、ソースコードの見た目は変わっていても、その仕組みはそこまで変わっていないように思えます。