嘿,今天是怎樣?
都沒有人交作業,是不是昨天的太小菜一疊了!
今天是昨天的延伸,
但說難也難不到哪裡去啦~
因為相信經過前面的兩天,
應該已經很清楚步驟了吧?
跟前兩天的相同,兔兔還是重新建立了一個專案,你們就看自己的決定囉,前置準備跳過!
首先,在專案裡的 ./src/components
資料夾中新增一個 AccordionMenuItem.vue
的元件:
完成後,增添以下內容:
<template>
</template>
<script>
export default {
name: "AccordionMenuItem",
}
</script>
一樣,把元件新增到畫面中,不過因為最後呈現出來的效果差異,所以我 App.vue
的樣式稍微修改了一下:
<template>
<div :class="[
'w-screen h-screen',
'flex flex-col',
'items-center',
'pt-5'
]"
>
<AccordionMenuItem />
</div>
</template>
<script>
import AccordionMenuItem from './components/AccordionMenuItem.vue'
export default {
data() {
return {
}
},
components: {
AccordionMenuItem,
}
}
</script>
OK,前面不重要的部分終於完成了
我們快速前進下一步驟!
在開始之前,我們可以先參考一下一般的手風琴選單是怎麼設計的:
(找不到好的圖,這張很小很模糊,抱歉。)
經過眼睛一眨 (?) 可以立馬歸納出幾點,就是選單項目左邊是字
,右邊是箭頭 icon
,然後項目的內容是可以被展開來顯示的
,也可以再收起來
。
那我們一步驟一步驟來!
其實我們只要完成一個項目就好了,
其他的項目用迴圈來完成。
首先,先來做出打開後的樣子:
<template>
<div class="w-80">
<label
:class="[
'px-4 py-2',
'border border-gray-300',
'hover:bg-gray-100',
'flex justify-between items-center',
'cursor-pointer',
'transition-all',
]"
>
<div>
選項
</div>
<div
:class="[
'w-9 h-9',
'hover:bg-gray-200',
'rounded-full',
'flex justify-center items-center',
'transition-all',
]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24" stroke="currentColor"
:class="[
'h-6 w-6',
'transition-all'
]"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</label>
<div
:class="[
'border border-t-0 border-gray-300',
'overflow-hidden transition-all'
]"
>
<div class="p-4 text-gray-600">
內容
</div>
</div>
</div>
</template>
<script>
export default {
name: "AccordionMenuItem",
}
</script>
這樣就有選單的雛形了!
資訊量可能較龐大了點,但是仔細看就會發現其實結構並不複雜~
實際上我們簡化一下,結構是這樣的:
<!-- 整個元件包裹起來 -->
<div>
<!-- 選單的選項 -->
<label>
<!-- 選項標題 -->
<div>
選項
</div>
<!-- 右邊的 icon 區塊 -->
<div>
<svg />
</div>
</label>
<!-- 選單內容區域 -->
<div>
<!-- 選單實際內容 -->
<div>
內容
</div>
</div>
</div>
是不是其實不複雜呢?
沒有問題的話,我們準備開始讓它動起來囉!
我們要先來完成的,就是選單收合的問題。
選單收合的實現,我們可以依靠 <input>
元素來完成。運用 <input>
元素且把類型設定成 checkbox
,我們就可以簡單的記錄開啟 / 關閉的狀況,也可以用 <label>
來觸發 <input>
元素的狀態改變,這樣就可以少寫很多 JS 的 onclick 了~
所以最基本的,把 <input>
加在 <label>
元素之中,且用運 tailwind 所提供的特別樣式 sr-only
來隱藏蹤跡:
<div class="w-80">
<label
:class="[
'px-4 py-2',
'border border-gray-300',
'hover:bg-gray-100',
'flex justify-between items-center',
'cursor-pointer',
'transition-all',
]"
>
+ <input type="checkbox" class="sr-only" />
<div>
選項
</div>
<div
:class="[
'w-9 h-9',
'hover:bg-gray-200',
'rounded-full',
'flex justify-center items-center',
'transition-all',
]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24" stroke="currentColor"
:class="[
'h-6 w-6',
'transition-all'
]"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</label>
<div
:class="[
'border border-t-0 border-gray-300',
'overflow-hidden transition-all'
]"
>
<div class="p-4 text-gray-600">
內容
</div>
</div>
</div>
加好之後,我們要在 vue 中使用 v-model 同步 <input>
的狀態並用變數記錄起來:
<template>
<div class="w-80">
<label
:class="[
'px-4 py-2',
'border border-gray-300',
'hover:bg-gray-100',
'flex justify-between items-center',
'cursor-pointer',
'transition-all',
]"
>
+ <input type="checkbox" class="sr-only" v-model="checked" />
<div>
選項
</div>
<div
:class="[
'w-9 h-9',
'hover:bg-gray-200',
'rounded-full',
'flex justify-center items-center',
'transition-all',
]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24" stroke="currentColor"
:class="[
'h-6 w-6',
'transition-all'
]"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</label>
<div
:class="[
'border border-t-0 border-gray-300',
'overflow-hidden transition-all'
]"
>
<div class="p-4 text-gray-600">
內容
</div>
</div>
</div>
</template>
<script>
export default {
name: "AccordionMenuItem",
+ data() {
+ return {
+ checked: false,
+ }
+ },
}
</script>
加好之後,我們把 checked 變數的狀態應用到以下三處:
checked && 'text-green-500'
和 transition-all
checked && '-rotate-180'
checked ? 'max-h-[300px]' : 'max-h-0'
(要使用最大高度或寬度,這樣不定寬度長度的內容才可以有過渡效果。)
那,就會是這個樣子:
<div class="w-80">
<label
:class="[
'px-4 py-2',
'border border-gray-300',
'hover:bg-gray-100',
'flex justify-between items-center',
'cursor-pointer',
'transition-all',
]"
>
<input type="checkbox" class="sr-only" v-model="checked" />
<div
:class="[
checked && 'text-green-500',
'transition-all'
]"
>
選項
</div>
<div
:class="[
'w-9 h-9',
'hover:bg-gray-200',
'rounded-full',
'flex justify-center items-center',
'transition-all',
]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24" stroke="currentColor"
:class="[
'h-6 w-6',
checked && '-rotate-180',
'transition-all'
]"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</label>
<div
:class="[
checked ? 'max-h-[300px]' : 'max-h-0',
'border border-t-0 border-gray-300',
'overflow-hidden transition-all'
]"
>
<div class="p-4 text-gray-600">
內容
</div>
</div>
</div>
有~ 非常好的效果~
但回想起在製作按鈕時的狀況就會不禁的覺得 ...
沒錯,選項中的內容替換還不方便!
所以我們當然就要用到 props 和 slot 啦!
我們一樣先來解決 slot 的部分吧~
為了達到超靈活的替換,我們要來做巢狀 slot!
這是目前的樣子:
<!-- 選項內容區塊 -->
<div
:class="[
checked ? 'max-h-[300px]' : 'max-h-0',
'border border-t-0 border-gray-300',
'overflow-hidden transition-all'
]"
>
<div class="p-4 text-gray-600">
內容
</div>
</div>
那現在終於可以來解釋為什麼選項內容區塊內又有一個 div,而且內距還是加在裡面那個 div 上了。
因為啊 ...
「因為什麼啦 ... ? 兔兔快說!」
因為這是我為了 slot 而預留的!
如果今天我們要做的是像原本的參考圖這樣子的話:
你可以注意到選單中的子選項是和外部沒有空隙的
,所以如果把內距加在外層,那麼裡面若是用 slot 插入其他元件時,就會留一個空隙
在那邊; 相反的,如果是直接寫文字在其中
,為了還要能插入元件而去掉內距,文字和邊框看起來又會太過接近
,很醜。
所以我們這邊要用具名的巢狀 slot
!
廢話不多說,我直接上範例:
<div
:class="[
checked ? 'max-h-[300px]' : 'max-h-0',
'border border-t-0 border-gray-300',
'overflow-hidden transition-all'
]"
>
<slot name="itemContent">
<div class="p-4 text-gray-600">
<slot name="itemText">
內容
</slot>
</div>
</slot>
</div>
這樣做很有趣哦!
如果我們只是想要在選項中加入純文字內容時,使用時只需要指名插槽 itemText
:
<!-- 使用時 -->
<AccordionMenuItem>
<template v-slot:itemText>
純文字選項內容
</template>
</AccordionMenuItem>
實際上渲染出來的內容就是這樣:
<div class="max-h-[300px] border border-t-0 border-gray-300 overflow-hidden transition-all">
<div class="p-4 text-gray-600">
純文字選項內容
</div>
</div>
但是,
如果今天我們要加入的是其他元件,我們只需要指定插槽名稱為 itemContent
:
<!-- 使用時 -->
<AccordionMenuItem>
<template v-slot:itemContent>
<v-link>連結 1</v-link>
<v-link>連結 2</v-link>
<v-link>連結 3</v-link>
</template>
</AccordionMenuItem>
實際上渲染出來的內容就是這樣:
<div class="max-h-[300px] border border-t-0 border-gray-300 overflow-hidden transition-all">
<v-link>連結 1</v-link>
<v-link>連結 2</v-link>
<v-link>連結 3</v-link>
</div>
這樣,不就能無痛解決那個空隙的問題了嗎?
是不是超級好玩的!
我每次寫起來都覺得很興奮呢~
那內容替換的問題解決了,我們處理 props 的部分啦!
那 Props 部分我們目前只需要傳入項目名稱而已,所以就增加吧!然後記得,要有預設內容哦:
<script>
export default {
name: "AccordionMenuItem",
props: {
itemName: {
default: "選項",
}
},
data() {
return {
checked: false,
}
},
}
</script>
然後,記得把 props 的內容應用到 template 上:
<div class="w-80">
<label
:class="[
'px-4 py-2',
'border border-gray-300',
'hover:bg-gray-100',
'flex justify-between items-center',
'cursor-pointer',
'transition-all',
]"
>
<input type="checkbox" class="sr-only" v-model="checked" />
<div
:class="[
checked && 'text-green-500',
'transition-all'
]"
>
{{ itemName }}
</div>
<div
:class="[
'w-9 h-9',
'hover:bg-gray-200',
'rounded-full',
'flex justify-center items-center',
'transition-all',
]"
>
...
那這樣,感覺都完成了~
我們就快來測試吧!
為了測試,兔兔這邊已經先寫好兩組資料了~大膽的拿去用吧!
list: {
groupName: "Abouts",
items: [
{ name: "關於兔兔教", content: "不是邪教,但不太正常 (?)。 不過可以為你在此獻上教義 ... (住嘴!)"},
{ name: "關於 Tailwind CSS", content: "超讚了啦,不用真是太可惜了!"},
{ name: "關於手風琴", content: "流浪到淡水時有機會可以看到。"},
{ name: "關於兔兔", content: "你想知道的太多了,去擲筊問神吧!!!"}
]
},
faq: {
groupName: "FAQ",
links: [
{ name: "四大超商", contents: [
"7-11","全家","萊爾富","OK",
]},
{ name: "付款方式", contents: [
"現金","ATM","信用卡","LINE PAY","五倍券",
]},
{ name: "取貨方式", contents: [
"宅配","超商取貨"
]},
]
}
加到 App.vue
的 data 中之後,我們就來開心快樂測試元件吧!
首先是用關於我們
的資料,看資料的內容會發現只是純文字,所以我們就用 v-for 來遍歷內容,然後把 content 差在指定插槽 itemText
吧:
<AccordionMenuItem
v-for="item in list.items"
:key="list.groupName.concat('-',item.name)"
:itemName="item.name"
>
<template v-slot:itemText>
{{ item.content }}
</template>
</AccordionMenuItem>
有,馬上就像樣了~
趁著手感還沒消失把第二個也做出來吧!
第二個我們就需要自己寫一個像清單的外框,然後指定插槽到 itemContent
:
<AccordionMenuItem
v-for="link in faq.links"
:key="faq.groupName.concat('-',link.name)"
:itemName="link.name"
>
<template v-slot:itemContent>
<div
:class="[
'p-3',
'first:border-0 border-t',
'hover:bg-gray-100 transition-all'
]"
v-for="content in link.contents"
:key="content"
>
{{ content }}
</div>
</template>
</AccordionMenuItem>
就會像這樣了! 完成~
超級方便的對吧? 對吧?
我想你一定也覺得很方便,那就拿去用吧~
(欸你這兔,少強迫推銷了)
好,那麼今天的部分結束囉~
下一個元件是 ... 簡易日曆!
有沒有覺得越來越難呀?
「沒有!」
沒有嗎 ... 非常好!
那就把作業交上來吧 XD
關於兔兔們:
( # 兔兔小聲說 )
聽說兔兔這禮拜要去聚會,
不知道有看到兔兔真面目的朋友會不會失望?