翻譯: 珈藍 from 迅雷前端
翻譯自 Evan Schultz 的文章 Do it with Elegance: How to Create Data-Driven User Interfaces in Vue
本文演示瞭如何利用 Vue 的動態元件根據 schema 來生成一個動態的表單生成器,在管理後臺、設定中心等類似的場景中,你完全可以利用這種思路來更效率地開發介面。
雖然我們通常在構建大部分的檢視時知道需要用到哪些元件,但有時我們直到執行時才知道它們是什麼元件(譯者注:動態元件)。這意味著我們需要基於應用程式狀態、使用者設定或來自 API 請求的響應結果來構建檢視。一個常見的情況是構建動態表單,其中所需的問題和元件由 JSON 物件配置,或者欄位根據使用者的答案進行更改。
所有現代的 JavaScript 框架都有處理動態元件的方法。這篇文章將向你展示如何在 Vue.JS 中實現它,它為上面的場景提供了一個非常優雅簡單的解決方案。
一旦你看到用 Vue.JS 實現它是多麼的簡單,你可能會受到啟發並且開始思考你以前從未考慮過的動態元件應用。
我們要先學會走才能學會跑,所以首先我將介紹動態元件的基礎知識,然後深入討論如何使用這些概念構建你自己的動態表單構造器。
基礎
Vue 有一個叫做 <component>
的內建元件,你可以在VueJS 指南的動態元件中瞭解完整的詳細資訊。
指南上寫道:
“你可以使用相同的掛載點並使用保留的元素在多個元件之間動態切換,並動態繫結到其 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 屬性或計算屬性上。
關於 props 和事件
元件不是孤立地存在,它們需要一種方式與周圍的世界交流。在 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,那我需要預先知道 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, 或郵件聯絡我。