1, todo basic DOM structure
The code is as follows:
<!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title></title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <meta name="robots" content="noindex, nofollow" /> <meta name="googlebot" content="noindex, nofollow" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" type="text/css" href="./css/index.css" /> <style id="compiled-css" type="text/css"> /* Combined with v-cloak */ [v-cloak] { display: none; } </style> </head> <body> <section class="todoapp"> <header class="header"> <h1>RoddyLD</h1> <input autofocus="autofocus" autocomplete="off" placeholder="Please enter a task" class="new-todo" /> </header> <section class="main"> <input type="checkbox" class="toggle-all" /> <label for="toggle-all"></label> <ul class="todo-list"> <h2>Template</h2> <li class="todo"> <div class="view"> <input type="checkbox" class="toggle" /> <label>having dinner</label> <button class="destroy"></button> </div> <input type="text" class="edit" /> </li> <li class="todo completed"> <div class="view"> <input type="checkbox" class="toggle" /> <label>Sleep sleep</label> <button class="destroy"></button> </div> <input type="text" class="edit" /> </li> <li class="todo editing"> <div class="view"> <input type="checkbox" class="toggle" /> <label>Beat beans</label> <button class="destroy"></button> </div> <input type="text" class="edit" /> </li> </ul> </section> <footer class="footer"> <span class="todo-count"><strong>2</strong> items left </span> <ul class="filters"> <li><a href="#/all">All</a></li> <li><a href="#/active" class="">Active</a></li> <li><a href="#/completed" class="">Completed</a></li> </ul> <button class="clear-completed" style="display: none;"> Clear completed </button> </footer> </section> <footer class="info"> <p>Double click to enter editing</p> <p>thank <a href="http://evanyou.me">Evan You</a></p> <p>thank <a href="http://todomvc.com"></a></p> </footer> </body> </html> <!-- Development environment version with helpful command line warnings --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
The browser runs as follows:
2, todo functional requirements analysis
1. Add task
requirement analysis | Key technical instructions used |
---|---|
Input content | v-model |
Press enter to add | @keyup.enter |
Execute add logic | methods of vue instance |
Render to page | v-for |
The new code is as follows:
<script> const app = new Vue({ el: ".todoapp", data: { // Value of bidirectional data binding todo: "", // Event array todoList: [], }, methods: { // New task addTodo() { // Non null judgment if (this.todo == "") { alert("The input is empty, please re-enter!"); return; } // Normal logic added this.todoList.push({ msg: this.todo, isCompleted: false }) console.log(this.todoList); // Clear the input box this.todo = ""; } }, }) </script>
The part where the dom structure changes has been circled red
Note: the following two-way data binding uses v-model Trim, not v-model; Use v-model Trim can remove the leading and trailing spaces in the input box
The code of dom structure change is as follows:
<header class="header"> <h1>RoddyLD</h1> <input autofocus="autofocus" autocomplete="off" placeholder="Please enter a task" @keyup.enter="addTodo" v-model.trim="todo" class="new-todo" /> </header>
<li class="todo" v-for="(item,index) in todoList"> <div class="view"> <input type="checkbox" class="toggle" /><label>{{item.msg}}</label> <button class="destroy"></button> </div> <input type="text" class="edit" /> </li>
The new function code has been completed. Let's take a look at the final result of the browser (you can find that it has been added successfully)
2. Click to change to completion status
Demand analysis:
- Click the checkbox to synchronize the value of completed in each item
- The completed class on the li element is controlled by the completed value
- V-bind: class = {completed: iscompleted} dynamically add or delete this class
Add the following two lines of code:
:class = "{completed:item.isCompleted}" v-model="item.isCompleted"
The part where the dom structure changes has been circled red
Click to change to complete status. It has been completed. Let's take a look at the final result of the browser (it can be found that it has been realized successfully)
3. Click delete
Demand analysis:
click × Delete using index
@click="del(index)"
Click to delete the code as follows:
// Delete task del(index){ this.todoList.splice(index, 1) }
The part where the dom structure changes has been circled red
4. Double click to enter editing, modification and saving
Demand analysis:
Double click the corresponding class name of li editing
:class = "{completed:item.isCompleted,editing:editTodo==item}"
After double clicking, assign the current item item to editTodo. The variables editTodo and item are the same
Double click to enter the editing state and display the current text in the text input box (item.msg)
Modify the value in the label label, bidirectional data binding v-model
Press the enter key or lose the focus, save and restore the changes to the default state (active) (move out of the editing class)
editTodo = undefined
The key codes are as follows:
// Register a global custom instruction ` v-focus` Vue.directive('focus', { // Execute custom logic when the element changes update: function (el) { console.log("trigger"); // Focus element el.focus() } })
The newly added code of dom structure is as follows:
:class="{completed:item.isCompleted,editing:editTodo==item}" @dblclick="editTodo=item" v-focus v-model="item.msg" @keyup.13="editTodo=undefined" @blur="editTodo=undefined"
The changed part of the code has been circled red
be careful:
The custom instruction must be written before the Vue instantiation is created, and the name cannot have uppercase letters
The reason for adding the user-defined instruction focus is that double clicking li can't focus. You need to click again to find the focus. Adding the user-defined instruction focus can solve this problem!
5. Status filtering at the bottom
Demand analysis:
Click the filter button at the bottom to switch to the selected class
Add another field in data to indicate the current status
The key codes are as follows:
computed: { // Filter out all items that match the current status filterTodoList() { // Judgment state if (this.filter == "All") { return this.todoList; } else if (this.filter == "Active") { return this.todoList.filter(ele => { return !ele.isCompleted; }) } else { return this.todoList.filter(ele => { return ele.isCompleted; }) } } },
The changed part of the code has been circled red
The browser runs as follows:
All tasks
Unfinished task
Task completed
6. Add ": key" in Li
Why add ": key". If you don't add: key, when you switch to Active and then select it, the normal logic should be that the item disappears in Active, but the actual effect is that the selected item does disappear, but the selected "√" will be rendered to the next item li! (as shown in the figure below)
"√" cannot appear in Active, so we want to add ": key" to enable vue to distinguish them. Otherwise, vue will only replace its internal attributes without triggering the transition effect.
Solution 1: use timestamp as ": key" value
We know that the timestamp is the number of seconds elapsed since January 1, 1970 (midnight of UTC/GMT), so it cannot be repeated, so we can use the timestamp to solve this problem. (as shown in the figure below: add timestamp)
However, we will find that it will report some wrong information and affect other code effects. For example, double clicking the focus effect will fail, so this method is not recommended!
Liberation method 2: add one more id field when adding
7. Calculate unfinished tasks
The core code is as follows:
// Calculate outstanding tasks activeNum() { if(this.todoList){ const activeList = this.todoList.filter(ele => { if (ele.isCompleted == false) { return true; } }) return activeList.length; } },
The changed part of the code has been circled red
Let's comment out some unimportant code first
Display the unfinished quantity when it is not 1, and add "s" when it is greater than 1
Calculation of incomplete key js code
The browser runs as follows
When there is only 1 unfinished task, s is not added
Add s when there are more than 1 unfinished tasks
8. Calculate select all and reverse selection
The core code is as follows
// Calculate select all and invert selection isCheckAll: { get() { // Number of selected items filtered out if (this.todoList) { const checkedNum = this.todoList.filter(v => { return v.isCompleted; }).length; // Calculate the total number of tasks const totalNum = this.todoList.length; // Return to select all and invert selection status return checkedNum == totalNum; } }, set(value) { console.log(value); // Set the selection status of all items to be consistent with the top check box this.todoList.forEach(ele => { ele.isCompleted = value; }) } }
The changed part of the code has been circled red
The browser runs as follows:
Click all to select
Then click all to deselect
9. Clear all completed tasks
The core code is as follows:
The changed part of the code has been circled red
10. Use browser cache to get data
The changed part of the code has been circled red
The browser runs as follows:
You can get data from the cache
3, All source codes are as follows
1. Code file structure
2.html all codes
<!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title></title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <meta name="robots" content="noindex, nofollow" /> <meta name="googlebot" content="noindex, nofollow" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" type="text/css" href="./css/index.css" /> <style id="compiled-css" type="text/css"> /* Combined with v-cloak */ [v-cloak] { display: none; } </style> </head> <body> <section class="todoapp"> <header class="header"> <h1>RoddyLD</h1> <input autofocus="autofocus" autocomplete="off" placeholder="Please enter a task" @keyup.enter="addTodo" v-model.trim="todo" class="new-todo" /> </header> <section class="main"> <input type="checkbox" class="toggle-all" id="toggle-all" v-model="isCheckAll" /> <label for="toggle-all"></label> <ul class="todo-list"> <li class="todo" :class="{completed:item.isCompleted,editing:editTodo==item}" @dblclick="editTodo=item" v-for="(item,index) in filterTodoList" :key="item.id"> <div class="view"> <input type="checkbox" class="toggle" v-model="item.isCompleted" /><label>{{item.msg}}</label> <button class="destroy" @click="del(index)"></button> </div> <input type="text" class="edit" v-focus v-model="item.msg" @keyup.13="editTodo=undefined" @blur="editTodo=undefined" /> </li> </ul> </section> <footer class="footer"> <span class="todo-count" v-show="activeNum!=0"><strong>{{activeNum}}</strong> item<span v-show="activeNum>1">s</span> left </span> <ul class="filters"> <li><a href="#/all" @click="filter='All'" :class="{selected:filter=='All'}">All</a></li> <li><a href="#/active" @click="filter='Active'" :class="{selected:filter=='Active'}">Active</a></li> <li><a href="#/completed" @click="filter='Completed'" :class="{selected:filter=='Completed'}">Completed</a></li> </ul> <button class="clear-completed" v-show="completedNum>0" @click="clearAll"> Clear completed </button> </footer> </section> <footer class="info"> <p>Double click to enter editing</p> <p>thank <a href="http://evanyou.me">Evan You</a></p> <p>thank <a href="http://todomvc.com"></a></p> </footer> </body> </html> <!-- Development environment version with helpful command line warnings --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> // Register a global custom instruction ` v-focus` Vue.directive('focus', { // Execute custom logic when the element changes update: function (el) { console.log("trigger"); // Focus element el.focus() } }) const app = new Vue({ el: ".todoapp", data: { // Value of bidirectional data binding todo: "", // Event array // todoList: [], todoList: JSON.parse(localStorage.getItem("todo")), // Edit this row of data editTodo: undefined, // Current filter status filter: "All", // Value of initialization id id: 1 }, methods: { // New task addTodo() { // Non null judgment // if (this.todo == "") { // alert("the input is empty, please re-enter!"); // return; // } if (!this.todoList) { this.todoList = [] } // Normal logic added this.todoList.push({ msg: this.todo, isCompleted: false, id: this.id, }) this.id++ console.log(this.todoList); // Clear the input box this.todo = ""; localStorage.setItem("todo", JSON.stringify(this.todoList)) }, // Delete task del(index) { this.todoList.splice(index, 1) }, // Delete completed tasks clearAll() { console.log(this.todoList); for (let i = 0; i < this.todoList.length; i++) { if (this.todoList[i].isCompleted == true) { this.todoList.splice(i, 1); i--; } } } }, // Calculation properties computed: { // Filter out all items that match the current status filterTodoList() { // Judgment state if (this.filter == "All") { return this.todoList; } else if (this.filter == "Active") { return this.todoList.filter(ele => { return !ele.isCompleted; }) } else { return this.todoList.filter(ele => { return ele.isCompleted; }) } }, // Calculate outstanding tasks activeNum() { if (this.todoList) { const activeList = this.todoList.filter(ele => { if (ele.isCompleted == false) { return true; } }) return activeList.length; } }, // Calculate completed tasks completedNum() { if (this.todoList) { const completedList = this.todoList.filter(ele => { if (ele.isCompleted == true) { return true; } }) return completedList.length; } }, // Calculate select all and invert selection isCheckAll: { get() { // Number of selected items filtered out if (this.todoList) { const checkedNum = this.todoList.filter(v => { return v.isCompleted; }).length; // Calculate the total number of tasks const totalNum = this.todoList.length; // Return to select all and invert selection status return checkedNum == totalNum; } }, set(value) { console.log(value); // Set the selection status of all items to be consistent with the top check box this.todoList.forEach(ele => { ele.isCompleted = value; }) } } }, }) </script>
3.css all codes
html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #4d4d4d; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 300; } :focus { outline: 0; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp h1 { position: absolute; top: -155px; width: 100%; font-size: 100px; font-weight: 100; text-align: center; color: rgba(175, 47, 47, 0.15); -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; border: 0; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } .toggle-all { width: 1px; height: 1px; border: none; /* Mobile Safari */ opacity: 0; position: absolute; right: 100%; bottom: 100%; } .toggle-all + label { width: 60px; height: 34px; font-size: 0; position: absolute; top: -52px; left: -13px; -webkit-transform: rotate(90deg); transform: rotate(90deg); } .toggle-all + label:before { content: '❯'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked + label:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: calc(100% - 43px); padding: 12px 16px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle { opacity: 0; } .todo-list li .toggle + label { /* Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ */ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: center left; } .todo-list li .toggle:checked + label { background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); } .todo-list li label { word-break: break-all; padding: 15px 15px 15px 60px; display: block; line-height: 1.2; transition: color 0.4s; } .todo-list li.completed label { color: #d9d9d9; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { color: #777; padding: 10px 15px; height: 20px; text-align: center; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; 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); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } /* completed */ .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #bfbfbf; font-size: 10px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio: 0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } }