用 Vue 包一個 List Component

  1. 用 Vue 包一個 List Component
    1. List = 編輯 Array 的界面
    2. 承前一篇,v-for BookForm
    3. 設計 List 元件
    4. 最後

用 Vue 包一個 List Component

這一篇包 Component 的前置知識,要先看一下鐵人賽的 Vue.js 進階心法
看完之後,在前端框架自訂 Component 的問題上,會類似「自定義型別」或「抽象資料型」的這種思考上。[1]

先了解到前端在處理資料時,在 Vue 官網教學上,其實學到了處理簡單型別的資料處理方式,將簡單型別透過 v-model 綁在畫面上的位置,就可以同時顯示資料,也編輯資料。

簡真實案例,可以說就是在處理 Object 與 Array 的問題,並不只是簡單型別而已。雖然可能可以將 Object 拆解成很多簡單型別進行各別欄位綁定來解決問題,上一篇試圖著保有物件型別的概念來處理問題。而這一篇,針對 Array 這個型別來處理。一樣希望可以保有「型別」這個概念來處理畫面。

List = 編輯 Array 的界面

第一個想法,就是處理 Array 的編輯問題,但是前端在處理 Array 的方式其實並沒有特定的做法,反而常見的做法是用 Table 來處理。
但是 Table 並不支援 RWD (要也只是讓它可以左右滑動),通常也不支援可以編輯的情況 (要也可以,但是就是定義在各自的欄位,綁上複雜的編輯函數)。

如何設計一個好的 List Component 就成為了這一篇文章的重點。
在讀 code 上會有更多的資訊可以讀,也希望在界面上有更多的隱喻可以了解實作。

以上一篇的物件為元素組成的陣列為例,這是一個「書籍」的陣列,裡頭的元素是物件。這依然是一個「抽象資料型別」,現在拿出紙筆畫一下,如果要將它變成列表,該怎麼做呢?。

1
2
3
4
5
6
7
8
9
10
11
[{
"name": "從自學到成功轉職軟體工程師",
"isbn": "6263330120",
"publish_date": "2022-01-24",s
"publish_house": "博碩文化",
"pages": 264,
"set_price": 520,
"author": "Chris"
}, {
...
}]

我想讓它成為重複出現的表單,這樣既可以編輯,也可以顯示,符合更多的可能性。
也符合真實案例的複雜程度,畫面就如同這樣。

那如果我們要用 vue 來寫要怎麼做呢?

承前一篇,v-for BookForm

List 需要的編輯行為: 新增元素、編輯元素、移除元素,顯示所有內容。在之後的設計上,會讓這些 method 跟著 List 這個 component。
在此就不管 v-model 的做法。直接使用「把 v-model 拆開」的 BookForm 接續下一步。

/src/views/Document.vue

<div class="book_list">
<div class="books" v-for="(form, index) in list" :key="form.isbn">
<BookForm
:data="form"
@update:name="onUpdateOneOfList($event, index)"
@update:isbn="onUpdateOneOfList($event, index)"
@update:publish_date="onUpdateOneOfList($event, index)"
@update:publish_house="onUpdateOneOfList($event, index)"
@update:pages="onUpdateOneOfList($event, index)"
@update:set_price="onUpdateOneOfList($event, index)"
@update:author="onUpdateOneOfList($event, index)"
>
</BookForm>
<div style="margin-left: 3.5em;">
<button class="delete" @click="onRemoveOneOfList(index)">X</button>
</div>
</div>
<div style="margin-top: .5em;">
<button class="create" @click="onAddOneToList"></button>
</div>
</div>
import { ref } from 'vue';
import BookForm from '@/components/BookForm.vue';

const list = ref([{
"name": "從自學到成功轉職軟體工程師",
"isbn": "6263330120",
"publish_date": "2022-01-24",
"publish_house": "博碩文化",
"pages": 264,
"set_price": 520,
"author": "Chris"
}, {
"name": "金魚都能懂的 CSS 必學屬性",
"isbn": "9864348825",
"publish_date": "2022-05-03",
"publish_house": "博碩文化",
"pages": 480,
"set_price": 720,
"author": "李建杭(Amos Li)"
}, {
"name": "約耳再談軟體",
"isbn": "6263246103",
"publish_date": "2023-09-14",
"publish_house": "碁峰資訊",
"pages": 336,
"set_price": 580,
"author": "約耳"
}])

function onAddOneToList() {
list.value = [
...list.value,
{
"name": "",
"isbn": "",
"publish_date": "",
"publish_house": "",
"pages": 0,
"set_price": 0,
"author": ""
},
]
}

function onUpdateOneOfList(one, index) {
list.value = [
...list.value.slice(0, index),
one,
...list.value.slice(index + 1),
]
}

function onRemoveOneOfList(index) {
list.value = [
...list.value.slice(0, index),
...list.value.slice(index + 1),
]
}

設計 List 元件

包成 Component 之後。

/src/views/Document.vue

為了讓 List 成為 component 而且元素型別是可以抽換的,並且這個 component 本身就可以新增、刪除、修改元素。
在刪除與修改元素時,還可以把 index 給換掉。

template

<List v-slot="{ form, update }" 
:list="list"
@add="onUpdateList"
@del="onUpdateList"
>
<BookForm
:data="form"
@update:name="onUpdateList(update($event))"
@update:isbn="onUpdateList(update($event))"
@update:publish_date="onUpdateList(update($event))"
@update:publish_house="onUpdateList(update($event))"
@update:pages="onUpdateList(update($event))"
@update:set_price="onUpdateList(update($event))"
@update:author="onUpdateList(update($event))"
></BookForm>
</List>

script

維持資料在 view 決定的角度來設計 component,讓 BookForm 成為 pure component

import { ref } from 'vue';
import BookForm from '@/components/BookForm.vue';

const list = ref([{
"name": "從自學到成功轉職軟體工程師",
"isbn": "6263330120",
"publish_date": "2022-01-24",
"publish_house": "博碩文化",
"pages": 264,
"set_price": 520,
"author": "Chris"
}, {
"name": "金魚都能懂的 CSS 必學屬性",
"isbn": "9864348825",
"publish_date": "2022-05-03",
"publish_house": "博碩文化",
"pages": 480,
"set_price": 720,
"author": "李建杭(Amos Li)"
}, {
"name": "約耳再談軟體",
"isbn": "6263246103",
"publish_date": "2023-09-14",
"publish_house": "碁峰資訊",
"pages": 336,
"set_price": 580,
"author": "約耳"
}])

function onUpdateList(new_list) {
list.value = new_list
}

/src/component/BookForm.vue

把原本 BookForm 的位置換成 slot。
從 slot 傳出 form 與 修改 form 的 onUpdateOneOfList

template

1
2
3
4
5
6
7
8
9
10
11
<div class="book_list">
<div class="books" v-for="(form, index) in list" :key="form.isbn">
<slot :data="form" :update="onUpdateOneOfList(index)"></slot>
<div style="margin-left: 3.5em;">
<button class="delete" @click="onRemoveOneOfList(index)">X</button>
</div>
</div>
<div style="margin-top: .5em;">
<button class="create" @click="onAddOneToList"></button>
</div>
</div>

script

把原本 method 裡改值的部份,改成吐出與 props.list 相同型別 (Array) 的事件。
將 onUpdateOneOfList 改成閉包,將 index 封裝在 List 裡面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const props = defineProps({
list: {
type: Array,
required: true
}
})

const emits = defineEmits([
'add', 'del', 'update'
])

function onAddOneToList() {
emits('add', [
...props.list.value,
{
"name": "",
"isbn": "",
"publish_date": "",
"publish_house": "",
"pages": 0,
"set_price": 0,
"author": ""
}
])
}

function onUpdateOneOfList(index) {
return function (one) {
emits('update', [
...props.list.value.slice(0, index),
one,
...props.list.value.slice(index + 1),
])
}
}
function onRemoveOneOfList(index) {
emits('del', [
...props.list.value.slice(0, index),
...props.list.value.slice(index + 1),
])
}

優點:

  • 可以將 List 的行為,跟著 List 的 component,編輯與 Form 的編輯相容。
  • 符合相同型別進出(props/emits) component 。
  • 外部使用可以直接撰寫,不用由實作決定 $event 是什麼,直接在設計上決定它是相同型別物件 (Array)。

缺點:

  • 沒有使用的 v-model 處理雙向綁定,將 v-model 拆開寫,需要注意怎麼拆。

最後

對於畫面的修改邏輯、與排版方式都可以封裝在 component 是基本的要求。
而在這個範例中,兩個版本對於物件的修改行為,都做到 component 上面了。
只是核心概念不太一樣,最後實作的方式也就不太一樣。

希望可以找到屬於你自己面對前端時,最佳解決處理資料與畫面的方案。


  1. 《CODE COMPLETE:軟體開發實務指南, 2/e (中文版) 》, 6.1 類別的基礎:抽象資料類型(ADTs) ↩︎