翻译: 珈蓝 from 迅雷前端javascript
翻译自 Evan Schultz 的文章 Do it with Elegance: How to Create Data-Driven User Interfaces in Vuehtml
本文演示了如何利用 Vue 的动态组件根据 schema 来生成一个动态的表单生成器,在管理后台、设置中心等相似的场景中,你彻底能够利用这种思路来更效率地开发界面。前端
虽然咱们一般在构建大部分的视图时知道须要用到哪些组件,但有时咱们直到运行时才知道它们是什么组件(译者注:动态组件)。这意味着咱们须要基于应用程序状态、用户设置或来自 API 请求的响应结果来构建视图。一个常见的状况是构建动态表单,其中所需的问题和组件由 JSON 对象配置,或者字段根据用户的答案进行更改。vue
全部现代的 JavaScript 框架都有处理动态组件的方法。这篇文章将向你展现如何在 Vue.JS 中实现它,它为上面的场景提供了一个很是优雅简单的解决方案。java
一旦你看到用 Vue.JS 实现它是多么的简单,你可能会受到启发而且开始思考你之前从未考虑过的动态组件应用。git
咱们要先学会走才能学会跑,因此首先我将介绍动态组件的基础知识,而后深刻讨论如何使用这些概念构建你本身的动态表单构造器。github
Vue 有一个叫作 <component>
的内置组件,你能够在VueJS 指南的动态组件中了解完整的详细信息。并发
指南上写道:app
“你可使用相同的挂载点并使用保留的元素在多个组件之间动态切换,并动态绑定到其 is 属性。”框架
这意味着切换组件能够向像下面这样简单:
<component :is="componentType">
复制代码
让咱们再多补充一点,看看发生了什么。咱们将建立两个组件叫作 DynamicOne 和 DynamicTwo - One 和 Two 都是同样的,因此我不会重复展现这两个的代码。
<template>
<div>Dynamic Component One</div>
</template>
<script> export default { name: 'DynamicOne', } </script>
复制代码
下面是一个可以在它们之间切换的快速示例,咱们在 App.vue 中设置咱们的组件。
import DynamicOne from './components/DynamicOne.vue';
import DynamicTwo from './components/DynamicTwo.vue';
export default {
name: 'app',
components: {
DynamicOne,
DynamicTwo,
},
data() {
return {
showWhich: 'DynamicOne',
};
},
};
复制代码
注意:showWhich
data 属性的值是字符串DynamicOne
-这是在组件的components
对象中建立的属性名。
在咱们的模板中,咱们将设置两个按钮来切换这两个动态组件。
<button @click="showWhich = 'DynamicOne'">Show Component One</button>
<button @click="showWhich = 'DynamicTwo'">Show Component Two</button>
<component :is="showWhich"></component>
复制代码
点击这两个按钮将会交换显示 DynamicOne 和 DynamicTwo
看到这你也许会想,“那又怎样呢?这很方便——但我用v-if
同样很简单”。
当你意识到<component>
能够像其余任何组件同样工做时,这个例子就开始发挥做用了,而且它能够与诸如v-for
之类的东西结合用于迭代集合,或者将is
绑定到 input 的属性、data 属性或计算属性上。
组件不是孤立地存在,它们须要一种方式与周围的世界交流。在 Vue 中,这种方式是经过 props 和事件实现的。
你能够用和其余组件同样的方式在动态组件上设置 props 和绑定事件,而且若是加载的组件不须要该属性,Vue 也不会报未知属性的错误。
让咱们来修改咱们的组件来展现一个问候组件。一个组件会接受firstName
和lastName
,另外一个会接受firstName
、lastName
和title
。
关于事件,咱们将在DynamicOne
中添加一个按钮,它将发射一个叫作"upperCase"的事件,在DynamicTwo
中,这个按钮将发射一个叫作"lowerCase"的事件。
把它们组合在一块儿,修改后的动态组件看起来像这样:
<component :is="showWhich" :firstName="person.firstName" :lastName="person.lastName" :title="person.title" @upperCase="switchCase('upperCase')" @lowerCase="switchCase('lowerCase')">
</component>
复制代码
不是全部的属性或事件都须要在咱们正在切换的动态组件上定义。
在这一点上,你可能会想知道,“若是组件是动态的,而且不是全部的组件都须要知道每一个可能的 props,那我须要预先知道 props 并在模板中声明它们吗?”
谢天谢地,答案是否认的。Vue 提供了一个快捷方式,你能够用v-bind
将一个对象的全部 key 都绑定到组件的 props 上。
这简化了模板:
<component :is="showWhich" v-bind="person" @upperCase="switchCase('upperCase')" @lowerCase="switchCase('lowerCase')">
</component>
复制代码
如今咱们拥有这些动态组件积木,咱们就能够开始在 Vue 基础上构建表单生成器了。
咱们从一个基本的表单模式开始 - 一个描述表单的字段,标签,选项等的 JSON 对象。首先,咱们从下列类型的输入表单开始:
初始模式是这样的:
schema: [
{
fieldType: 'SelectList',
name: 'title',
multi: false,
label: 'Title',
options: ['Ms', 'Mr', 'Mx', 'Dr', 'Madam', 'Lord'],
},
{
fieldType: 'TextInput',
placeholder: 'First Name',
label: 'First Name',
name: 'firstName',
},
{
fieldType: 'TextInput',
placeholder: 'Last Name',
label: 'Last Name',
name: 'lastName',
},
{
fieldType: 'NumberInput',
placeholder: 'Age',
name: 'age',
label: 'Age',
minValue: 0,
},
];
复制代码
看起来很是简单:能够配置标签,占位符等,选择列表还列出了可能的选项options
。在这个例子中,咱们将保持组件的实现一直如此简单。
TextInput.vue - template
<div>
<label>{{label}}</label>
<input type="text" :name="name" placeholder="placeholder">
</div>
复制代码
TextInput.vue - script
export default {
name: 'TextInput',
props: ['placeholder', 'label', 'name'],
};
复制代码
SelectList.vue - template
<div>
<label>{{label}}</label>
<select :multiple="multi">
<option v-for="option in options" :key="option">
{{option}}
</option>
</select>
</div>
复制代码
SelectList.vue - script
export default {
name: 'SelectList',
props: ['multi', 'options', 'name', 'label'],
};
复制代码
要根据上面定义的模式生成表单,须要添加如下内容:
<component v-for="(field, index) in schema" :key="index" :is="field.fieldType" v-bind="field">
</component>
复制代码
表单效果以下:
若是生成表单但不绑定数据,它会有用吗?可能不会。咱们目前正在生成一个表单,但没有办法将数据绑定到它。你的第一反应多是在模式中添加一个value
属性,而且在组件中使用v-model
,以下所示:
<input type="text" :name="name" v-model="value" :placeholder="placeholder">
复制代码
这种方法存在一些潜在的缺陷,但咱们最关心的是 Vue 会给咱们一个错误/警告:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
found in
---> <TextInput> at src/components/v4/TextInput.vue
<FormsDemo> at src/components/DemoFour.vue
<App> at src/App.vue
<Root>
复制代码
尽管 Vue 确实提供了语法糖,使组件状态的双向绑定更容易,但框架仍然偏向于单向数据流。咱们试图直接在组件内修改父组件的数据,因此 Vue 会向咱们发出警告。
仔细看看v-model
,它没有太多的魔力,因此让咱们按照Vue 的表单输入组件指南中的描述来分解它。
<input v-model="something">
复制代码
和下面相同的
<input v-bind:value="something" v-on:input="something = $event.target.value">
复制代码
随着魔法揭示,咱们想要完成的是:
咱们经过绑定到value
并发出@input
事件来通知父组件值已经发生变化,从而完成此操做。
来看看咱们的TextInput
组件
<div>
<label>{{label}}</label>
<input type="text" :name="name" :value="value" @input="$emit('input',$event.target.value)" :placeholder="placeholder">
</div>
复制代码
因为父组件负责提供该值,所以它也负责处理绑定到它本身的组件状态。为此,咱们能够在组件上使用v-model
:
FormGenerator.vue - template
<component v-for="(field, index) in schema" :key="index" :is="field.fieldType" v-model="formData[field.name]" v-bind="field">
</component>
复制代码
注意咱们如何使用v-model ="formData[field.name]"
。咱们须要在这个 data 属性上设置一个对象:
export default {
data() {
return {
formData: {
firstName: 'Evan'
},
}
复制代码
咱们能够将对象留空,或者若是咱们有一些咱们想要设置的初始字段值,咱们能够在这里指定它们。
如今咱们已经完成了生成表单的工做,而且发现这个组件承担了至关多的责任。虽然这不是复杂的代码,但若是表单生成器自己是一个可复用组件,那将会很好。
对于这个表单生成器,咱们但愿将模式做为一个 prop 传递给它,而且可以在组件之间创建数据绑定。
用生成器的模板是这样:
GeneratorDemo.vue - template
<form-generator :schema="schema" v-model="formData">
</form-generator>
复制代码
这大大简化了父组件。它只关心FormGenerator
,而不关心每一个可用的输入类型、链接的事件等等。
接下来,建立一个名为FormGenerator
的组件。这几乎是复制粘贴最初的代码而后进行一些微小但关键的调整:
v-modle
改成:value
,而后用@input
处理事件value
和schema
到 props 上updateForm
方法FormGenerator 组件以下:
FormGenerator.vue - template
<component v-for="(field, index) in schema" :key="index" :is="field.fieldType" :value="formData[field.name]" @input="updateForm(field.name, $event)" v-bind="field">
</component>
复制代码
FormGenerator.vue - template
import NumberInput from '@/components/v5/NumberInput';
import SelectList from '@/components/v5/SelectList';
import TextInput from '@/components/v5/TextInput';
export default {
name: 'FormGenerator',
components: {NumberInput, SelectList, TextInput},
props: ['schema', 'value'],
data() {
return {
formData: this.value || {},
};
},
methods: {
updateForm(fieldName, value) {
this.$set(this.formData, fieldName, value);
this.$emit('input', this.formData);
},
},
};
复制代码
因为formData
属性并不知道咱们传入的每个可能的字段,咱们使用this.$set
,这样 Vue 的响应系统就能够跟踪它的任何变化,并容许FormGenerator
组件跟踪它本身的内部状态。
如今咱们有了一个基本的、可复用的表单生成器。
在组件内使用它:
GeneratorDemo.vue - template
<form-generator :schema="schema" v-model="formData">
</form-generator>
复制代码
GeneratorDemo.vue - script
import FormGenerator from '@/components/v5/FormGenerator'
export default {
name: "GeneratorDemo",
components: { FormGenerator },
data() {
return {
formData: {
firstName: 'Evan'
},
schema: [{ /* .... */ },
}
复制代码
如今你已经看到了表单生成器如何利用 Vue 的基础动态组件建立一些高度动态的、数据驱动的 UI。我鼓励你好好研究下GitHub上的示例代码或者在CodeSanbox上实践。若是你有任何问题或者想聊一聊,能够随时经过 Twitter, Github, 或邮件联系我。