├── Model.ts ├── tsconfig.json ├── tsd.json ├── styles.css ├── .gitignore ├── TodoListComponent.ts ├── TodoApp.ts ├── Validators.ts ├── index.html └── TodoService.ts /Model.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: number; 3 | name: string; 4 | state: TodoState; 5 | } 6 | 7 | export enum TodoState { 8 | Active = 1, 9 | Complete = 2 10 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "declaration": true, 5 | "module": "system", 6 | "sourceMap": true, 7 | "experimentalDecorators": true 8 | } 9 | } -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "jquery/jquery.d.ts": { 9 | "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .text-strikethrough { 2 | text-decoration: line-through; 3 | } 4 | 5 | .text-giant { 6 | font-size: 200%; 7 | } 8 | 9 | .completed-indicator { 10 | color: lawngreen 11 | } 12 | 13 | .todo-item .completed { 14 | display: none; 15 | } 16 | 17 | .todo-item.completed .completed { 18 | display: block; 19 | } 20 | 21 | .todo-item.completed .incomplete { 22 | display: none; 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | .DS_Store 31 | 32 | *.css 33 | 34 | .vscode 35 | 36 | *.js 37 | *.d.ts 38 | *.js.map 39 | 40 | typings/ -------------------------------------------------------------------------------- /TodoListComponent.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodoState } from './Model'; 2 | import '//code.jquery.com/jquery-1.12.1.min.js'; 3 | 4 | export default class TodoListComponent { 5 | 6 | private $el: JQuery; 7 | 8 | constructor(el: HTMLElement) { 9 | this.$el = $(el); 10 | } 11 | 12 | render(todos) { 13 | 14 | this.$el.html(''); 15 | 16 | if (!todos.length) { 17 | this.$el.html( 18 | "
" + 19 | " You've completed everything you needed to do!" + 20 | "
" 21 | ); 22 | 23 | return; 24 | } 25 | 26 | for(var index in todos) { 27 | var todo = todos[index]; 28 | this.renderTodo(todo).appendTo(this.$el); 29 | } 30 | } 31 | 32 | private renderTodo(todo) { 33 | return $( 34 | "
" + 35 | "
" + 36 | "
" + 37 | " " + 38 | " " + 39 | "
" + 40 | "
" + 41 | " " + todo.name + "" + 42 | " " + todo.name + "" + 43 | "
" + 44 | "
" + 45 | "
" + 46 | "
" 47 | ).on('click', function() { 48 | var event = document.createEvent('CustomEvent'); 49 | event.initCustomEvent('todo-toggle', true, true, { todoId: todo.id }); 50 | this.dispatchEvent(event); 51 | }); 52 | } 53 | } -------------------------------------------------------------------------------- /TodoApp.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodoState } from './Model'; 2 | import TodoService, { ITodoService } from './TodoService'; 3 | import TodoListComponent from './TodoListComponent'; 4 | 5 | export class TodoApp { 6 | 7 | private todoService: ITodoService; 8 | private todoList: TodoListComponent; 9 | 10 | constructor(el, todos) { 11 | 12 | this.todoService = new TodoService(todos); 13 | this.initialize(el); 14 | } 15 | 16 | addTodo(todoName) { 17 | try { 18 | this.todoService.add(todoName); 19 | } catch(x) { 20 | console.error(x) 21 | } 22 | 23 | this.renderTodos(); 24 | } 25 | 26 | clearCompleted() { 27 | this.todoService.clearCompleted(); 28 | this.renderTodos(); 29 | } 30 | 31 | toggleTodoState(todoId) { 32 | this.todoService.toggle(todoId); 33 | this.renderTodos(); 34 | } 35 | 36 | renderTodos() { 37 | var todos = this.todoService.getAll(); 38 | this.todoList.render(todos); 39 | } 40 | 41 | initialize(el) { 42 | 43 | var _this = this; 44 | 45 | var addTodoFormEl = el.getElementsByClassName('add-todo')[0], 46 | addTodoNameEl = addTodoFormEl.getElementsByTagName('input')[0], 47 | todoListEl = el.getElementsByClassName('todo-list')[0], 48 | clearCompletedEl = el.getElementsByClassName('clear-completed')[0]; 49 | 50 | addTodoFormEl.addEventListener('submit', function(evnt) { 51 | _this.addTodo(addTodoNameEl.value) 52 | addTodoNameEl.value = ''; 53 | evnt.preventDefault(); 54 | }); 55 | 56 | todoListEl.addEventListener('todo-toggle', function(evnt) { 57 | var todoId = evnt.details.todoId; 58 | _this.todoService.toggle(todoId); 59 | _this.renderTodos(); 60 | }); 61 | 62 | clearCompletedEl.addEventListener('click', function() { 63 | _this.clearCompleted(); 64 | }); 65 | 66 | this.todoList = new TodoListComponent(todoListEl); 67 | 68 | this.renderTodos(); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Validators.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodoState } from './Model'; 2 | 3 | @validatable 4 | export class ValidatableTodo implements Todo { 5 | 6 | id: number; 7 | 8 | @required 9 | @regex(`^[a-zA-Z ]*$`) 10 | name: string; 11 | 12 | state: TodoState; 13 | } 14 | 15 | export interface ValidatableTodo extends IValidatable { 16 | } 17 | 18 | 19 | export interface IValidatable { 20 | validate(): IValidationResult[]; 21 | } 22 | 23 | export interface IValidationResult { 24 | isValid: boolean; 25 | message: string; 26 | property?: string; 27 | } 28 | 29 | export interface IValidator { 30 | (instance: Object): IValidationResult; 31 | } 32 | 33 | export function validate(): IValidationResult[] { 34 | let validators: IValidator[] = [].concat(this._validators), 35 | errors: IValidationResult[] = []; 36 | 37 | for (let validator of validators) { 38 | 39 | let result = validator(this); 40 | 41 | if (!result.isValid) { 42 | errors.push(result); 43 | } 44 | 45 | } 46 | 47 | return errors; 48 | } 49 | 50 | export function validatable(target: Function) { 51 | 52 | target.prototype.validate = validate; 53 | 54 | } 55 | 56 | export function required(target: Object, propertyName: string) { 57 | 58 | let validatable = <{ _validators: IValidator[] }>target, 59 | validators = (validatable._validators || (validatable._validators = [])); 60 | 61 | validators.push(function(instance) { 62 | 63 | let propertyValue = instance[propertyName], 64 | isValid = propertyValue != undefined; 65 | 66 | if (typeof propertyValue === 'string') { 67 | isValid = propertyValue && propertyValue.length > 0; 68 | } 69 | 70 | return { 71 | isValid, 72 | message: `${propertyName} is required`, 73 | property: propertyName 74 | } 75 | 76 | }) 77 | 78 | } 79 | 80 | export function regex(pattern: string) { 81 | 82 | let expression = new RegExp(pattern); 83 | 84 | return function regex(target: Object, propertyName: string) { 85 | 86 | let validatable = <{ _validators: IValidator[] }>target, 87 | validators = (validatable._validators || (validatable._validators = [])); 88 | 89 | validators.push(function(instance) { 90 | 91 | let propertyValue = instance[propertyName], 92 | isValid = expression.test(propertyValue); 93 | 94 | return { 95 | isValid, 96 | message: `${propertyName} does not match ${expression}`, 97 | property: propertyName 98 | } 99 | 100 | }) 101 | 102 | }; 103 | 104 | } 105 | 106 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TypeScript Todo List 8 | 9 | 10 | 11 | 12 |
13 |
14 |

TypeScript Todo List

15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 | 51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 | 59 | 60 | 72 | 73 | -------------------------------------------------------------------------------- /TodoService.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodoState } from './Model'; 2 | import { ValidatableTodo } from './Validators'; 3 | 4 | export interface ITodoService { 5 | add(todo: Todo): Todo; 6 | add(todo: string): Todo; 7 | clearCompleted(): void; 8 | getAll(): Todo[]; 9 | getById(todoId: number): Todo; 10 | toggle(todoId: number): void; 11 | } 12 | 13 | let _lastId = 0; 14 | 15 | function generateTodoId(): number { 16 | return _lastId += 1; 17 | } 18 | 19 | function clone(src: T): T { 20 | var clone = JSON.stringify(src); 21 | return JSON.parse(clone); 22 | }; 23 | 24 | 25 | export default class TodoService implements ITodoService { 26 | 27 | private todos: Todo[] = []; 28 | 29 | constructor(todos: string[]) { 30 | if (todos) { 31 | todos.forEach(todo => this.add(todo)); 32 | } 33 | } 34 | 35 | // Accepts a todo name or todo object 36 | add(todo: Todo): Todo 37 | add(todo: string): Todo 38 | @log 39 | add(input): Todo { 40 | 41 | var todo = new ValidatableTodo(); 42 | todo.id = generateTodoId(); 43 | todo.state = TodoState.Active; 44 | 45 | if (typeof input === 'string') { 46 | todo.name = input; 47 | } 48 | else if (typeof input.name === 'string') { 49 | todo.name = input.name; 50 | } else { 51 | throw 'Invalid Todo name!'; 52 | } 53 | 54 | let errors = todo.validate(); 55 | 56 | if(errors.length) { 57 | 58 | let combinedErrors = errors.map(x => `${x.property}: ${x.message}`); 59 | throw `Invalid Todo: ${combinedErrors}`; 60 | 61 | } 62 | 63 | this.todos.push(todo); 64 | 65 | return todo; 66 | }; 67 | 68 | 69 | clearCompleted(): void { 70 | 71 | this.todos = this.todos.filter( 72 | x => x.state == TodoState.Active 73 | ); 74 | } 75 | 76 | 77 | getAll(): Todo[] { 78 | return clone(this.todos); 79 | }; 80 | 81 | 82 | getById(todoId: number): Todo { 83 | var todo = this._find(todoId); 84 | return clone(todo); 85 | }; 86 | 87 | toggle(todoId: number): void { 88 | 89 | var todo = this._find(todoId); 90 | 91 | if (!todo) return; 92 | 93 | switch (todo.state) { 94 | case TodoState.Active: 95 | todo.state = TodoState.Complete; 96 | break; 97 | 98 | case TodoState.Complete: 99 | todo.state = TodoState.Active; 100 | break; 101 | } 102 | } 103 | 104 | private _find(todoId: number): Todo { 105 | var filtered = this.todos.filter( 106 | x => x.id == todoId 107 | ); 108 | 109 | if (filtered.length) { 110 | return filtered[0]; 111 | } 112 | 113 | return null; 114 | } 115 | } 116 | 117 | function log(target: Object, methodName: string, descriptor: TypedPropertyDescriptor) { 118 | 119 | let originalMethod = descriptor.value; 120 | 121 | descriptor.value = function(...args) { 122 | 123 | console.log(`${methodName}(${JSON.stringify(args)})`) 124 | 125 | let returnValue = originalMethod.apply(this, args); 126 | 127 | console.log(`${methodName}(${JSON.stringify(args)}) => ${JSON.stringify(returnValue)}`) 128 | 129 | return returnValue; 130 | } 131 | } 132 | 133 | 134 | --------------------------------------------------------------------------------