iT邦幫忙

第 12 屆 iThome 鐵人賽

0
自我挑戰組

跟 VueJS 認識的30天系列 第 13

[DAY13] 跟 Vue.js 認識的30天 - Vue 模組自定義事件(Custom Events)

每次寫筆記都在想要怎麼寫得讓大家(包含未來的我)看得懂,所以每次都要寫很久,但好像自己的內容有點太無聊了 XD 。

自定義事件( Custom Events )命名

因自定義事件的名稱跟模組名稱和 props 命名不同,在使用時並不會有轉換的問題,所以怎麼命名就怎麼使用,命名用 myCustomEvent ,使用時也必須使用 v-on:myCustomEvent ,這時用 v-on:my-custom-event 是無效的。

在 HTML 中使用時,為了要遵守 HTML 的規範,所以自定義事件( Custom Events )名會被轉換成小寫,也因此在 HTML 中使用 v-on:myCustomEvent 時,會自動轉換成 v-on:mycustomevent ,最後會因為事件名稱不相同(myCustomEvent != mycustomevent)而導致該監聽器無法作用。

因此 Vue 文件建議在一律以 kebab-case (短橫線分隔)來命名自定義事件( Custom Events )。

模組的 v-model

在講自定義事件的 v-model 前,先來了解一下 v-model 是如何產生作用的。

如果對 v-model 不太熟悉,可以先參考DAY09 | 跟 Vue.js 認識的30天 - Vue 的資料雙向綁定(v-model)

在一般情況下,我們想要雙向綁定就會使用 v-model ,如下:

<input type="text" v-model="name">

<input> 的 value 值會受 Vue 實例資料 name 的影響,而 name 也同樣受到 <input> 的 value 改變的影響。

v-model 是如何達到雙向綁定的,其實就是透過 v-bind:valuev-on:input

<input type="text" v-bind:value="name" v-on:input="name = $event.target.value">

也就是說 v-model 做了2件事:

  • <input> 的 value 值綁定了 Vue 實例資料 name

  • <input> 在輸入( input 事件)時,會讓 Vue 實例資料 name = 輸入的值(也就是 value )。

如何在模組上使用 v-model

在模組上的 v-model ,做的事情跟上面的 <input> 很相似,但還是有些微不一樣,模組上的 v-model 是如何作用的。

在預設的情況下, v-model 被綁定的屬性是 value , 被監聽的事件是 input 事件。

<!--以下2個 blog-input 做了相同的事情-->
<!--模組的 v-model 一樣是由 v-bind:value 跟 v-on:input 組成的-->
<!-- 使用 $event 接收 $emit 的第2個參數值-->
<blog-input v-model="title"><blog-post>
<blog-input v-bind:value="title" v-on:input="title = $event"><blog-post>

<script>
Vue.component('blog-input', {
  props:['value'],
  template:`<input type="text" :value="value" @input="$emit('input', $event.target.value)">`
})
</script>

模組中的單選或複選按鈕的 v-model

在模組中使用 v-model 的時候,預設所綁定的屬性(prop)是 value ,預設綁定的事件是 input 事件。但是單選(type="radio")或複選(type="checkbox")按鈕中的 value 屬性並不是決定按鈕是否被點選的關鍵,決定點選的是 checked 屬性,所以可以透過模組 model 屬性來變更會影響按鈕結果的屬性名(prop)跟事件。

<!-- agree為布林值-->
<blog-checkbox v-model="agree"><blog-checkbox>
<blog-checkbox v-bind:checked="agree" v-on:change="agree = $event"><blog-checkbox>

<script>
Vue.component('blog-checkbox', {
  model:{
    // 預設的屬性跟事件
    /*
    prop:'value',
    event: 'input'
    */
    // 模組要綁定的屬性(prop)
    prop:'checked',
    // 模組要監聽的事件
    event: 'change'
  },
  props:{
    // 一定要聲明要綁定的屬性名
    checked:Boolean
  },
  template:`<input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)">`
})
</script>

https://ithelp.ithome.com.tw/upload/images/20210128/20127553zCwTckH8tq.png

模組的修飾符

.native

可以透過加入修飾符 .native 來讓模組標籤直接監聽原生事件(如 focusclick 等等非自定義的事件),而不需再透過內層 $emit 來傳遞事件。

<component-native @focus.native="focusCount"></component-native>
<script>
Vue.component('component-native',{
  template:`<input type="text">`
})
</script>

加入修飾符 .native 後,模組標籤所監聽的事件就會被內層根元素所繼承,讓內層根元素監聽並傳遞同樣的事件,原理如下:

<component-native @focus="focusCount"></component-native>
<script>
Vue.component('component-native',{
  template:`<input type="text" @focus="$emit('focus')">`
})
</script>

修飾符 .native 無法作用的情況

通常 input 標籤會伴隨著 label 出現,但這時如果一樣使用修飾符 .native ,繼承事件的根元素就是 label 而不是 input

同樣使用修飾符 .nativefocus 事件,測試看看:

<component-native-fail @focus.native="focusCount"></component-native-fail>
<script>
Vue.component("component-native-fail", {
  // 繼承 focus 事件的是 label
  template: `<label>姓名
  <input type="text">
  </label>`
});
</script>

同時間測試了一下其他的原生事件,將 focus 事件改成了 clickmouseout 事件後,發現 focusCount 是可以正常觸發的,所以後來想了下應該是因為只有 input 標籤有 focus 事件,而 label 標籤沒有,所以才會導致 focusCount() 觸發失敗。

修飾符 .native 無法作用解決方法

如果真的出現修飾符 .native 無法作用的狀況,用來取代修飾符 .native 的方法,就是透過 Vue 屬性 $listeners 來改變模組內的繼承元素。

<component-listener @focus="focusCount"></component-listener>
<script>
Vue.component("component-listener", {
  // 繼承 focus 事件的是 label
  template: `<label>姓名
  <input type="text" v-on="$listeners">
  </label>`
});
</script>

以上就是透過 v-on="$listeners" 讓模組標籤的事件繼承元素變成 <input> 而非 <label>

另外可以透過 computed 決定繼承的事件的狀況,例如某些事件要保留參數(預設是不帶第2個參數的),或是繼承的事件只有部分等等。

<component-listener-computed @focus="focusCount" @mouseout="mouseOutCount"  v-model="text"></component-listener-computed>
<script>
Vue.component("component-listener-computed", {
  props: ["value"],
  template: `<label>姓名
  <input type="text" v-on="customListeners">
  </label>`,
  computed:{
    customListeners(){      
      // 排除 focus 事件
      const {focus, ...listeners} = this.$listeners;
      // 讓 input 事件能將參數傳遞出去
      const vm = this;
      listeners.input = (e)=>{
        vm.$emit('input',e.target.value)
      }
      return listeners
    }
  }
});
</script>

有關 $listener 更多的資訊可以參考下方介紹:

Vue - $listener

QUICK VUE TIP - EVENT LISTENERS WITH THIS.$LISTENERS

.sync

在之前DAY12 | 跟 Vue.js 認識的30天 - Vue 模組資料傳遞(props)有提到過 props 是單向數據流,內層無法改變外層的 props 值,如果真的要由內層去改動外層資料的話,就必須先用 props 傳遞資料進內層,再透過 $emit 將資料往外層傳遞。

<!-- 1 透過 props 先傳資料 name 進入 component -->
<!-- 3 新資料被傳遞出來後,在做更新,並影響 props 的值 -->
<!-- 4 將新 name 的值透過 props 在傳遞進去 component -->
<data-transfer :name="name" @update:name="name = $event"></data-transfer>
<script>
Vue.component('data-transfer',{
  props:['name'],
  // 2 透過$emit(自定義事件名,欲傳遞的值) 來將想改變的prop值傳送出去
  template:`<div>
    <span>姓名</span>
    <p @click="$emit('update:name', 'Celeste')">{{name}}</p>
  </div>`
})
const vm = new Vue({
  el:'#vm',
  data:{
    name: 'Michelle'
  }
})
</script>

要從內層去改變外層傳遞進來的 prop ,有一個更簡單的方法,那就是透過修飾符 .sync 來達成模組內外的 prop 值連動,修飾符 .sync 的原理就像上面的範例,但是使用 .sync 可以不用在外層模組標籤上寫下接收自定義事件。

要點就是外層加入要傳遞的 prop 上加入修飾符 .sync ,內層要改變該 prop 值的地方,使用 $emit('update:propName',該 prop 欲改變的值) 來做事件的傳遞。

<component-sync :name.sync="name"></component-sync>
<script>
Vue.component('component-sync',{
  props:['name'],
  template:`<div>
  <span>姓名</span>
  <span @click='$emit("update:name", "Linda")'>{{name}}</span>
  </div>`
})
const vm = new Vue({
  el:'#vm',
  data:{
    name: 'Michelle'
  }
})
</script>

還可以結合 v-bind="物件名" 來一次雙向綁定所有物件屬性值到模組內。

v-bind="物件名" 一次綁定所有物件屬性的方法,可以參考Vue.js - 传入一个对象的所有 property

<component-object-sync v-bind.sync="post"></component-object-sync>
<!-- v-bind="post" 的原理
<component-object-sync
v-bind:title="post.title"
v-bind:author="post.author"
v-bind:gender="post.gender"></component-object-sync>
-->
<script>
Vue.component('component-object-sync',{
  // 外層真正綁定的 prop 是該物件的所有屬性名,而非整個 post,如果 props 寫成 post ,會出錯。 
  props:['title','author','gender'],
  template:`<div>
    <p><span>標題</span>
    <span>{{title}}</span></p>
    <p><span>請點作者</span>
    <span @click='$emit("update:author", "Celeste")'>{{author}}</span></p>
    <p><span>請點性別</span>
    <span @click='$emit("update:gender", "female")'>{{gender}}</span></p>
  </div>`
})
const vm = new Vue({
  el: "#vm",
  data: {
    post:{
      title:'Celeste\'s Blog',
      author:'Michelle',
      gender:'male'
    }
  }
});

</script>

修飾符 .sync 注意事項

  • 不能結合使用表達式。

  • 要綁定物件時,一定要使用變數,而不能直接使用物件實字(Object Literals)來綁定物件。

Demo:DAY13 | 跟 Vue.js 認識的30天 - Vue 模組自定義事件(Custom Events)

參考資料:

Vue.js - 自定义事件

Vue - $listener


上一篇
[DAY12]跟 Vue.js 認識的30天 - Vue 模組資料傳遞(`props`)
下一篇
[DAY14]跟 Vue.js 認識的30天 - Vue 模組插槽(`slot`)
系列文
跟 VueJS 認識的30天21

尚未有邦友留言

立即登入留言