特别声明,本文来源于@Evan Schultz的《Do it with Elegance: How to Create Data-Driven User Interfaces in Vue》。
虽然我们通常知道在应用程序中构建大多数视图(View)时需要哪些组件(Component),但很多时候我们并不知道这些组件在运行时是什么。这意味着我们需要基于应用程序状态、用户首选项或API的响应来构建一个页面。一个常见的情况是构建动态表单,其中需要整清楚的是:组件是由JSON对象配置的,还是基于用户的答案而更改的字段来构建。
所有现代JavaScript框架(比如我们熟悉的Vue、React等)都有处理动态组件的方法。这篇文章将向您展示如何在Vue中实现它。JavaScript为上述场景提供了一个非常优雅和简单的解决方案。
一旦你看到使用Vue是这么的容易,你可能会受到启发,看到你从未考虑过的动态组件的应用程序!
在运行之前,我们需要先走一步,先介绍动态组件的基础知识,然后深入了解如何使用这些概念来构建你自己的动态表单构造器。
基础知识
Vue有一个内置组件,称为<component>
。你可以在Vue指南中看到有关于动态组件的完整细节。
Vue指南是这样描述的:
你可以使用相同的挂载点,并使用保留元素动态地在多个组件之间切换,并动态绑定到它的属性。
这意味着在组件之间进行交换变得很简单:
<component :is="componentType">
让我们把它具体化一些,看看会发生什么。首先创建两个组件,分别名为DynamicOne
和DynamicTwo
。这两个组件中的代码是相同的,只是name
的值不一样:
<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'
}
}
}
注意,
data
中的showWhich
属性值是DynamicOne
的字符串值,这是components
对象中创建的name
的值。
在我们的模板中,我们将设置两个按钮来切换两个动态组件:
<button @click="showWhich = 'DynamicOne'">Show Component One</button>
<button @click="showWhich = 'DynamicTwo'">Show Component Two</button>
<component :is="showWhich"></component>
点击按钮将会用DynamicTwo
替换DynamicOne
。
这个时候你可能会说,这又怎么样?是很方便,但也可以使用v-if
来实现同样的效果。
当你意识到<component>
和其他组件一样工作时,这个示例就开始发挥作用了,它可以与v-for
这样的Vue指令结合起来使用,用于对迭代集合,或者使用:is
可以绑定到一个input
、data
的props
或者是computed
属性。
props和events
组件不是孤立存在的 —— 它们需要一种与周围世界交流的方式。如果使用Vue的话,那么这种交流是通过props
和events
来完成的。
你可以在动态组件上指定与其他组件相同的属性和事件绑定,如果加载的组件不需要该属性,Vue将不会抱怨未知的"Attributes”或“properties”。
让我们修改组件显示的内容。DynamicOne
组件只接受firstName
和lastName
,而另一个组件DynamicTwo
接受firstName
、lastName
和title
。
对于事件,我们将在DynamicOne
中添加一个按钮,它将发出(emit
)一个名为upperCase
的事件;而在DynamicTwo
中将发出一个名为lowerCase
事件。
把它放在一起,动态组件看起来像这样:
<component
:is="showWhich"
:firstName="person.firstName"
:lastName="person.lastName"
:title="person.title"
@upperCase="switchCase('upperCase')"
@lowerCase="switchCase('lowerCase')">
</component>
事实上并不是每个属性或事件都需要定义在我们正在切换的动态组件上。
先要了解props
此时,你可能会想,“如果组件是动态的,而不是每个组件都需要知道所有可能的props
—— 需要知道props
之前,并在模板中声明它们吗?”
值得庆幸的是,答案是否定的。Vue提供了一个快捷方式,可以使用v-bind
将对象的所有键绑定到组件的props
。
这个时候模板可以简化为:
<component
:is="showWhich"
v-bind="person"
@upperCase="switchCase('upperCase')"
@lowerCase="switchCase('lowerCase')">
</component>
表单形式
现在我们已经有了一个动态组件区块,接下来可以开始构建一个表单生成器。
让我们从一个基本的表单模式开始 —— 一个JSON对象,它描述了表单的字段、标签和选项等:
- 文本和数字输入域
- 一个下拉列表
开始的schema
看起来像这样:
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
}
]
非常简单。在本例中,将保持这些简单的组件实现。
TextInput.vue
:
<template>
<div>
<label>{{ label }}</label>
<input type="text" :name="name" :placeholder="placeholder" />
</div>
</template>
<script>
export default {
name: 'TextInput',
props: ['placeholder', 'lable', 'name']
}
</script>
SelectList.vue
:
<template>
<div>
<label>{{ label }}</label>
<select :multiple="nulti">
<option v-for="option in options" :key="option">{{ option }}</option>
</select>
</div>
</template>
<script>
export default {
name:'SelectList',
props: ['multi', 'options', 'name', 'label']
}
</script>
基于这个schema
要生成表单,还要加加下面的内容:
<component
v-for="(field, index) in schema"
:key="index"
:is="field.fieldType"
v-bind="field">
</component>
结果如下:
数据绑定
如果表单生成但不绑定数据,估计也没啥有用。上面生成了一个表单,但没有绑定数据。你可以第一反应就是在组件中使用v-model
来绑定schema
中的value
属性。
<input
type="text"
:name="name"
v-model="value"
:placeholder="placeholder">
这种方法有一些潜在的缺陷,但是Vue会给我们反馈相应的错误或警告信息:
虽然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>
由于父类负责提供值,所以它也负责处理绑定到它自己的组件状态。为此,我们可以在<component>
签上使用v-model
:
FormGenerator.vue
:
<component
v-for="(field, index) in schema"
:key="index"
:is="field.fieldType"
v-model="formData[field.name]"
v-bind="field">
</component>
注意,我们如何使用v-model="formDtata[field.name]"
。我们需要在数据属性上提供一个对象:
export default {
data() {
return {
formData: {
firstName: 'Evan'
},
}
}
}
我们可以让对象为空,或者如果我们有一些我们想要设置的初始字段,就可以在这里指定它们。
现在我们已经完成了生成表单的过程,这个组件承担了相当多的责任。虽然代码不是很复杂,但是表单生成器自身要是可复用的组件,那就更好了。
让生成器可重用
对于这个表单生成器,我们希望将schema
传递给prop
,并能够在组件之间设置数据绑定。
当使用生成器时,模板GeneratorDemo.vue
变为:
<form-generator :schema="schema" v-model="formData"></form-generator>
这对父组件进行了相当大的清理。它只关心FormGenerator
,并不关心可以使用的每个输入类型、连接事件等。
接下来,创建一个名为FormGenerator
的组件。这将最初可复制的代码变得并不重要,重要的是做了一些关键的调整:
- 从
v-model
改变:value
和@input
事件处理 - 在
props
中添加value
和schema
- 实现
updateForm
FormGenerator
组件变成这样:
<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>
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
:
<form-generator :schema="schema" v-model="formData"></form-generator>
import FormGenerator from '@/components/v5/FormGenerator'
export default {
name: "GeneratorDemo",
components: { FormGenerator },
data() {
return {
formData: {
firstName: 'Evan'
},
schema: [{ /* .... */ },
}
}
}
现在,你已经了解了表单生成器。通过这个简单的示例学习了如何利用Vue的动态组件的基础来创建一些高度动态,数据驱动的UI。文章中整个示例的代码可以在GitHub上获取到,或者在CodeSandbox上进行修改。如果你有任何问题或想要聊的地方,可以通过Twitter、GitHub或电子邮件联系我。你也可以在下面的评论中与我们一起探讨相关的话题。
如需转载,烦请注明出处:https://www.w3cplus.com/vue/how-to-create-data-driven-user-interfaces-in-vue.html