iT邦幫忙

2021 iThome 鐵人賽

DAY 19
1
Modern Web

不只懂 Vue 語法:Vue.js 觀念篇系列 第 19

不只懂 Vue 語法:請示範如何使用 Vue 3 的 teleport?

  • 分享至 

  • xImage
  •  

問題回答

teleport 是 Vue 3 新增功能。teleport 就像是多啦A夢的「隨意門」一樣,只要設定了 teleport,就能夠把 DOM 內容隨意塞去畫面上任個地方。例如當使用遮蓋整個網頁畫面的 lightbox 或 modal 元件時,把它們塞到 body 讓渲染就非常方便。

基本語法

今天來點輕鬆的學習。teleport 的使用很簡單,只需要把要移動的元素包起來即可:

<teleport to="body">
    <p> 移動我到 body </p>
</teleport>

Teleport 有兩個屬性:

  • to:移動的目的地。
  • disabled
    • true(預設):元素不會被移動。
    • false:元素會被移動到 to 指定的位置。

使用 to 屬性來指定目的地:

<teleport to="body"></teleport>
<teleport to=".box"></teleport>
<teleport to="#box"></teleport>

簡單示範:

程式碼:

<div class="box"></div>
<button @click="isDisabled = !isDisabled">移動</button>
<teleport to=".box" :disabled="isDisabled">
    <p>移動我到 box</p>
</teleport>
data() {
    return {
      isDisabled: true,
    };
}
.box {
  background: pink;
  height: 300px;
}

本身 p 是在 box 以外:

移動後:

配合使用 disabled,保留資料狀態

當動態切換 disabled 時,該元素的資料狀態會得以保留,例如我在旁邊加上點擊次數,每次點擊時都會累加 1:

可以有多個 teleport 指向同一個目的地

最後,我們可以在多個元素上使用 teleport,並移動到同一目的地:

<div class="box"></div>

<teleport to=".box">
    <p>文字 1</p>
</teleport>
<teleport to=".box">
    <p>文字 2</p>
</teleport>

結果就會在 .box 裏,依序出現「文字1」、「文字2」

常用例子:fullscreen modal

網絡上有很多例子都是示範 fullscreen modal。當我們要設定 fullscreen modal 時,通常會用到 position: fixed 來設置 modal。舉例說:

<div class="box">
    <Modal />
</div>

.box 是父層,如果 .box設定了 transitionperspectivefilter 屬性時,<Modal /> 就無法以 viewport 作定位,只會以 .box 作定位。以致整個 modal 會跑版。

postion:fixed 的 MDN 解釋:

以打開縮圖瀏覽圖片為例,每個Image 元件裏都有一個 Lightbox 元件,用作放大顯示這個圖片。

App.vue

<template>
  <div class="img-area">
    <Image v-for="url in urls" :key="url" :url="url" />
  </div>
</template>
  data() {
    return {
      urls: [
        "https://images.unsplash.com/photo-1632774240308-f54b5e4145ef?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1588&q=80",
        "https://images.unsplash.com/photo-1632714657775-232ce19778a7?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1635&q=80",
        "https://images.unsplash.com/photo-1632753043704-280dd515e81a?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1587&q=80",
        "https://images.unsplash.com/photo-1627322307804-c87605cf3a89?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1665&q=80",
      ],
    };
  },
.img-area {
  /* 父層加了 filter,以致 lightbox 跑版 */
  filter: drop-shadow(16px 16px 20px grey);
}

Image.vue

<template>
  <a href="#" @click="isOpen = true">
    <img :src="url" alt="" />
  </a>
  <Lightbox :url="url" v-if="isOpen" @click="closeLightBox" />
</template>

Lightbox.vue

<template>
  <div class="lightbox">
    <img :src="url" alt="">
  </div>
</template>
<style scoped>
.lightbox {
  background: rgba(0, 0, 0, 0.3);
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

img {
  width: 500px;
  height: 500px;
  object-fit: cover;
}
</style>

因為 App.vue .img-area 加了 filter 屬性,因此,Lightbox 不再以 viewport 為錨點,變為以 .img-area 為錨點,以致跑版:

為了解決以上情況,可以使用 teleport 把 Lightbox 元件移動到 body 渲染:

Image.vue

<teleport to="body">
    <Lightbox :url="url" v-if="isOpen" @click="closeLightBox" />
</teleport>

結果:

完整程式碼

https://codesandbox.io/s/teleport-image-modal-wyrst?file=/src/components/Lightbox.vue:176-442

加強版 image gallery

藉這些機會再練習一下 Vue,改良目前這個範例:

  • 在圖片下會有按鈕直接打開 Lightbox。
  • 在 Lightbox 裏也要顯示所有 thumbnail,點擊時可以切換呈現其他圖片。

程式碼的改動:

  • 我把 Lightbox 拆出來,不再放在每個 image 元件裏,以及設定所需的 emit 事件來實現功能。
  • 這次測試在父層的 CSS 加上 transform 屬性作測試。

結果

完整程式碼

https://codesandbox.io/s/teleport-image-gallery-k13u5

總結

  • teleport 是 Vue 3 新增的功能。作用是把一個 DOM 元素移動到指定地方渲染。
  • teleport 可設定 disabled 屬性,決定是否要移動到 to 屬性所指定的位置。並且會保留資料狀態。
  • 可以有多個 teleport 指向同一個目的地。
  • teleport 常用於 fullscreen modal 這些功能上。

參考資料

Vue 3 - Teleport
Vue3 Composition API - Teleport 瞬移!
重新認識 Vue.js - 編譯作用域與 Slot 插槽


上一篇
不只懂 Vue 語法:什麼是 slot?請示範 slot 的用法?
下一篇
不只懂 Vue 語法:試解釋 computed、watch 與 methods 的差異?
系列文
不只懂 Vue 語法:Vue.js 觀念篇31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言