--- title: 世界初!?Angular4を使ってtodoMVCよりも先にtodoアプリを作ってみた tags: Angular2 JavaScript AngularJS4 Angular author: komatsu-kenta slide: false --- ##はじめに 先日Angular 4がリリースされました。 個人的に新しいjsのフレームワークを試す際には[TodoMVC](http://todomvc.com)を利用しているのですが Angular 4用のtodoアプリはまだ公開されていなかったので素振りがてらに作ってみました。 ##環境構築 まずは、angular-cliをグローバルにインストールします。 ```none npm install -g @angular/cli ``` 事前にTypeScriptもインストールしておきましょう。 ```none npm install typescript -g ``` todoアプリ用のプロジェクトを作成します。 ```none ng new todo ``` npmインストールを行い、サーバーを起動します。 ```none cd todo npm install ng serve --open ``` http://localhost:4200 にアクセスし、 app works!と表示されれば環境構築は成功です。 ##実装手順 ファイル構成は下記のようにしました。 ``` . ├──index.html ├──main.ts ├──polyfills.ts ├──styles.css ├──test.ts ├──tsconfig.app.json ├──tsconfig.spec.json ├──typings.d.ts │ ├──app │ ├──component.css │ ├──component.html │ ├──component.ts │ ├──module.ts │ └──store.ts │ ├──assets │ └──.gitkeep │ └──environments ├──environment.prod.ts ├──environment.ts └──favicon.ico ``` 下記がtodoアプリのソースになります。 Angular2との互換性が高くほぼtodoMVCのソースを使いまわすことができました。 ```ts:component.ts import { Component } from '@angular/core'; import {TodoStore, Todo} from './store'; @Component({ selector: 'todo-app', templateUrl: './component.html', styleUrls: ['./component.css'] }) export class TodoApp { todoStore: TodoStore; newTodoText = ''; constructor(todoStore: TodoStore) { this.todoStore = todoStore; } stopEditing(todo: Todo, editedTitle: string) { if(editedTitle.length !== 0){ todo.title = editedTitle.trim(); } todo.editing = false; } updateEditingTodo(todo: Todo, editedTitle: string) { editedTitle = editedTitle.trim(); todo.editing = false; if (editedTitle.length === 0) { return this.todoStore.remove(todo); } todo.title = editedTitle; } editTodo(todo: Todo) { todo.editing = true; } removeCompleted() { this.todoStore.removeCompleted(); } toggleCompletion(todo: Todo) { this.todoStore.toggleCompletion(todo); } remove(todo: Todo){ this.todoStore.remove(todo); } addTodo() { if (this.newTodoText.trim().length) { this.todoStore.add(this.newTodoText); this.newTodoText = ''; } } } ``` ```html:component.html

todos

``` ```ts:store.ts export class Todo { completed: Boolean; editing: Boolean; private _title: String; get title() { return this._title; } set title(value: String) { this._title = value.trim(); } constructor(title: String) { this.completed = false; this.editing = false; this.title = title.trim(); } } export class TodoStore { todos: Array; constructor() { let persistedTodos = JSON.parse(localStorage.getItem('angular2-todos') || '[]'); // Normalize back into classes this.todos = persistedTodos.map( (todo: {_title: String, completed: Boolean}) => { let ret = new Todo(todo._title); ret.completed = todo.completed; return ret; }); } private updateStore() { localStorage.setItem('angular2-todos', JSON.stringify(this.todos)); } private getWithCompleted(completed: Boolean) { return this.todos.filter((todo: Todo) => todo.completed === completed); } allCompleted() { return this.todos.length === this.getCompleted().length; } setAllTo(completed: Boolean) { this.todos.forEach((t: Todo) => t.completed = completed); this.updateStore(); } removeCompleted() { this.todos = this.getWithCompleted(false); this.updateStore(); } getRemaining() { return this.getWithCompleted(false); } getCompleted() { return this.getWithCompleted(true); } toggleCompletion(todo: Todo) { todo.completed = !todo.completed; this.updateStore(); } remove(todo: Todo) { this.todos.splice(this.todos.indexOf(todo), 1); this.updateStore(); } add(title: String) { this.todos.push(new Todo(title)); this.updateStore(); } } ``` ```ts:module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { TodoApp } from './component'; import { TodoStore } from './store'; @NgModule({ declarations: [ TodoApp ], imports: [ BrowserModule, FormsModule, HttpModule ], providers: [TodoStore], bootstrap: [TodoApp] }) export class AppModule { } ``` ```ts:main.ts import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule); ``` ```html:index.html Todo Loading... ``` ```css:component.css html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #4d4d4d; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; font-weight: 300; } button, input[type="checkbox"] { outline: none; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp h1 { position: absolute; top: -155px; width: 100%; font-size: 100px; font-weight: 100; text-align: center; color: rgba(175, 47, 47, 0.15); -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; border: 0; outline: none; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } label[for='toggle-all'] { display: none; } .toggle-all { position: absolute; top: -55px; left: -12px; width: 60px; height: 34px; text-align: center; border: none; /* Mobile Safari */ } .toggle-all:before { content: '❯'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: 506px; padding: 13px 17px 12px 17px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle:after { content: url('data:image/svg+xml;utf8,'); } .todo-list li .toggle:checked:after { content: url('data:image/svg+xml;utf8,'); } .todo-list li label { white-space: pre-line; word-break: break-all; padding: 15px 60px 15px 15px; margin-left: 45px; display: block; line-height: 1.2; transition: color 0.4s; } .todo-list li.completed label { color: #d9d9d9; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { color: #777; padding: 10px 15px; height: 20px; text-align: center; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a.selected, .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #bfbfbf; font-size: 10px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } .toggle-all { -webkit-transform: rotate(90deg); transform: rotate(90deg); -webkit-appearance: none; appearance: none; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } ``` ```css:style.css body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #4d4d4d; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; font-weight: 300; } ``` Angular2との互換性が無い部分は 「*ngFor="#todo of todoStore.todos" 」だけでした。 「*ngFor=let todo of todoStore.todos」にする必要がありあます。 通常のタグでのテンプレート参照変数は現在も使えますが、ループ時には使えない 仕様になったみたいです。 ##新機能 todoアプリを作る際にはほとんどangular4の新機能を使う場面が無かったので その一部をこちらで紹介したいと思います。 - *ngIf/Else これまで*ngifは指定した要素の表示、非表示を切り替える程度のものでしたが、 今回からはelseを指定することにより下記のように表示させる要素そのものを 切り替えることが出来るようになりました。 ```ts
isValid === true
isValid === false ``` - ng-template 上記でも使っているng-templateタグですがもともとはtemplateタグとして 使われていたものです。 現在はどちらで書いても利用することができますが、次回のアップデートの際には ng-templateしか使えなくなるのではないでしょうか。 - as これまでは非同期で情報を取得したい場合には asyncパイプと?.を組み合わせる必要がありました。 ```

{{ (user | async)?.name }}

{{ (user | async)?.age }}
``` asを使うことで*ngIf="userの判定結果を 利用できるようになります。 ```ts

{{ userModel.name }}

{{ userModel.date }}
``` ?.が消え、asyncパイプも1回にできるので 単純にみやすいだけでなく、性能面でも 改善が見込めます。 - Titlecase uppercase,lowercase等のpipe処理に 単語の1文字目を大文字にするtitlecaseが 追加されました。 ```ts

{{ 'hoge fuga piyo' | titlecase }}

``` ##おまけ Angular2のtodoアプリにはtodoの編集時に文字数を0にしてfocusアウトすると空のtodoができてしまうという現象が起きます。 他のサンプルアプリにはそんなバグ無いんですけどね。 ##所感 AngularからAngular 2への変化はすさまじかったですが、Angular 2からAngular 4に関しては純粋なバージョンアップといった感じですね。 Angular 2に関しては批判も多かったのであまり期待せずにAngular 4を触ってみたのですが、思っていたより使いやすかったのでこれからも使っていこうと思います。 ##参考 - TodoMVC http://todomvc.com/ - What's new in Angular 4? http://blog.ninja-squad.com/2017/03/24/what-is-new-angular-4/