VueJS2 高级教程(六)
原文:Pro Vue.js 2
十四、处理事件
在这一章中,我继续描述内置的 Vue.js 指令,重点放在用于事件处理的v-on指令,以及表 14-1 放在上下文中的指令。
表 14-1
将 v-on 指令放在上下文中
|问题
|
回答
|
| --- | --- |
| 这是什么? | v-on指令用于监听和响应事件。 |
| 为什么有用? | 该指令使访问组件数据或在响应事件时调用方法变得容易,并使事件处理成为 Vue.js 开发的一个集成部分。 |
| 如何使用? | v-on指令应用于您感兴趣的事件的 HTML 元素,当您指定的事件被触发时,它的表达式被求值。 |
| 有什么陷阱或限制吗? | 正如在“管理事件传播”一节中所描述的,只要您在应用指令时牢记 DOM 事件传播模型,v-on指令就能一致地工作,并且通常很容易使用。 |
| 有其他选择吗? | 如果你对表单元素触发的事件感兴趣,那么在第十五章中描述的v-model指令可能更合适。 |
表 14-2 总结了本章内容。
表 14-2
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 处理由元素发出的事件 | 使用v-on指令 | 3, 7 |
| 获取事件的详细信息 | 使用事件对象 | four |
| 响应指令表达式之外的事件 | 使用方法处理事件,并接收事件对象作为参数 | 5, 6 |
| 处理来自同一元素的多个事件 | 对您想要接收的每个事件应用v-on指令,或者使用事件对象检测事件类型 | 8, 9 |
| 管理事件传播 | 使用事件传播修饰符 | 10–14 |
| 基于按键或鼠标活动过滤事件 | 使用鼠标和键盘修饰符 | 15–17 |
为本章做准备
在这一章中,我继续使用第十四章中的templatesanddata项目。为了准备本章,我简化了应用的根组件,如清单 14-1 所示。
小费
你可以从 https://github.com/Apress/pro-vue-js-2 下载本章以及本书其他章节的示例项目。
<template>
<div class="container-fluid">
<div class="bg-primary text-white m-2 p-3 text-center">
<h3>{{ name }}</h3>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "Lifejacket"
}
}
}
</script>
Listing 14-1Simplifying the Content of the App.vue File in the src Folder
保存对App.vue文件的修改,运行templatesanddata文件夹中清单 14-2 所示的命令,启动 Vue.js 开发工具。
npm run serve
Listing 14-2Starting the Development Tools
打开一个新的浏览器窗口并导航至http://localhost:8080以查看如图 14-1 所示的内容。
图 14-1
运行示例应用
处理事件
Vue.js 提供了v-on指令,用于为事件创建绑定。用户与 HTML 元素交互的结果会触发事件。所有元素都支持一组核心事件,这些事件由特定于特定元素所特有的特性的事件来补充。在清单 14-3 中,我使用了v-on指令来告诉 Vue.js 当用户点击组件模板中的h3元素时我希望它如何响应。
<template>
<div class="container-fluid">
<div class="bg-primary text-white m-2 p-3 text-center">
<h3 v-on:click="name = 'Clicked!'">{{ name }}</h3>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "Lifejacket"
}
}
}
</script>
Listing 14-3Handling an Event in the App.vue File in the src Folder
v-on指令的应用遵循前面章节建立的模式,我在图 14-2 中对其进行了分解。
图 14-2
v-on 指令的剖析
指令的名称后跟一个冒号,然后是一个指定事件名称的参数。当事件被触发时,表达式将被调用,本例中的表达式是一段 JavaScript 代码,它更改了name属性的值。要查看结果,保存更改并在浏览器窗口中点击h3元素的内容,产生如图 14-3 所示的效果。
图 14-3
处理事件
单击元素触发click事件,Vue.js 通过评估指令的表达式来响应。只有第一次单击时才会有明显的变化,您必须重新加载浏览器才能将应用重置为其原始状态。
了解事件和事件对象
有许多不同类型的事件可用,我在本章中使用的事件在表 14-3 中描述。
注意
参见 https://developer.mozilla.org/en-US/docs/Web/Events 了解所有可用事件的详细信息。
表 14-3
本章中使用的事件
|事件
|
描述
|
| --- | --- |
| click | 当在元素边界内按下并释放鼠标按钮时,会触发此事件。 |
| mousedown | 当在元素边界内按下鼠标按钮时,会触发此事件。 |
| mousemove | 当鼠标指针在元素边界内移动时,会触发事件。 |
| mouseleave | 当鼠标指针离开元素边界时,会触发此事件。 |
| keydown | 按下按键时会触发此事件。 |
当浏览器触发事件时,它们产生一个描述事件的对象,称为事件对象。事件对象定义了提供事件信息的属性和方法,可用于控制事件的处理方式。在表 14-4 中,我描述了对 Vue.js 开发最有用的事件对象属性。(如果您熟悉 web 开发,您可能想知道由事件对象定义的方法和其他属性。正如您将了解到的,您不需要将它们直接与v-on指令一起使用,它会处理大量的事件处理细节。)
表 14-4
有用的事件对象属性
|财产
|
描述
|
| --- | --- |
| target | 该属性返回表示触发事件的 HTML 元素的 DOM 对象 |
| currentTarget | 该属性返回表示处理事件的 HTML 元素的 DOM 对象。与target属性的区别在“管理事件传播”一节中解释。 |
| type | 此属性返回事件类型。 |
| key | 对于键盘事件,此属性返回与事件相关的键。 |
v-on指令通过名为$event的变量使事件对象可用。在清单 14-4 中,我已经更新了指令的表达式,以便在事件被触发时显示type属性的值。
<template>
<div class="container-fluid">
<div class="bg-primary text-white m-2 p-3 text-center">
<h3 v-on:click="name = $event.type">{{ name }}</h3>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "Lifejacket"
}
}
}
</script>
Listing 14-4Using an Event Object in the App.vue File in the src Folder
v-on指令通过点击h3元素来处理click事件原因,并在表达式求值前将浏览器创建的事件对象赋给$event变量,产生如图 14-4 所示的结果。
图 14-4
使用事件对象
使用方法处理事件
正如前面的例子所展示的,当事件被触发时,v-on指令将评估 JavaScript 的片段,但是更常见的方法是调用方法。使用方法可以最大限度地减少模板中的代码量,并允许一致地处理事件。在清单 14-5 中,我为组件添加了一个方法,并更新了v-on指令的绑定,这样当h3元素的click事件被触发时,该方法将被调用。
<template>
<div class="container-fluid">
<div class="bg-primary text-white m-2 p-3 text-center">
<h3 v-on:click="handleEvent">{{ name }}</h3>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "Lifejacket"
}
},
methods: {
handleEvent($event) {
this.name = $event.type;
}
}
}
</script>
Listing 14-5Using a Method in the App.vue File in the src Folder
我已经将该指令的表达式设置为handleEvent,它告诉v-on在click事件被触发时调用该方法并向其传递$event对象,这产生了如图 14-5 所示的结果。(您不必使用$event作为方法参数的名称,但我倾向于使用,因为它使参数的目的很明显。)
小费
Vue.js 通常对事件触发时调用的方法名称不太严格,但是如果您使用的方法名称也是内置的 JavaScript 关键字,比如delete,您将会收到一个错误。
只指定一个方法名是有用的,但是使用方法的真正好处是当您有多个相同事件类型的源产生不同的结果时。在这些情况下,v-for指令可以调用一个带有参数的方法来决定事件的处理方式,如清单 14-6 所示。
<template>
<div class="container-fluid">
<div class="bg-primary text-white m-2 p-3 text-center">
<h3 v-on:click="handleEvent('Soccer Ball', $event)">{{ name }}</h3>
</div>
<div class="bg-primary text-white m-2 p-3 text-center">
<h3 v-on:click="handleEvent('Stadium', $event)">{{ name }}</h3>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "Lifejacket"
}
},
methods: {
handleEvent(name, $event) {
this.name = `${name} - ${$event.type}`;
}
}
}
</script>
Listing 14-6Using Method Arguments in the App.vue File in the src Folder
我添加了另一个绑定了v-on的h3元素。两个绑定表达式都调用了handleEvent方法,但是它们为第一个参数提供了不同的值。结果是根据点击的元素向用户显示不同的消息,如图 14-5 所示。
图 14-5
使用参数调用方法
使用指令速记
v-on指令有两种形式。如图 14-2 所示,手写形式由指令名、冒号和事件名组成。简短形式结合了@符号和事件名称,因此v-on:click也可以表示为@click。这意味着像这样的指令:
...
<h3 v-on:click="handleEvent('Soccer Ball', $event)">{{ name }}</h3>
...
也可以这样表达:
...
<h3 @click="handleEvent('Soccer Ball', $event)">{{ name }}</h3>
...
在长格式和简写格式之间的选择是个人喜好,不会改变指令的行为方式。
组合事件、方法和重复元素
当调用v-on指令时提供参数的能力在与v-for指令结合使用时变得更加有用。在清单 14-7 中,我使用了v-for指令为数组中的对象重复一组元素,并使用v-on指令为每个元素设置事件处理程序。
<template>
<div class="container-fluid">
<h3 class="bg-primary text-white text-center mt-2 p-2">{{message}}</h3>
<table class="table table-sm table-striped table-bordered">
<tr><th>Index</th><th>Name</th><th>Actions</th></tr>
<tr v-for="(name, index) in names" v-bind:key="name">
<td>{{index}}</td>
<td>{{name}}</td>
<td>
<button class="btn btn-sm bg-primary text-white"
v-on:click="handleClick(name)">
Select
</button>
</td>
</tr>
</table>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
message: "Ready",
names: ["Kayak", "Lifejacket", "Soccer Ball", "Stadium"]
}
},
methods: {
handleClick(name) {
this.message = `Select: ${name}`;
}
}
}
</script>
Listing 14-7Repeating Elements in the App.vue File in the src Folder
v-for指令为names数组中的每个对象复制表中的行。该副本包括所有的数据绑定和指令,这意味着在每个表行中有一个带有针对click事件的v-on绑定的button元素,如下所示:
...
<button class="btn btn-sm bg-primary text-white" v-on:click="handleClick(name)">
Select
</button>
...
使用v-for别名的值设置v-on绑定表达式,这意味着每个button元素将使用创建时正在处理的对象调用handleClick方法。handleClick方法使用其参数值来设置message属性的值,该值在h3元素中显示给用户。其效果是点击表格中的按钮显示 names 数组中的相应值,如图 14-6 所示。
图 14-6
重复内容中的事件处理
监听来自同一元素的多个事件
有两种方法可以处理同一元素上不同类型的事件。第一种方法是对每个事件分别应用v-on指令,如清单 14-8 所示。
<template>
<div class="container-fluid">
<h3 class="bg-primary text-white text-center mt-2 p-2">{{message}}</h3>
<table class="table table-sm table-striped table-bordered">
<tr><th>Index</th><th>Name</th><th>Actions</th></tr>
<tr v-for="(name, index) in names" v-bind:key="name">
<td>{{index}}</td>
<td>{{name}}</td>
<td>
<button class="btn btn-sm bg-primary text-white"
v-on:click="handleClick(name)"
v-on:mousemove="handleMouseEvent(name, $event)"
v-on:mouseleave="handleMouseEvent(name, $event)">
Select
</button>
</td>
</tr>
</table>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
counter: 0,
message: "Ready",
names: ["Kayak", "Lifejacket", "Soccer Ball", "Stadium"]
}
},
methods: {
handleClick(name) {
this.message = `Select: ${name}`;
},
handleMouseEvent(name, $event) {
if ($event.type == "mousemove") {
this.message = `Move in ${name} ${this.counter++}`;
} else {
this.counter = 0;
this.message = "Ready";
}
}
}
}
</script>
Listing 14-8Handling Multiple Event Types in the App.vue File in the src Folder
我添加了v-on指令来处理mousemove和mouseleave事件。当鼠标指针移动到一个元素上时触发mousemove事件,当指针离开该元素时触发mouseleave事件。这两个事件都由handleMouseEvent方法处理,该方法使用$event变量来确定事件类型,并更新message和counter属性。结果是,当指针移动到button元素上时,显示一条消息和计数器,然后当指针离开按钮占据的屏幕区域时,这些消息和计数器被重置,如图 14-7 所示。
图 14-7
响应几种类型的事件
监听多个事件的第二种方法是应用不带事件参数的v-on指令,并将指令的表达式设置为一个对象,该对象的属性名是事件类型,其值指定应该调用的方法,如清单 14-9 所示。
<template>
<div class="container-fluid">
<h3 class="bg-primary text-white text-center mt-2 p-2">{{message}}</h3>
<table class="table table-sm table-striped table-bordered">
<tr><th>Index</th><th>Name</th><th>Actions</th></tr>
<tr v-for="(name, index) in names" v-bind:key="name">
<td>{{index}}</td>
<td>{{name}}</td>
<td>
<button class="btn btn-sm bg-primary text-white"
v-on="buttonEvents"
v-bind:data-name="name">
Select
</button>
</td>
</tr>
</table>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
buttonEvents: {
click: this.handleClick,
mousemove: this.handleMouseEvent,
mouseleave: this.handleMouseEvent
},
counter: 0,
message: "Ready",
names: ["Kayak", "Lifejacket", "Soccer Ball", "Stadium"]
}
},
methods: {
handleClick($event) {
let name = $event.target.dataset.name;
this.message = `Select: ${name}`;
},
handleMouseEvent($event) {
let name = $event.target.dataset.name;
if ($event.type == "mousemove") {
this.message = `Move in ${name} ${this.counter++}`;
} else {
this.counter = 0;
this.message = "Ready";
}
}
}
}
</script>
Listing 14-9Handling Multiple Events in a Single Directive in the App.vue File in the src Folder
名为buttonEvents的data属性返回一个具有click、mousemove,和mouseleave属性的对象,对应我要处理的事件,这些属性的值就是每个事件应该调用的方法。这种方法的局限性是它不支持向方法传递参数,方法只接收$event参数。为了解决这个问题,我使用了v-bind指令为每个button元素添加一个data-name属性,如下所示:
...
<button class="btn btn-sm bg-primary text-white" v-on="buttonEvents"
v-bind:data-name="name">
...
$event对象提供了对触发事件的 HTML 元素的访问,我使用dataset属性来访问定制的data-属性,以便在处理这样的事件时获得data-name值:
...
let name = $event.target.dataset.name;
...
结果与清单 14-8 相同,但是不需要在button元素上定义多个v-on指令,这会产生一个难以阅读的模板。
小费
如果您愿意,可以在v-on指令的表达式中直接将事件映射对象定义为字符串文字,尽管仍然会产生难以理解的模板。
使用事件处理修饰符
为了保持事件处理方法简单和集中,v-on指令支持一组修饰符,用于执行通常需要 JavaScript 语句的常见任务。表 14-5 描述了一组v-on事件处理修饰符。
表 14-5
v-on 事件处理修饰符
|修饰语
|
描述
|
| --- | --- |
| stop | 这个修饰符相当于在事件对象上调用stopPropagation方法,如“停止事件传播”一节所述。 |
| prevent | 这个修饰符相当于在事件对象上调用preventDefault方法。这个修改器在第十五章中演示。 |
| capture | 该修饰符启用事件传播的捕获模式,如“在捕获阶段接收事件”一节所述。 |
| self | 只有当事件来源于指令所应用的元素时,这个修饰符才会调用 handler 方法,如“只处理目标阶段事件”一节中所演示的。 |
| once | 这个修饰符将阻止相同类型的后续事件调用 handler 方法,如“防止重复事件”一节中所示。 |
| passive | 此修改器将启用被动事件侦听,这将提高触摸事件的性能,在移动设备上尤其有用。参见 https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 了解何时使用该选项的详细说明。 |
管理事件传播
stop、capture和self修饰符用于通过 HTML 元素的层次结构管理事件的传播。当事件被触发时,浏览器通过三个阶段来定位事件的处理程序:捕获、目标和冒泡阶段。为了演示这些阶段如何工作——以及如何使用v-on修饰符来控制它们——我已经更新了示例应用中的组件,如清单 14-10 所示。
<template>
<div class="container-fluid">
<div id="outer-element" class="bg-primary p-4 text-white h3"
v-on:click="handleClick">
Outer Element
<div id="middle-element" class="bg-secondary p-4"
v-on:click="handleClick">
Middle Element
<div id="inner-element" class="bg-info p-4"
v-on:click="handleClick">
Inner Element
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
methods: {
handleClick($event) {
console.log(`handleClick target: ${$event.target.id}`
+ ` currentTarget: ${$event.currentTarget.id}`);
}
}
}
</script>
Listing 14-10Handling Events in the App.vue File in the src Folder
该模板包含三个嵌套的div元素,一个id属性和一个v-on指令被配置为使用handleClick方法处理click事件。该方法接收$event对象,并使用target属性获取触发事件的元素的详细信息,并使用currentTarget属性获取其v-on指令正在处理事件的元素的详细信息。
为了理解为什么在处理一个事件时会涉及到两个属性,保存对组件的修改,然后点击内部元素所占据的浏览器窗口部分,如图 14-8 所示。
图 14-8
触发一组嵌套元素中的事件
默认情况下,v-on指令将接收目标和冒泡阶段的事件,这意味着事件将首先由触发事件的元素(目标)上的v-on元素接收,然后依次由它的每个前身接收,从直接的父元素一直到body元素。您可以在已经写入 JavaScript 控制台的消息中看到这一点。
...
handleClick target: inner-element currentTarget: inner-element
handleClick target: inner-element currentTarget: middle-element
handleClick target: inner-element currentTarget: outer-element
...
第一条消息显示了事件的目标阶段,触发事件的元素上的v-on指令调用了handleClick方法。在目标阶段,$event对象的target和currentTarget属性是相同的。
第二条和第三条消息显示了冒泡阶段,其中事件沿着 HTML 文档向上传播到每个父元素,导致该事件类型的每个元素的v-on指令调用其方法。在这个阶段,$event对象的target属性返回触发事件的元素,currentTarget属性返回其v-on指令当前正在处理事件的元素。图 14-9 显示了目标阶段和气泡阶段的事件流程。
小费
并非所有类型的事件都有泡沫阶段。根据经验,特定于单个元素的事件——比如获得和失去焦点——不会冒泡。应用于多个元素的事件(例如单击被多个元素占据的屏幕区域)将冒泡。您可以通过读取$event对象的bubbles属性来查看特定事件是否将经历冒泡阶段。
图 14-9
目标和气泡阶段
在捕获阶段接收事件
捕获阶段为元素提供了在目标阶段之前处理事件的机会。在捕获阶段,浏览器从body元素开始,沿着与冒泡阶段相反的路径,沿着元素的层次结构向目标前进,并给予每个元素处理事件的更改,如图 14-10 所示。
图 14-10
捕获阶段
除非应用了capture修饰符,否则v-on指令不会在捕获阶段接收事件,如清单 14-11 所示。
<template>
<div class="container-fluid">
<div id="outer-element" class="bg-primary p-4 text-white h3"
v-on:click.capture="handleClick">
Outer Element
<div id="middle-element" class="bg-secondary p-4"
v-on:click.capture="handleClick">
Middle Element
<div id="inner-element" class="bg-info p-4"
v-on:click="handleClick">
Inner Element
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
methods: {
handleClick($event) {
console.log(`handleClick target: ${$event.target.id}`
+ ` currentTarget: ${$event.currentTarget.id}`);
}
}
}
</script>
Listing 14-11Using the Capture Phase in the App.vue File in the src Folder
应用修饰符时,先使用句点,然后使用修饰符名称,如下所示:
...
v-on:click.capture="handleClick"
...
将capture修饰符添加到v-on指令意味着元素将在捕获阶段而不是冒泡阶段接收事件。您可以通过单击内部元素并检查浏览器的 JavaScript 控制台中显示的消息来查看效果。
...
handleClick target: inner-element currentTarget: outer-element
handleClick target: inner-element currentTarget: middle-element
handleClick target: inner-element currentTarget: inner-element
...
消息显示事件已经沿着 HTML 文档向下进行,触发了外层元素和中间元素上的v-on指令。
仅处理目标阶段事件
您可以限制v-on指令,使其只响应目标阶段的事件。self修饰符确保只有被应用了指令的元素触发的事件才会被处理,如清单 14-12 所示。
<template>
<div class="container-fluid">
<div id="outer-element" class="bg-primary p-4 text-white h3"
v-on:click.capture="handleClick">
Outer Element
<div id="middle-element" class="bg-secondary p-4"
v-on:click.self="handleClick">
Middle Element
<div id="inner-element" class="bg-info p-4"
v-on:click="handleClick">
Inner Element
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
methods: {
handleClick($event) {
console.log(`handleClick target: ${$event.target.id}`
+ ` currentTarget: ${$event.currentTarget.id}`);
}
}
}
</script>
Listing 14-12Selecting Target Phase Events in the App.vue File in the src Folder
我已经在中间元素的v-on指令上应用了self修饰符。如果单击内部元素,您将看到浏览器的 JavaScript 控制台中显示以下消息:
...
handleClick target: inner-element currentTarget: outer-element
handleClick target: inner-element currentTarget: inner-element
...
第一条消息是从外部元素接收的,它的v-on指令使用 capture 修饰符,因此在捕获阶段接收事件。第二条消息来自内部元素,它是目标,在目标阶段接收事件。没有来自中间元素的消息,因为self修饰符已经阻止它在冒泡阶段接收事件。如果您单击中间的元素,将会看到这些消息:
...
handleClick target: middle-element currentTarget: outer-element
handleClick target: middle-element currentTarget: middle-element
...
这些消息中的第一条同样来自外部元素,它在捕获阶段首先获取事件。第二条消息来自目标阶段的中间元素,这是由self修饰符允许的。
停止事件传播
stop修饰符中止一个事件的传播,防止它被任何后续元素的v-on指令处理。在清单 14-13 中,我将stop指令应用于中间元素的v-on指令。
<template>
<div class="container-fluid">
<div id="outer-element" class="bg-primary p-4 text-white h3"
v-on:click.capture="handleClick">
Outer Element
<div id="middle-element" class="bg-secondary p-4"
v-on:click.stop="handleClick">
Middle Element
<div id="inner-element" class="bg-info p-4"
v-on:click="handleClick">
Inner Element
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
methods: {
handleClick($event) {
console.log(`handleClick target: ${$event.target.id}`
+ ` currentTarget: ${$event.currentTarget.id}`);
}
}
}
</script>
Listing 14-13Stopping Event Propagation in the App.vue File in the src Folder
stop修饰符阻止一个事件继续它通常的进程,但是不会停止传播,直到它到达它所应用的元素。在本例中,这意味着由中间元素触发的元素在目标阶段停止之前仍将经历捕获阶段。因为外部元素的v-on指令是用capture修饰符配置的,所以浏览器的 JavaScript 控制台中会显示以下消息:
...
handleClick target: middle-element currentTarget: outer-element
handleClick target: middle-element currentTarget: middle-element
...
组合事件处理修饰符
您可以将多个修饰符应用于单个v-on指令。修改器按照指定的顺序进行处理,这意味着通过在不同的组合中使用相同的修改器可以获得不同的效果。这种组合将在目标阶段处理事件,然后阻止它们进一步传播。
...
v-on:click.self.stop="handleClick"
...
但是如果这些修改器被颠倒,那么事件将在目标和气泡阶段停止(因为停止修改器是第一个)。
...
v-on:click.stop.self="handleClick"
...
因此,仔细考虑组合修饰符的后果,彻底测试组合,并一致地应用它们是很重要的。
防止重复事件
once修饰符阻止一个v-on指令多次调用它的方法。这不会停止正常的事件传播过程,但会阻止一个元素在第一个事件被处理后参与其中。在清单 14-14 中,我将once修饰符应用于组件模板的内部元素。
<template>
<div class="container-fluid">
<div id="outer-element" class="bg-primary p-4 text-white h3"
v-on:click.capture="handleClick">
Outer Element
<div id="middle-element" class="bg-secondary p-4"
v-on:click.stop="handleClick">
Middle Element
<div id="inner-element" class="bg-info p-4"
v-on:click.once="handleClick">
Inner Element
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
methods: {
handleClick($event) {
console.log(`handleClick target: ${$event.target.id}`
+ ` currentTarget: ${$event.currentTarget.id}`);
}
}
}
</script>
Listing 14-14Stopping Duplicate Events in the App.vue File in the src Folder
第一次单击内部元素时,您会看到所有的v-on指令都响应,如下所示:
...
handleClick target: inner-element currentTarget: outer-element
handleClick target: inner-element currentTarget: inner-element
handleClick target: inner-element currentTarget: middle-element
...
外部元素配置有capture修饰符,因此它在捕获阶段获取事件。这是第一次由内部元素处理click事件,因此事件的目标阶段就像平常一样。最后,中间的元素在冒泡阶段接收事件,此时stop修改器阻止它继续前进。
当您再次单击内部元素时,您将看到一组不同的消息:
...
handleClick target: inner-element currentTarget: outer-element
handleClick target: inner-element currentTarget: middle-element
...
once修饰符阻止内部元素的v-on指令调用handleClick方法,但不阻止事件传播到其他元素。
使用鼠标事件修改器
当您需要明确触发事件的条件时,v-on指令提供了一组修饰符来简化鼠标事件的处理。表 14-6 描述了一组v-on鼠标事件修改器。
表 14-6
v-on 鼠标事件修饰符
|修饰语
|
描述
|
| --- | --- |
| left | 此修改器仅选择由鼠标左键触发的事件。 |
| middle | 该修改器仅选择由鼠标中键触发的事件。 |
| right | 此修改器仅选择由鼠标右键触发的事件。 |
当应用于v-on指令时,这些修饰符将被处理的事件限制为那些由指定的鼠标按钮触发的事件,如清单 14-15 所示。
<template>
<div class="container-fluid">
<div id="outer-element" class="bg-primary p-4 text-white h3"
v-on:mousedown="handleClick">
Outer Element
<div id="middle-element" class="bg-secondary p-4"
v-on:mousedown="handleClick">
Middle Element
<div id="inner-element" class="bg-info p-4"
v-on:mousedown.right="handleClick">
Inner Element
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
methods: {
handleClick($event) {
console.log(`handleClick target: ${$event.target.id}`
+ ` currentTarget: ${$event.currentTarget.id}`);
}
}
}
</script>
Listing 14-15Using a Mouse Modifier in the App.vue File in the src Folder
在清单中,我修改了v-on指令,以便它们监听mousedown事件,当任何鼠标按钮被按在一个元素上时就会触发该事件,并对内部元素上的v-on指令应用了right修饰符,以便它只接收鼠标右键的事件。
描述的修饰符不会阻止事件的传播。相反,它们阻止应用它们的v-on指令处理使用其他鼠标按钮触发的事件。如果您左键单击内部元素,您将在浏览器的 JavaScript 控制台中看到以下消息,这些消息表明应用于中间和外部元素的v-on指令接收到该事件,即使它不是由内部元素的指令处理的:
...
handleClick target: inner-element currentTarget: middle-element
handleClick target: inner-element currentTarget: outer-element
...
如果右键单击内部元素,那么所有三个v-on指令都将处理该事件,产生以下消息:
...
handleClick target: inner-element currentTarget: inner-element
handleClick target: inner-element currentTarget: middle-element
handleClick target: inner-element currentTarget: outer-element
...
使用键盘事件修饰符
Vue.js 提供了一组键盘事件修饰符,用于限制由一个v-on指令处理的键盘事件,其方式类似于上一节描述的鼠标修饰符。在清单 14-16 中,我修改了组件,使其具有一个我用来接收键盘事件的input元素。
<template>
<div class="container-fluid">
<div class="bg-primary p-4 text-white h3">
{{message}}
</div>
<input class="form-control bg-light" placeholder="Type here..."
v-on:keydown.ctrl="handleKey" />
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
message: "Ready"
}
},
methods: {
handleKey($event) {
this.message = $event.key;
}
}
}
</script>
Listing 14-16Receiving Keyboard Events in the App.vue File in the src Folder
这个例子将ctrl修饰符应用于keydown事件,这意味着当控制键被按下时,只有keydown事件才会调用handleKey事件。您可以通过键入input元素并观察显示在div元素的文本插值绑定中的字符来查看效果。数据绑定显示当控制键单独按下或与另一个键组合按下时调用handleKey方法,否则不调用,如图 14-11 所示。
图 14-11
使用修改器过滤关键事件
表 14-7 显示了 Vue.js 提供的一组按键事件修饰符。其中一些修饰符用于可以与其他修饰符结合使用的按键,如 Shift 和 Control 键,其余的则便于指定通常需要的按键,如 Tab 和空格键。
表 14-7
v-on 键盘事件修饰符
|修饰语
|
描述
|
| --- | --- |
| enter | 该修改器选择回车键。 |
| tab | 此修饰符选择 Tab 键。 |
| delete | 此修改器选择删除键。 |
| esc | 该修饰符选择退出键。 |
| space | 该修改器选择空格键。 |
| up | 此修改器选择向上箭头键。 |
| down | 此修改器选择向下箭头键。 |
| left | 该修改器选择左箭头键。 |
| right | 该修改器选择右箭头键。 |
| ctrl | 该修改器选择控制键。 |
| alt | 该修改器选择 Alt 键。 |
| shift | 该修改器选择 Shift 键。 |
| meta | 该修改器选择元键。 |
| exact | 该修饰键仅选择指定的修饰键,如表后所述。 |
exact修饰符进一步限制了一个事件绑定,这样只有当指定的键被按下时它才会被调用。例如,清单 14-17 中的修饰符将只在只按下控制键时调用handleKey方法,而不是在控制键和 Shift 键一起被按下时。
<template>
<div class="container-fluid">
<div class="bg-primary p-4 text-white h3">
{{message}}
</div>
<input class="form-control bg-light" placeholder="Type here..."
v-on:keydown.ctrl.exact="handleKey" />
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
message: "Ready"
}
},
methods: {
handleKey($event) {
this.message = $event.key;
}
}
}
</script>
Listing 14-17Matching Keys Exactly in the App.vue File in the src Folder
摘要
在这一章中,我演示了使用v-on指令响应事件的不同方式。我向您展示了如何在应用指令时指定一个或多个事件,如何在生成重复内容时调用方法和接收参数,以及如何使用修饰符来更改事件行为和选择特定的鼠标按钮或键。在下一章中,我将描述使用表单元素的 Vue.js 特性。
十五、使用表单元素
在这一章中,我描述了v-model,它是用于 HTML 表单元素的内置指令。v-model指令在表单元素和数据值之间创建了一个双向数据绑定,并确保应用保持一致,不管数据如何变化。我还将向您展示如何将v-model指令与前面章节中描述的一些内置指令结合起来,以验证用户输入到表单中的数据。表 15-1 将v-model指令置于上下文中。
表 15-1
将 v-model 指令放在上下文中
|问题
|
回答
|
| --- | --- |
| 这是什么? | v-model 指令在 HTML 表单元素和数据属性之间创建双向绑定,确保两者保持一致。 |
| 为什么有用? | 使用表单元素是大多数 web 应用的重要组成部分,v-model指令负责创建数据绑定,而无需担心不同表单元素工作方式的差异。 |
| 如何使用? | v-model指令应用于input、select和textarea元素,其表达式是绑定应该创建到的数据属性的名称。 |
| 有什么陷阱或限制吗? | 指令不能和 Vuex 数据存储一起使用,我在第二十章中描述过。 |
| 还有其他选择吗? | 如果愿意,您可以手动创建所需的绑定,如本章的“创建双向模型绑定”一节所述。 |
表 15-2 总结了本章内容。
表 15-2
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 在数据属性和表单元素之间创建双向数据绑定 | 使用v-model指令 | 1–9 |
| 将输入值格式化为数字 | 使用number修改器 | 10, 11 |
| 延迟绑定更新 | 使用lazy修改器 | Twelve |
| 修剪输入值中的空白 | 使用trim修改器 | Thirteen |
| 用用户选择的表单填充数组 | 使用v-model指令绑定到一个数组 | Fourteen |
| 间接绑定到值 | 使用带有v-model指令的自定义值 | 15–17 |
| 确保用户提供了应用可以使用的值 | 使用v-model指令验证收集的表单数据 | 18–21 |
为本章做准备
在这一章中,我继续使用第十四章中的templatesanddata项目。为了准备本章,我简化了应用的根组件,如清单 15-1 所示。
小费
你可以从 https://github.com/Apress/pro-vue-js-2 下载本章以及本书其他章节的示例项目。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
Value: {{ dataValue }}
</div>
<div class="bg-primary m-2 p-2 text-white">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox"
v-on:change="handleChange" />
Data Value
</label>
</div>
</div>
<div class="text-center m-2">
<button class="btn btn-secondary" v-on:click="reset">
Reset
</button>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
dataValue: false
}
},
methods: {
reset() {
this.dataValue= false;
},
handleChange($event) {
this.dataValue = $event.target.checked;
}
}
}
</script>
Listing 15-1Simplifying the Content of the App.vue File in the src Folder
保存对App.vue文件的修改,运行templatesanddata文件夹中清单 15-2 所示的命令,启动 Vue.js 开发工具。
npm run serve
Listing 15-2Starting the Development Tools
打开一个新的浏览器窗口并导航至http://localhost:8080以查看图 15-1 所示的内容。
图 15-1
运行示例应用
创建双向模型绑定
到目前为止,我在本书的这一部分中创建的所有数据绑定都是单向的,这意味着数据从组件的script元素流向template元素,以便显示给用户。
示例应用展示了数据的单向流动。当复选框的状态改变时,v-on指令调用handleChange方法,设置dataValue属性。该数据变化触发更新,通过文本插值数据绑定显示,如图 15-2 所示。
图 15-2
使用单向数据绑定
当表单元素是应用中唯一的更改源时,单向数据绑定工作得很好。当用户有其他方式进行更改时,它们就不那么有效了,比如例子中的 Reset 按钮,它的v-on指令调用reset方法,将dataValue设置为false。文本插值绑定正确地反映了新值,但是input元素不知道这个变化,并且失去同步,如图 15-3 所示。
图 15-3
单向数据绑定的局限性
添加双向绑定
表单元素需要数据双向流动。当用户操作元素时,数据必须从表单流向数据模型,比如在字段中键入内容或选中复选框。当通过其他方式修改数据模型时,数据也必须向另一个方向流动,例如示例中的 Reset 按钮,以确保始终向用户呈现一致的数据。在清单 15-3 中,我在复选框和data属性之间创建了一个绑定。
...
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
Value: {{ dataValue }}
</div>
<div class="bg-primary m-2 p-2 text-white">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox"
v-on:change="handleChange"
v-bind:checked="dataValue" />
Data Value
</label>
</div>
</div>
<div class="text-center m-2">
<button class="btn btn-secondary" v-on:click="reset">
Reset
</button>
</div>
</div>
</template>
...
Listing 15-3Creating a Binding in the App.vue File in the src Folder
我使用了v-bind指令来设置元素的checked属性,这确保了点击重置按钮具有取消选中复选框的效果,如图 15-4 所示。
图 15-4
附加数据绑定的效果
现在在dataValue属性和复选框之间有了一个双向绑定。当复选框被选中和取消选中时,input元素发送change事件,这导致v-on指令调用handleChange方法,该方法设置dataValue值。在另一个方向,当dataValue由于点击 Reset 按钮而改变时,v-bind指令设置input元素的checked属性,该属性选中或取消选中复选框。
双向数据绑定是有效使用 HTML 表单的基础。在 Vue.js 应用中,数据模型是权威的,对数据模型的更改可以以不同的方式产生,所有这些都必须准确地反映在用户看到的表单元素中。
添加另一个输入元素
表单元素的 HTML 和 DOM 规范并不一致,不同元素类型的工作方式也有差异,这必须反映在用于创建双向数据绑定的v-on和v-bind指令中。在清单 15-4 中,我添加了一个文本input元素,它展示了两个不同表单元素之间的区别。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Data Value: {{ dataValue }}</div>
<div>Other Value: {{ otherValue || "(Empty)" }}</div>
</div>
<div class="bg-primary m-2 p-2 text-white">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox"
v-on:change="handleChange"
v-bind:checked="dataValue" />
Data Value
</label>
</div>
</div>
<div class="bg-primary m-2 p-2">
<input type="text" class="form-control"
v-on:input="handleChange"
v-bind:value="otherValue" />
</div>
<div class="text-center m-2">
<button class="btn btn-secondary" v-on:click="reset">
Reset
</button>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
dataValue: false,
otherValue: ""
}
},
methods: {
reset() {
this.dataValue = false;
this.otherValue = "";
},
handleChange($event) {
if ($event.target.type == "checkbox") {
this.dataValue = $event.target.checked;
} else {
this.otherValue = $event.target.value;
}
}
}
}
</script>
Listing 15-4Expanding the Form Elements in the App.vue File in the src Folder
此示例显示了为不同类型的元素创建双向绑定所需的差异。在处理复选框时,我必须监听change事件并绑定到checked属性,但对于文本输入,我监听input事件并绑定到value属性。我必须在handleChange事件中做类似的修改,为复选框设置checked属性,为文本输入设置value属性。结果是现在有两个表单元素,每个都有一个带有data属性的双向绑定,如图 15-5 所示。
图 15-5
添加另一个输入元素
简化双向绑定
创建绑定所需的差异使设置绑定的过程变得复杂,并且很容易混淆不同元素类型的需求,最终使用错误的事件、属性或特性。
Vue.js 提供了v-model指令;它简化了双向绑定,自动处理元素类型之间的差异,可以用在input、select和textarea元素上。在清单 15-5 中,我使用v-model指令简化了绑定。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Data Value: {{ dataValue }}</div>
<div>Other Value: {{ otherValue || "(Empty)" }}</div>
</div>
<div class="bg-primary m-2 p-2 text-white">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox"
v-model="dataValue" />
Data Value
</label>
</div>
</div>
<div class="bg-primary m-2 p-2">
<input type="text" class="form-control" v-model="otherValue" />
</div>
<div class="text-center m-2">
<button class="btn btn-secondary" v-on:click="reset">
Reset
</button>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
dataValue: false,
otherValue: ""
}
},
methods: {
reset() {
this.dataValue = false;
this.otherValue = "";
}
//handleChange($event) {
// if ($event.target.type == "checkbox") {
// this.dataValue = $event.target.checked;
// } else {
// this.otherValue = $event.target.value;
// }
//}
}
}
</script>
Listing 15-5Simplifying Two-Way Bindings in the App.vue File in the src Folder
v-model指令的表达式是为其创建双向绑定的属性。不需要在方法中接收事件,这允许我移除handleChange方法,效果是将组件重新聚焦于其数据和内容,而不是连接它们的管道。
绑定到表单元素
在继续之前,我将演示如何使用v-model指令为不同类型的表单元素创建绑定。元素之间的大部分差异由v-model指令处理,但是一个简单的绑定目录意味着您可以复制并粘贴到您自己的项目中,而不必找出每个项目所需的确切方法。
绑定到文本字段
最简单的绑定是创建到配置为允许用户输入文本的input元素。在清单 15-6 中,我使用了为纯文本和密码设置的input元素,以及使用v-model指令的双向绑定。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Name: {{ name }} </div>
<div>Password: {{ password }}</div>
<div>Details: {{ details }}</div>
</div>
<div class="bg-primary m-2 p-2 text-white">
<div class="form-group">
<label>Name</label>
<input class="form-control" v-model="name" />
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-control" v-model="password" />
</div>
<div class="form-group">
<label>Details</label>
<textarea class="form-control" v-model="details" />
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "Bob",
password: "secret",
details: "Has admin access"
}
}
}
</script>
Listing 15-6Binding to Text Fields in the App.vue File in the src Folder
我创建了两个input元素,其中一个默认为常规文本字段,另一个配置为密码字段,还有一个textarea元素。这三个元素都使用v-model指令来创建一个双向数据绑定,绑定到由组件定义的数据属性,产生如图 15-6 所示的结果。
图 15-6
绑定到文本字段
绑定到单选按钮和复选框
在清单 15-7 中,我用复选框和单选按钮替换了前一个例子中的元素,为用户提供了一组受限的选项。和前面的例子一样,每个元素都使用v-model指令创建一个带有数据属性的双向数据绑定。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Name: {{ name }} </div>
<div>Has Admin Access: {{ hasAdminAccess }}</div>
</div>
<div class="bg-primary m-2 p-2 text-white">
<div class="form-check">
<input class="form-check-input" type="radio"
v-model="name" value="Bob" />
<label class="form-check-label">Bob</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio"
v-model="name" value="Alice" />
<label class="form-check-label">Alice</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox"
v-model="hasAdminAccess" />
<label class="form-check-label">Has Admin Access?</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "Bob",
hasAdminAccess: true
}
}
}
</script>
Listing 15-7Binding to Checkboxes and Radio Buttons in the App.vue File in the src Folder
本例中的关键区别在于,当使用单选按钮时,每个元素都必须配置一个value属性,以便v-model指令知道如何更新数据属性的值。清单 15-7 中的元素产生如图 15-7 所示的结果。
图 15-7
绑定到单选按钮和复选框
可以将v-model指令与v-for和v-bind指令结合起来,从一组值中生成表单元素,如清单 15-8 所示,当呈现给用户的选项只有在运行时才知道时,这很有用。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Name: {{ name }} </div>
</div>
<div class="bg-primary m-2 p-2 text-white">
<div class="form-check" v-for="n in allNames" v-bind:key="n">
<input class="form-check-input" type="radio"
v-model="name" v-bind:value="n" />
<label class="form-check-label">{{ n }}</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
allNames: ["Bob", "Alice", "Joe"],
name: "Bob"
}
}
}
</script>
Listing 15-8Generating Radio Buttons in the App.vue File in the src Folder
必须使用v-bind指令来设置input元素的value属性;否则,Vue.js 不会将属性值作为表达式进行计算。图 15-8 显示了列表 15-8 的结果。
图 15-8
从数据值生成表单元素
绑定到选择元素
选择元素允许以紧凑的方式向用户呈现有限数量的选择,如清单 15-9 所示。定义用户可用选项的option元素可以静态定义,或者使用v-for指令定义,或者如清单所示,混合使用两者。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Name: {{ name }} </div>
</div>
<div class="bg-primary m-2 p-2 text-white">
<div class="form-group">
<label>Selected Names</label>
<select class="form-control" v-model="name">
<option value="all">Everyone</option>
<option v-for="n in allNames" v-bind:key="n"
v-bind:value="n">Just {{ n}}</option>
</select>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
allNames: ["Bob", "Alice", "Joe"],
name: "Bob"
}
}
}
</script>
Listing 15-9Binding to a Select Element in the App.vue File in the src Folder
与单选按钮一样,v-bind指令必须用于设置value属性。图 15-9 显示了列表 15-9 的结果。
图 15-9
绑定到选择的元素
使用垂直模型修改器
v-model指令提供了三个绑定来改变它创建的双向绑定。这些修改器在表 15-3 中描述,并在以下章节中演示。
表 15-3
v-model 指令的修饰符
|修饰语
|
描述
|
| --- | --- |
| number | 这个修饰符将输入的值解析成一个数字,然后将它赋给数据属性。 |
| trim | 这个修饰符在将输入赋给 data 属性之前,从输入中删除任何前导和尾随空白。 |
| lazy | 此修饰符更改 v-model 指令侦听的事件,以便仅当用户离开 input 元素时更新 data 属性。 |
将值格式化为数字
number修饰符解决了当类型属性被设置为number时input元素工作方式的奇怪之处,并在清单 15-10 中演示。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Amount: {{ amount }}, Amount + 10 = {{ amount + 10 }}</div>
</div>
<div class="form-group">
<label>Amount</label>
<input type="number" class="form-control" v-model="amount" />
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
amount: 100
}
}
}
</script>
Listing 15-10Using a Numeric Input Element in the App.vue File in the src Folder
HTML5 规范为type属性添加了一系列值,而number值告诉浏览器只接受数字和小数点的击键。但是用户输入的值被浏览器显示为字符串。这与动态 JavaScript 输入系统结合起来产生了一个问题,通过将 Amount 字段中的值更改为任意数字,比如 101,就可以看到这个问题。当值改变时,v-model指令响应由input元素生成的事件,并使用它接收的字符串值更新名为amount的data属性,产生如图 15-10 左侧所示的效果。
图 15-10
数字修饰符的效果
这个问题出现在文本插值绑定中,它给amount值加 10。由于v-model指令已经用一个字符串更新了 amount,JavaScript 将数据绑定的表达式解释为字符串连接,而不是加法,这意味着101 + 10产生一个结果10110。在清单 15-11 中,我在v-model指令中添加了number修饰符,避免了这个问题。
...
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Amount: {{ amount }}, Amount + 10 = {{ amount + 10 }}</div>
</div>
<div class="form-group">
<label>Amount</label>
<input type="number" class="form-control" v-model.number="amount" />
</div>
</div>
</template>
...
Listing 15-11Applying a Modifier in the App.vue File in the src Folder
number修饰符告诉v-model指令将用户输入的值转换成一个数字,然后用它来更新应用。
警告
number修饰符不对input元素允许的字符施加任何限制,如果用户输入的值包含非数字字符,它将允许指令用字符串更新数据模型。当使用这个修饰符时,您必须通过将input元素的type属性设置为number来确保用户只能输入数字。
延迟更新
默认情况下,v-model指令会在每次按键后更新数据模型,并生成input或textarea元素。lazy修饰符改变了v-model指令监听的事件,因此只有当导航到另一个组件时才执行更新。在清单 15-12 中,我将修饰符应用于组件模板中的input元素。
...
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Amount: {{ amount }}, Amount + 10 = {{ amount + 10 }}</div>
</div>
<div class="form-group">
<label>Amount</label>
<input type="number" class="form-control" v-model.number.lazy="amount" />
</div>
</div>
</template>
...
Listing 15-12Applying a Modifier in the App.vue File in the src Folder
在input元素失去焦点之前,lazy修饰符将阻止amount属性被更新,通常是当用户切换到另一个表单元素或者在元素外单击鼠标按钮时。
删除空白字符
trim修饰符从用户输入的文本中删除开头和结尾的空白字符,有助于避免用户很难看到的验证错误。在清单 15-13 中,我向组件添加了一个文本input元素,并在其v-model指令上使用了trim修饰符。
注意
trim修饰符只影响用户输入到元素中的值。如果应用的另一部分将data属性设置为有前导或尾随空格的字符串,这些字符将通过input元素显示给用户。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Name: **{{name}}** </div>
</div>
<div class="form-group">
<label>Name</label>
<input type="text" class="form-control" v-model.trim="name" />
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "Bob"
}
}
}
</script>
Listing 15-13Trimming Whitespace in the App.vue File in the src Folder
我用星号包围了显示值的文本插值绑定,以帮助强调任何前导或尾随空白。要测试修饰符,请保存对组件的更改,并输入以空格开头或结尾的字符串。这些字符将从分配给data属性的字符串中删除,产生如图 15-11 所示的结果。
图 15-11
修剪空白字符
绑定到不同的数据类型
v-model指令能够调整它绑定到数据模型的方式,这使得以通常对 web 应用开发有用的方式使用表单元素成为可能,如以下部分所述。
选择一组项目
如果将v-model指令应用于复选框并绑定到作为数组的数据属性,那么选中和取消选中该框将在数组中添加和删除一个值。这比解释更容易演示,在清单 15-14 中,我使用了v-for指令来生成一组复选框,并使用v-model指令将它们绑定到一个数组。
<template>
<div class="container-fluid">
<div class="bg-info m-2 p-2 text-white">
<div>Selected Cities: {{ cities }}</div>
</div>
<div class="form-check m-2" v-for="city in cityNames" v-bind:key="city">
<label class="form-check-label">
<input type="checkbox" class="form-check-input"
v-model="cities" v-bind:value="city" />
{{city}}
</label>
</div>
<div class="text-center">
<button v-on:click="reset" class="btn btn-info">Reset</button>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
cityNames: ["London", "New York", "Paris", "Berlin"],
cities: []
}
},
methods: {
reset() {
this.cities = [];
}
}
}
</script>
Listing 15-14Creating a Binding to an Array in the App.vue File in the src Folder
v-for指令为cityName数组中的每个值创建一个 checkbox 元素,它提供了一组值,用户可以从中进行选择。每个input元素都配置了一个value属性,该属性指定了城市名,当它被选中时将被添加到cities数组中,当它被取消选中时将被删除。v-model指令被配置为创建一个到cities数组的双向数据绑定,该数组将由所选的值填充。双向绑定意味着,如果应用的另一部分——比如本例中的reset方法——从数组中删除值,那么v-model指令将自动取消选中该框。我使用了一个文本插值绑定来显示选中的值,如图 15-12 所示。
图 15-12
创建到数组的数据绑定
使用 SELECT 元素绑定到数组
使用配置为允许多重选择的 select 元素可以实现类似的效果,如下所示:
...
<div class="form-control">
<label>City</label>
<select multiple class="form-control" v-model="cities">
<option v-for="city in cityNames" v-bind:key="city">
{{city}}
</option>
</select>
</div>
...
v-for指令应用于option元素,并用将呈现给用户的选项填充select元素。v-model指令被配置为绑定到一个数组,其工作方式与清单 15-14 中展示的复选框组相同。
我更喜欢复选框,因为大多数用户并不知道需要按住修饰键——比如 Windows 上的 Shift 或 Control 键来进行连续和非连续的选择。向用户呈现一组复选框是一种更明显的方法,我更喜欢在select元素旁边显示解释性文本。
为表单元素使用自定义值
复选框的一个常见模式是间接切换一个值,以便由input元素及其v-model绑定提供的真/假值被计算属性或数据绑定表达式转换为不同的值。在清单 15-15 中,我通过使用v-bind指令将一个元素分配给对应于引导 CSS 类的类,演示了这种模式。
<template>
<div class="container-fluid">
<div class="m-2 p-2 text-white" v-bind:class="elemClass">
<div>Value: {{ elemClass }}</div>
</div>
<div class="form-check m-2">
<label class="form-check-label">
<input type="checkbox" class="form-check-input"
v-model="dataValue" />
Dark Color
</label>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
dataValue: false,
}
},
computed: {
elemClass() {
return this.dataValue ? "bg-primary" : "bg-info";
}
}
}
</script>
Listing 15-15Indirect Values in the App.vue File in the src Folder
复选框上的v-model指令设置名为dataValue的数据属性,该属性仅由elemClass计算属性使用,div元素上的v-bind指令使用该属性设置类成员。切换复选框的效果是在bg-primary和bg-info类之间移动div元素,Bootstrap 使用它来设置背景颜色,如图 15-13 所示。
图 15-13
使用复选框设置的间接值
当复选框设置的data属性以这种方式使用时,转换true / false值的计算属性可以通过应用true-value和false-value属性来消除,如清单 15-16 所示。
<template>
<div class="container-fluid">
<div class="m-2 p-2 text-white" v-bind:class="dataValue">
<div>Value: {{ dataValue }}</div>
</div>
<div class="form-check m-2">
<label class="form-check-label">
<input type="checkbox" class="form-check-input"
v-model="dataValue" true-value="bg-primary"
false-value="bg-info" />
Dark Color
</label>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
dataValue: "bg-info"
}
}
}
</script>
Listing 15-16Simplifying Bindings in the App.vue File in the src Folder
true-value和false-value属性被v-model指令用来设置dataValue,这意味着不需要一个计算属性来将用户的选择转换成可以被v-bind指令使用的类名。结果是一样的——切换复选框改变了div元素的类成员资格——但是代码更简单、更干净。
为单选按钮和选择元素使用自定义值
v-model指令可以与v-bind指令结合使用,以支持也可用于单选按钮和选择元素的值。在清单 15-17 中,我添加了这两种类型的元素,并对它们进行了配置,使它们使用上一节中介绍的类名。
<template>
<div class="container-fluid">
<div class="m-2 p-2 text-white" v-bind:class="dataValue">
<div>Value: {{ dataValue }}</div>
</div>
<div class="form-check m-2">
<label class="form-check-label">
<input type="checkbox" class="form-check-input"
v-model="dataValue" v-bind:true-value="darkColor"
v-bind:false-value="lightColor" />
Dark Color
</label>
</div>
<div class="form-group m-2 p-2 bg-secondary">
<label>Color</label>
<select v-model="dataValue" class="form-control">
<option v-bind:value="darkColor">Dark Color</option>
<option v-bind:value="lightColor">Light Color</option>
</select>
</div>
<div class="form-check-inline m-2">
<label class="form-check-label">
<input type="radio" class="form-check-input"
v-model="dataValue" v-bind:value="darkColor" />
Dark Color
</label>
</div>
<div class="form-check-inline m-2">
<label class="form-check-label">
<input type="radio" class="form-check-input"
v-model="dataValue" v-bind:value="lightColor" />
Light Color
</label>
</div>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
darkColor: "bg-primary",
lightColor: "bg-info",
dataValue: "bg-info"
}
}
}
</script>
Listing 15-17Using Custom Values for Other Elements in the App.vue File in the src Folder
为了避免在每个元素上重复类名,我使用了v-bind指令,通过名为darkColor和lightColor的数据属性为input和option元素设置value属性。结果是一组管理div元素的类成员资格的表单元素,而不需要计算属性来转换true / false值,如图 15-14 所示。
图 15-14
对其他元素使用自定义值
验证表单数据
一旦开始使用表单元素,就需要开始考虑数据验证。用户将在文本字段中输入任何内容,确保您的应用接收到运行所需的数据非常重要。在接下来的部分中,我将解释如何验证表单数据,为了做好准备,我已经更新了组件,使其包含一个简单的表单,如清单 15-18 所示。
注意
在这一节中,我将向您展示如何编写您自己的验证代码,因为这很有趣,并且它演示了如何将本章和前面章节中描述的一些功能组合起来,以产生有用的功能。在实际项目中,我建议你依靠一个优秀的、广泛使用的开源包来进行 Vue.js 表单验证,比如vuelidate ( https://github.com/monterail/vuelidate ),我在第一部分中用于 SportsStore 项目的,或者VeeValidate ( http://vee-validate.logaretm.com )。
<template>
<div class="container-fluid">
<div class="btn-primary text-white my-2 p-2">
Name: {{ name }}, Category: {{ category }}, Price: {{ price }}
</div>
<form v-on:submit.prevent="handleSubmit">
<div class="form-group">
<label>Name</label>
<input v-model="name" class="form-control" />
</div>
<div class="form-group">
<label>Category</label>
<input v-model="category" class="form-control" />
</div>
<div class="form-group">
<label>Price</label>
<input type="number" v-model.number="price" class="form-control" />
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</form>
</div>
</template>
<script>
export default {
name: "MyComponent",
data: function () {
return {
name: "",
category: "",
price: 0
}
},
methods: {
handleSubmit() {
console.log(`FORM SUBMITTED: ${this.name} ${this.category} `
+ ` ${this.price}`);
}
}
}
</script>
Listing 15-18Creating a Form in the App.Vue File in the src Folder
该组件定义了名为name、category和price的data属性,这些属性通过input元素呈现给用户,这些元素已经应用了v-model指令。输入值的详细信息显示在表单元素上方,如图 15-15 所示。
图 15-15
向用户呈现表单
input元素包含在一个form元素中,该元素应用了v-on指令,如下所示:
...
<form v-on:submit.prevent="handleSubmit">
...
当用户点击类型被设置为submit的button元素时,触发submit事件。需要使用 prevent 修饰符来阻止浏览器向 HTTP 服务器提交表单,这是该事件的默认操作。该指令的表达式调用了一个名为handleSubmit的方法,该方法会将数据发送到实际应用中的 web 服务,但在本例中只是将一条消息写到浏览器的 JavaScript 控制台。单击 Submit 按钮,您将在 JavaScript 控制台中看到如下消息:
...
FORM SUBMITTED: Running Shoes Running 100
...
我在第十九章中解释了如何使用 web 服务,但是对于这一章,重点是验证用户输入的数据。本章这一部分的目标是控制由handleSubmit方法显示的消息,以便它只在用户为所有表单域输入有效数据时才显示。当执行验证时,有一套清晰的要求是很重要的,对于这个例子,每个属性的验证要求在表 15-4 中描述。
表 15-4
验证要求
|名字
|
描述
|
| --- | --- |
| name | 用户必须提供至少包含三个字符的值。 |
| category | 用户必须提供一个仅包含字母的值。 |
| price | 用户必须提供一个仅包含数字且介于 1 和 1,000 之间的值。 |
定义验证规则
当您验证数据时,您会发现同一组规则被重复应用于用户提供的不同数据值。为了减少代码重复,定义一次验证规则并重用它们是一个好主意。为了定义表 15-4 中所需的验证规则,我在src文件夹中添加了一个名为validationRules.js的 JavaScript 文件,并添加了清单 15-19 中所示的代码。
理解用户输入错误数据的原因
用户在应用中输入错误数据需要数据验证有几个原因。第一种类型的坏数据发生在用户不理解所需的数据或数据必须以何种格式表达时。我遇到的一个常见例子是要求信用卡在数字组之间有或没有空格。如果您需要用特定的格式来表示公共数据值,您应该让用户清楚。更好的是,完全避免这个问题,接受所有的通用格式,并自动将它们转换成您喜欢的格式。
第二类坏数据发生在用户不关心过程,只想得到结果的时候。如果你让用户忍受冗长而复杂的表单,或者一遍又一遍地重复一个过程,那么你会看到很多毫无意义的结果。为了最大限度地减少这种问题,请只向用户询问您真正需要的数据,尽可能提供合理的默认值,并且请记住,很少有用户会像您希望的那样关心您的产品和服务。
对于坏数据的最终原因,你无能为力,这是用户试图颠覆应用的地方。总会有恶意用户,你的产品和服务越有价值,吸引的恶意用户就越多。数据验证本身不足以避免这类问题,您必须付出适当的努力来进行端到端的安全审查、监控和补救。
function required(name) {
return {
validator: (value) => value != "" && value !== undefined && value !== null,
message: `A value is required for ${name}`
}
}
function minLength(name, minlength) {
return {
validator: (value) => String(value).length >= minlength,
message: `At least ${minlength} characters are required for ${name}`
}
}
function alpha(name) {
return {
validator: (value) => /^[a-zA-Z]*$/.test(value),
message: `${name} can only contain letters`
}
}
function numeric(name) {
return {
validator: (value) => /^[0-9]*$/.test(value),
message: `${name} can only contain digits`
}
}
function range(name, min, max) {
return {
validator: (value) => value >= min && value <= max,
message: `${name} must be between ${min} and ${max}`
}
}
export default {
name: [minLength("Name", 3)],
category: [required("Category"), alpha("Category")],
price: [numeric("Price"), range("Price", 1, 1000)]
}
Listing 15-19The Contents of the validationRules.js File in the src Folder
这个 JavaScript 文件定义了验证函数,我将使用这些函数来检查用户输入的值以及这些函数的组合,以验证每个属性。
执行验证
下一步是添加将执行验证的代码。在清单 15-20 中,我添加了使用下一节定义的验证规则所需的方法和属性。
<template>
<div class="container-fluid">
<div class="bg-danger text-white my-2 p-2" v-if="errors">
<h5>The following problems have been found:</h5>
<ul>
<template v-for="(errors) in validationErrors">
<li v-for="error in errors" v-bind:key="error">{{error}}</li>
</template>
</ul>
</div>
<form v-on:submit.prevent="handleSubmit">
<div class="form-group">
<label>Name</label>
<input v-model="name" class="form-control" />
</div>
<div class="form-group">
<label>Category</label>
<input v-model="category" class="form-control" />
</div>
<div class="form-group">
<label>Price</label>
<input type="number" v-model.number="price" class="form-control" />
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">
Submit
</button>
</div>
</form>
</div>
</template>
<script>
import validation from "./validationRules";
import Vue from "vue";
export default {
name: "MyComponent",
data: function () {
return {
name: "",
category: "",
price: 0,
validationErrors: {},
}
},
computed: {
errors() {
return Object.values(this.validationErrors).length > 0;
}
},
methods: {
validate(propertyName, value) {
let errors = [];
Object(validation)[propertyName].forEach(v => {
if (!v.validator(value)) {
errors.push(v.message);
}
});
if (errors.length > 0) {
Vue.set(this.validationErrors, propertyName, errors);
} else {
Vue.delete(this.validationErrors, propertyName);
}
},
validateAll() {
this.validate("name", this.name);
this.validate("category", this.category);
this.validate("price", this.price);
return this.errors;
},
handleSubmit() {
if (this.validateAll()) {
console.log(`FORM SUBMITTED: ${this.name} ${this.category} `
+ ` ${this.price}`);
}
}
}
}
</script>
Listing 15-20Performing Validation in the App.vue File in the src Folder
使用清单 15-20 中定义的规则,validate方法用于验证单个data属性,这些规则通过import语句访问。验证错误的详细信息存储在名为validationErrors的data属性中,每个input字段都有一个属性设置为需要呈现给用户的验证消息数组。
小费
我在清单 15-20 中使用的Vue.delete方法是在第十三章中描述的Vue.set方法的对应物,用于从对象中移除一个属性,这样 Vue.js 就能意识到变化。
当用户点击提交按钮时,handleSubmit方法调用validateAll方法,后者调用每个data属性的validate方法,并执行动作——在本例中记录一条消息——只有在没有验证问题的情况下。
在组件的模板中,我使用v-if指令来控制div元素的可见性,然后依赖v-for指令来枚举validationErrors对象的属性,然后再次为每条消息创建li元素。
结果是,当点击提交按钮时,用户已经输入到表单中的值被检查,如图 15-16 所示,该图显示了在没有改变任何输入字段的情况下点击按钮时显示的错误。
图 15-16
验证表单数据
响应实时变化
基本的验证工作正常,但是只有当用户单击提交按钮时才执行检查。更平滑的方法是在用户向input元素中输入数据时验证数据,提供更直接的反馈。我不想立即开始显示错误,所以我会等到用户点击一次提交后再响应更改,如清单 15-21 所示。
...
<script>
import validation from "./validationRules";
import Vue from "vue";
export default {
name: "MyComponent",
data: function () {
return {
name: "",
category: "",
price: 0,
validationErrors: {},
hasSubmitted: false
}
},
watch: {
name(value) { this.validateWatch("name", value) },
category(value) { this.validateWatch("category", value) },
price(value) { this. validateWatch("price", value) }
},
computed: {
errors() {
return Object.values(this.validationErrors).length > 0;
}
},
methods: {
validateWatch(propertyName, value) {
if (this.hasSubmitted) {
this.validate(propertyName, value);
}
},
validate(propertyName, value) {
let errors = [];
Object(validation)[propertyName].forEach(v => {
if (!v.validator(value)) {
errors.push(v.message);
}
});
if (errors.length > 0) {
Vue.set(this.validationErrors, propertyName, errors);
} else {
Vue.delete(this.validationErrors, propertyName);
}
},
validateAll() {
this.validate("name", this.name);
this.validate("category", this.category);
this.validate("price", this.price);
return this.errors;
},
handleSubmit() {
this.hasSubmitted = true;
if (this.validateAll()) {
console.log(`FORM SUBMITTED: ${this.name} ${this.category} `
+ ` ${this.price}`);
}
}
}
}
</script>
...
Listing 15-21Responding to Changes in the App.vue File in the src Folder
我在script元素中添加了一个watch部分,用于定义观察者。我在第十七章解释了观察器,但是这个特性允许一个组件在它的一个数据值改变时接收通知。在这个例子中,我已经向watch部分添加了函数,这样当name、category和price属性发生变化时,我将收到通知,并使用这个通知来调用一个名为validateWatch的方法,该方法仅在提交按钮至少被单击一次后才验证属性值,这通过一个名为hasSubmitted的新data属性来管理。其结果是,一旦点击提交按钮,显示给用户的错误信息会在编辑时立即更新,如图 15-17 所示。
图 15-17
验证错误的即时反馈
摘要
在本章中,我解释了双向数据绑定的使用,并描述了如何使用v-model指令创建它们。我还演示了如何使用 v-model 指令和 Vue.js 的其他关键特性来验证用户输入到 HTML 表单中的数据。在下一章,我将描述如何使用组件来给应用添加结构。