例子依然来自 Mdn Web Docs,加上了我本人的了解。长乐未央,长毋相忘。某种意义上算是 MDN web docs 中 Vue 教程的翻译,但又加上了本人的了解。
地址是: https://developer.mozilla.org…
进化: 编辑组件
当初咱们的组件依然不太完满,因为它不能编辑,输错了,就输错了。对此咱们的解决方案是引入一个编辑组件。还是固定的步骤,首先在 src/components 下建设一个文件,咱们将其命名为 ToDoItemEditForm.vue。而后将上面你的代码复制到这个文件中:
<template>
<form class="stack-small" @submit.prevent="onSubmit">
<div>
<label class="edit-label">Edit Name for "{{label}}"</label>
<input :id="id" type="text" autocomplete="off" v-model.lazy.trim="newLabel" />
</div>
<div class="btn-group">
<button type="button" class="btn" @click="onCancel">
勾销
<span class="visually-hidden"> 正在编辑 {{label}}</span>
</button>
<button type="submit" class="btn btn__primary">
保留
<span class="visually-hidden"> 对 {{label}} 进行批改 </span>
</button>
</div>
</form>
</template>
<script>
export default {
props: {
label: {
type: String,
required: true,
},
id: {
type: String,
required: true,
},
},
data() {
return {newLabel: this.label,};
},
methods: {onSubmit() {if (this.newLabel && this.newLabel !== this.label) {this.$emit("item-edited", this.newLabel);
}
},
onCancel() {this.$emit("edit-cancelled");
},
},
};
</script>
<style scoped>
.edit-label {
font-family: Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #0b0c0c;
display: block;
margin-bottom: 5px;
}
input {
display: inline-block;
margin-top: 0.4rem;
width: 100%;
min-height: 4.4rem;
padding: 0.4rem 0.8rem;
border: 2px solid #565656;
}
form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
form > * {flex: 0 0 100%;}
</style>
咱们大抵的解读一下这个文件,在这个文件外面咱们创立了一个表单,表单中的 input 用于编辑待办事项的名称,用 v -model 和 data 中的 newLabel 建设了双向绑定。同时咱们申明了这个组件接管的音讯(变量)prop。
在这个表单中有一个保留和勾销按钮。
- 点击保留按钮时,组件会通过 emit 收回 item-edited 事件。
- 点击勾销按钮时,组件会通过 emit 收回 edit-cancelled 事件
当初咱们来革新一下待办组件,为待办组件增加上编辑和删除性能。咱们的设计指标是在待办上面呈现一个编辑和删除按钮, 当点击编辑按钮的时候,待办组件暗藏,编辑组件呈现。这是一种互斥的关系,咱们须要用一个变量来示意这样的状态,咱们在 data 外面进行申明:
data () {
return {
isDone: this.done,
isEditing: false
}
}
那怎么通关 isEditing 来管制待办组件的显示和不显示呢,咱们能够应用 Vue 指令 if else 指令来实现。咱们为待办组件的 template 最外层再增加一个 div,同时也是为了管制款式,如果!isEditing = true 就代表以后不处于编辑状态。同时在待办列表上面增加一个编辑和删除按钮,为编辑按钮绑定处理事件,将 isEditing = true。像上面这样:
<template>
<div class="stack-small" v-if="!isEditing">
<div class="custom-checkbox">
<input type="checkbox" :id="id" :checked="isDone" class="checkbox" @change="$emit('todo-complete')"/>
<label :for="id" class="checkbox-label"> {{label}}</label>
</div>
<div class="btn-group">
<button type="button" class="btn" @click="toggleToItemEditForm">
编辑 <span class="visually-hidden">{{label}}</span>
</button>
<button type="button" class="btn btn__danger" @click="deleteToDo">
删除 <span class="visually-hidden">{{label}}</span>
</button>
</div>
</div>
<to-do-item-edit-form v-else :id="id" :label="label"></to-do-item-edit-form>
</template>
<script>
import ToDoItemEditForm from './ToDoItemEditForm.vue'
export default {
name: 'ToDoItem',
components: {ToDoItemEditForm},
props: {label: {required: true, type: String},
done: {default: false, type: Boolean},
id: {required: true, type: String}
},
data () {
return {
isDone: this.done,
isEditing: false
}
},
methods: {deleteToDo () {this.$emit('item-deleted')
},
toggleToItemEditForm () {this.isEditing = true}
}
}
</script>
<style scoped>
.custom-checkbox > .checkbox-label {
font-family: Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 400;
font-size: 16px;
font-size: 1rem;
line-height: 1.25;
color: #0b0c0c;
display: block;
margin-bottom: 5px;
}
.custom-checkbox > .checkbox {
font-family: Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 400;
font-size: 16px;
font-size: 1rem;
line-height: 1.25;
box-sizing: border-box;
width: 100%;
height: 40px;
height: 2.5rem;
margin-top: 0;
padding: 5px;
border: 2px solid #0b0c0c;
border-radius: 0;
appearance: none;
}
.custom-checkbox > input:focus {
outline: 3px dashed #fd0;
outline-offset: 0;
box-shadow: inset 0 0 0 2px;
}
.custom-checkbox {
font-family: Arial, sans-serif;
-webkit-font-smoothing: antialiased;
font-weight: 400;
font-size: 1.6rem;
line-height: 1.25;
display: block;
position: relative;
min-height: 40px;
margin-bottom: 10px;
padding-left: 40px;
clear: left;
}
.custom-checkbox > input[type="checkbox"] {
-webkit-font-smoothing: antialiased;
cursor: pointer;
position: absolute;
z-index: 1;
top: -2px;
left: -2px;
width: 44px;
height: 44px;
margin: 0;
opacity: 0;
}
.custom-checkbox > .checkbox-label {
font-size: inherit;
font-family: inherit;
line-height: inherit;
display: inline-block;
margin-bottom: 0;
padding: 8px 15px 5px;
cursor: pointer;
touch-action: manipulation;
}
.custom-checkbox > label::before {
content: "";
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
width: 40px;
height: 40px;
border: 2px solid currentcolor;
background: transparent;
}
.custom-checkbox > input[type="checkbox"]:focus + label::before {
border-width: 4px;
outline: 3px dashed #228bec;
}
.custom-checkbox > label::after {
box-sizing: content-box;
content: "";
position: absolute;
top: 11px;
left: 9px;
width: 18px;
height: 7px;
transform: rotate(-45deg);
border: solid;
border-width: 0 0 5px 5px;
border-top-color: transparent;
opacity: 0;
background: transparent;
}
.custom-checkbox > input[type="checkbox"]:checked + label::after {opacity: 1;}
@media only screen and (min-width: 40rem) {
label,
input,
.custom-checkbox {
font-size: 19px;
font-size: 1.9rem;
line-height: 1.31579;
}
}
</style>
当初当咱们的页面如下所示:
当点击了编辑之后:
但你会发现点击勾销不能返回,点击保留没反馈。起因在于咱们的编辑组件收回的事件没有被待办组件所理睬,当编辑待办组件收回点击勾销按钮事件,待办组件该当将编辑组件暗藏,也就是将 isEditing 置为 false。当编辑待办组件按钮收回点击保留按钮事件,咱们同样该当将编辑组件暗藏,所以咱们待办组件中的 template 的编辑待办模板标签变成了上面这样:
<to-do-item-edit-form v-else :id="id" :label="label" @item-edited="itemEdited" @edit-cancelled="editCancelled"></to-do-item-edit-form>
待办组件的 methods 多了两个办法 itemEdited 和 editCancelled:
itemEdited (newLabel) {this.$emit('item-edited', newLabel)
this.isEditing = false
},
editCancelled () {this.isEditing = false}
当初咱们点击勾销就能回到待办组件,然而点击保留依然没有反馈,起因在于咱们在待办组件外面收回的事件没有被 App 组件所解决,待办列表的数据在 App 外面。所以当初咱们就须要转到 App 组件外面,解决这个事件。首先在待办组件上申明解决此事件的办法:
<to-do-item :label="item.label" :done="item.done" :id="item.id" @todo-complete="updateToDoStatus(item.id)" @item-edited="editToDo(item.id,$event)" @item-deleted="deleteToDo(item.id)"></to-do-item>
$event 是一个非凡的 Vue 变量,用于携带子组件传递过去的数据。当初咱们须要在 methods 增加 editToDo 和 deleteToDo 办法:
editToDo (toDoId, newLabel) {const toDoEdit = this.ToDoItems.find((item) => item.id === toDoId)
toDoEdit.label = newLabel
},
deleteToDo (toDoId) {const deleteToDoIndex = this.ToDoItems.findIndex(item => item.id === toDoId)
this.ToDoItems.splice(deleteToDoIndex, 1)
}
一个小问题
到目前为止所有看起来都很好,然而如果用一下会发现还是会有一点小问题:
-
尝试选中待办事项的复选框
- 而后点击该待办事项的的编辑按钮
- 而后点击勾销
选中状态被失落了,待办事项的统计也出了问题,当你选中会发现统计数据跟你预期的相同,它变成了 0:
起因在于加载组件时,复选框的状态取决于 isDone,而 isDone 又取决于 done,这个 done 又由内部传入,所以当咱们选中一个初始状态为未选中的复选框而后点击编辑,又点击勾销,就相当于待办组件又从新被加载,失落了选中的状态。然而侥幸的是解决这个问题也比较简单,咱们能够将 isDone 转为一个计算属性,计算属性会保留扭转。在 Vue 的官网文档是这么介绍计算属性的:
计算属性是基于它们的响应式依赖进行缓存的。只在相干响应式依赖产生扭转时它们才会从新求值
这也就是咱们将其转换为计算属性的时,点击选中按钮,计算属性会被计算一次,咱们点击勾销返回时,因为计算属性的依赖并没有产生更新,所以咱们的选中状态得以保留。你看在实践中咱们对 Vue 的一些了解更加深刻了。最后我对计算属性的了解是相比于在插值表达式中写逻辑判断,可维护性更高,就让 template 外面只负责显示,另一个方面是计算属性只有在相干响应式依赖产生扭转才会从新求值,这对于一些大型页面来说,如果咱们将简单的逻辑判断写在插值表达式外面,每次渲染都要再执行一遍运算,这会相当损耗性能。当初咱们能够借助计算属性来保留状态。
首先咱们将待办组件的 data 中勾销上面这一行:
isDone: this.done,
而后在待办组件的计算属性如下申明:
computed: {isDone () {return this.done}
},
当初你再保留并从新加载,就会发现问题曾经解决。是不是很有成就感了呢!
自定义事件与原生事件
在这个例子中让咱们感到有点不清晰的,恐怕也就是自定义事件和原生事件了吧。上面这个流程图会让你对事件流转更加清晰:
ref 与焦点治理
咱们简直实现了一个小型的利用,但目前认真扫视的话,它的体验依然有些美中不足。比方咱们只用键盘来实现下面的操作。咱们能够借助 tabs 这个键盘上的按钮,来实现编辑、保留待办。让咱们从新加载页面,而后按 tab 键,你会发现待办的输入框上会有一个蓝色框框,代表咱们当初处于输入框,向上面这样:
这个蓝色的框框咱们权且称之为焦点(focus),再次按下 tab 键,这个焦点会被挪动到点击增加按钮上。再次按 tab 键,焦点会呈现在第一个待办的复选框上。接着按,它会停留在第一个待办的编辑按钮上。而后按下 Enter 键,而后编辑按钮隐没,呈现的是咱们的编辑待办组件,咱们的焦点也隐没了。这样的交互可能让用户的体验没有那么良好。当你再次按下 tab 键,焦点呈现在哪里,这取决于你应用的浏览器。同样的,如果你按 table 让焦点再次出现,按下保留或勾销编辑,焦点会再次隐没。
为了给用户更好的体验,咱们将增加代码来管制焦点,以便在编辑表单呈现的时候,让焦点呈现在编辑表单的输入框上。当用户在编辑表单中勾销编辑或保留编辑,让焦点从新回到编辑按钮。为了做到这一点,咱们都须要对 Vue 如何工作有更加深刻一点的了解。
Virtual DOM(虚构 DOM) and refs
Vue 和支流的前端框架一样,抉择应用虚构 DOM 来治理结点,这意味着 Vue 在内存中保留了应用程序所有的结点代表(原文为 representation),这意味着任何更新都会先达到内存中的结点。而后再对页面理论结点所需的更新进行批量同步。
间接读写实在 DOM 的结点绝对于虚构结点来说是有些低廉的,虚构结点会有更好的性能。而后这也意味着在框架外面你不能够通过浏览器的原生 APIs 来在操纵 HTML 元素(向 Document.getElementById),这会导致虚构 DOM 和实在 DOM 同步呈现问题(失去同步 原文为 going out of sync)。
然而如果你的确是须要操纵实在 DOM 的结点(像设置焦点),你能够抉择应用 Vue ref。对于自定义的组件,你能够抉择应用 refs 间接拜访子组件的内部结构,然而留神,要小心应用,这会让你的代码看起来难以了解。
如果你想在组件中应用 ref,你须要在想要拜访的元素上增加 ref 属性,并未该属性的值提供字符串标识符。留神在一个组件中 ref 必须是惟一的。
在待办组件外面增加 ref
首先咱们对 ToDoItem.vue 进行革新,在编辑按钮上为它增加 ref:
<button type="button" class="btn" @click="toggleToItemEditForm" ref="editButton">
编辑 <span class="visually-hidden">{{label}}</span>
</button>
而后咱们就能够拿到这个结点了,让咱们在 toggleToItemEditForm 里尝试获取一下 ref:
toggleToItemEditForm () {console.log(this.$refs.editButton)
this.isEditing = true
}
点击编辑按钮你就会在控制台发现输入了 ref 所在按钮的结点。
nextTick 办法
当用户保留或勾销他们的编辑时,咱们心愿焦点回到编辑按钮上。所以咱们要对 ToDoItem 组件中的 itemEdited 和 editCancelled 中的办法进行革新。咱们创立一个不带参数的办法,在这个办法中咱们获取下面的 ref,ref 目前处于编辑按钮上,咱们拿到这个按钮而后设置焦点即可。
focusOnEditButton () {
const editButtonRef = this.$refs.editButton
editButtonRef.focus()}
而后在 itemEdited 和 editCancelled 调用即可:
itemEdited (newLabel) {this.$emit('item-edited', newLabel)
this.isEditing = false
this.focusOnEditButton();},
editCancelled () {
this.isEditing = false
this.focusOnEditButton();},
然而即便做了革新,你尝试保留 / 勾销待办也会发现,焦点没有按咱们料想的回到编辑按钮上,同时在控制台也会看到上面的报错:
[Vue warn]: Error in v-on handler: "TypeError: editButtonRef is undefined"
found in
---> <ToDoItemEditForm> at src/components/ToDoItemEditForm.vue
<ToDoItem> at src/components/ToDoItem.vue
<App> at src/App.vue
<Root> vue.esm.js:5105
TypeError: editButtonRef is undefined
focusOnEditButton ToDoItem.vue:60
editCancelled ToDoItem.vue:56
VueJS 4
onCancel ToDoItemEditForm.vue:43
VueJS 38
toggleToItemEditForm ToDoItem.vue:47
VueJS 21
当咱们点击编辑按钮的时候,咱们还能拿到这个 ref,为什么点击勾销和保留就拿不到了呢?起因在于当咱们点击编辑按钮,此时将 isEditing 设置 true,咱们将不再渲染组件的编辑按钮,这也就意味着 ref 援用不到按钮,因而咱们无奈取得编辑按钮。
然而这也仿佛有些说不通,在咱们拜访 ref 之前,咱们不是将 isEditing 设置为 false 了吗?那按钮不就应该显示了吗?或者说渲染了吗?那为什么咱们取不到呢?这也就是虚构 DOM 发挥作用的中央,Vue 试图优化批量更改,所以在 DOM 上的更新可能不会立马更新,它会放在一个队列外面,因而当咱们调用 focusOnEditButton 时,编辑按钮尚未渲染。咱们须要等到下一个 DOM 的更新周期之后,ref 能力获取到按钮。
那有没有什么方法能让在 DOM 更新之后再调用 focusOnEditButton 办法呢,Vue 提供了一个名为 $nextTick 的办法,该办法接管一个回调函数,回调函数会在 DOM 更新之后被调用,所以咱们的 focusOnEditButton 就变成了上面这样:
focusOnEditButton () {this.$nextTick(()=>{
const editButtonRef = this.$refs.editButton
editButtonRef.focus()})
}
当初咱们咱们按编辑进入编辑表单,在编辑表单点击勾销或返回会发现编辑按钮上就呈现了焦点。
Vue 的生命周期
Vue 的生命周期是一个相当重要的概念,咱们在后面的文章都回避了他,起因在于这个概念配合具体了例子,会让了解更加清晰。当初咱们还没有实现的事件是,在咱们点击编辑按钮的时候,将焦点挪动到表单的输入框,然而编辑按钮位于待办组件,编辑待办的输入框位于编辑组件。所以咱们不能在编辑按钮的点击事件中设置焦点。咱们能够借助点击编辑按钮时都会将 ToDoItemEditForm 组件从新挂载来解决这个问题。
那到底该如何动手呢?Vue 的组件会经验一系列阶段,咱们称之为生命周期。这个生命周期从元素被创立增加到 VDOM 开始,始终到它们被虚构 DOM 中被移除完结。
在每个阶段 Vue 都会有触发的办法,这对于数据获取之类的事件会很有用,比方你须要在组件渲染之前或属性之后获取数据。每个阶段调用的办法如下,按触发程序进行排列:
- beforeCreate: 在实例刚在被创立,且未初始化实现时调用。
- created: 在实例创立实现后调用。此时实例已实现初始化,然而还没有挂载。
- beforeMount: 在挂载开始之前被调用:相干的 render 函数首次被调用。
- mounted: 在组件挂载之后调用。此时能够拜访实例上的属性和办法。
- beforeUpdate: 在数据更新之前调用。
- updated: 在数据更新之后调用。此时能够拜访更新后的 DOM。
- beforeDestroy: 在实例销毁之前调用。
- destroyed: 在实例销毁之后调用
当初让咱们为 ToDoItemEditForm 的输入框增加一个 ref,如下所示:
<input :id="id" type="text" autocomplete="off" v-model.lazy.trim="newLabel" ref="labelInput" />
接下来让咱们再导出的 script 对象中增加一个属性 mounted,留神这个属性和计算属性平级,咱们在 mounted 中获取输入框的 ref。
mounted () {
const labelInputRef = this.$refs.labelInput
labelInputRef.focus()}
留神咱们这里并没有应用 nextTick,起因在于在 Vue 的申明周期外面 mounted 被调用的时候,组件曾经被挂载,咱们能够拜访属性和办法了。
删除的焦点挪动
目前还没有思考的一个问题是,删除的时候焦点应该挪动到哪里,我想此时用户该当关注的是统计信息,也就是还有多少待办。让咱们把眼帘转移到 App.vue 中的统计信息。咱们还是为统计信息增加上 ref。
<h2 id="list-summary" ref="listSummary" tabindex="-1">{{listSummary}}</h2>
当初咱们曾经取得了统计信息的 ref,咱们就能够在删除待办的时候,将焦点挪动到这下面:
deleteToDo (toDoId) {const deleteToDoIndex = this.ToDoItems.findIndex(item => item.id === toDoId)
this.ToDoItems.splice(deleteToDoIndex, 1)
this.$refs.listSummary.focus()}
当初当你删除一个待办,焦点将会被挪动调待办的统计信息上。
总结一下
通过这个例子咱们将 (一) (二) (三) 的概念串联了起来,在实践中领会实践最为粗浅。依照布局来说,自身打算三篇介绍完 Vue 的相干理念,然而前面一边学一边发现,一些理念还是在实践中领会比拟深。感激 MDN Web Docs 提供的代码示例,十分具体。