├── LICENSE ├── index.html └── app.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 LeanCloud 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LeanTodo • Vue.js 6 | 7 | 30 | 31 | 32 |
33 |

LeanTodo

34 |
35 | 36 | 37 |
38 | 39 | 40 |
41 |
42 | 88 |
89 | 93 | Fork me on GitHub 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | AV.init({ 2 | appId: 'ozewwcwsyq92g2hommuxqrqzg6847wgl8dtrac6suxzko333', 3 | appKey: 'ni0kwg7h8hwtz6a7dw9ipr7ayk989zo5y8t0sn5gjiel6uav', 4 | serverURL: 'https://ozewwcws.lc-cn-n1-shared.com', 5 | }); 6 | 7 | const Todo = AV.Object.extend('Todo'); 8 | 9 | // visibility filters 10 | const filters = { 11 | all: (todos) => todos, 12 | active: (todos) => todos.filter((todo) => !todo.done), 13 | completed: (todos) => todos.filter((todo) => todo.done), 14 | }; 15 | 16 | // app Vue instance 17 | const app = new Vue({ 18 | // app initial state 19 | data: { 20 | todos: [], 21 | content: '', 22 | editingId: null, 23 | editingContent: '', 24 | visibility: 'all', 25 | username: '', 26 | password: '', 27 | user: null, 28 | }, 29 | 30 | mounted() { 31 | onHashChange(); 32 | if (AV.User.current()) { 33 | this.user = AV.User.current().toJSON(); 34 | this.fetchTodos(); 35 | } 36 | }, 37 | 38 | beforeDestroy() { 39 | if (this.unbind) { 40 | this.unbind(); 41 | } 42 | }, 43 | 44 | // computed properties 45 | // https://vuejs.org/guide/computed.html 46 | computed: { 47 | filteredTodos() { 48 | return filters[this.visibility](this.todos); 49 | }, 50 | remaining() { 51 | return filters.active(this.todos).length; 52 | }, 53 | }, 54 | 55 | filters: { 56 | pluralize(n) { 57 | return n === 1 ? 'item' : 'items'; 58 | }, 59 | }, 60 | 61 | // methods that implement data logic. 62 | // note there's no DOM manipulation here at all. 63 | methods: { 64 | upsertTodo(todo) { 65 | for (let i = 0; i < this.todos.length; i++) { 66 | if (this.todos[i].objectId === todo.objectId) { 67 | this.$set(this.todos, i, todo); 68 | return; 69 | } 70 | } 71 | this.todos.unshift(todo); 72 | }, 73 | 74 | removeTodo(todo) { 75 | for (let i = 0; i < this.todos.length; i++) { 76 | if (this.todos[i].objectId === todo.objectId) { 77 | this.$delete(this.todos, i); 78 | break; 79 | } 80 | } 81 | }, 82 | 83 | handleCreateTodo() { 84 | const content = this.content.trim(); 85 | if (!content) return; 86 | this.content = ''; 87 | 88 | this.createTodoObject(content).then((todoObject) => { 89 | this.upsertTodo({ objectId: todoObject.id, done: false, content }); 90 | }); 91 | }, 92 | 93 | handleEditTodo(todo) { 94 | this.editingId = todo.objectId; 95 | this.editingContent = todo.content; 96 | }, 97 | 98 | handleFinishEditTodo(todo) { 99 | this.editingId = null; 100 | const content = this.editingContent.trim(); 101 | if (content === todo.content) { 102 | return; 103 | } 104 | if (content) { 105 | todo.content = content; 106 | this.upsertTodo(todo); 107 | this.updateTodoObject(todo.objectId, { content: todo.content }); 108 | } else { 109 | this.removeTodo(todo); 110 | this.removeTodoObject(todo.objectId); 111 | } 112 | }, 113 | 114 | handleToggleDone(todo) { 115 | this.upsertTodo(todo); 116 | this.updateTodoObject(todo.objectId, { done: todo.done }); 117 | }, 118 | 119 | handleRemoveTodo(todo) { 120 | this.removeTodo(todo); 121 | this.removeTodoObject(todo.objectId); 122 | }, 123 | 124 | handleRemoveCompleted() { 125 | const completed = filters.completed(this.todos); 126 | this.todos = filters.active(this.todos); 127 | this.removeTodoObject(completed.map((todo) => todo.objectId)); 128 | }, 129 | 130 | handleSignUp() { 131 | AV.User.signUp(this.username, this.password) 132 | .then((user) => { 133 | this.user = user.toJSON(); 134 | this.username = ''; 135 | this.password = ''; 136 | }) 137 | .catch(displayError); 138 | }, 139 | 140 | handleLogin() { 141 | AV.User.logIn(this.username, this.password) 142 | .then((user) => { 143 | this.user = user.toJSON(); 144 | this.username = ''; 145 | this.password = ''; 146 | this.fetchTodos(); 147 | }) 148 | .catch(displayError); 149 | }, 150 | 151 | handleLogout() { 152 | AV.User.logOut(); 153 | this.user = null; 154 | if (this.unbind) { 155 | this.unbind(); 156 | } 157 | }, 158 | 159 | async fetchTodos() { 160 | const query = new AV.Query(Todo) 161 | .equalTo('user', AV.User.current()) 162 | .descending('createdAt'); 163 | try { 164 | const todoObjects = await query.find(); 165 | this.todos = todoObjects.map((todoObj) => todoObj.toJSON()); 166 | 167 | if (this.unbind) { 168 | return; 169 | } 170 | const liveQuery = await query.subscribe(); 171 | const upsert = (todoObject) => this.upsertTodo(todoObject.toJSON()); 172 | const remove = (todoObject) => this.removeTodo(todoObject.toJSON()); 173 | liveQuery.on('create', upsert); 174 | liveQuery.on('update', upsert); 175 | liveQuery.on('enter', upsert); 176 | liveQuery.on('leave', remove); 177 | liveQuery.on('delete', remove); 178 | this.unbind = () => { 179 | liveQuery.off('create', upsert); 180 | liveQuery.off('update', upsert); 181 | liveQuery.off('enter', upsert); 182 | liveQuery.off('leave', remove); 183 | liveQuery.off('delete', remove); 184 | liveQuery.unsubscribe(); 185 | }; 186 | } catch (error) { 187 | displayError(error); 188 | } 189 | }, 190 | 191 | async createTodoObject(content) { 192 | const acl = new AV.ACL(); 193 | acl.setReadAccess(AV.User.current(), true); 194 | acl.setWriteAccess(AV.User.current(), true); 195 | try { 196 | const todo = new Todo({ 197 | content, 198 | done: false, 199 | user: AV.User.current(), 200 | }); 201 | todo.setACL(acl); 202 | return todo.save(); 203 | } catch (error) { 204 | displayError(error); 205 | } 206 | }, 207 | 208 | async updateTodoObject(objectId, { content, done } = {}) { 209 | try { 210 | const todo = AV.Object.createWithoutData('Todo', objectId); 211 | await todo.save({ content, done }); 212 | } catch (error) { 213 | displayError(error); 214 | } 215 | }, 216 | 217 | async removeTodoObject(objectId) { 218 | try { 219 | if (Array.isArray(objectId)) { 220 | const todos = objectId.map((id) => 221 | AV.Object.createWithoutData('Todo', id) 222 | ); 223 | await AV.Object.destroyAll(todos); 224 | } else { 225 | const todo = AV.Object.createWithoutData('Todo', objectId); 226 | await todo.destroy(); 227 | } 228 | } catch (error) { 229 | displayError(error); 230 | } 231 | }, 232 | }, 233 | 234 | // a custom directive to wait for the DOM to be updated 235 | // before focusing on the input field. 236 | // https://vuejs.org/guide/custom-directive.html 237 | directives: { 238 | 'todo-focus': function (el, value) { 239 | if (value) { 240 | el.focus(); 241 | } 242 | }, 243 | }, 244 | }); 245 | 246 | function displayError(error) { 247 | console.error(error); 248 | if (error instanceof Error) { 249 | if (error.error) { 250 | alert(error.error); // API Error 251 | } else { 252 | alert(error.message); 253 | } 254 | } 255 | } 256 | 257 | function onHashChange() { 258 | const visibility = window.location.hash.replace(/#\/?/, ''); 259 | if (filters[visibility]) { 260 | app.visibility = visibility; 261 | } else { 262 | app.visibility = 'all'; 263 | window.location.hash = ''; 264 | } 265 | } 266 | 267 | window.addEventListener('hashchange', onHashChange); 268 | 269 | // mount 270 | app.$mount('.todoapp'); 271 | --------------------------------------------------------------------------------