├── Procfile ├── app ├── components │ ├── content │ │ ├── content.css │ │ ├── content.component.ts │ │ └── content.html │ ├── footer │ │ ├── footer.html │ │ └── footer.component.ts │ └── header │ │ ├── header.html │ │ └── header.component.ts ├── models │ └── todo.model.ts ├── main.ts ├── app.html ├── common │ └── services │ │ └── logger.service.ts ├── app.component.ts ├── system-config.js └── services │ └── todo.service.ts ├── images ├── sample1.png ├── sample2.png └── sample3.png ├── .vscode └── settings.json ├── .editorconfig ├── typings.json ├── tsconfig.json ├── docs ├── 01.md ├── 02.md ├── 03.md ├── 05.md └── 04.md ├── .gitignore ├── LICENSE ├── index.html ├── package.json ├── karma-test.shim.js ├── README.md └── karma.conf.js /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start-heroku -------------------------------------------------------------------------------- /app/components/content/content.css: -------------------------------------------------------------------------------- 1 | .complate { 2 | text-decoration: line-through; 3 | } -------------------------------------------------------------------------------- /images/sample1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitsuruog/angular2-todo-tutorial/HEAD/images/sample1.png -------------------------------------------------------------------------------- /images/sample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitsuruog/angular2-todo-tutorial/HEAD/images/sample2.png -------------------------------------------------------------------------------- /images/sample3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitsuruog/angular2-todo-tutorial/HEAD/images/sample3.png -------------------------------------------------------------------------------- /app/components/footer/footer.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/models/todo.model.ts: -------------------------------------------------------------------------------- 1 | export class Todo { 2 | constructor(public id:number, 3 | public title:string, 4 | public isCompleted:boolean) { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/*.js": { "when": "$(basename).ts"}, 5 | "**/*.js.map": true 6 | } 7 | } -------------------------------------------------------------------------------- /app/main.ts: -------------------------------------------------------------------------------- 1 | import {bootstrap} from '@angular/platform-browser-dynamic'; 2 | import {Logger} from './common/services/logger.service'; 3 | import {AppComponent} from './app.component'; 4 | 5 | bootstrap(AppComponent, [Logger]); 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | 10 | -------------------------------------------------------------------------------- /app/components/header/header.html: -------------------------------------------------------------------------------- 1 |
2 |

Todos

3 |
4 |
5 | 6 | 7 |
8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /app/common/services/logger.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class Logger { 5 | 6 | constructor() { 7 | } 8 | 9 | log(message:any) { 10 | console.log(message); 11 | } 12 | 13 | error(message:any) { 14 | console.error(message); 15 | } 16 | 17 | warn(message:any) { 18 | console.warn(message); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {}, 3 | "devDependencies": {}, 4 | "ambientDevDependencies": { 5 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#26c98c8a9530c44f8c801ccc3b2057e2101187ee" 6 | }, 7 | "ambientDependencies": { 8 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "typings/main", 15 | "typings/main.d.ts", 16 | ".tmp" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /docs/01.md: -------------------------------------------------------------------------------- 1 | # 1. TodoModelクラスを作成する 2 | 3 | Todoのデータを格納するためのModelクラスを作成します。Todoのデータ構造は次の通りです。 4 | 5 | - id: Todoを一意に表すID 6 | - title: Todoの内容 7 | - isCompleted: Todoが完了してるかどうか(true=完了) 8 | 9 | ModelクラスはTypeScriptのクラスで定義します。 10 | 11 | :pencil2: **app/models/todo.model.ts** 12 | ```ts 13 | export class Todo { 14 | constructor(public id:number, 15 | public title:string, 16 | public isCompleted:boolean) { 17 | } 18 | } 19 | ``` 20 | 21 | > :sparkles: `public`を指定すると、コンストラクタの変数名と同じプロパティ名で外部からアクセスすることができます。 22 | -------------------------------------------------------------------------------- /app/components/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {TodoService} from '../../services/todo.service'; 3 | 4 | @Component({ 5 | selector: 'todo-header', 6 | templateUrl: 'app/components/header/header.html' 7 | }) 8 | export class TodoHeaderComponent { 9 | 10 | title:string; 11 | 12 | constructor(private service:TodoService) { 13 | } 14 | 15 | addTodo() { 16 | if (this.title.trim().length) { 17 | this.service.add(this.title); 18 | this.title = null; 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/components/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | import {TodoService} from '../../services/todo.service'; 3 | import {Todo} from "../../models/todo.model"; 4 | 5 | @Component({ 6 | selector: 'todo-footer', 7 | templateUrl: 'app/components/footer/footer.html' 8 | }) 9 | export class TodoFooterComponent { 10 | 11 | @Input() 12 | todos:Todo[]; 13 | 14 | constructor(private service:TodoService) { 15 | } 16 | 17 | getCompletedCount() { 18 | return this.service.getComplatedCount(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/components/content/content.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | import {TodoService} from '../../services/todo.service'; 3 | import {Todo} from "../../models/todo.model"; 4 | 5 | @Component({ 6 | selector: 'todo-content', 7 | templateUrl: 'app/components/content/content.html', 8 | styleUrls: ['app/components/content/content.css'] 9 | }) 10 | export class TodoContentComponent { 11 | 12 | @Input() 13 | todos:Todo[]; 14 | 15 | constructor(private service:TodoService) { 16 | } 17 | 18 | toggleComplate(todo:Todo) { 19 | this.service.toggleComplate(todo); 20 | } 21 | 22 | deleteTodo(todo:Todo) { 23 | this.service.remove(todo); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {TodoContentComponent} from './components/content/content.component'; 3 | import {TodoHeaderComponent} from './components/header/header.component'; 4 | import {TodoFooterComponent} from './components/footer/footer.component'; 5 | import {TodoService} from './services/todo.service'; 6 | import {Todo} from "./models/todo.model"; 7 | 8 | @Component({ 9 | selector: 'my-app', 10 | templateUrl: 'app/app.html', 11 | providers: [TodoService], 12 | directives: [TodoContentComponent, TodoFooterComponent, TodoHeaderComponent] 13 | }) 14 | export class AppComponent { 15 | todos: Todo[]; 16 | 17 | constructor(private service: TodoService) { 18 | this.todos = this.service.todos; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/components/content/content.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /docs/02.md: -------------------------------------------------------------------------------- 1 | # 2. TodoServiceクラスを作成する 2 | 3 | TodoModelクラスを操作するためのServiceクラスを作成します。まず最低限Todoアプリで必要となるServiceクラスのI/Fを定義します。 4 | 5 | - add(): Todoを登録します。 6 | - remove(): Todoを削除します。 7 | - toggleComplate(): Todoの完了状態を変更します。 8 | 9 | :pencil2: **app/services/todo.service.ts** 10 | ```ts 11 | import {Injectable} from "@angular/core"; 12 | import {Todo} from "../models/todo.model"; 13 | 14 | @Injectable() 15 | export class TodoService { 16 | constructor() {} 17 | add():void {} 18 | remove():void {} 19 | toggleComplate():void {} 20 | } 21 | ``` 22 | 23 | > :sparkles: `@Injectable()`は、decorator(デコレータ)と呼ばれるものです。これが付けられたServiceクラスは、アプリケーション内のComponentにて利用することがでるようになります。個人的には自作した全てのSerivceクラスには付与して置いた方がいいと考えています。 24 | 25 | > :sparkles: `decorator(デコレータ)`は、将来Javascriptの共通APIとして利用できるよう、現在議論されている最新の言語仕様です。(アノテーションではなくデコレータです。) 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | .tmp 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | .idea 37 | typings 38 | jspm_packages 39 | bower_components 40 | report 41 | 42 | 43 | app/**/*.js 44 | !app/**/system-config.js 45 | app/**/*.map -------------------------------------------------------------------------------- /app/system-config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | map: { 3 | 'rxjs': '/node_modules/rxjs', 4 | '@angular': '/node_modules/@angular' 5 | }, 6 | packages: { 7 | 'app': { 8 | main: 'index.js', 9 | defaultExtension: 'js' 10 | }, 11 | '@angular/core': { 12 | main: 'index.js', 13 | defaultExtension: 'js' 14 | }, 15 | '@angular/compiler': { 16 | main: 'index.js', 17 | defaultExtension: 'js' 18 | }, 19 | '@angular/common': { 20 | main: 'index.js', 21 | defaultExtension: 'js' 22 | }, 23 | '@angular/platform-browser': { 24 | main: 'index.js', 25 | defaultExtension: 'js' 26 | }, 27 | '@angular/platform-browser-dynamic': { 28 | main: 'index.js', 29 | defaultExtension: 'js' 30 | }, 31 | '@angular/router': { 32 | main: 'index.js', 33 | defaultExtension: 'js' 34 | }, 35 | 'rxjs': { 36 | defaultExtension: 'js' 37 | } 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mitsuru Ogawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /app/services/todo.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {Todo} from "../models/todo.model"; 3 | 4 | const STORAGE_KEY = 'angular2-todo'; 5 | 6 | @Injectable() 7 | export class TodoService { 8 | 9 | todos:Todo[]; 10 | 11 | constructor() { 12 | const persistedTodos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); 13 | this.todos = persistedTodos.map(todo => { 14 | return new Todo( 15 | todo.id, 16 | todo.title, 17 | todo.isCompleted 18 | ); 19 | }); 20 | } 21 | 22 | add(title:string):void { 23 | let newTodo = new Todo( 24 | Math.floor(Math.random() * 100000), // ランダムにIDを発番する 25 | title, 26 | false 27 | ); 28 | this.todos.push(newTodo); 29 | this.save(); 30 | } 31 | 32 | remove(todo:Todo):void { 33 | const index = this.todos.indexOf(todo); 34 | this.todos.splice(index, 1); 35 | this.save(); 36 | } 37 | 38 | toggleComplate(todo:Todo):void { 39 | this.todos.filter(t => t.id === todo.id) 40 | .map(t => t.isCompleted = !t.isCompleted); 41 | this.save(); 42 | } 43 | 44 | getComplatedCount():number { 45 | // TODO newTodoのInputがchangeするたびに呼び出されている 46 | return this.todos.filter(todo => todo.isCompleted).length; 47 | } 48 | 49 | private save():void { 50 | console.log('saving : ', this.todos); 51 | localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular2 Todo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 25 | 34 | 35 | 36 | 37 | Loading... 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-todo-tutorial", 3 | "version": "1.0.0", 4 | "description": "angular 2 Todo application sample", 5 | "scripts": { 6 | "tsc": "tsc", 7 | "tsc:w": "tsc -w", 8 | "lite": "lite-server", 9 | "http-server": "http-server", 10 | "test": "karma start karma.conf.js", 11 | "coverage": "node_modules/.bin/remap-istanbul -i report/coverage/coverage-final.json -o report/coverage/ -t html", 12 | "start": "concurrently -r \"npm run tsc:w\" \"npm run lite\" ", 13 | "start-heroku": "concurrently \"npm run tsc\" \"npm run http-server\" ", 14 | "postinstall": "typings install" 15 | }, 16 | "author": "mitsuruog ", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@angular/common": "2.0.0-rc.4", 20 | "@angular/compiler": "2.0.0-rc.4", 21 | "@angular/core": "2.0.0-rc.4", 22 | "@angular/forms": "0.2.0", 23 | "@angular/http": "2.0.0-rc.4", 24 | "@angular/platform-browser": "2.0.0-rc.4", 25 | "@angular/platform-browser-dynamic": "2.0.0-rc.4", 26 | "@angular/router": "3.0.0-alpha.7", 27 | "@angular/router-deprecated": "2.0.0-rc.2", 28 | "@angular/upgrade": "2.0.0-rc.4", 29 | "systemjs": "0.19.27", 30 | "core-js": "^2.4.0", 31 | "reflect-metadata": "^0.1.3", 32 | "rxjs": "5.0.0-beta.6", 33 | "zone.js": "^0.6.12", 34 | "bootstrap": "^3.3.6", 35 | "es6-promise": "^3.0.2", 36 | "es6-shim": "^0.35.0" 37 | }, 38 | "devDependencies": { 39 | "concurrently": "^2.0.0", 40 | "http-server": "^0.9.0", 41 | "jasmine-core": "^2.4.1", 42 | "karma": "^0.13.22", 43 | "karma-chrome-launcher": "^0.2.2", 44 | "karma-coverage": "^0.5.5", 45 | "karma-jasmine": "^0.3.7", 46 | "karma-mocha-reporter": "^2.0.0", 47 | "remap-istanbul": "^0.5.1", 48 | "typescript": "^1.8.10", 49 | "typings":"^1.0.4", 50 | "eslint": "^2.3.0", 51 | "jscs": "^2.11.0", 52 | "lite-server": "^2.2.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /karma-test.shim.js: -------------------------------------------------------------------------------- 1 | /*global jasmine, __karma__, window*/ 2 | Error.stackTraceLimit = Infinity; 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; 4 | 5 | __karma__.loaded = function () { 6 | }; 7 | 8 | 9 | function isJsFile(path) { 10 | return path.slice(-3) == '.js'; 11 | } 12 | 13 | function isSpecFile(path) { 14 | return path.slice(-8) == '.spec.js'; 15 | } 16 | 17 | function isBuiltFile(path) { 18 | var builtPath = '/base/app/'; 19 | return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath); 20 | } 21 | 22 | var allSpecFiles = Object.keys(window.__karma__.files) 23 | .filter(isSpecFile) 24 | .filter(isBuiltFile); 25 | 26 | // Load our SystemJS configuration. 27 | System.config({ 28 | baseURL: '/base' 29 | }); 30 | 31 | System.config( 32 | { 33 | map: { 34 | 'rxjs': 'node_modules/rxjs', 35 | '@angular': 'node_modules/@angular', 36 | 'app': 'app' 37 | }, 38 | packages: { 39 | 'app': { 40 | main: 'main.js', 41 | defaultExtension: 'js' 42 | }, 43 | '@angular/core': { 44 | main: 'index.js', 45 | defaultExtension: 'js' 46 | }, 47 | '@angular/compiler': { 48 | main: 'index.js', 49 | defaultExtension: 'js' 50 | }, 51 | '@angular/common': { 52 | main: 'index.js', 53 | defaultExtension: 'js' 54 | }, 55 | '@angular/platform-browser': { 56 | main: 'index.js', 57 | defaultExtension: 'js' 58 | }, 59 | '@angular/platform-browser-dynamic': { 60 | main: 'index.js', 61 | defaultExtension: 'js' 62 | }, 63 | 'rxjs': { 64 | defaultExtension: 'js' 65 | } 66 | } 67 | }); 68 | 69 | Promise.all([ 70 | System.import('@angular/core/testing'), 71 | System.import('@angular/platform-browser-dynamic/testing') 72 | ]).then(function (providers) { 73 | var testing = providers[0]; 74 | var testingBrowser = providers[1]; 75 | 76 | testing.setBaseTestProviders(testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, 77 | testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS); 78 | 79 | }).then(function() { 80 | // Finally, load all spec files. 81 | // This will run the tests directly. 82 | return Promise.all( 83 | allSpecFiles.map(function (moduleName) { 84 | return System.import(moduleName); 85 | })); 86 | }).then(__karma__.start, __karma__.error); 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular2でTodoアプリを作る 2 | 3 | これはAngular2を利用したTodoアプリを作成するワークショップ資料です。 4 | 5 | > このハンズオンは以下のバージョンで動作するように作成されています。ご利用の際はご注意ください。 6 | 7 | - node@5.11.1 8 | - npm@3.8.6 9 | - typescript@1.8.10 10 | - angular@2.0.0-rc.4 11 | 12 | ## 作るもの 13 | 14 | ワークショップでは簡単なTodoアプリを作成します。作成する機能は次の通りです。 15 | 16 | - Todoの作成 17 | - Todoの削除 18 | - Todoのクローズ 19 | - Todoの保存 20 | 21 | ![](/images/sample1.png) 22 | 23 | Demo: https://angular2-todo.herokuapp.com/ 24 | 25 | フロントエンド開発の経験がある方は、下の[プロジェクト構成]と[作成手順]の内容に沿って自分の力で進めてください。 26 | あまり経験の無い方は、[docs](/docs)に細かな手順を記載したものがありますので、見ながら進めてください。 27 | 28 | ## 凡例 29 | 30 | - :sparkles: は、より深くAngular2について知るためのTipsを表します。 31 | - :black_large_square: は、ターミナルなどCUIでの操作を表します。 32 | - :pencil2: は、ソースコードの編集を表します。 33 | 34 | ## 事前準備 35 | 36 | 始める前にハンズオンのscaffold(ひな形)を準備します。 37 | 38 | - [mitsuruog/angular2-minimum-starter: Minimum starter kit for angular2](https://github.com/mitsuruog/angular2-minimum-starter) 39 | 40 | それでは、scaffoldをcloneして必要なモジュールをインストールします。 41 | 42 | :black_large_square: 43 | ``` 44 | git clone --depth 1 https://github.com/mitsuruog/angular2-minimum-starter.git angular2-todo 45 | cd angular2-todo 46 | npm install 47 | ``` 48 | 49 | モジュールのインストールが終わったところで、実際にアプリケーションを動かしてみましょう。 50 | 51 | :black_large_square: 52 | ``` 53 | npm start 54 | ``` 55 | 56 | この画面が表示されたら準備はOKです。 57 | 58 | ![](/images/sample2.png) 59 | 60 | :warning: 本ワークショップでは[Bootstrap](http://getbootstrap.com/)を使って画面をデザインしています。画面を変更する際はBootstrapのドキュメントも合わせて参照してください。 61 | 62 | - [Bootstrap · The world's most popular mobile-first and responsive front-end framework.](http://getbootstrap.com/) 63 | 64 | ## ツール 65 | 66 | Augular2のブラウザでのデバックを容易にするため、以下のツール(Chrome extensions)を導入することを推奨します。 67 | 68 | - [Augury - Chrome Web Store](https://chrome.google.com/webstore/detail/augury/elgalmkoelokbchhkhacckoklkejnhcd?hl=en) 69 | 70 | ## プロジェクト構成 71 | 72 | 完成した場合、次のようなプロジェクト構成になる予定です。 73 | 74 | ``` 75 | app 76 | ├── app.component.ts 77 | ├── app.html 78 | ├── common 79 | │   └── services 80 | │   └── logger.service.ts 81 | ├── components 82 | │   ├── content 83 | │   │   ├── content.component.ts 84 | │   │   ├── content.css 85 | │   │   └── content.html 86 | │   ├── footer 87 | │   │   ├── footer.component.ts 88 | │   │   └── footer.html 89 | │   └── header 90 | │   ├── header.component.ts 91 | │   └── header.html 92 | ├── main.ts 93 | ├── models 94 | │   └── todo.model.ts 95 | ├── services 96 | │   └── todo.service.ts 97 | └── system-config.js 98 | ``` 99 | 100 | ## コンポーネント関連図 101 | 102 | Angular2はComponentベースのフレームワークです。画面を作成するために幾つかのComponentを作成していきます。次に本ワークショップで作成するCompoenntとそれらと画面との関連図を示します。 103 | 104 | ![](/images/sample3.png) 105 | 106 | ## 作成手順 107 | 108 | 以下の手順に沿って作成してください。 109 | 110 | 1. [TodoModelクラスを作成する](/docs/01.md) 111 | 1. [TodoServiceクラスを作成する](/docs/02.md) 112 | 1. [Todo新規作成を作成する](/docs/03.md) 113 | 1. [Todo一覧と消化状況を表示する](/docs/04.md) 114 | 1. [Todoのクローズと削除機能を作成する](/docs/05.md) 115 | 116 | ## カスタマイズ 117 | 118 | 上記の機能が完成した場合は、お好みでカスタマイズしてみましょう。以下にカスタマイズの例を提示します。 119 | 120 | - アニメーションを付ける 121 | - Todoの保存先をAPIサーバーにする 122 | - ユニットテストを書く 123 | - ルーティングを追加する(例: ` /completed`でアクセスすると完了済みのTodoのみ表示する) 124 | - Todoにカテゴリを追加する 125 | 126 | など 127 | 128 | ## まとめ 129 | 130 | 簡単なTodo作り方についてワークショップにて体験しました。 131 | 132 | さらにAnuglar2について学習したい場合は、まず本家の`tutorial`や`developer guide`を実際にやってみることをオススメします。 133 | 134 | - [Angular Docs - ts](https://angular.io/docs/ts/latest/) 135 | 136 | `ng-japan(日本Angularユーザーグループ)`のslackチャネルに参加することで、技術的な質問を行うことができます。また`#ng_jp`でtweetすると誰かが答えてくれるかもしれません。 137 | 138 | - [Join ng-japan on Slack!](https://ng-japan-invite.herokuapp.com/) 139 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: 'angular2-minimum-starter), 8 | 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['jasmine'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | // Polyfills. 18 | 'node_modules/es6-shim/es6-shim.js', 19 | 20 | 'node_modules/reflect-metadata/Reflect.js', 21 | 22 | // System.js for module loading 23 | 'node_modules/systemjs/dist/system-polyfills.js', 24 | 'node_modules/systemjs/dist/system.src.js', 25 | 26 | // Zone.js dependencies 27 | 'node_modules/zone.js/dist/zone.js', 28 | 'node_modules/zone.js/dist/jasmine-patch.js', 29 | 'node_modules/zone.js/dist/sync-test.js', 30 | 'node_modules/zone.js/dist/async-test.js', 31 | 'node_modules/zone.js/dist/fake-async-test.js', 32 | 33 | // RxJs. 34 | { pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false }, 35 | { pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false }, 36 | 37 | 38 | {pattern: 'karma-test.shim.js', included: true, watched: true}, 39 | 40 | // paths loaded via module imports 41 | // Angular itself 42 | {pattern: 'node_modules/@angular/**/*.js', included: false, watched: true}, 43 | {pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: true}, 44 | 45 | // Our built application code 46 | {pattern: 'app/**/*.js', included: false, watched: true}, 47 | 48 | // paths loaded via Angular's component compiler 49 | // (these paths need to be rewritten, see proxies section) 50 | {pattern: 'app/**/*.html', included: false, watched: true}, 51 | {pattern: 'app/**/*.css', included: false, watched: true}, 52 | 53 | // paths to support debugging with source maps in dev tools 54 | {pattern: 'app/**/*.ts', included: false, watched: false}, 55 | {pattern: 'app/**/*.js.map', included: false, watched: false} 56 | ], 57 | 58 | // list of files to exclude 59 | exclude: [], 60 | 61 | // proxied base paths 62 | proxies: { 63 | // required for component assests fetched by Angular's compiler 64 | "/app/": "/base/app/" 65 | }, 66 | 67 | // preprocess matching files before serving them to the browser 68 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 69 | preprocessors: { 70 | "app/**/!(*spec|*mock).js": ['coverage'] 71 | }, 72 | 73 | coverageReporter: { 74 | dir: 'report/coverage/', 75 | reporters: [{ 76 | type: 'json', 77 | subdir: '.', 78 | file: 'coverage-final.json', 79 | }] 80 | }, 81 | 82 | // test results reporter to use 83 | // possible values: 'dots', 'progress' 84 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 85 | reporters: ['mocha', 'coverage'], 86 | 87 | 88 | // web server port 89 | port: 9876, 90 | 91 | 92 | // enable / disable colors in the output (reporters and logs) 93 | colors: true, 94 | 95 | 96 | // level of logging 97 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 98 | logLevel: config.LOG_INFO, 99 | 100 | 101 | // enable / disable watching file and executing tests whenever any file changes 102 | autoWatch: true, 103 | 104 | 105 | // start these browsers 106 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 107 | browsers: ['Chrome'], 108 | 109 | 110 | // Continuous Integration mode 111 | // if true, Karma captures browsers, runs the tests and exits 112 | singleRun: false, 113 | 114 | // Concurrency level 115 | // how many browser should be started simultaneous 116 | concurrency: Infinity 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /docs/03.md: -------------------------------------------------------------------------------- 1 | # 3. Todo新規作成を作成する 2 | 3 | Todo新規作成機能を作成します。手順は以下の通りです。 4 | 5 | - TodoServiceクラスに作成機能を追加する 6 | - Todo新規作成部品(TodoHeaderComponent)を作成する 7 | - AppComponentにTodoHeaderComponentを追加する 8 | 9 | ## TodoServiceクラスに作成機能を追加する 10 | 11 | Todoを実際に登録する機能をServiceクラスに追加します。今回はlocalStorageに保存します。 12 | 13 | まず、localStorageのKeyを定義した後に`add()`を変更します。localStorageへの値の保存部分は、後の処理でもたびたび利用するため`save()`として定義しておきます。 14 | 15 | :pencil2: **app/services/todo.service.ts** 16 | ```diff 17 | import {Injectable} from "@angular/core"; 18 | import {Todo} from "../models/todo.model"; 19 | 20 | + const STORAGE_KEY = 'angular2-todo'; 21 | 22 | @Injectable() 23 | export class TodoService { 24 | 25 | + todos:Todo[] = []; 26 | 27 | constructor() {} 28 | 29 | - add():void { 30 | + add(title:string):void { 31 | + let newTodo = new Todo( 32 | + Math.floor(Math.random() * 100000), // ランダムにIDを発番する 33 | + title, 34 | + false 35 | + ); 36 | + this.todos.push(newTodo); 37 | + this.save(); 38 | } 39 | 40 | remove():void {} 41 | toggleComplate():void {} 42 | 43 | + private save():void { 44 | + console.log('saving : ', this.todos); 45 | + localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos)); 46 | + } 47 | } 48 | ``` 49 | 50 | ## Todo新規作成部品(TodoHeaderComponent)を作成する 51 | 52 | まず、画面上にTodo作成エリアを表示するためにComponentを作成します。Angualr2のComponentは主にTemplateとComponentの2つから構成されます。 53 | 54 | :pencil2: **app/components/header/header.html** 55 | ```html 56 |
57 |

Todos

58 |
59 |
60 | 61 | 62 |
63 | 64 |
65 |
66 | ``` 67 | 68 | - `[(ngModel)]`は、Component側の`title`との間で値の変更があった場合に、双方向で変更内容を共有します(two-way binding)。 69 | - `(click)`は、Clickイベントハンドラです。ボタンがクリックされるとComponent側の`addTodo`を呼び出します。 70 | 71 | > :sparkles: その他のテンプレートシンタックスについては、公式の[Angular Cheat Sheet](https://angular.io/docs/ts/latest/guide/cheatsheet.html)が一番良くまとまっています。 72 | 73 | :pencil2: **app/components/header/header.component.ts** 74 | ```ts 75 | import {Component} from '@angular/core'; 76 | import {TodoService} from '../../services/todo.service'; 77 | 78 | @Component({ 79 | selector: 'todo-header', 80 | templateUrl: 'app/components/header/header.html' 81 | }) 82 | export class TodoHeaderComponent { 83 | 84 | title:string; 85 | 86 | constructor(private service:TodoService) {} 87 | 88 | addTodo() { 89 | if (this.title.trim().length) { 90 | this.service.add(this.title); 91 | this.title = null; 92 | } 93 | } 94 | 95 | } 96 | ``` 97 | 98 | > :sparkles: Componentクラス内のクラス変数とコンストラクタの引数で渡されたServiceは、Componentクラス内で`this`を使って参照できます。 99 | 100 | > :sparkles: Componentのテンプレートを指定する方法は、`templateUrl`で外部ファイルを指定する方法と、`template`で直接Component内にテンプレートを記述する方法があります。 101 | 102 | ## AppComponentにTodoHeaderComponentを追加する 103 | 104 | (⚠️:warning: `app.html`と`app.component.ts`の中身は一度全てクリアした上で進めてください。:warning::warning:) 105 | 106 | 先ほど作成したTodoHeaderComponentは、AppComponentに追加することで画面上に表示することができます。 107 | 108 | :pencil2: **app/app.component.ts** 109 | ```ts 110 | import {Component} from '@angular/core'; 111 | import {TodoHeaderComponent} from './header/header.component'; 112 | import {TodoService} from '../services/todo.service'; 113 | 114 | @Component({ 115 | selector: 'my-app', 116 | templateUrl: 'app/components/app.html', 117 | providers: [TodoService], 118 | directives: [TodoHeaderComponent] 119 | }) 120 | export class AppComponent { 121 | constructor() {} 122 | } 123 | ``` 124 | 125 | TodoHeaderComponentとTodoServiceは、それぞれ`providers`と`directives`に設定することで初めてComponent上で利用することができます。 126 | 127 | 続いて、TodoHeaderComponentを画面上に配置します。 128 | 129 | :pencil2: **app/app.html** 130 | ```html 131 |
132 | 133 |
134 | 135 | 138 | ``` 139 | 140 | `todo-header`はTodoHeaderComponentの`selector`に設定した名前です。このように、Angular2では作成したComponentをあらゆるところで簡単に配置することができます。 141 | 142 | 以上でこの章は終了です。 143 | 144 | 画面上にTodo作成エリアが表示されて、Todoが正しくlocalStorageに保存されていれば次に進んでください。 145 | -------------------------------------------------------------------------------- /docs/05.md: -------------------------------------------------------------------------------- 1 | # 5. Todoのクローズと削除機能を作成する 2 | 3 | Todoのクローズと削除機能を作成します。手順は以下の通りです。 4 | 5 | - ServiceクラスにTodoの完了と削除機能を作成する。 6 | - Todo表示部品(TodoContentComponent)に完了と削除機能を追加する。 7 | - Todo完了時スタイルを変更する。 8 | 9 | ## ServiceクラスにTodoの完了と削除機能を作成する 10 | 11 | 早速、ServiceクラスにTodoの完了と削除機能を作成しましょう。Todoの完了は、Todoの `isCompleted=false`にすることで表現します。 12 | 13 | :pencil2: **app/services/todo.service.ts** 14 | ```diff 15 | @Injectable() 16 | export class TodoService { 17 | 18 | // ... 省略 19 | 20 | - remove():void { 21 | + remove(todo:Todo):void { 22 | + const index = this.todos.indexOf(todo); 23 | + this.todos.splice(index, 1); 24 | + this.save(); 25 | } 26 | 27 | - toggleComplate():void { 28 | + toggleComplate(todo:Todo):void { 29 | + this.todos.filter(t => t.id === todo.id) 30 | + .map(t => t.isCompleted = !t.isCompleted); 31 | + this.save(); 32 | } 33 | 34 | // ... 省略 35 | 36 | } 37 | ``` 38 | 39 | ## Todo表示部品(TodoContentComponent)に完了と削除機能を追加する 40 | 41 | Todo表示部品(TodoContentComponent)に次の操作部品を追加します。 42 | 43 | - Todoの完了:チェックボックスで操作する 44 | - Todoの削除:「削除」ボタンで操作する 45 | 46 | :pencil2: **app/components/content/content.component.ts** 47 | ```diff 48 | import {Component, Input} from '@angular/core'; 49 | import {Todo} from "../../models/todo.model"; 50 | + import {TodoService} from "../../services/todo.service"; 51 | 52 | @Component({ 53 | selector: 'todo-content', 54 | templateUrl: 'app/components/content/content.html' 55 | }) 56 | export class TodoContentComponent { 57 | 58 | @Input() 59 | todos:Todo[]; 60 | 61 | - constructor() {} 62 | + constructor(private service:TodoService) {} 63 | + 64 | + toggleComplate(todo:Todo) { 65 | + this.service.toggleComplate(todo); 66 | + } 67 | + 68 | + deleteTodo(todo:Todo) { 69 | + this.service.remove(todo); 70 | + } 71 | 72 | } 73 | ``` 74 | 75 | :pencil2: **app/components/content/content.html** 76 | diff 77 | ```diff 78 | 98 | ``` 99 | 100 | - `[checked]`は、右の`todo.isCompleted`の結果が`true`になった場合、チェックをつけます。 101 | 102 | ### Todo完了時スタイルを変更する 103 | 104 | 最後に、Todo完了時にTodoに取り消し線を追加するようにスタイルを微調整します。TodoContentComponentにカスタムスタイル用のCSSを追加して、Todoの完了状態に応じてスタイルを変更します。 105 | 106 | :pencil2: **app/components/content/content.css** 107 | ```css 108 | .complate { 109 | text-decoration: line-through; 110 | } 111 | ``` 112 | 113 | 作成したスタイルをComponent側で利用できるようにします。 114 | 115 | :pencil2: **app/components/content/content.component.ts** 116 | ```diff 117 | import {Component, Input} from '@angular/core'; 118 | import {Todo} from "../../models/todo.model"; 119 | import {TodoService} from "../../services/todo.service"; 120 | 121 | @Component({ 122 | selector: 'todo-content', 123 | - templateUrl: 'app/components/content/content.html' 124 | + templateUrl: 'app/components/content/content.html', 125 | + styleUrls: ['app/components/content/content.css'] 126 | }) 127 | export class TodoContentComponent { 128 | 129 | // ... 省略 130 | // 131 | } 132 | ``` 133 | 134 | > :sparkles: ComponentのCSSを指定する方法は、`styleUrls`で外部ファイルを指定する方法と、`styles`で直接Component内にCSSを記述する方法があります。 135 | 136 | > :sparkles: `styleUrls`, `styles`で指定されたスタイルは、Component内で閉じた形で展開されているため、外部のスタイルに影響しません。 137 | 138 | :pencil2: **app/components/content/content.html** 139 | ```diff 140 | 161 | ``` 162 | 163 | - `[class.complate]`は、Anuglar1の`ng-class`と等価です。 164 | 165 | 以上でTodoの作成はすべて終了です。 :tada::tada::tada: 166 | -------------------------------------------------------------------------------- /docs/04.md: -------------------------------------------------------------------------------- 1 | # 4. Todo一覧と消化状況を表示する 2 | 3 | Todo一覧と消化状況を表示します。手順は以下の通りです。 4 | 5 | - localStorageからTodoを取得する 6 | - Todo表示部品(TodoContentComponent)を作成する 7 | - AppComponentにTodoContentComponentを追加する 8 | - Todo消化状況を取得する 9 | - Todo消化状況表示部品(TodoFooterComponent)を作成する 10 | - AppComponentにTodoFooterComponentを追加する 11 | 12 | ## localStorageからTodoを取得する 13 | 14 | まず、localStorageからTodoを取得する機能をServiceクラスに追加します。 15 | 16 | :pencil2: **app/services/todo.service.ts** 17 | ```diff 18 | import {Injectable} from "@angular/core"; 19 | import {Todo} from "../models/todo.model"; 20 | 21 | const STORAGE_KEY = 'angular2-todo'; 22 | 23 | @Injectable() 24 | export class TodoService { 25 | 26 | todos:Todo[] = []; 27 | 28 | constructor() { 29 | + const persistedTodos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); 30 | + // localStorageの内容をTodoModelクラスに詰め替える 31 | + this.todos = persistedTodos.map(todo => { 32 | + return new Todo( 33 | + todo.id, 34 | + todo.title, 35 | + todo.isCompleted 36 | + ); 37 | + }); 38 | } 39 | 40 | // ...省略 41 | } 42 | ``` 43 | 44 | ## Todo表示部品(TodoContentComponent)を作成する 45 | 46 | Todoの一覧を表示するエリアを作成します。Todoが1件も登録されていない場合は、登録されてない旨を表示します。 47 | 48 | :pencil2: **app/components/content/content.component.ts** 49 | ```ts 50 | import {Component, Input} from '@angular/core'; 51 | import {Todo} from "../../models/todo.model"; 52 | 53 | @Component({ 54 | selector: 'todo-content', 55 | templateUrl: 'app/components/content/content.html' 56 | }) 57 | export class TodoContentComponent { 58 | 59 | @Input() 60 | todos:Todo[]; 61 | 62 | constructor() {} 63 | 64 | } 65 | ``` 66 | 67 | - `@Input()`は、Component外部から値を注入するためのデコレータです。これが指定されているプロパティは、Component外部から値を受け取ることができます。 68 | 69 | :pencil2: **app/components/content/content.component.ts** 70 | ```html 71 | 84 | ``` 85 | - `*ngFor`は、Angular1の`ng-repeat`と等価です。 86 | - `*ngIf`は、Angular1の`ng-if`と等価です。 87 | 88 | ## AppComponentにTodoContentComponentを追加する 89 | 90 | TodoContentComponentを画面上に表示するためには、AppComponentに登録する必要があります。また、Todoの一覧は後ほど登場するComponentで再利用するため、AppComponent内に保持しておくようにします。 91 | 92 | > :sparkles: Todo一覧の共有方法はSharedServiceを利用するなど、他にもアプローチの仕方が存在します。 93 | 94 | :pencil2: **app/app.component.ts** 95 | ```diff 96 | import {Component} from '@angular/core'; 97 | import {TodoHeaderComponent} from './header/header.component'; 98 | + import {TodoContentComponent} from "./content/content.component"; 99 | import {TodoService} from '../services/todo.service'; 100 | import {Todo} from "../models/todo.model"; 101 | 102 | @Component({ 103 | selector: 'my-app', 104 | templateUrl: 'app/components/app.html', 105 | providers: [TodoService], 106 | - directives: [TodoHeaderComponent] 107 | + directives: [TodoHeaderComponent, TodoContentComponent] 108 | }) 109 | export class AppComponent { 110 | 111 | + todos:Todo[]; 112 | 113 | - constructor() { 114 | + constructor(private service:TodoService) { 115 | + this.todos = this.service.todos; 116 | } 117 | } 118 | ``` 119 | 120 | :pencil2: **app/app.html** 121 | ```diff 122 |
123 | 124 | + 125 |
126 | 127 | 130 | ``` 131 | 132 | - `[todos]="todos"`は、TodoContentComponentの`todos`対してAppComponentの`todos`を渡します。 133 | 134 | ## Todo消化状況を取得する 135 | 136 | 次にTodoの消化状況を算出する関数をServiceクラスに作成します。 137 | 138 | :pencil2: **app/services/todo.service.ts** 139 | ```diff 140 | import {Injectable} from "@angular/core"; 141 | import {Todo} from "../models/todo.model"; 142 | 143 | const STORAGE_KEY = 'angular2-todo'; 144 | 145 | @Injectable() 146 | export class TodoService { 147 | 148 | todos:Todo[] = []; 149 | 150 | // ... 省略 151 | 152 | + getComplatedCount():number { 153 | + return this.todos.filter(todo => todo.isCompleted).length; 154 | + } 155 | 156 | private save():void { 157 | console.log('saving : ', this.todos); 158 | localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos)); 159 | } 160 | } 161 | ``` 162 | 163 | ## Todo消化状況表示部品(TodoFooterComponent)を作成する 164 | 165 | Todoの消化状況はFooterに表示することにします。フッターには消化状況とTodoの合計を表示します。 166 | 167 | :pencil2: **app/components/footer/footer.html** 168 | ```html 169 | 172 | ``` 173 | 174 | :pencil2: **app/components/footer/footer.component.ts** 175 | ```ts 176 | import {Component, Input} from '@angular/core'; 177 | import {TodoService} from '../../services/todo.service'; 178 | import {Todo} from "../../models/todo.model"; 179 | 180 | @Component({ 181 | selector: 'todo-footer', 182 | templateUrl: 'app/components/footer/footer.html' 183 | }) 184 | export class TodoFooterComponent { 185 | 186 | @Input() 187 | todos:Todo[]; 188 | 189 | constructor(private service:TodoService) {} 190 | 191 | getCompletedCount() { 192 | return this.service.getComplatedCount(); 193 | } 194 | 195 | } 196 | ``` 197 | 198 | ## AppComponentにTodoFooterComponentを追加する 199 | 200 | TodoFooterComponentを画面上に表示するためには、これまでと同様にAppComponentに登録する必要があります。 201 | 202 | :pencil2: **app/app.component.ts** 203 | ```diff 204 | import {Component} from '@angular/core'; 205 | import {TodoHeaderComponent} from './header/header.component'; 206 | import {TodoContentComponent} from "./content/content.component"; 207 | + import {TodoFooterComponent} from "./footer/footer.component"; 208 | import {TodoService} from '../services/todo.service'; 209 | import {Todo} from "../models/todo.model"; 210 | 211 | @Component({ 212 | selector: 'my-app', 213 | templateUrl: 'app/components/app.html', 214 | providers: [TodoService], 215 | - directives: [TodoHeaderComponent, TodoContentComponent] 216 | + directives: [TodoHeaderComponent, TodoContentComponent, TodoFooterComponent] 217 | }) 218 | export class AppComponent { 219 | 220 | todos:Todo[]; 221 | 222 | constructor(private service:TodoService) { 223 | this.todos = this.service.todos; 224 | } 225 | } 226 | 227 | ``` 228 | 229 | :pencil2: **app/app.html** 230 | ```diff 231 |
232 | 233 | 234 | + 235 |
236 | 237 | 240 | ``` 241 | 242 | 以上でこの章は終了です。 243 | 244 | 画面上にTodo一覧と消化状況が表示されていれば、次に進んでください。 245 | --------------------------------------------------------------------------------