Try   HackMD

[Vuetify] Vuetify 元件中常看到的 <template v-slot:activator="{on, attrs}" 是什麼東東?

tags: 前端筆記 Vuetify Vue

最近專案需要頻繁地使用 Vuetify,雖然官方文件寫的很用心,也有很多範例,但是我 Vue 的知識還是太淺,看了同事的程式碼才更理解

TODO:

2022/03/31

整理:

同事及網路上的使用範例

  1. 同事大神的程式碼(使用 popup 效果做出彈跳式地日曆選擇器):
<v-dialog ref="endDateDialogRef" v-model="endDateModal" :return-value.sync="dates.endDate" :retain-focus="false" persistent width="290px" > <template v-slot:activator="{ on, attrs }"> <v-text-field v-model="dates.endDate" label="迄日" append-icon="mdi-calendar" readonly v-bind="attrs" outlined v-on="on" /> </template> <v-date-picker v-model="dates.endDate" > <v-spacer /> <v-btn text color="primary" @click="endDateModal = false" > Cancel </v-btn> <v-btn text color="primary" @click="$refs.endDateDialogRef.save(dates.endDate)" > OK </v-btn> </v-date-picker> </v-dialog>
  1. 網路上找到的範例,一樣是點擊後出現 popup 的日曆

比對兩個範例得知如果要有這樣子的效果,必須要使用 <v-dialog><template v-slot:activator="..."> 包覆 <input> 並在關閉 <v-dialog> 前插入 <v-date-picker>。其他的 properties 暫時先不管他,因為其他的看官網文件應該沒問題,但是讓我最好奇的是:到底 <template v-slot:activator="..."> 是什麼東東。

Scoped slot(作用域插槽)

一開始我理解 slot 就認為它知能做一個像是包裝紙(wrapper)的東西,讓 parent 可以把 parent 的 template 給 child(也就是 slot),沒想到 slot 還有另一個很酷的功能,也就是標題的 scoped slot(作用域插槽)。
透過 scoped slot 讓開發者也可以把 slot 所屬的 component 有辦法將自身的 state 丟給 parent 使用(雖然語法看起來很怪)。

假設目前我有兩個 components,App.vue(parent) 以及 PlaySlot.vue(child),在 child 中用 slot,把該 component 當作 wrapper:

// App.vue <template> <div id="app"> <play-slot> <h1>Hello</h1> </play-slot> </div> </template>
// PlaySlot.vue <template> <div> <slot></slot> <p>In Play slot</p> </div> </template>

沒什麼稀奇,因為 slot wrapper 會把在 parent 中間的 template 插進去。

<div> <div> <h1>Hello</h1> <p>In Play slot</p> </div> </div>

那如果 child 自己也有 state 想要傳給 parent 呢?
<slot name="something" :keyName="value" /> 如 parent 傳資料給 child,可用像 props 的語法 :keyName="value",也可以給名字 name="" <template v-slot:Name="slotProps",讓程式碼的閱讀性更加。

// PlaySlot.vue <template> <div> <slot></slot> <p>In Play slot</p> // slot 也可以把 child state 向上傳 <slot name="activator" :user="user"></slot> // === <slot name="activator" v-bind:user="user"> </div> </template> <script> // child 自己的 state export default { data() { return { user: 'Lun' } } } </script>
// App.vue <template> <div id="app"> <play-slot> <h1>Hello</h1> <template v-slot:activator="slotProps"> <h2>{{ slotPrps.user }}</h2> </template> </play-slot> </div> </template>

編譯後就會得到:

<div> <div> <h1> Hello </h1> <p> In Play Slot </p> <h2> Lun </h2> </div> </div>

除了把資料丟給 parent,<slot> 也可以從 parent 接資料後再把資料丟給 parent:

// App.vue <div> <play-slot :number="number"> <h1>Hello</h1> <template v-slot:test="props"> // 從 parent 得到的 number 又藉由 slot 丟到 parent 了 <h2>{{ props.number }}</h2> </template> </play-slot> </div> // PlaySlot.vue <template> <div> <slot></slot> <p>{{ number }}</p> <p>In Play slot</p> <slot name="test" v-bind:number="number"></slot> </div> </template>

所以 scoped slot 不僅可以丟 child 自己本身的 state 到 parent,也可以從 parent 拿到 state 後再藉由 scoped slot 丟還給 parent。
但不要忘記,有一條鐵的紀律要記住:

Everything in the parent template is compiled in parent scope; everything in the child template is compiled in the child scope.
所有出現在 parent template 會被編譯為 parent 作用域,反之,所有出現在 child template 就回被編譯為 child 作用域。

Scoped slot 從 child 傳 state 到 parent 就像是 function

根據 Vue 的官方文件,scoped slot 底層就像是定義一個單個 argument function 一樣,因此也可以使用解構(destructure)

function (slotProps) { // ... slot content ... }
<current-user v-slot="{ user }"> {{ user.firstName }} </current-user>

也可以寫 default

<current-user v-slot="{ user = { firstName: 'Guest' } }"> {{ user.firstName }} </current-user>

回到 Vuetify 的範例

<v-dialog v-model="endDateModal" ... > <template v-slot:activator="{ on, attrs }"> <v-text-field v-model="dates.endDate" v-bind="attrs" v-on="on" ... /> </template> <v-date-picker v-model="dates.endDate" > .... </v-date-picker> </v-dialog>

1. <template> 是從 parent 取得 props 後再藉由 scoped slot 丟回 parent

如前面所述,這個例子的 <template v-slot:activator="{on, attrs}"> 是一個 scoped slot,所以它其實就是把 child 本身的 state / 從 parent 拿到的 state 拿回給 parent,讓 parent template 可以取得需要的 state。

要怎麼確定 <template> 真的有拿 parent 的 state?

// 把 template 的解構拿掉,換成一般的樣子 <v-dialog> <template v-slot:activator="slotProps"> <v-text-field v-model="dates.endDate" v-bind="attrs" v-on="on" @click="test(slotProps)" ... /> </template> </v-dialog> <srcipt> ... test(slotProps) { console.log(slotProps) } </script>

執行完確實會出現 slotProps,代表是 scoped slot 傳上去給 parent 的證明。

所以不使用解構的話,程式碼可以這樣子看:

<v-dialog v-model="endDateModal" ... > // 中規中矩,不適用 destructure <template v-slot:activator="slotProps"> <v-text-field v-model="dates.endDate" v-bind="slotProps.attrs" v-on="slotProps.on" ... /> </template> <v-date-picker v-model="dates.endDate" > .... </v-date-picker> </v-dialog>

2. activator 也是幕後功成之一

根據 Vuetify 的文件<v-dialog>v-slot:activator 會觸發 <v-dialog> component(藉由點擊或者其他 event),且 <v-dialog> 會傳事件給 <template>(事件被包裝成物件,且 keyon)。
如果開發者想要使用怎麼便利的元件,就必須乖乖使用 V-slot:activator

3. v-onv-bind 也可以收物件

因為 <template> 接到的資料為:

{attrs: {....}, on: {click: callback} }

根據文件v-on 可以接物件(v-on="{eventName: callbak}")。

<!-- object syntax (2.4.0+) --> <button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

同理v-bind="attrs" 就是接物件包裝好的 HTML accessibility 屬性。

參考資料

  1. 程式範例

  2. 文章