Vue - Extend Vue component with values for props and slots

  • 1843
  • 0
  • Vue
  • 2020-12-12

談討關於繼承組件以及覆寫 props 與 slots!

 

關於重新包裝組件的方式,本篇圍繞著 Render Functions 與 Template。

 

 

前言

在工作誤打誤撞下,開始接觸 Vue 一陣子,

在環境中使用了 Quasar framework

在組件高複用下,經常為了統一樣式,增加不少行數,

因此筆者把常用的組件設定樣式後再封裝一次。

 

為了讓組件能夠持續使用原有的 props 以及 slot

設定上遇到了不少困難。

 

本文從理解簡單的 component 建立,

接著實作 components extends

以及 template  render functoins 的方式。

 

由於筆者屬於新手入門,當然本篇也以初學者的角度分享,

環境是使用 options api 的方式,所以不探討 composition api

 

專案範例放置於:

https://github.com/explooosion/vue-extend-slot-example

 


 

目錄

 


 

前置作業

本專案使用 @vue/cli 建立

npm install -g @vue/cli @vue/cli-service-global
# or
yarn global add @vue/cli @vue/cli-service-global
vue create hello-world

為了簡單使用 fontawesome,在 index.html 新增 CDN

[ public / index.html ]


<link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
    integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous" />

 


 

ㄧ、簡單的組件建立

簡單建立一個 Button 的 component。

利用 v-on, v-bind 方式將 listeners attrs 綁給 button

在範例組件中,筆者提供了:

  • 3 個 props ( type, label, bold )
  • 2 種 slots ( default, #after )
  • 1 個 event ( greet )

[ Button.vue ]

<template>
  <button
    v-on="$listeners"
    v-bind="$attrs"
    :type="type"
    :style="`font-weight: ${bold ? 'bold' : 'normal'}`"
    @click="$emit('greet', 'Hello Vue')"
  >
    <slot></slot>
    {{ label }}
    <slot name="after"></slot>
  </button>
</template>
export default {
  name: "Button",
  props: {
    type: {
      type: String,
      default: "button",
    },
    label: {
      type: String,
      default: "",
    },
    bold: {
      type: Boolean,
      default: false,
    },
  },
};
  • type: buttoon type [ button, submit, reset ]
  • label: button 的文字
  • bold: 如果 true 則為粗體字

 

使用範例:

[ App.vue ]

<!-- with props -->
<Button label="Button" />

<!-- with props, default slot -->
<Button label="Submit" type="submit" :bold="true">#</Button>

<!-- with props, after slot -->
<Button label="Reset" type="reset" :bold="true">
  <template #after>@</template>
</Button>

 

畫面預覽:

 


 

二、使用 components 包裝 Button

使用 components 的方式可以想成你又把 Button 組件再包一層起來。

一個 wrapper 的概念,而 DOM 實際上只會有一層 element render 出來

 

假如想重新封裝一次 Button,並且預先指定好 props slots

以下範例筆者讓 label 為 Resettype reset

並且 slot #after 塞入了一個 redo icon。

[ ButtonTemplate.vue ]

<template>
  <Button :label="label" :type="type" v-on="$listeners" v-bind="$attrs">
    <template #after><i class="fas fa-redo"></i></template>
  </Button>
</template>
import Button from "./Button";

export default {
  name: "ButtonTemplate",
  components: {
    Button,
  },
  props: {
    type: {
      type: String,
      default: "reset",
    },
    label: {
      type: String,
      default: "Reset",
    },
  },
};

 

三種使用範例:

[ App.vue ]

<!-- basic -->
<ButtonTemplate @greet="onGreet" />

<!-- with props -->
<ButtonTemplate
  label="New Reset"
  type="reset"
  :bold="true"
  @greet="onGreet"
/>

<!-- with slots -->
<ButtonTemplate @greet="onGreet">
  <template><i class="fas fa-sync-alt"></i></template>
  <template #after><i class="fas fa-sync-alt"></i></template>
</ButtonTemplate>

 

畫面預覽:

 

關於上述 3 種使用情境:

1. basic

直接使用我們重新封裝過的,沒甚麼問題。

 

2. with props

再次傳遞 Button 組件提供的 props,在外觀上似乎沒問題,

但是查看 DOM tree,bold 被誤當 attrs 加上去了。

因為我們沒有在 ButtonTemplate.vue 告知他是 props

 

這時你可以把 Button 的 props 透過 spread 方式: ...Button.props 補進來:

[ ButtonTemplate.vue ]

import Button from "./Button";

export default {
  name: "ButtonTemplate",
  components: {
    Button,
  },
  props: {
    ...Button.props, // add here
    type: {
      type: String,
      default: "reset",
    },
    label: {
      type: String,
      default: "Reset",
    },
  },
};

 

重新檢視原始碼,就不會被當屬性顯示!

 

雖然沒被當屬性顯示,但我們傳遞的 bold 也失效了,

因為你接收了 props: bold 但沒做任何處理。

 

沒有成功接收到 props: bold 的畫面:

 

這時你可以在 v-bind 補上 $options.propsData 就沒問題囉!

[ ButtonTemplate.vue ]

<template>
  <Button
    :label="label"
    :type="type"
    v-on="$listeners"
    v-bind="[$attrs, $options.propsData]"
  >
    <template #after><i class="fas fa-redo"></i></template>
  </Button>
</template>

 

成功後的畫面:

 

3. with slots

這段我們將 default, 與 name 為 #after 的 slots 傳遞進去,但很明顯地,並沒有成功傳入。

可以嘗試在 mounted 印出 this.$scopedSlots,可以發現多了 after default 

 

我們可以利用 v-for 將這段 slots 補進來:

[ ButtonTemplate.vue ]

<template>
  <Button :label="label" :type="type" v-on="$listeners" v-bind="$attrs">
    <template #after><i class="fas fa-redo"></i></template>
    <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
      <slot :name="slot" v-bind="scope" />
    </template>
  </Button>
</template>

畫面預覽:

 

完整的 components 方式:

[ ButtonTemplate.vue ]

<template>
  <Button
    :label="label"
    :type="type"
    v-on="$listeners"
    v-bind="[$attrs, $options.propsData]"
  >
    <template #after><i class="fas fa-redo"></i></template>
    <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
      <slot :name="slot" v-bind="scope" />
    </template>
  </Button>
</template>
import Button from "./Button";

export default {
  name: "ButtonTemplate",
  components: {
    Button,
  },
  props: {
    ...Button.props,
    type: {
      type: String,
      default: "reset",
    },
    label: {
      type: String,
      default: "Reset",
    },
  },
};

 

好處是你可以設定 name,透過 Vue.js devtools 找到你的組件。

同時你也可以發現,ButtonTemplate 其實是個 wrapper 概念。

 


 

三、使用 extends 繼承 Button

使用 extends 的方式,有別於 component,並不是又包了一層,

而是繼承原有的組件,並且再覆寫原本的 render 內容。

本篇暫不討論 mixins

 

使用 extends 的方式,以下範例筆者讓 label 為 Search

後續範例則在 slot 塞入了一個 search icon。

[ ButtonFunctional.vue ]

import Button from "./Button";

export default {
  extends: Button,
  props: {
    label: {
      type: String,
      default: "Search",
    },
  },
};
  • label: 在順序上,如果沒有傳遞 props,就會使用 default value

 

三種使用範例:

[ App.vue ]

<!-- basic -->
<ButtonFunctional @greet="onGreet" />

<!-- with props -->
<ButtonFunctional
  label="New Search"
  type="button"
  :bold="true"
  @greet="onGreet"
/>

<!-- with slots -->
<ButtonFunctional @greet="onGreet">
  <template><i class="fas fa-search-plus"></i></template>
  <template #after><i class="fas fa-search-plus"></i></template>
</ButtonFunctional>

 

畫面預覽:

 

關於上述 3 種使用情境:

似乎都是沒甚麼問題。

 

如果想讓特定 slot search icon,該怎麼做?

由於我們使用 extends,因此改使用 render functions 方式。

 

在 render functions 中,筆者參考官方的文件 Slots

使用 scopedSlots 方式去設定,其中:

  • h: CreateElement 
  • ctx: RenderContext 

[ ButtonFunctional.vue ]

import Button from "./Button";

export default {
  extends: Button,
  functional: true,
  props: {
    label: {
      type: String,
      default: "Search",
    },
  },
  render(h, ctx) {
    return h(
      "Button",
      {
        ...ctx.data,
        props: {
          ...ctx.props,
          /** use props or extend here */
        },
        scopedSlots: {
          ...ctx.scopedSlots,
          default: () => h("i", { class: ["fas", "fa-search"] }),
          /** use slots or extend here */
        },
      },
      ...(ctx.children || [])
    );
  },
};
  • functional: true。由於我們組件屬於無狀態,又需要接收 props,因此設置為 true,把組件寫成 functinal component。
  • props: 預設值可寫在最外層的 props,如果寫在 render functions 的 props,則代表強制蓋掉指定的 props
  • scopedSlots:  將原本的 scopedSlots 展開,並於 default 處設定我們的 search icon,你也可以把 default: 根據 slot name 改成 after: 
  • ctx.children: 如果我們有在 ButtonFunctional 標籤內新增內容,那就是 child 囉!

 

 

關於 functional

由於該組件不處理 data,我們可以轉換成無狀態的寫法,

筆者使用 Functional Component + render function 方式,因此把 functional 設置為 true

 

當然你也可以改使用 Functional Component + Vue Template,改寫後跟前面組件 ButtonTemplate 結構類似,如下:
<template functional>
  <Button
    :label="props.label"
    v-on="$listeners"
    v-bind="[$attrs, $options.propsData]"
  >
    <template><i class="fas fa-search"></i></template>
    <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
      <slot :name="slot" v-bind="scope" />
    </template>
  </Button>
</template>
import Button from "./Button";

export default {
  extends: Button,
  props: {
    label: {
      type: String,
      default: "Search",
    },
  },
};

 

 

關於 children  範例:

<ButtonFunctional>
  child1
  <span>child2</span>
</ButtonFunctional>

 

畫面預覽:

 

有個小缺點是你無法透過 Vue.js devtools 找到你的組件。

 


 

專案範例放置於:

https://github.com/explooosion/vue-extend-slot-example

 

Reference

 

有勘誤之處,不吝指教。ob'_'ov