點燈坊

失くすものさえない今が強くなるチャンスよ

Vue 之 Event

Sam Xiao's Avatar 2020-08-04

Vue Component 的 Prop,只能解決資料由外層 Component 傳給內層 Component,若要將資料從內層 Component 傳到外層 Component 呢 ? 此時就要使用 Event。

Version

macOS Catalina 10.15.6
WebStorm 2020.2
Vue 2.6.11

Custom Event

event000

按下內層 component 的 Click Me,外層 component 將顯示內層 component 傳出的 10

App.vue

<template>
  <div>
    <my-button @my-click="outerClick"></my-button>
    <p>{{ msg }}</p>
  </div>
</template>

<script>
import myButton from '@/components/MyButton'

let outerClick = function(v) {
  this.msg = v
}

export default {
  name: 'App',
  components: {
    myButton
  },
  data: () => ({
    msg: ''
  }),
  methods: {
    outerClick
  }
}
</script>

第 3 行

<my-button @my-click="outerClick"></my-button>

使用 MyButton component,並對 my-click event 綁定到 outerClick()

Event binding 要加 event 名稱前加上 @,event handler 名稱不必加上 ()

11 行

let outerClick = function(v) {
  this.msg = v
}

定義 my-click event 的 handler,會接收 event 傳出的資料 並指定在 msg model 顯示。

MyButton.vue

<template>
  <button @click="innerClick">Click Me</button>
</template>

<script>
let innerClick = function() {
  this.$emit('my-click', 10)
}

export default {
  name: 'MyButton',
  methods: {
    innerClick
  }
}
</script>

第 2 行

<button @click="innerClick">Click Me</button>

定義 MyButton component 的 HTML template,注意其 <button></button>click event 綁定到 MyButton 自己的 innerClick()

第 6 行

let innerClick = function() {
  this.$emit('my-click', 10)
};

使用 Vue Instance 所提供的 $emit() 發出 event。

  • 第 1 個 argument 為 event 名稱
  • 第 2 個 argument 為想透過 event 所傳出的值
  • 若有多個值,可繼續其他參數

Event 名稱必須為 kebab-case,不能使用 CamelCase 或 camelCase,這裏 Vue 並不會如 prop 自動幫你轉成 kebab-case

MyCounter Again

event001

  • +1 會對內層 counter + 1
  • Emit Counter 會將內層 counter 傳給外層顯示

App.vue

<template>
  <div>
    <my-counter :start-counter="outerCounter" @emit-counter="showCounter"></my-counter>
    <h1>{{ outerCounter }}</h1>
  </div>
</template>

<script>
import myCounter from '@/components/MyCounter.vue'

let showCounter = function(v) {
  this.outerCounter = v
}

export default {
  name: 'App',
  components: {
    myCounter
  },
  data: () => ({
    outerCounter: 0
  }),
  methods: {
    showCounter
  }
}
</script>

第 3 行

<my-counter :start-counter="outerCounter" @emit-counter="showCounter"></my-counter>

使用自定義的 MyCounter component。

  • outerCounter model 傳進 startCounter prop 當初始值
  • 捕捉 emit-counter event 顯示傳出的 counter 值

第 4 行

<h1>{{ outerCounter }}</h1>

在外層 component 顯示內層 component 的 counter。

11 行

let showCounter = function(v) {
  this.outerCounter = v
}

由外層 component 的 showCounter() 接收 emit-counter event,接收內部傳出的 counter 值給 outerCounter model 顯示。

MyCounter.vue

<template>
  <div>
    <h1>{{ innerCounter }}</h1>
    <button @click="onAddCounter">+1</button>
    <button @click="onEmitCounter">Emit Counter</button>
  </div>
</template>

<script>
let onAddCounter = function() {
  this.innerCounter++
}

let onEmitCounter = function() {
  this.$emit('emit-counter', this.innerCounter)
}

export default {
  name: 'MyCounter',
  props: [
    'startCounter'
  ],
  data: function() {
    return {
      innerCounter: this.startCounter
    }
  },
  methods: {
    onAddCounter,
    onEmitCounter
  }
};
</script>

第 1 行

<div>
  <h1>{{ innerCounter }}</h1>
  <button @click="onAddCounter">+1</button>
  <button @click="onEmitCounter">Emit Counter</button>
</div>

定義 MyCounter component 的 HTML template,注意有兩個 <button/>,分別對應到 onAddCounter()onEmitCounter()

  • onAddCounter() 負責 innerCounter 的累加

  • onEmitCounter() 負責發出 event 通知外層 component

20 行

props: [
  'startCounter'
],

宣告 startCounter prop,讓外層藉由 startCounter prop 傳入 counter 的初始值。

23 行

data: function() {
  return {
    innerCounter: this.startCounter
  }
},

定義 innerCounter model,由 startCounter prop 定義其初始值。

10 行

let onAddCounter = function() {
  this.innerCounter++
}

innerCounter model 做累加。

14 行

let onEmitCounter = function() {
  this.$emit('emit-counter', this.innerCounter)
}

這行是關鍵,對外發出 emit-counter event,並傳出目前的 innerCounter model。

sync Modifier

event002

回想剛剛的 MyCounter 範例:

  1. MyCounter component 透過 emit-counter event 傳出目前 innerCounter model
  2. 外層再透過 showCounter() 接收 emit-counter event 傳來的值
  3. 將 event 接收值指定到 outerCounter model
  4. HTML template 顯示 outerCounter model

實務上這種內層 component 的 model 一改變,外層的 model 也要立即改變的應用非常多,Vue 另外提供了 sync modifier,可以少寫很多 code。

App.vue

<template>
  <div>
    <my-counter :start-counter.sync="outerCounter"></my-counter>
    <h1>{{ outerCounter }}</h1>
  </div>
</template>

<script>
import myCounter from '@/components/MyCounter.vue'

export default {
  name: 'App',
  components: {
    myCounter
  },
  data: () => ({
    outerCounter: 0
  })
}
</script>

第 3 行

<my-counter :start-counter.sync="outerCounter"></my-counter>

一樣透過 start-counter prop 傳入 outerCounter model,但多了 sync modifier,表示將來 MyCounter component 內部 model 有任何改變,則 outerCounter model 會同步跟著改變,已經不需要綁定 showCounter()

MyCounter.vue

<template>
  <div>
    <h1>{{ innerCounter }}</h1>
    <button @click="onAddCounter">+1</button>
    <button @click="onEmitCounter">Emit Counter</button>
  </div>
</template>

<script>
let onAddCounter = function() {
  this.innerCounter++
}

let onEmitCounter = function() {
  this.$emit('update:startCounter', this.innerCounter)
}

export default {
  name: 'MyCounter',
  props: [
    'startCounter'
  ],
  data: function() {
    return {
      innerCounter: this.startCounter
    }
  },
  methods: {
    onAddCounter,
    onEmitCounter
  }
}
</script>

14 行

let onEmitCounter = function() {
  this.$emit('update:startCounter', this.innerCounter)
}

由原本 emit emit-counter event,改成 update:startCounter event,第二個參數一樣傳出 innerCounter model。

Vue 規定若要使用 sync modifier,必須 emit update:prop 這種格式的 event,如此 sync modifer 才收得到

v-model Directive

event003

sync 看起來已經很像 two way binding,可以使用更精簡的 v-model directive 。

App.vue

<template>
  <div>
    <my-counter v-model="outerCounter"></my-counter>
    <h1>{{ outerCounter }}</h1>
  </div>
</template>

<script>
import MyCounter from '@/components/MyCounter.vue'

export default {
  name: 'App',
  components: {
    MyCounter
  },
  data: () => ({
    outerCounter: 0
  })
}
</script>

第 3 行

<my-counter v-model="outerCounter"></my-counter>

直接在 MyCounter component 使用 v-model directive,並綁定到 outerCounter model。

MyCounter.vue

<template>
  <div>
    <h1>{{ innerCounter }}</h1>
    <button @click="onAddCounter">+1</button>
    <button @click="onEmitCounter">Emit Counter</button>
  </div>
</template>

<script>
let onAddCounter = function() {
  this.innerCounter++
}

let onEmitCounter = function() {
  this.$emit('input', this.innerCounter);
}

export default {
  name: 'MyCounter',
  props: [
    'value'
  ],
  data: function() {
    return {
      innerCounter: this.value
    }
  },
  methods: {
    onAddCounter,
    onEmitCounter
  }
}
</script>

20 行

props: [
  'value'
],

v-model directive 預設使用 value prop,所以要自行宣告。

14 行

let onEmitCounter = function() {
  this.$emit('input', this.innerCounter);
}

v-model directive 預設接收 input event,所以要自行 emit。

要使用 v-model directive,要遵循 Vue 的兩項規定:

  1. 使用 value prop
  2. 使用 input event

sync modifier 與 v-model directive 兩者功能相同,但 v-model 觀念比較容易理解,程式碼也比較少,個人偏好 v-model

native Modifier

event004

目前外層可以透過內層 component 發出的 custom event 接收到 data,但如第一個 MyButton component,其實是內層 component 監聽 click DOM event 並且發出自己的 my-click custom event。

既然源頭是 click DOM event,外層 component 可直接綁定到內層 component 的 click DOM event。

App.vue

<template>
  <div>
    <my-button @click.native="outerClick"></my-button>
    <p>{{ msg }}</p>
  </div>
</template>

<script>
import MyButton from '@/components/MyButton.vue'

let outerClick = function() {
  this.msg = 'Button clicked'
}

export default {
  name: 'App',
  components: {
    MyButton
  },
  data: () => ({
    msg: ''
  }),
  methods: {
    outerClick
  }
}
</script>

第 2 行

<my-button @click.native="outerClick"></my-button>

想直接綁定 MyButtonclick DOM event,由於是原生 event,只要在 @click 加上 native modifier,Vue 就會幫你將 outerClick() 直接綁定到原生的 click DOM event。

11 行

let outerClick = function() {
  this.msg = 'Button clicked'
}

由於 outerClick() 直接綁定到 DOM event,所以就收不到內層 MyButton component 所傳出的資料,因此稍作修改。

MyButton.vue

<template>
  <button>Click Me</button>
</template>

<script>
export default {
  name: 'MyCounter',
}
</script>

也因為 click.native 直接綁定到原生的 DOM event,因此就不需要另外 emit event 了。

Conclusion

  • 透過 event 能使 mode 由內層 component 傳到外層 component
  • sync modifier 讓我們直接將 component 內外 model 同步
  • v-model directive 算是 sync modifier 的簡化版,讓我們更直覺實現 component 的 two way binding
  • native modifier 能讓我們直接綁定 component 內的 DOM event,不用再自行 emit event

Reference

Vue, Custom Event