翻譯: 珈藍 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, 或郵件聯繫我。