|
1 | 1 | <template> |
2 | | - <div id="app"> |
3 | | - <img src="./assets/logo.png"> |
4 | | - <h1>{{ msg }}</h1> |
5 | | - <h2>Essential Links</h2> |
6 | | - <ul> |
7 | | - <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li> |
8 | | - <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li> |
9 | | - <li><a href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li> |
10 | | - <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li> |
11 | | - </ul> |
12 | | - <h2>Ecosystem</h2> |
13 | | - <ul> |
14 | | - <li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li> |
15 | | - <li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li> |
16 | | - <li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li> |
17 | | - <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li> |
18 | | - </ul> |
19 | | - </div> |
| 2 | + <div id="app"> |
| 3 | + <section class="todoapp"> |
| 4 | + <header class="header"> |
| 5 | + <h1>todos</h1> |
| 6 | + <input class="new-todo" |
| 7 | + autofocus autocomplete="off" |
| 8 | + placeholder="What needs to be done?" |
| 9 | + v-model="newTodo" |
| 10 | + @keyup.enter="addTodo"> |
| 11 | + </header> |
| 12 | + |
| 13 | + <section class="main" v-show="todos.length" v-cloak> |
| 14 | + <input class="toggle-all" type="checkbox" v-model="allDone"> |
| 15 | + <ul class="todo-list"> |
| 16 | + <li v-for="todo in filteredTodos" |
| 17 | + class="todo" |
| 18 | + :key="todo.id" |
| 19 | + :class="{ completed: todo.completed, editing: todo == editedTodo }"> |
| 20 | + |
| 21 | + <div class="view"> |
| 22 | + <input class="toggle" type="checkbox" v-model="todo.completed"> |
| 23 | + <label @dblclick="editTodo(todo)">{{ todo.title }}</label> |
| 24 | + <button class="destroy" @click="removeTodo(todo)"></button> |
| 25 | + </div> |
| 26 | + |
| 27 | + <input class="edit" type="text" |
| 28 | + v-model="todo.title" |
| 29 | + v-todo-focus="todo == editedTodo" |
| 30 | + @blur="doneEdit(todo)" |
| 31 | + @keyup.enter="doneEdit(todo)" |
| 32 | + @keyup.esc="cancelEdit(todo)"> |
| 33 | + </li> |
| 34 | + </ul> |
| 35 | + </section> |
| 36 | + |
| 37 | + <footer class="footer" v-show="todos.length" v-cloak> |
| 38 | + <span class="todo-count"><strong>{{ remaining }}</strong> {{ remaining | pluralize }} left</span> |
| 39 | + <ul class="filters"> |
| 40 | + <li><a href="#/all" :class="{ selected: visibility == 'all' }">All</a></li> |
| 41 | + <li><a href="#/active" :class="{ selected: visibility == 'active' }">Active</a></li> |
| 42 | + <li><a href="#/completed" :class="{ selected: visibility == 'completed' }">Completed</a></li> |
| 43 | + </ul> |
| 44 | + <button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">Clear completed</button> |
| 45 | + </footer> |
| 46 | + </section> |
| 47 | + |
| 48 | + <footer class="info"> |
| 49 | + <p>Double-click to edit a todo</p> |
| 50 | + <p>Written by <a href="http://evanyou.me">Evan You</a></p> |
| 51 | + <p>Converted by <a href="http://toni.uebernickel.info">Toni Uebernickel</a></p> |
| 52 | + <p>Part of <a href="http://todomvc.com">TodoMVC</a></p> |
| 53 | + </footer> |
| 54 | + </div> |
20 | 55 | </template> |
21 | 56 |
|
22 | 57 | <script lang="ts"> |
23 | | -import Vue from "vue"; |
24 | | -import Component from 'vue-class-component'; |
| 58 | +import { Component, Vue, Watch } from 'vue-property-decorator' |
| 59 | +
|
| 60 | +import Todo from './Todo' |
| 61 | +import TodoStorage from './TodoStorage' |
| 62 | +
|
| 63 | +Vue.filter('pluralize', (n: number): string => { |
| 64 | + return n === 1 ? 'item' : 'items' |
| 65 | +}) |
| 66 | +
|
| 67 | +Vue.directive('todo-focus', (el, binding) => { |
| 68 | + if (binding.value) { |
| 69 | + el.focus() |
| 70 | + } |
| 71 | +}) |
| 72 | +
|
| 73 | +const filters = { |
| 74 | + all(todos: Todo[]): Todo[] { |
| 75 | + return todos |
| 76 | + }, |
| 77 | +
|
| 78 | + active(todos: Todo[]): Todo[] { |
| 79 | + return todos.filter((todo: Todo) => { |
| 80 | + return !todo.completed |
| 81 | + }) |
| 82 | + }, |
| 83 | +
|
| 84 | + completed(todos: Todo[]): Todo[] { |
| 85 | + return todos.filter((todo: Todo) => { |
| 86 | + return todo.completed |
| 87 | + }) |
| 88 | + } |
| 89 | +} |
25 | 90 |
|
26 | 91 | @Component({ |
27 | 92 | name: 'app' |
28 | 93 | }) |
29 | 94 | export default class App extends Vue { |
30 | | - data () { |
31 | | - return { |
32 | | - msg: 'Welcome to Your Vue.js App, supporting TypeScript' |
| 95 | + private storage: TodoStorage = new TodoStorage() |
| 96 | +
|
| 97 | + private todos: Todo[] = this.storage.fetch() |
| 98 | + private visibility: string = 'all' |
| 99 | +
|
| 100 | + private newTodo: string = '' |
| 101 | + private editedTodo: Todo|null = null |
| 102 | + private beforeEditCache: string|null = null |
| 103 | +
|
| 104 | + constructor() { |
| 105 | + super() |
| 106 | +
|
| 107 | + window.addEventListener('hashchange', this.onHashChange) |
| 108 | + } |
| 109 | +
|
| 110 | + get filteredTodos(): Todo[] { |
| 111 | + return filters[this.visibility](this.todos) |
| 112 | + } |
| 113 | +
|
| 114 | + get remaining(): number { |
| 115 | + return filters.active(this.todos).length |
| 116 | + } |
| 117 | +
|
| 118 | + get allDone(): boolean { |
| 119 | + return this.remaining === 0 |
| 120 | + } |
| 121 | +
|
| 122 | + set allDone(value: boolean) { |
| 123 | + this.todos.forEach((todo: Todo) => { |
| 124 | + todo.completed = value |
| 125 | + }) |
| 126 | + } |
| 127 | +
|
| 128 | + addTodo() { |
| 129 | + const title = this.newTodo && this.newTodo.trim() |
| 130 | + if (!title) { |
| 131 | + return |
33 | 132 | } |
| 133 | +
|
| 134 | + this.todos.push({ |
| 135 | + id: this.storage.nextUid(), |
| 136 | + title: title, |
| 137 | + completed: false |
| 138 | + }) |
| 139 | +
|
| 140 | + this.newTodo = '' |
34 | 141 | } |
35 | | -} |
36 | | -</script> |
37 | 142 |
|
38 | | -<style lang="scss"> |
39 | | -#app { |
40 | | - font-family: 'Avenir', Helvetica, Arial, sans-serif; |
41 | | - -webkit-font-smoothing: antialiased; |
42 | | - -moz-osx-font-smoothing: grayscale; |
43 | | - text-align: center; |
44 | | - color: #2c3e50; |
45 | | - margin-top: 60px; |
46 | | -} |
| 143 | + removeTodo(todo) { |
| 144 | + this.todos.splice(this.todos.indexOf(todo), 1) |
| 145 | + } |
47 | 146 |
|
48 | | -h1, h2 { |
49 | | - font-weight: normal; |
50 | | -} |
| 147 | + editTodo(todo: Todo) { |
| 148 | + this.beforeEditCache = todo.title |
| 149 | + this.editedTodo = todo |
| 150 | + } |
51 | 151 |
|
52 | | -ul { |
53 | | - list-style-type: none; |
54 | | - padding: 0; |
55 | | -} |
| 152 | + doneEdit(todo) { |
| 153 | + if (!this.editedTodo) { |
| 154 | + return |
| 155 | + } |
56 | 156 |
|
57 | | -li { |
58 | | - display: inline-block; |
59 | | - margin: 0 10px; |
60 | | -} |
| 157 | + this.editedTodo = null |
| 158 | +
|
| 159 | + todo.title = todo.title.trim() |
| 160 | + if (!todo.title) { |
| 161 | + this.removeTodo(todo) |
| 162 | + } |
| 163 | + } |
61 | 164 |
|
62 | | -a { |
63 | | - color: #42b983; |
| 165 | + cancelEdit(todo) { |
| 166 | + this.editedTodo = null |
| 167 | + todo.title = this.beforeEditCache |
| 168 | + } |
| 169 | +
|
| 170 | + removeCompleted() { |
| 171 | + this.todos = filters.active(this.todos) |
| 172 | + } |
| 173 | +
|
| 174 | + @Watch('todos', { deep: true}) |
| 175 | + onTodosChange() { |
| 176 | + this.storage.save(this.todos) |
| 177 | + } |
| 178 | +
|
| 179 | + onHashChange() { |
| 180 | + const visibility = window.location.hash.replace(/#\/?/, '') |
| 181 | +
|
| 182 | + if (filters[visibility]) { |
| 183 | + this.visibility = visibility |
| 184 | + } else { |
| 185 | + window.location.hash = '' |
| 186 | +
|
| 187 | + this.visibility = 'all' |
| 188 | + } |
| 189 | + } |
64 | 190 | } |
| 191 | +</script> |
| 192 | + |
| 193 | +<style lang="scss"> |
| 194 | +@import url('https://unpkg.com/todomvc-app-css@2.0.6/index.css'); |
| 195 | +
|
| 196 | +[v-cloak] { display: none; } |
65 | 197 | </style> |
0 commit comments