iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 26
0
Modern Web

RRR撞到不負責之 Laravel + Nuxt.js 踩坑全紀錄系列 第 26

Day 26. 手把手造個輪子 - Form 表單 (文長慎入)

講了這麼多,也該是要造個輪子練練手感了。事實上使用 Vue 等前端框架,最主要就是 component 可以重複利用,所以練手感歸練手感,如果沒有太多的 UI/UX 需求,可以多多使用開源 components,將時間放在刀口上 (例如癱在床上吸個貓 XD)。另外因為接下來是手把手造輪子,所以各位鐵人大大如果覺得囉嗦,可以直接到這裡看完整的 code!

在大多數的系統中都會有表單的「新增」、「修改」和「檢視」三種模式,今天要來寫個 component 讓我們只要痛苦一次,之後各種好用 (我們會用到部分 iview 的 components) !

  1. 規劃一下,form 上的每個欄位,基本上會有:

    • 欄位顯示名稱 (field label)
    • 欄位類型 (field type)
    • 欄位值 (field value)
    • 錯誤訊息 (error messages)
  2. 所以我們在 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,
        };
    }
}
  1. 接著,我們可以把常見的 form 資料類型加到 <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>
  1. 我們在上個步驟中,可以看到我們少了 placeholder、select 的 options 資料以及第三方套件 component 需要的特殊屬性資料,因此我們為 Field.vue 增加 options 屬性。
export default {
    name: 'field',
    props: {
        // ...
        options: { type: Object, default: () => { return {}; }},
    },
    // ...
}
  1. 利用 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>
  1. 由於欄位資料是由外面給進來的,而實際要發送 API 也是在上層 component 發出的,因此要透過監看 fieldValue 製作 v-model 雙向綁定。
export default {
    // ...
    watch: {
        fieldValue() {
            this.inputEmit();
        }
    },
    methods: {
        inputEmit() {
            this.$emit('input', this.fieldValue);
        },
    }
}
  1. 接著再把「欄位顯示名稱」和「錯誤訊息區塊」補上,就有個七分樣可以使用了!
<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>
  1. 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>

https://gitlab.com/semantic-lab/2020-it-30/raw/master/images/day-25/form.gif

  1. 至目前為止,透過 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>
    
  2. 回到 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 就可以快速建置好三種不同類型的頁面。

https://gitlab.com/semantic-lab/2020-it-30/raw/master/images/day-25/form-complete.gif

Field.vue 這樣的寫法雖然方便但也是有缺點:

  1. 在 「檢視」模式下,角色 欄位的值是顯示 1,這個部分 Field.vue 需要再依各個不同欄位類型,進行顯示調整。
  2. 把各種資料類型 component 都放到 Field.vue 中,同時又包含「檢視」模式,雖然後續方便好使用,但在維護的部分可能就比較辛苦 (尤其時間久了之後,大概就只有上帝看得懂了),所以如何拆分、重構,就等到系統需求明確之後再重新考量。

手把手寫範例果然又臭又長阿 XD,各位鐵人大大如果覺得麻煩可以直接到這裡看完整 code 就好。 今天表單範例就是讓大家練練手感,感覺一下使用 component 組合頁面是多麼愜意的事。在實際的系統開發上,是不是要使用開源的 components 或是有必要自己造輪子 (Field.vue) 還是老話一句:「視需求而定」!

明天沒意外會寫個完整的登入、登出以及透過 middleware 保護瀏覽頁面的小專案 (希望不要冨樫 ><" ),那就先這樣啦!


上一篇
Day 25. 說好的 window 和 document 呢?
下一篇
Day 27. 居家旅行、殺人滅口,必備良藥 -「註冊」、「登入」與「登出」
系列文
RRR撞到不負責之 Laravel + Nuxt.js 踩坑全紀錄31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言