├── README.md
├── package.json
├── src
├── App.ts
├── Footer.ts
├── Header.ts
├── Todo.ts
├── TodoList.ts
├── TodoService.ts
├── di-tokens.ts
├── getName.ts
├── index.html
├── server.ts
├── state
│ ├── application-state.ts
│ ├── applicationStateFactory.ts
│ ├── reducers.ts
│ ├── todoActions.ts
│ └── ui-state.ts
└── styles.css
├── tsconfig.json
└── webpack.config.js
/README.md:
--------------------------------------------------------------------------------
1 | # Angular 2 RxJs Redux-like sample App
2 |
3 | This repository is an example of how to build a Flux/Redux-like Angular 2 application using RxJs and Functional Reactive Programming. This application has a single atom of state, and is based upon two constructs:
4 |
5 | - the action dispatcher
6 | - the application state observable
7 |
8 | See this blog post for further details on the application architecture: [Angular 2 Application Architecture - Building apps with RxJs and Functional Reactive Programming (vs Redux)](http://localhost:5000/angular-2-application-architecture-building-applications-using-rxjs-and-functional-reactive-programming-vs-redux/)
9 |
10 | ## Installation
11 |
12 | To install the application, make sure to have npm 3 or higher and node 4 or higher, and follow the following steps:
13 |
14 | git clone https://github.com/jhades/angular2-rxjs-example.git
15 | npm install
16 |
17 | ## Running the application
18 |
19 | The application uses the webpack-dev-server to produce an in-memory development bundle. It also has a node.js REST backend with a simple in-memory data store. The way that this works is that we hit the node.js backend server, which will proxy the bundle.js request to the webpack development server.
20 |
21 | In order to run the application, first start the webpack-dev-server in one terminal:
22 |
23 | npm run webpack-dev-server
24 |
25 | Then in another terminal window, run the node.js server that will both serve the REST API and the HTML/CSS of the application:
26 |
27 | npm start
28 |
29 | You can access the application in the followwing Url:
30 |
31 | [http://localhost:8080/](http://localhost:8080)
32 |
33 | You can add, remove or toggle a TODO item. Notice when adding the Todo that the message panel in the footer shows the status of what is going on.
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular2-rxjs-example",
3 | "version": "1.0.0",
4 | "description": "An example of how to build an Angular 2 application using RxJs",
5 | "scripts": {
6 | "webpack-dev-server": "webpack-dev-server --colors --display-error-details --content-base src --port 8081",
7 | "start": "cd src &&ts-node server.ts"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/jhades/angular2-rxjs-example.git"
12 | },
13 | "author": "jhades.dev@gmail.com",
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/jhades/angular2-rxjs-example/issues"
17 | },
18 | "homepage": "https://github.com/jhades/angular2-rxjs-example#readme",
19 | "devDependencies": {
20 | "http-proxy": "^1.13.0",
21 | "ts-loader": "^0.7.2",
22 | "typescript": "^1.7.5",
23 | "webpack": "^1.12.9",
24 | "webpack-dev-server": "^1.14.0"
25 | },
26 | "dependencies": {
27 | "angular2": "^2.0.0-beta.2",
28 | "es6-promise": "^3.0.2",
29 | "immutable": "^3.7.6",
30 | "ng2-translate": "^1.2.4",
31 | "reflect-metadata": "^0.1.2",
32 | "rxjs": "^5.0.0-beta.0",
33 | "zone.js": "^0.5.10"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/App.ts:
--------------------------------------------------------------------------------
1 |
2 | ///
3 |
4 | import "angular2/bundles/angular2-polyfills";
5 | import {Component, provide, Inject} from "angular2/core";
6 | import {HTTP_PROVIDERS} from "angular2/http";
7 | import {Header} from "./Header";
8 | import {TodoList} from "./TodoList";
9 | import {Todo} from "./Todo";
10 | import {Footer} from "./Footer";
11 | import {TodoService} from "./TodoService";
12 | import {LoadTodosAction, AddTodoAction, StartBackendAction, EndBackendAction, Action} from "./state/todoActions";
13 | import {List} from "immutable";
14 | import {bootstrap} from "angular2/platform/browser";
15 | import {dispatcher, state, initialState} from "./di-tokens";
16 | import {Subject} from "rxjs/Subject";
17 | import {applicationStateFactory} from "./state/applicationStateFactory";
18 | import {Observable} from "rxjs/Observable";
19 | import {ApplicationState} from "./state/application-state";
20 | import {Observer} from "rxjs/Observer";
21 | import 'rxjs/add/operator/map';
22 | import 'rxjs/add/operator/scan';
23 | import 'rxjs/add/operator/share';
24 | import {UiState, initialUiState} from "./state/ui-state";
25 | import './getName';
26 |
27 | @Component({
28 | selector: 'app',
29 | directives: [Header, TodoList, Footer],
30 | template: `
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {{uiStateMessage | async}}
43 | Add, Remove and Complete TODOs
44 |
45 |
46 | `
47 | })
48 | export class App {
49 |
50 | constructor(@Inject(dispatcher) private dispatcher: Observer,
51 | @Inject(state) private state: Observable,
52 | private todoService: TodoService) {
53 |
54 | this.loadInitialData();
55 | }
56 |
57 | get size() {
58 | return this.state.map((state: ApplicationState) => state.todos.size);
59 | }
60 |
61 | get uiStateMessage() {
62 | return this.state.map((state: ApplicationState) => state.uiState.message);
63 | }
64 |
65 |
66 | onAddTodo(description) {
67 | let newTodo = new Todo({id:Math.random(), description});
68 |
69 | this.dispatcher.next(new StartBackendAction('Saving Todo...'));
70 |
71 | this.todoService.saveTodo(newTodo)
72 | .subscribe(
73 | res => {
74 | this.dispatcher.next(new AddTodoAction(newTodo));
75 | this.dispatcher.next(new EndBackendAction(null));
76 | },
77 | err => {
78 | this.dispatcher.next(new EndBackendAction('Error occurred: '));
79 | }
80 | );
81 | }
82 |
83 | loadInitialData() {
84 | this.todoService.getAllTodos()
85 | .subscribe(
86 | res => {
87 | let todos = (res.json()).map((todo: any) =>
88 | new Todo({id:todo.id, description:todo.description,completed: todo.completed}));
89 |
90 | this.dispatcher.next(new LoadTodosAction(List(todos)));
91 | },
92 | err => console.log("Error retrieving Todos")
93 | );
94 |
95 | }
96 |
97 | }
98 |
99 | bootstrap(App, [
100 | HTTP_PROVIDERS,
101 | TodoService,
102 | provide(initialState, {useValue: {todos: List([]), uiState: initialUiState}}),
103 | provide(dispatcher, {useValue: new Subject()}),
104 | provide(state, {useFactory: applicationStateFactory, deps: [new Inject(initialState), new Inject(dispatcher)]})
105 | ]);
--------------------------------------------------------------------------------
/src/Footer.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input,ChangeDetectionStrategy} from 'angular2/core';
2 |
3 | @Component({
4 | selector: 'todo-footer',
5 | changeDetection: ChangeDetectionStrategy.OnPush,
6 | template: `
7 |
10 | `
11 | })
12 | export class Footer {
13 |
14 | @Input() count: number = 0;
15 |
16 | }
--------------------------------------------------------------------------------
/src/Header.ts:
--------------------------------------------------------------------------------
1 | import {Component, Output,EventEmitter} from 'angular2/core';
2 |
3 |
4 | @Component({
5 | selector:'todo-header',
6 | template: `
7 |
13 | `
14 | })
15 | export class Header {
16 |
17 | @Output() todo = new EventEmitter();
18 |
19 | addTodo(input) {
20 | this.todo.emit(input.value);
21 | input.value = "";
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/src/Todo.ts:
--------------------------------------------------------------------------------
1 |
2 | import {List,Record} from 'immutable';
3 |
4 | const TodoRecord = Record({
5 | id: 0,
6 | description: "",
7 | completed: false
8 | });
9 |
10 | export class Todo extends TodoRecord {
11 |
12 | id:number;
13 | description:string;
14 | completed: boolean;
15 |
16 | constructor(props) {
17 | super(props);
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/src/TodoList.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, Output, EventEmitter, Inject} from 'angular2/core';
2 | import {Todo} from "./Todo";
3 | import {List} from 'immutable';
4 | import {TodoService} from "./TodoService";
5 | import {ToggleTodoAction, DeleteTodoAction, Action} from './state/todoActions';
6 | import {dispatcher,state} from "./di-tokens";
7 | import {Observer} from "rxjs/Observer";
8 | import {Observable} from "rxjs/Observable";
9 | import {ApplicationState} from "./state/application-state";
10 |
11 |
12 | @Component({
13 | selector: 'todo-list',
14 | template: `
15 |
16 |
17 | Mark all as complete
18 |
27 |
28 | `
29 | })
30 | export class TodoList {
31 |
32 | constructor(private todoService: TodoService,
33 | @Inject(dispatcher) private dispatcher: Observer,
34 | @Inject(state) private state: Observable) {
35 |
36 | }
37 |
38 | get todos() {
39 | return this.state.map((state: ApplicationState) => state.todos);
40 | }
41 |
42 | onToggleTodo(todo: Todo) {
43 |
44 | this.dispatcher.next(new ToggleTodoAction(todo));
45 |
46 | this.todoService.toggleTodo(todo)
47 | .subscribe(
48 | res => console.log('todo toggled successfully'),
49 | err => console.log('error toggling todo')
50 | );
51 | }
52 |
53 | delete(todo:Todo) {
54 | this.dispatcher.next(new DeleteTodoAction(todo));
55 |
56 | this.todoService.deleteTodo(todo)
57 | .subscribe(
58 | res => console.log('todo toggled successfully'),
59 | err => console.log('error toggling todo')
60 | );
61 |
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/src/TodoService.ts:
--------------------------------------------------------------------------------
1 |
2 | import {Injectable,Inject} from 'angular2/core';
3 | import {Http,Headers,URLSearchParams} from 'angular2/http';
4 | import {Todo} from "./Todo";
5 | import {List} from 'immutable';
6 | import {Observable} from "rxjs/Observable";
7 |
8 |
9 | @Injectable()
10 | export class TodoService {
11 |
12 | http:Http;
13 |
14 | constructor(http:Http) {
15 | this.http = http;
16 | }
17 |
18 | getAllTodos() {
19 | return this.http.get('/todo');
20 | }
21 |
22 | saveTodo(newTodo: Todo) : Observable> {
23 | var headers = new Headers();
24 | headers.append('Content-Type', 'application/json; charset=utf-8');
25 |
26 | return this.http.post('/todo', JSON.stringify(newTodo.toJS()),{headers});
27 | }
28 |
29 | deleteTodo(deletedTodo: Todo) {
30 | let params = new URLSearchParams();
31 | params.append('id', '' + deletedTodo.id );
32 |
33 | return this.http.delete('/todo', {search: params});
34 | }
35 |
36 |
37 | toggleTodo(toggled: Todo) {
38 | var headers = new Headers();
39 | headers.append('Content-Type', 'application/json; charset=utf-8');
40 | return this.http.put('/todo', JSON.stringify(toggled.toJS()),{headers});
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/src/di-tokens.ts:
--------------------------------------------------------------------------------
1 |
2 | import {OpaqueToken} from "angular2/core";
3 |
4 | export const initialState = new OpaqueToken("initialState");
5 | export const dispatcher = new OpaqueToken("dispatcher");
6 | export const state = new OpaqueToken("state");
7 |
--------------------------------------------------------------------------------
/src/getName.ts:
--------------------------------------------------------------------------------
1 | Object.prototype.getName = function() {
2 | var funcNameRegex = /function (.{1,})\(/;
3 | var results = (funcNameRegex).exec((this).constructor.toString());
4 | return (results && results.length > 1) ? results[1] : "";
5 | };
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | angular2-rxjs-example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 |
2 | ///
3 |
4 | let express = require('express');
5 | let bodyParser = require('body-parser');
6 | let _ = require('lodash');
7 | var httpProxy = require('http-proxy');
8 |
9 | var proxy = httpProxy.createProxyServer();
10 |
11 | let app = express();
12 |
13 | let todos = [];
14 |
15 | app.use(express.static('.'));
16 | app.use(bodyParser.json());
17 | app.use(bodyParser.text());
18 |
19 | app.route('/todo')
20 | .get((req, res) => {
21 | console.log(JSON.stringify(todos));
22 | res.send(todos);
23 | })
24 | .put((req, res) => {
25 | let json = req.body;
26 | let toggled = _.find(todos, (todo) => todo.id == json.id);
27 | toggled.completed = !toggled.completed;
28 | console.log(JSON.stringify(todos));
29 | res.send();
30 | })
31 | .delete((req,res) => {
32 | console.log('removing todo with id = ' + req.query.id);
33 | todos = _.remove(todos,(todo) => todo.id != req.query.id );
34 | console.log(JSON.stringify(todos));
35 | res.send();
36 | })
37 | .post((req, res) => {
38 | todos.push(req.body);
39 | console.log(JSON.stringify(todos));
40 | setTimeout(() => res.send(), 1000);
41 | });
42 |
43 | app.all('/bundle.js', function (req, res) {
44 | proxy.web(req, res, {
45 | target: 'http://localhost:8081'
46 | });
47 | });
48 |
49 | let server = app.listen(8080, function() {
50 | console.log("Server running at http://localhost:" + server.address().port);
51 | });
--------------------------------------------------------------------------------
/src/state/application-state.ts:
--------------------------------------------------------------------------------
1 |
2 | import {Todo} from "../Todo";
3 | import {List} from "immutable";
4 | import {UiState} from "./ui-state";
5 |
6 |
7 | export interface ApplicationState {
8 | todos: List,
9 | uiState: UiState
10 | }
--------------------------------------------------------------------------------
/src/state/applicationStateFactory.ts:
--------------------------------------------------------------------------------
1 |
2 | import {Observable} from "rxjs/Observable";
3 | import {Action} from "./todoActions";
4 | import {ApplicationState} from "./application-state";
5 | import {dispatcher} from "../di-tokens";
6 | import {calculateTodos, calculateUiState} from "./reducers";
7 | import {UiState, initialUiState} from "./ui-state";
8 | import {BehaviorSubject} from "rxjs/Rx";
9 |
10 | function wrapIntoBehaviorSubject(init, obs) {
11 | const res = new BehaviorSubject(init);
12 | obs.subscribe(s => res.next(s));
13 | return res;
14 | }
15 |
16 | export function applicationStateFactory(initialState: ApplicationState, actions: Observable): Observable {
17 |
18 | let appStateObservable = actions.scan( (state: ApplicationState, action) => {
19 |
20 | console.log("Processing action " + action.getName());
21 |
22 | let newState: ApplicationState = {
23 | todos: calculateTodos(state.todos, action),
24 | uiState: calculateUiState(state.uiState, action)
25 | };
26 |
27 | console.log({
28 | todos: newState.todos.toJS(),
29 | uiState: newState.uiState
30 | });
31 |
32 | return newState;
33 |
34 | } , initialState)
35 | .share();
36 |
37 | return wrapIntoBehaviorSubject(initialState, appStateObservable);
38 | }
--------------------------------------------------------------------------------
/src/state/reducers.ts:
--------------------------------------------------------------------------------
1 |
2 | import {List} from 'immutable';
3 | import {Todo} from "../Todo";
4 | import {
5 | LoadTodosAction, AddTodoAction, ToggleTodoAction, DeleteTodoAction, StartBackendAction, EndBackendAction
6 | } from './todoActions';
7 | import {UiState, initialUiState} from "./ui-state";
8 |
9 | export function calculateTodos(state: List, action) {
10 | if (!state) {
11 | return List([]);
12 | }
13 |
14 | if (action instanceof LoadTodosAction) {
15 | return List(action.todos);
16 | }
17 | else if (action instanceof AddTodoAction) {
18 | return state.push(action.newTodo);
19 | }
20 | else if (action instanceof ToggleTodoAction) {
21 | return toggleTodo(state, action);
22 | }
23 | else if (action instanceof DeleteTodoAction) {
24 | let index = state.findIndex((todo) => todo.id === action.todo.id);
25 | return state.delete(index);
26 | }
27 | else {
28 | return state;
29 | }
30 | }
31 |
32 | function toggleTodo(state, action) {
33 | let index = state.findIndex((todo: Todo) => todo.id === action.todo.id);
34 | let toggled:Todo = state.get(index);
35 | return state.set(index, new Todo({id:toggled.id, description:toggled.description, completed:!toggled.completed}) );
36 | }
37 |
38 | export function calculateUiState(state: UiState, action) {
39 | if (!state) {
40 | return initialUiState;
41 | }
42 |
43 | if (action instanceof StartBackendAction) {
44 | return new UiState(true, action.message);
45 | }
46 | else if (action instanceof EndBackendAction) {
47 | return new UiState(false, action.message ? action.message : initialUiState.message);
48 | }
49 | else {
50 | return state;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/state/todoActions.ts:
--------------------------------------------------------------------------------
1 |
2 | import {List} from 'immutable';
3 | import {Todo} from "../Todo";
4 |
5 | export class LoadTodosAction {
6 |
7 | constructor(public todos: List) {
8 |
9 | }
10 | }
11 |
12 | export class AddTodoAction {
13 |
14 | constructor(public newTodo: Todo) {
15 |
16 | }
17 |
18 | }
19 |
20 | export class ToggleTodoAction {
21 | constructor(public todo: Todo) {
22 |
23 | }
24 | }
25 |
26 | export class DeleteTodoAction {
27 |
28 | constructor(public todo: Todo) {
29 |
30 | }
31 | }
32 |
33 | export class StartBackendAction {
34 |
35 | constructor(public message:string) {
36 |
37 | }
38 |
39 | }
40 |
41 | export class EndBackendAction {
42 |
43 | constructor(public message: string) {
44 |
45 | }
46 | }
47 |
48 | export type Action = LoadTodosAction | AddTodoAction | ToggleTodoAction | DeleteTodoAction | StartBackendAction | EndBackendAction;
49 |
--------------------------------------------------------------------------------
/src/state/ui-state.ts:
--------------------------------------------------------------------------------
1 |
2 | export class UiState {
3 |
4 | constructor(public actionOngoing: boolean, public message:string) {
5 |
6 | }
7 |
8 | }
9 |
10 | export const initialUiState = {
11 | actionOngoing: false,
12 | message: 'Ready'
13 | };
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 | [ng-cloak] {
3 | display: none; }
4 |
5 | hr {
6 | margin: 20px 0;
7 | border: 0;
8 | border-top: 1px dashed #c5c5c5;
9 | border-bottom: 1px dashed #f7f7f7; }
10 |
11 | .learn a {
12 | font-weight: normal;
13 | text-decoration: none;
14 | color: #b83f45; }
15 |
16 | .learn a:hover {
17 | text-decoration: underline;
18 | color: #787e7e; }
19 |
20 | .learn h3, .learn h4, .learn h5 {
21 | margin: 10px 0;
22 | font-weight: 500;
23 | line-height: 1.2;
24 | color: #000; }
25 |
26 | .learn h3 {
27 | font-size: 24px; }
28 |
29 | .learn h4 {
30 | font-size: 18px; }
31 |
32 | .learn h5 {
33 | margin-bottom: 0;
34 | font-size: 14px; }
35 |
36 | .learn ul {
37 | padding: 0;
38 | margin: 0 0 30px 25px; }
39 |
40 | .learn li {
41 | line-height: 20px; }
42 |
43 | .learn p {
44 | font-size: 15px;
45 | font-weight: 300;
46 | line-height: 1.3;
47 | margin-top: 0;
48 | margin-bottom: 0; }
49 |
50 | #issue-count {
51 | display: none; }
52 |
53 | .quote {
54 | border: none;
55 | margin: 20px 0 60px 0; }
56 |
57 | .quote p {
58 | font-style: italic; }
59 |
60 | .quote p:before {
61 | content: '“';
62 | font-size: 50px;
63 | opacity: .15;
64 | position: absolute;
65 | top: -20px;
66 | left: 3px; }
67 |
68 | .quote p:after {
69 | content: '”';
70 | font-size: 50px;
71 | opacity: .15;
72 | position: absolute;
73 | bottom: -42px;
74 | right: 3px; }
75 |
76 | .quote footer {
77 | position: absolute;
78 | bottom: -40px;
79 | right: 0; }
80 |
81 | .quote footer img {
82 | border-radius: 3px; }
83 |
84 | .quote footer a {
85 | margin-left: 5px;
86 | vertical-align: middle; }
87 |
88 | .speech-bubble {
89 | position: relative;
90 | padding: 10px;
91 | background: rgba(0, 0, 0, 0.04);
92 | border-radius: 5px; }
93 |
94 | .speech-bubble:after {
95 | content: '';
96 | position: absolute;
97 | top: 100%;
98 | right: 30px;
99 | border: 13px solid transparent;
100 | border-top-color: rgba(0, 0, 0, 0.04); }
101 |
102 | .learn-bar > .learn {
103 | position: absolute;
104 | width: 272px;
105 | top: 8px;
106 | left: -300px;
107 | padding: 10px;
108 | border-radius: 5px;
109 | background-color: rgba(255, 255, 255, 0.6);
110 | transition-property: left;
111 | transition-duration: 500ms; }
112 |
113 | @media (min-width: 899px) {
114 | .learn-bar {
115 | width: auto;
116 | padding-left: 300px; }
117 | .learn-bar > .learn {
118 | left: 8px; } }
119 |
120 | html, body {
121 | margin: 0;
122 | padding: 0; }
123 |
124 | button {
125 | margin: 0;
126 | padding: 0;
127 | border: 0;
128 | background: none;
129 | font-size: 100%;
130 | vertical-align: baseline;
131 | font-family: inherit;
132 | font-weight: inherit;
133 | color: inherit;
134 | -webkit-appearance: none;
135 | appearance: none;
136 | -webkit-font-smoothing: antialiased;
137 | -moz-font-smoothing: antialiased;
138 | font-smoothing: antialiased; }
139 |
140 | body {
141 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
142 | line-height: 1.4em;
143 | background: #f5f5f5;
144 | color: #4d4d4d;
145 | min-width: 230px;
146 | max-width: 550px;
147 | margin: 0 auto;
148 | -webkit-font-smoothing: antialiased;
149 | -moz-font-smoothing: antialiased;
150 | font-smoothing: antialiased;
151 | font-weight: 300; }
152 |
153 | button, input[type="checkbox"] {
154 | outline: none; }
155 |
156 | .hidden {
157 | display: none; }
158 |
159 | #todoapp {
160 | background: #fff;
161 | margin: 130px 0 40px 0;
162 | position: relative;
163 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); }
164 |
165 | #todoapp input::-webkit-input-placeholder {
166 | font-style: italic;
167 | font-weight: 300;
168 | color: #e6e6e6; }
169 |
170 | #todoapp input::-moz-placeholder {
171 | font-style: italic;
172 | font-weight: 300;
173 | color: #e6e6e6; }
174 |
175 | #todoapp input::input-placeholder {
176 | font-style: italic;
177 | font-weight: 300;
178 | color: #e6e6e6; }
179 |
180 | #todoapp h1 {
181 | position: absolute;
182 | top: -155px;
183 | width: 100%;
184 | font-size: 100px;
185 | font-weight: 100;
186 | text-align: center;
187 | color: rgba(175, 47, 47, 0.15);
188 | -webkit-text-rendering: optimizeLegibility;
189 | -moz-text-rendering: optimizeLegibility;
190 | text-rendering: optimizeLegibility; }
191 |
192 | #new-todo, .edit {
193 | position: relative;
194 | margin: 0;
195 | width: 100%;
196 | font-size: 24px;
197 | font-family: inherit;
198 | font-weight: inherit;
199 | line-height: 1.4em;
200 | border: 0;
201 | outline: none;
202 | color: inherit;
203 | padding: 6px;
204 | border: 1px solid #999;
205 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
206 | box-sizing: border-box;
207 | -webkit-font-smoothing: antialiased;
208 | -moz-font-smoothing: antialiased;
209 | font-smoothing: antialiased; }
210 |
211 | #new-todo {
212 | padding: 16px 16px 16px 60px;
213 | border: none;
214 | background: rgba(0, 0, 0, 0.003);
215 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); }
216 |
217 | #main {
218 | position: relative;
219 | z-index: 2;
220 | border-top: 1px solid #e6e6e6; }
221 |
222 | label[for='toggle-all'] {
223 | display: none; }
224 |
225 | #toggle-all {
226 | position: absolute;
227 | top: -55px;
228 | left: -12px;
229 | width: 60px;
230 | height: 34px;
231 | text-align: center;
232 | border: none;
233 | /* Mobile Safari */ }
234 |
235 | #toggle-all:before {
236 | content: '❯';
237 | font-size: 22px;
238 | color: #e6e6e6;
239 | padding: 10px 27px 10px 27px; }
240 |
241 | #toggle-all:checked:before {
242 | color: #737373; }
243 |
244 | #todo-list {
245 | margin: 0;
246 | padding: 0;
247 | list-style: none; }
248 |
249 | #todo-list li {
250 | position: relative;
251 | font-size: 24px;
252 | border-bottom: 1px solid #ededed; }
253 |
254 | #todo-list li:last-child {
255 | border-bottom: none; }
256 |
257 | #todo-list li.editing {
258 | border-bottom: none;
259 | padding: 0; }
260 |
261 | #todo-list li.editing .edit {
262 | display: block;
263 | width: 506px;
264 | padding: 13px 17px 12px 17px;
265 | margin: 0 0 0 43px; }
266 |
267 | #todo-list li.editing .view {
268 | display: none; }
269 |
270 | #todo-list li .toggle {
271 | text-align: center;
272 | width: 40px;
273 | /* auto, since non-WebKit browsers doesn't support input styling */
274 | height: auto;
275 | position: absolute;
276 | top: 0;
277 | bottom: 0;
278 | margin: auto 0;
279 | border: none;
280 | /* Mobile Safari */
281 | -webkit-appearance: none;
282 | appearance: none; }
283 |
284 | #todo-list li .toggle:after {
285 | content: url('data:image/svg+xml;utf8, '); }
286 |
287 | #todo-list li .toggle:checked:after {
288 | content: url('data:image/svg+xml;utf8, '); }
289 |
290 | #todo-list li label {
291 | white-space: pre;
292 | word-break: break-word;
293 | padding: 15px 60px 15px 15px;
294 | margin-left: 45px;
295 | display: block;
296 | line-height: 1.2;
297 | transition: color 0.4s; }
298 |
299 | #todo-list li.completed label {
300 | color: #d9d9d9;
301 | text-decoration: line-through; }
302 |
303 | #todo-list li .destroy {
304 | display: none;
305 | position: absolute;
306 | top: 0;
307 | right: 10px;
308 | bottom: 0;
309 | width: 40px;
310 | height: 40px;
311 | margin: auto 0;
312 | font-size: 30px;
313 | color: #cc9a9a;
314 | margin-bottom: 11px;
315 | transition: color 0.2s ease-out; }
316 |
317 | #todo-list li .destroy:hover {
318 | color: #af5b5e; }
319 |
320 | #todo-list li .destroy:after {
321 | content: '×'; }
322 |
323 | #todo-list li:hover .destroy {
324 | display: block; }
325 |
326 | #todo-list li .edit {
327 | display: none; }
328 |
329 | #todo-list li.editing:last-child {
330 | margin-bottom: -1px; }
331 |
332 | #footer {
333 | color: #777;
334 | padding: 10px 15px;
335 | height: 20px;
336 | text-align: center;
337 | border-top: 1px solid #e6e6e6; }
338 |
339 | #footer:before {
340 | content: '';
341 | position: absolute;
342 | right: 0;
343 | bottom: 0;
344 | left: 0;
345 | height: 50px;
346 | overflow: hidden;
347 | 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); }
348 |
349 | #todo-count {
350 | float: left;
351 | text-align: left; }
352 |
353 | #todo-count strong {
354 | font-weight: 300; }
355 |
356 | #filters {
357 | margin: 0;
358 | padding: 0;
359 | list-style: none;
360 | position: absolute;
361 | right: 0;
362 | left: 0; }
363 |
364 | #filters li {
365 | display: inline; }
366 |
367 | #filters li a {
368 | color: inherit;
369 | margin: 3px;
370 | padding: 3px 7px;
371 | text-decoration: none;
372 | border: 1px solid transparent;
373 | border-radius: 3px; }
374 |
375 | #filters li a.selected, #filters li a:hover {
376 | border-color: rgba(175, 47, 47, 0.1); }
377 |
378 | #filters li a.selected {
379 | border-color: rgba(175, 47, 47, 0.2); }
380 |
381 | #clear-completed, html #clear-completed:active {
382 | float: right;
383 | position: relative;
384 | line-height: 20px;
385 | text-decoration: none;
386 | cursor: pointer;
387 | position: relative; }
388 |
389 | #clear-completed:hover {
390 | text-decoration: underline; }
391 |
392 | #info {
393 | margin: 65px auto 0;
394 | color: #bfbfbf;
395 | font-size: 18px;
396 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
397 | text-align: center; }
398 |
399 | #info p {
400 | line-height: 1; }
401 |
402 | #info a {
403 | color: inherit;
404 | text-decoration: none;
405 | font-weight: 400; }
406 |
407 | #info a:hover {
408 | text-decoration: underline; }
409 |
410 | /*
411 | Hack to remove background from Mobile Safari.
412 | Can't use it globally since it destroys checkboxes in Firefox
413 | */
414 | @media screen and (-webkit-min-device-pixel-ratio: 0) {
415 | #toggle-all, #todo-list li .toggle {
416 | background: none; }
417 | #todo-list li .toggle {
418 | height: 40px; }
419 | #toggle-all {
420 | -webkit-transform: rotate(90deg);
421 | transform: rotate(90deg);
422 | -webkit-appearance: none;
423 | appearance: none; } }
424 |
425 | @media (max-width: 430px) {
426 | #footer {
427 | height: 50px; }
428 | #filters {
429 | bottom: 10px; } }
430 |
431 |
432 | /*# sourceMappingURL=maps/todo.a785b905.css.map */
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "target": "es5",
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "removeComments": true,
9 | "sourceMap": true,
10 | "declaration": true
11 | },
12 | "exclude": [
13 | "node_modules"
14 | ]
15 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: "./src/App.ts",
3 | output: {
4 | filename: "bundle.js"
5 | },
6 | devtool: 'source-map',
7 | resolve: {
8 | extensions: ['', '.webpack.js', '.web.js', '.ts', '.js']
9 | },
10 | module: {
11 | loaders: [
12 | { test: /\.ts$/, loader: 'ts-loader' }
13 | ]
14 | }
15 | };
16 |
--------------------------------------------------------------------------------