iT邦幫忙

2021 iThome 鐵人賽

DAY 27
2
Modern Web

排版神器 Tailwind CSS~和兔兔一起快速上手漂亮的元件開發!系列 第 28

Day 27:「流浪到淡水!」- 手風琴選單

Day27-Banner

嘿,今天是怎樣?
都沒有人交作業,是不是昨天的太小菜一疊了!

今天是昨天的延伸,
但說難也難不到哪裡去啦~

因為相信經過前面的兩天,
應該已經很清楚步驟了吧?

跟前兩天的相同,兔兔還是重新建立了一個專案,你們就看自己的決定囉,前置準備跳過!

carrotPoint 建立空白元件

首先,在專案裡的 ./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,前面不重要的部分終於完成了
我們快速前進下一步驟!
 

carrotPoint 手風琴

在開始之前,我們可以先參考一下一般的手風琴選單是怎麼設計的:

(找不到好的圖,這張很小很模糊,抱歉。)

經過眼睛一眨 (?) 可以立馬歸納出幾點,就是選單項目左邊是字右邊是箭頭 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>

是不是其實不複雜呢?

沒有問題的話,我們準備開始讓它動起來囉!
 

carrotPoint 動起來

我們要先來完成的,就是選單收合的問題。

選單收合的實現,我們可以依靠 <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 的部分吧~
為了達到超靈活的替換,我們要來做巢狀 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

那 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',
        ]"
      >
  ...

那這樣,感覺都完成了~
我們就快來測試吧!
 

carrotPoint 測試時間

為了測試,兔兔這邊已經先寫好兩組資料了~大膽的拿去用吧!

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

carrotPoint 給你們的回家作業:


關於兔兔們:


 


( # 兔兔小聲說 )

聽說兔兔這禮拜要去聚會,
不知道有看到兔兔真面目的朋友會不會失望?


上一篇
Day 26:「按鈕博物館」- 輕鬆變化各種按鈕元件
下一篇
Day 28:「今天幾月幾號啊?」- 簡易日曆
系列文
排版神器 Tailwind CSS~和兔兔一起快速上手漂亮的元件開發!32

1 則留言

0
孤獨一隻雞
iT邦新手 3 級 ‧ 2021-09-29 00:19:41

還真的有流浪到淡水系列

搋兔 iT邦新手 5 級 ‧ 2021-09-29 01:23:52 檢舉

當然,那可是我與手風琴的邂逅之地

我要留言

立即登入留言