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,跟 addEventListener
的 passive
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 綁定,這樣做的優點:
- 移除 HTML template 時,就移除了 handler function,很容易維護
- 因不需要在 js 手動綁定 event listener,ViewModel 的 code 可單純只有 logic 且為 DOM-free,很容易測試
- 當 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
- text, textarea 使用
value
property 及input
event - checkboxes, radiobuttons 使用
checked
property 及change
event - 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>
必須滿足
- bind
value
property 到modelValue
prop - 在
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>
沒有留言:
張貼留言