Vue imitation todo super detailed explanation (with source code)

Posted by BobcatM on Sun, 30 Jan 2022 10:18:43 +0100

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 analysisKey technical instructions used
Input contentv-model
Press enter to add@keyup.enter
Execute add logicmethods of vue instance
Render to pagev-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;
  }
}

4. References

https://cn.vuejs.org/
https://wscgm.csb.app/

Topics: Vue