講了這麼多,也該是要造個輪子練練手感了。事實上使用 Vue 等前端框架,最主要就是 component 可以重複利用,所以練手感歸練手感,如果沒有太多的 UI/UX 需求,可以多多使用開源 components,將時間放在刀口上 (例如癱在床上吸個貓 XD)。另外因為接下來是手把手造輪子,所以各位鐵人大大如果覺得囉嗦,可以直接到這裡看完整的 code!
在大多數的系統中都會有表單的「新增」、「修改」和「檢視」三種模式,今天要來寫個 component 讓我們只要痛苦一次,之後各種好用 (我們會用到部分 iview 的 components) !
規劃一下,form 上的每個欄位,基本上會有:
所以我們在 component 建立 Field.vue。
export default {
    name: 'field',
    props: {
        label: { type: String, default: '' },
        type: { type: String, default: 'text' },
        value: { required: true },
        errorMessages: { type: Array, default: () => { return []; } },
    },
    data() {
        return {
            // 由於我們欄位值會變動,所以要把 props 中的 value 賦值到 data 中的屬性
            fieldValue: this.value,
        };
    }
}
<template> 當中 (這裡以 text, password, select, datetime 作為範例)。<template>
    <div>
        <!-- text -->
        <template v-if="type === 'text'">
            <input type="text" v-model="fieldValue" />
        </template>
        <!-- password -->
        <template v-if="type === 'password'">
            <input type="password" v-model="fieldValue" />
        </template>
        <!-- select -->
        <template v-if="type === 'select'">
            <select v-model="fieldValue">
            </select>
        </template>
        <!-- datetime -->
        <template v-if="type === 'datetime'">
            <DatePicker type="date" v-model="fieldValue">
            </DatePicker>
        </template>
    </div>
</template>
Field.vue 增加 options 屬性。export default {
    name: 'field',
    props: {
        // ...
        options: { type: Object, default: () => { return {}; }},
    },
    // ...
}
options 補齊 <template> 裡的資料<template>
    <div>
        <!-- ... -->
        <!-- select -->
        <template v-if="type === 'select'">
            <select v-model="fieldValue">
                <option v-for="(item, index) in options.selectOptions" :key="index"
                :value="item.value">
                    {{item.label}}
                </option>
            </select>
        </template>
        <!-- datetime -->
        <template v-if="type === 'datetime'">
            <DatePicker :type="options.type || 'date'"
                v-model="fieldValue" :format="options.formate">
            </DatePicker>
        </template>
    </div>
</template>
fieldValue 製作 v-model 雙向綁定。export default {
    // ...
    watch: {
        fieldValue() {
            this.inputEmit();
        }
    },
    methods: {
        inputEmit() {
            this.$emit('input', this.fieldValue);
        },
    }
}
<template>
    <div>
        <div>
            <div class="label inline-block">{{label}}</div>
            <div class="field inline-block">
                <!-- text ... -->
                <!-- password ... -->
                <!-- select ... -->
                <!-- datetime ... -->
        </div>
        <div>
            <ul>
                <li v-for="(message, index) in errorMessages" :key="index">
                    {{message}}
                </li>
            </ul>
        </div>
    </div>
</template>
page 底下新增 user/_id/_type/index.vue 來測試看看吧!<template>
    <div>
        <div class="form">
            <field v-for="(field, key) in registerForm.schema" :key="key"
                :label="field.label"
                :type="field.type"
                v-model="registerForm.data[key]"
                :errorMessages="registerForm.errorMessages[key]"
                :options="field.options">
            </field>
        </div>
    </div>
</template>
<script>
import Field from '../components/Field';
export default {
    name: 'index',
    components: {Field},
    data() {
        return {
            registerForm: undefined,
        };
    },
    async asyncData() {
        const registerForm = {
            schema: {
                name: { label: '姓名', type: 'text' },
                email: { label: 'Email', type: 'text' },
                password: { label: '密碼', type: 'password' },
                role: { label: '角色', type: 'select', options: {
                    selectOptions: [
                        { label: '管理者', value: 0 },
                        { label: '一般使用者', value: 1 },
                    ],
                } },
                effectiveAt: { label: '生效日期', type: 'datetime' },
            },
            data: {
                name: undefined,
                email: undefined,
                password: undefined,
                role: 1,
                effectiveAt: undefined,
            },
            errorMessages: {},
        };
        return { registerForm };
    }
}
</script>

至目前為止,透過 Field.vue 我們可以輕鬆透過資料快速建立表單,完成「新增」、「修改」,接下來再調整一下 Field.vue
props 中加入 edit 屬性:export default {
    // ...
    props: {
        // ...
        edit: { type: Boolean, default: true }
    }
    // ...
}
<template> 中,透過 v-if 增加顯示文字的部分:<template>
        <!-- ... -->
        <div class="field inline-block">
            <template v-if="edit">
                <!-- text ... -->
                <!-- password ... -->
                <!-- select ... -->
                <!-- datetime ... -->
            </template>
            <template v-else>
                <span>{{ fieldValue }}</span>
            </template>
        </div>
</template>
回到 user/_id/_type/index.vue,我們根據 _id 與 _type,來調整表單是用來「新增」、「修改」或是「檢視」
<template>
    <div>
        <div class="form">
            <field v-for="(field, key) in registerForm.schema" :key="key"
                :label="field.label"
                :type="field.type"
                v-model="registerForm.data[key]"
                :errorMessages="registerForm.errorMessages[key]"
                :options="field.options"
                :edit="toEdit">
            </field>
        </div>
    </div>
</template>
<script>
import Field from '~/components/Field';
// 模擬用的 API
const simulateAPI = (response, success = true) => {
    return new Promise((resolve, rejected) => {
        if (success) {
            resolve(response);
        } else {
            rejected(response);
        }
    });
};
export default {
    // ...
    async asyncData({ params, redirect }) {
        // 從 route 上面取得的參數 id 與 type
        const userId = params['id'];
        const type = params['type'];
        const toEdit = type === 'create' || type === 'edit';
        const getUserData = type === 'edit' || type === 'view';
        const registerForm = {
            schema: {
                // 移除 password 欄位,有需要再加上來即可
                // password: { label: '密碼', type: 'password' },
            },
            // ...
        };
        if (getUserData) {
            try {
                // 模擬呼叫 API 取得 data:
                const expectedResponse = {
                    name: 'albert',
                    email: 'account@test.org',
                    role: 1,
                    effectiveAt: '2019-09-27',
                };
                const allResponses = await Promise.all([
                    simulateAPI(expectedResponse),
                ]);
                registerForm.data = allResponses[0];
            } catch (error) {
                redirect('/');
            }
        } else {
            // 「新增表單」(註冊表單) 才需要用到
            registerForm.schema.password = { label: '密碼', type: 'password' };
            registerForm.schema.password_confirmation = { label: '確認密碼', type: 'password' };
        }
        return { registerForm, userId, toEdit };
    }
}
</script>
上面的範例現在只要透過 route 的改變,即可快速轉換成「新增」、「修改」和「檢視」三種不同狀態的表單,而 page component 就專注於取得資料即可。如果在系統中有各種表單資料需要填寫,透過 Field.vue component 就可以快速建置好三種不同類型的頁面。

Field.vue 這樣的寫法雖然方便但也是有缺點:
角色 欄位的值是顯示 1,這個部分 Field.vue 需要再依各個不同欄位類型,進行顯示調整。Field.vue 中,同時又包含「檢視」模式,雖然後續方便好使用,但在維護的部分可能就比較辛苦 (尤其時間久了之後,大概就只有上帝看得懂了),所以如何拆分、重構,就等到系統需求明確之後再重新考量。手把手寫範例果然又臭又長阿 XD,各位鐵人大大如果覺得麻煩可以直接到這裡看完整 code 就好。 今天表單範例就是讓大家練練手感,感覺一下使用 component 組合頁面是多麼愜意的事。在實際的系統開發上,是不是要使用開源的 components 或是有必要自己造輪子 (Field.vue) 還是老話一句:「視需求而定」!
明天沒意外會寫個完整的登入、登出以及透過 middleware 保護瀏覽頁面的小專案 (希望不要冨樫 ><" ),那就先這樣啦!