App.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. <script setup vapor>
  2. import {
  3. reactive,
  4. computed,
  5. onMounted,
  6. onUnmounted,
  7. watchPostEffect,
  8. } from 'vue'
  9. const STORAGE_KEY = 'todos-vuejs-3.x'
  10. const todoStorage = {
  11. fetch() {
  12. const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
  13. todos.forEach((todo, index) => {
  14. todo.id = index
  15. })
  16. todoStorage.uid = todos.length
  17. return todos
  18. },
  19. save(todos) {
  20. localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
  21. },
  22. }
  23. const filters = {
  24. all(todos) {
  25. return todos
  26. },
  27. active(todos) {
  28. return todos.filter(todo => {
  29. return !todo.completed
  30. })
  31. },
  32. completed(todos) {
  33. return todos.filter(function (todo) {
  34. return todo.completed
  35. })
  36. },
  37. }
  38. function pluralize(n) {
  39. return n === 1 ? 'item' : 'items'
  40. }
  41. const state = reactive({
  42. todos: todoStorage.fetch(),
  43. editedTodo: null,
  44. newTodo: '',
  45. beforeEditCache: '',
  46. visibility: 'all',
  47. remaining: computed(() => {
  48. return filters.active(state.todos).length
  49. }),
  50. remainingText: computed(() => {
  51. return ` ${pluralize(state.remaining)} left`
  52. }),
  53. filteredTodos: computed(() => {
  54. return filters[state.visibility](state.todos)
  55. }),
  56. allDone: computed({
  57. get: function () {
  58. return state.remaining === 0
  59. },
  60. set: function (value) {
  61. state.todos.forEach(todo => {
  62. todo.completed = value
  63. })
  64. },
  65. }),
  66. })
  67. watchPostEffect(() => {
  68. todoStorage.save(state.todos)
  69. })
  70. onMounted(() => {
  71. window.addEventListener('hashchange', onHashChange)
  72. onHashChange()
  73. })
  74. onUnmounted(() => {
  75. window.removeEventListener('hashchange', onHashChange)
  76. })
  77. function onHashChange() {
  78. const visibility = window.location.hash.replace(/#\/?/, '')
  79. if (filters[visibility]) {
  80. state.visibility = visibility
  81. } else {
  82. window.location.hash = ''
  83. state.visibility = 'all'
  84. }
  85. }
  86. function addTodo() {
  87. const value = state.newTodo && state.newTodo.trim()
  88. if (!value) {
  89. return
  90. }
  91. state.todos.push({
  92. id: todoStorage.uid++,
  93. title: value,
  94. completed: false,
  95. })
  96. state.newTodo = ''
  97. }
  98. function removeTodo(todo) {
  99. state.todos.splice(state.todos.indexOf(todo), 1)
  100. }
  101. function editTodo(todo) {
  102. state.beforeEditCache = todo.title
  103. state.editedTodo = todo
  104. }
  105. function doneEdit(todo) {
  106. if (!state.editedTodo) {
  107. return
  108. }
  109. state.editedTodo = null
  110. todo.title = todo.title.trim()
  111. if (!todo.title) {
  112. removeTodo(todo)
  113. }
  114. }
  115. function cancelEdit(todo) {
  116. state.editedTodo = null
  117. todo.title = state.beforeEditCache
  118. }
  119. function removeCompleted() {
  120. state.todos = filters.active(state.todos)
  121. }
  122. // vapor custom directive
  123. const vTodoFocus = (el, value) => {
  124. watchPostEffect(() => value() && el.focus())
  125. }
  126. </script>
  127. <template>
  128. <section class="todoapp">
  129. <header class="header">
  130. <h1>todos</h1>
  131. <input
  132. class="new-todo"
  133. autofocus
  134. autocomplete="off"
  135. placeholder="What needs to be done?"
  136. v-model="state.newTodo"
  137. @keyup.enter="addTodo"
  138. />
  139. </header>
  140. <section class="main" v-show="state.todos.length">
  141. <input
  142. id="toggle-all"
  143. class="toggle-all"
  144. type="checkbox"
  145. v-model="state.allDone"
  146. />
  147. <label for="toggle-all">Mark all as complete</label>
  148. <ul class="todo-list">
  149. <li
  150. v-for="todo in state.filteredTodos"
  151. class="todo"
  152. :key="todo.id"
  153. :class="{
  154. completed: todo.completed,
  155. editing: todo === state.editedTodo,
  156. }"
  157. >
  158. <div class="view">
  159. <input class="toggle" type="checkbox" v-model="todo.completed" />
  160. <label @dblclick="editTodo(todo)">{{ todo.title }}</label>
  161. <button class="destroy" @click="removeTodo(todo)"></button>
  162. </div>
  163. <input
  164. class="edit"
  165. type="text"
  166. v-model="todo.title"
  167. v-todo-focus="todo === state.editedTodo"
  168. @blur="doneEdit(todo)"
  169. @keyup.enter="doneEdit(todo)"
  170. @keyup.escape="cancelEdit(todo)"
  171. />
  172. </li>
  173. </ul>
  174. </section>
  175. <footer class="footer" v-show="state.todos.length">
  176. <span class="todo-count">
  177. <strong>{{ state.remaining }}</strong>
  178. <span>{{ state.remainingText }}</span>
  179. </span>
  180. <ul class="filters">
  181. <li>
  182. <a href="#/all" :class="{ selected: state.visibility === 'all' }"
  183. >All</a
  184. >
  185. </li>
  186. <li>
  187. <a
  188. href="#/active"
  189. :class="{ selected: state.visibility === 'active' }"
  190. >Active</a
  191. >
  192. </li>
  193. <li>
  194. <a
  195. href="#/completed"
  196. :class="{ selected: state.visibility === 'completed' }"
  197. >Completed</a
  198. >
  199. </li>
  200. </ul>
  201. <button
  202. class="clear-completed"
  203. @click="removeCompleted"
  204. v-show="state.todos.length > state.remaining"
  205. >
  206. Clear completed
  207. </button>
  208. </footer>
  209. </section>
  210. </template>