├── 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 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/app/components/header/header.html:
--------------------------------------------------------------------------------
1 |
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 |
2 | -
3 |
4 |
5 |
10 |
11 | {{i + 1}}. {{todo.title}}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | - Todoがありません。
20 |
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 | 
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 | 
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 | 
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 |
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 |
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 |
79 | -
80 |
81 |
82 | +
87 |
88 | {{i + 1}}. {{todo.title}}
89 |
90 |
91 |
92 | +
93 |
94 |
95 |
96 | - Todoがありません。
97 |
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 |
141 | -
142 |
143 |
144 |
149 | -
150 | +
151 | {{i + 1}}. {{todo.title}}
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 | - Todoがありません。
160 |
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 |
72 | -
73 |
74 |
75 | {{i + 1}}. {{todo.title}}
76 |
77 |
78 |
79 |
80 |
81 |
82 | - Todoがありません。
83 |
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 |
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 |
--------------------------------------------------------------------------------