├── 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 |
8 | 9 |
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 | 18 |
    19 |
  • 20 |
    21 | 22 | 23 | 24 |
    25 |
  • 26 |
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 | --------------------------------------------------------------------------------