2022/03/21

Vue.js Essentials 3

Event Handling

Listening to Evnets

可使用 v-on directive (通常縮寫為 @)監聽 DOM events,並執行某些 js script

v-on:click="methodName"@click="methodName" 都可以

<div id="basic-event">
  <button @click="counter += 1">Add 1</button>
  <p>The button above has been clicked {{ counter }} times.</p>
</div>

<script>
Vue.createApp({
  data() {
    return {
      counter: 0
    }
  }
}).mount('#basic-event')
</script>

Method Event Handlers

如果 js logic 比較複雜,v-on 可改用 method 而不是 attribute

<div id="event-with-method">
  <!-- `greet` is the name of a method defined below -->
  <button @click="greet">Greet</button>
</div>

<script>
Vue.createApp({
  data() {
    return {
      name: 'Vue.js'
    }
  },
  methods: {
    greet(event) {
      // `this` inside methods points to the current active instance
      alert('Hello ' + this.name + '!')
      // `event` is the native DOM event
      if (event) {
        alert(event.target.tagName)
      }
    }
  }
}).mount('#event-with-method')
</script>

Methods in Inline Handlers

可在 inline js statement 使用 method

<div id="inline-handler">
  <button @click="say('hi')">Say hi</button>
  <button @click="say('what')">Say what</button>
</div>

<script>
Vue.createApp({
  methods: {
    say(message) {
      alert(message)
    }
  }
}).mount('#inline-handler')
</script>

如果需要使用原本的 DOM event,可用 $event 傳入

<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<script>
Vue.createApp({
  methods: {
    warn(message, event) {
      // now we have access to the native event
      if (event) {
        event.preventDefault()
      }
      alert(message)
    }
  }
}).mount('#inline-handler')
</script>

Multiple Event Handlers

可用 , 區隔,同時使用多個 methods

<!-- both one() and two() will execute on button click -->
<button @click="one($event), two($event)">
  Submit
</button>

<script>
Vue.createApp({
  methods: {
    one(event) {
      // first handler logic...
    },
    two(event) {
      // second handler logic...
    }
  }
}).mount('#inline-handler')
</script>

Event Modifiers

在 event handler 裡面呼叫 event.preventDefault()event.stopPropagation() 很常見。vue 提供 v-on 使用的 event modifier

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive
<!-- the click event's propagation will be stopped -->
<a @click.stop="doThis"></a>

<!-- the submit event will no longer reload the page -->
<form @submit.prevent="onSubmit"></form>

<!-- modifiers can be chained -->
<a @click.stop.prevent="doThat"></a>

<!-- just the modifier -->
<form @submit.prevent></form>

<!-- use capture mode when adding the event listener -->
<!-- i.e. an event targeting an inner element is handled here before being handled by that element -->
<div @click.capture="doThis">...</div>

<!-- only trigger handler if event.target is the element itself -->
<!-- i.e. not from a child element -->
<div @click.self="doThat">...</div>

注意:順序很重要

@click.prevent.self 會停止所有 clicks

@click.self.prevent 只會停止 clicks on the element itself

<!-- the click event will be triggered at most once -->
<a @click.once="doThis"></a>

.once 可用在 component events,跟其他 modifier 不同。

vue 提供 .passive modifier,跟 addEventListenerpassive option 一樣

<!-- the scroll event's default behavior (scrolling) will happen -->
<!-- immediately, instead of waiting for `onScroll` to complete  -->
<!-- in case it contains `event.preventDefault()`                -->
<div @scroll.passive="onScroll">...</div>

.passive 在改善 mobile device 方面特別有用

注意:.passive.prevent 不能一起使用,因為 .prevent 會被忽略,造成 browser 發生 warning。 passive 會跟 browser 互動,不需要 prevent the event's default behavior

Key Modifiers

在監聽 keyboard events 時,常需要檢查 keys,vue 提供 v-on@ 使用 key modifier

<!-- only call `vm.submit()` when the `key` is `Enter` -->
<input @keyup.enter="submit" />

可使用 KeyboardEvent.key 定義的 key names,要轉為 kebab-case

<!-- 當 $event.key 為 PageDown 時,才會呼叫該 handler -->
<input @keyup.page-down="onPageDown" />

以下為常用的 key 的 aliases

  • .enter
  • .tab
  • .delete (captures both "Delete" and "Backspace" keys)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

System Modifier Keys

可限制某個 modifier key 被按下時,才會驅動 mouse/keyboard event listener

  • .ctrl
  • .alt
  • .shift
  • .meta 在 Mac 是 ⌘,在 Windows 是 ⊞,在 Sun Microsystems keyboard 是 ◆,在特殊的 MIT and Lisp machine keyboard (ex: Knight kryboard, space-cadetkeyboard) 是 META,在 Symbolics keyboards 是 META 或 Meta
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

modifier keys 通常是用 keyup event,但 keyup.ctrl 只會在按下 ctrl 時被 trigger,不會在 release ctrl 時被 trigger

.exact modifier

.exact 可控制 system modifier 的 exact combination

<!-- this will fire even if Alt or Shift is also pressed -->
<button @click.ctrl="onClick">A</button>

<!-- this will only fire when Ctrl and no other keys are pressed -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- this will only fire when no system modifiers are pressed -->
<button @click.exact="onClick">A</button>

mouse button modifier

  • .left
  • .right
  • .middle

Why Listeners in HTML?

vue 的 event modifier 策略違反了傳統的 "separation of concerns" 規則,因為 handler function 與 expression 跟 ViewModel 綁定,這樣做的優點:

  1. 移除 HTML template 時,就移除了 handler function,很容易維護
  2. 因不需要在 js 手動綁定 event listener,ViewModel 的 code 可單純只有 logic 且為 DOM-free,很容易測試
  3. 當 ViewModel 被刪除時,所有 event listener 也自動被移除,不需要手動移除

Form Input Bindings

Basic Usage

可使用 v-model 在 form input, textarea, select 產生 two-way data binding,根據 input type 自動更新 element。v-model 是根據 user input event 更新 data + 特殊 edge case 的 syntax sugar

注意:v-model 會忽略 fome element 裡面初始的 value, checked, selected attributes,會使用 current active instance data 作為 source of truth,因此要在 js 的 data option 裡面宣告初始值。

v-model 會因為不同的 input element 使用不同的 properties,產生不同 events

  1. text, textarea 使用 value property 及 input event
  2. checkboxes, radiobuttons 使用 checked property 及 change event
  3. select 使用 value 為 prop 及 change event

注意:v-model 在 IME (Chinese, Japanese, Korean..) 語系中,在 IME composition 時,並不會讓 v-model 更新,如果需要處理輸入法的異動更新,要改用 input event listener 及 value binding

  • Text
<input v-model="message" placeholder="edit me" />
<p>Message is: {{ message }}</p>

<script>
Vue.createApp({
  data() {
    return {
      message: ''
    }
  }
}).mount('#v-model-basic')
</script>
  • Multiline Text
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<br />
<textarea v-model="message" placeholder="add multiple lines"></textarea>

<script>
Vue.createApp({
  data() {
    return {
      message: ''
    }
  }
}).mount('#v-model-textarea')
</script>

textarea 不能用 interpolation

<!-- bad -->
<textarea>{{ text }}</textarea>

<!-- good -->
<textarea v-model="text"></textarea>
  • Checkbox
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

<script>
Vue.createApp({
  data() {
    return {
      checked: false
    }
  }
}).mount('#v-model-checkbox')
</script>
<div id="v-model-multiple-checkboxes">
  <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
  <label for="jack">Jack</label>
  <input type="checkbox" id="john" value="John" v-model="checkedNames" />
  <label for="john">John</label>
  <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
  <label for="mike">Mike</label>
  <br />
  <span>Checked names: {{ checkedNames }}</span>
</div>

<script>
Vue.createApp({
  data() {
    return {
      checkedNames: []
    }
  }
}).mount('#v-model-multiple-checkboxes')
</script>
  • Radio
<div id="v-model-radiobutton">
  <input type="radio" id="one" value="One" v-model="picked" />
  <label for="one">One</label>
  <br />
  <input type="radio" id="two" value="Two" v-model="picked" />
  <label for="two">Two</label>
  <br />
  <span>Picked: {{ picked }}</span>
</div>

<script>
Vue.createApp({
  data() {
    return {
      picked: ''
    }
  }
}).mount('#v-model-radiobutton')
</script>
  • Select
<div id="v-model-select" class="demo">
  <select v-model="selected">
    <option disabled value="">Please select one</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <span>Selected: {{ selected }}</span>
</div>

<script>
Vue.createApp({
  data() {
    return {
      selected: ''
    }
  }
}).mount('#v-model-select')
</script>

如果 v-model expression 的初始值跟 option 不吻合,<select> element 會以 "unselected" state 被 rendered。在 iOS 會造成 user 無法選擇第一個 element,因為 iOS 不會 fire a change event。

建議用 empty value 增加一個 disabled option,類似上面提供的例子一樣


multiple select

<select v-model="selected" multiple>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>
<br />
<span>Selected: {{ selected }}</span>

<script>
Vue.createApp({
  data() {
    return {
      selected: ''
    }
  }
}).mount('#v-model-select')
</script>

v-for 實作dynamic options

<div id="v-model-select-dynamic" class="demo">
  <select v-model="selected">
    <option v-for="option in options" :value="option.value">
      {{ option.text }}
    </option>
  </select>
  <span>Selected: {{ selected }}</span>
</div>

<script>
Vue.createApp({
  data() {
    return {
      selected: 'A',
      options: [
        { text: 'One', value: 'A' },
        { text: 'Two', value: 'B' },
        { text: 'Three', value: 'C' }
      ]
    }
  }
}).mount('#v-model-select-dynamic')
</script>

Value Bindings

radio, checkbox, select option 中,v-model binding values 通常是 static string (checkbox 是 booleans)

<!-- `picked` is a string "a" when checked -->
<input type="radio" v-model="picked" value="a" />

<!-- `toggle` is either true or false -->
<input type="checkbox" v-model="toggle" />

<!-- `selected` is a string "abc" when the first option is selected -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

如果要 dynamic property,可使用 v-bind

  • Checkbox

true-value, false-value 不會影響 input 的 value attribute

<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
// when checked:
vm.toggle === 'yes'
// when unchecked:
vm.toggle === 'no'
  • Radio
<input type="radio" v-model="pick" v-bind:value="a" />
// when checked:
vm.pick === vm.a
  • Select Options
<select v-model="selected">
  <!-- inline object literal -->
  <option :value="{ number: 123 }">123</option>
</select>
// when selected:
typeof vm.selected // => 'object'
vm.selected.number // => 123

Modifiers

  • .lazy

v-model 預設會在每一次 input event 發生時,同步 input data,可增加 lazy modifier,修改為 change event 後同步資料

<!-- synced after "change" instead of "input" -->
<input v-model.lazy="msg" />
  • .number

如果想讓 user input 自動 typecast 為 number,可用 .number

<input v-model.number="age" type="number" />

如果 input value 無法被 parseFloat() parsing 時,會回傳原始 value

  • .trim

自動 trim space

<input v-model.trim="msg" />

v-model with Components

vue component 可產生 reusable inputs with customized behavior


Components Basics

以下為 vue component 的 example,通常在 vue application,會使用 Single File Component,而不是 string template。

Component 是 reusable instances with a name

<div id="components-demo">
  <button-counter></button-counter>
</div>

<script>
// Create a Vue application
const app = Vue.createApp({})

// Define a new global component called button-counter
app.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button v-on:click="count++">
      You clicked me {{ count }} times.
    </button>`
})

app.mount('#components-demo')
</script>

component 就是 reusable instance,故能夠使用data, computed, watch, methods, and lifecycle hooks

Reusing Components

每一個 component 都有各自的 instance,獨立的 count

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

Organizing Components

在 app 裡面會使用 tree of nested components

例如會有 components for header, sidebar, content area

為了在 templates 裡面使用 component,必須要先向 Vue 註冊,有兩種註冊類型:global 與 local。

component method 是 global component

const app = Vue.createApp({})

// global component
app.component('my-component-name', {
  // ... options ...
})

Passing Data to Child Components with Props

props 是可以跟 component 註冊的 custom attribute

例如 blog post component,可用 props 提供 component 接受的 list of props

'title'變成該 component 的 property,然後就能在 template 裡面使用

<div id="blog-post-demo" class="demo">
  <blog-post title="My journey with Vue"></blog-post>
  <blog-post title="Blogging with Vue"></blog-post>
  <blog-post title="Why Vue is so fun"></blog-post>
</div>

<script>
const app = Vue.createApp({})

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-post-demo')
</script>

<div id="blog-posts-demo">
  <blog-post
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
  ></blog-post>
</div>

<script>
const App = {
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}

const app = Vue.createApp(App)

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-posts-demo')
</script>

可使用 v-bind 做 dynamic pass props,在一開始不知道有多少 content 資料的時候很有用。

Listening to Child Components Events

ex: 要為 blogpost 增加 accessibility feature,把文字放大

增加 postFontSize data property

<div id="blog-posts-events-demo" class="demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
       v-for="post in posts"
       :key="post.id"
       :title="post.title"
       @enlarge-text="postFontSize += 0.1"
    ></blog-post>
  </div>
</div>

<script>
const app = Vue.createApp({
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue'},
        { id: 2, title: 'Blogging with Vue'},
        { id: 3, title: 'Why Vue is so fun'}
      ],
      postFontSize: 1
    }
  }
})

app.component('blog-post', {
  props: ['title'],
  template: `
    <div class="blog-post">
      <h4>{{ title }}</h4>
      <button @click="$emit('enlargeText')">
        Enlarge text
      </button>
    </div>
  `
})

app.mount('#blog-posts-events-demo')
</script>

可用 emits option 檢查 all the events that a component emits

app.component('blog-post', {
  props: ['title'],
  emits: ['enlargeText']
})
  • Emitting a value with an Event

可在 $emit 增加第二個參數

<button @click="$emit('enlargeText', 0.1)">
  Enlarge text
</button>
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>

或是 event handler 為 method,可在第一個參數傳入 value

<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
methods: {
  onEnlargeText(enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}
  • Using v-model on Components

custom events 可產生 custom inputs 跟 v-model 一起使用

<input v-model="searchText" />

跟上面一樣

<input :value="searchText" @input="searchText = $event.target.value" />

如果用在 component,就跟這個一樣

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>

在 component 裡面的 <input> 必須滿足

  1. bind value property 到 modelValue prop
  2. input,新的 value 會產生 update:modelValue event
app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
})

現在就可以使用 v-model

<custom-input v-model="searchText"></custom-input>

另一個在 component 實作 v-model 的方法,是用 computed properties 定義 getter, setter,get 要回傳 modelValue property,set 要產生相關 event

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input v-model="value">
  `,
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
})

Content Distribution with Slots

可傳送 content 給 component

<div id="slots-demo" class="demo">
  <alert-box>
    Something bad happened.
  </alert-box>
</div>

<script>
const app = Vue.createApp({})

app.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

app.mount('#slots-demo')
</script>

slot 裡面就放了 Something bad happened.

Dynamic Components

<div id="dynamic-component-demo" class="demo">
  <button
     v-for="tab in tabs"
     v-bind:key="tab"
     v-bind:class="['tab-button', { active: currentTab === tab }]"
     v-on:click="currentTab = tab"
   >
    {{ tab }}
  </button>

  <component v-bind:is="currentTabComponent" class="tab"></component>
</div>

<script>
const app = Vue.createApp({
  data() {
    return {
      currentTab: 'Home',
      tabs: ['Home', 'Posts', 'Archive']
    }
  },
  computed: {
    currentTabComponent() {
      return 'tab-' + this.currentTab.toLowerCase()
    }
  }
})

app.component('tab-home', {
  template: `<div class="demo-tab">Home component</div>`
})
app.component('tab-posts', {
  template: `<div class="demo-tab">Posts component</div>`
})
app.component('tab-archive', {
  template: `<div class="demo-tab">Archive component</div>`
})

app.mount('#dynamic-component-demo')
</script>

DOM Template Parsing Caveats

如果想直接在 DOM 撰寫 template,vue 會從 DOM 取得 template string,這樣可能會發生 browser 在 native HTML parsing 的警告

某些 html element 有限制裡面可以放的 element,ex: ul, ol, table, select

當這樣寫的時候,會產生警告

<table>
  <blog-post-row></blog-post-row>
</table>

解決方式,用 is ,裡面一定要用 "vue:" 為 prefix

<table>
  <tr is="vue:blog-post-row"></tr>
</table>

html attribute name 為 case-insensitive

如果在 in-DOM template 使用 camelCased prop name, event handler parameters,需要改為 kebab-cased (hyphen-delimited)

// camelCase in JavaScript

app.component('blog-post', {
  props: ['postTitle'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
})
<!-- kebab-case in HTML -->

<blog-post post-title="hello!"></blog-post>

References

Vue Guide

重新認識 Vue.js

沒有留言:

張貼留言