This is the third article in the Vue 3.0 advanced series. It is recommended that you read the nature of the Vue 3.0 directive and what happens after Vue 3.0 $EMIT before reading this article. Before looking at concrete examples, Bob will briefly introduce bidirectional binding, which consists of two one-way binding:
- Model – > View data binding;
- View – > Model event binding.
Pay attention to “the road of full stack Repair fairy” read 4 free e-books of Po Ge original (total download 30,000 +) and 9 advanced series of Vue 3 tutorials.
In Vue :value implements model-to-view data binding, while @event implements view-to-model event binding:
<input :value="searchText" @input="searchText = $event.target.value" />
Copy the code
In forms, we can easily implement bidirectional binding by using built-in V-model directives, such as . After the introduction of the above content, The next Po ge will take a simple example as a starting point, take you step by step to uncover the secret behind the two-way binding.
<div id="app">
<input v-model="searchText" />
<p>Search: {{searchText}}</p>
</div>
<script>
const { createApp } = Vue
const app = createApp({
data() {
return {
searchText: "Po Boy"
}
}
})
app.mount('#app')
</script>
Copy the code
In the example above, we applied the V-Model directive to the input search box, and when the contents of the input box changed, the contents of the P tag were updated synchronously.
To uncover the secrets behind the V-Model directive, we can use the Vue 3 Template Explorer online tool to see the Template compiled:
<input v-model="searchText" />
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode,
withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock("input", {
"onUpdate:modelValue": $event => (searchText = $event)
}, null.8 /* PROPS */["onUpdate:modelValue"])),
[
[_vModelText, searchText]
])
}
}
Copy the code
In the render function generated by the < INPUT V-model =”searchText” /> template, we see the withDirectives function described in the Vue 3.0 Advanced Directive Probe article, which is used to add directive information to the VNode object, It is defined in the Run-time core/ SRC/cache. Ts file:
// packages/runtime-core/src/directives.ts
export function withDirectives<T extends VNode> (vnode: T, directives: DirectiveArguments) :T {
const internalInstance = currentRenderingInstance
// Omit some code
const instance = internalInstance.proxy
const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
// When mounted and updated, fires the same behavior regardless of other hook functions
if (isFunction(dir)) { // Handle function type instructions
dir = {
mounted: dir,
updated: dir
} as ObjectDirective
}
bindings.push({ // Save the instruction information to the vnode.dirs array
dir, instance, value,
oldValue: void 0, arg, modifiers
})
}
return vnode
}
Copy the code
In addition, in the rendering function generated by the template, we see the vModelText instruction. From its name, we guess that the instruction is related to the model, so we first analyze the vModelText instruction.
First, vModelText instruction
The vModelText directive is a directive of type ObjectDirective, which defines three hook functions:
created
: called before the attribute or event listener of the bound element is applied.mounted
: called after the parent component of the bound element has been mounted.beforeUpdate
: called before updating the VNode containing the component.
// packages/runtime-dom/src/directives/vModel.ts
type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
// ...
},
mounted(el, { value }) {
// ..
},
beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
// ..}}Copy the code
Next, we will examine each hook function one by one, starting with the created hook function.
1.1 created a hook
// packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
const castToNumber = number || el.type === 'number' // Whether to convert to a numeric type
// With the lazy modifier, the value of the input box is synchronized with the data after the change event is triggered
addEventListener(el, lazy ? 'change' : 'input'.e= > {
if ((e.target as any).composing) return // The combined input is in progress
let domValue: string | number = el.value
if (trim) { // Automatically filter the first and last whitespace characters entered by the user
domValue = domValue.trim()
} else if (castToNumber) { // Automatically converts user input values to numeric types
domValue = toNumber(domValue)
}
el._assign(domValue) // Update the model
})
if (trim) {
addEventListener(el, 'change'.() = > {
el.value = el.value.trim()
})
}
if(! lazy) { addEventListener(el,'compositionstart', onCompositionStart)
addEventListener(el, 'compositionend', onCompositionEnd)
// Safari < 10.2&uiWebView doesn't fire compositionEnd When
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
addEventListener(el, 'change', onCompositionEnd)
}
},
}
Copy the code
For the created method, it deconstructs the modifiers added to the V-model directive, where the.lazy,. Number, and.trim modifiers can be added. Here’s a quick look at three modifiers:
-
Lazy: By default, v-model synchronizes the value of the input box with the data after each input event is triggered. You can add the lazy modifier to synchronize after the change event.
<! Update change now instead of input --> <input v-model.lazy="msg" /> Copy the code
-
.number modifier: If you want to automatically convert user input values to numeric types, you can add the number modifier to the V-Model. This is often useful because even when type=”number”, the value of the HTML input element always returns a string. If the value cannot be resolved by parseFloat(), the original value is returned.
<input v-model.number="age" type="number" /> Copy the code
-
.TRIM modifier: You can add the trim modifier to the V-Model if you want to automatically filter the leading and trailing whitespace characters entered by the user.
<input v-model.trim="msg" /> Copy the code
Within the Created method, ModelAssigner is obtained through the getModelAssigner function, which is used to update model objects.
// packages/runtime-dom/src/directives/vModel.ts
const getModelAssigner = (vnode: VNode): AssignerFn= > {
constfn = vnode.props! ['onUpdate:modelValue']
return isArray(fn) ? value= > invokeArrayFns(fn, value) : fn
}
Copy the code
For our example, the ModelAssigner object obtained by the getModelAssigner function is the $event => (searchText = $event) function. After the ModelAssigner object is obtained, we can update the values of the model. The rest of the code in the Created method is relatively simple and is not covered in detail. Here we introduce compositionStart and CompositionEnd events.
Chinese, Japanese, Korean and so on need to use the input method combination input, even English, also can use combination input operation such as word selection. In some scenarios, we want to wait for the user to compose the text before performing the corresponding operation, rather than each letter typed. For example, in a keyword search scenario, wait for the user to complete the search by typing the letter “A” before performing the search. To do this, we need compositionStart and comPOSItionEnd events. Also, note that the CompositionStart event occurs before the Input event, so you can use it to optimize the experience of Chinese input.
With compositionStart and comPOSItionEnd events in mind, let’s look at onCompositionStart and onCompositionEnd event handlers:
function onCompositionStart(e: Event) {
;(e.target as any).composing = true
}
function onCompositionEnd(e: Event) {
const target = e.target as any
if (target.composing) {
target.composing = false
trigger(target, 'input')}}// Triggers the specified event on the element
function trigger(el: HTMLElement, type: string) {
const e = document.createEvent('HTMLEvents')
e.initEvent(type.true.true)
el.dispatchEvent(e)
}
Copy the code
In the onCompositionStart event handler, a composing attribute is added to the E.varget object and set to true. In the change event or input event callback, an E.varget object will be returned if its composing property is true. When the combined input is complete, in the onCompositionEnd event handler, the value of target.com Posture is set to false and the input event is manually triggered:
// packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
// Omit some code
addEventListener(el, lazy ? 'change' : 'input'.e= > {
if ((e.target as any).composing) return
// ...}})},Copy the code
A created hook function creates a mounted hook.
1.2 mounted hook
// packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
// set value on mounted so it's after min/max for type="range"
mounted(el, { value }) {
el.value = value == null ? ' ' : value
},
}
Copy the code
Mounted hook logic is simple. If value is null, set the value of the element to an empty string. Otherwise, use value.
1.3 the beforeUpdate hook
// packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
if (document.activeElement === el) {
if (trim && el.value.trim() === value) {
return
}
if ((number || el.type === 'number') && toNumber(el.value) === value) {
return}}const newValue = value == null ? ' ' : value
if(el.value ! == newValue) {// If the old and new values are not equal, the update operation is performed
el.value = newValue
}
}
}
Copy the code
The V-Model directive can be applied not only to input and textarea elements, but also to checkboxes, Radio, and Select. Note, however, that while each of these elements uses v-model directives, checkboxes, checkboxes, and checkboxes are actually performed by different directives. Here we use the checkbox as an example to see the result of template compilation after applying the V-model directive:
<input type="radio" value="One" v-model="picked" />
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { vModelRadio: _vModelRadio, createVNode: _createVNode,
withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock("input", {
type: "radio".value: "One"."onUpdate:modelValue": $event => (picked = $event)
}, null.8 /* PROPS */["onUpdate:modelValue"])), [
[_vModelRadio, picked]
])
}
}
Copy the code
As can be seen from the above code, after v-Model instruction is applied in the single box, the bidirectional binding function will be handed over to vModelRadio instruction to realize. Besides vModelRadio vModelSelect and vModelCheckbox instructions, they are defined in the runtime – dom/SRC/directives/vModel ts file, interested friend can study on their own.
The V-Model is essentially syntax sugar. It listens for user input events to update data and performs special processing in certain scenarios. Note that the V-Model ignores the initial value, Checked, and selected attribute values of all form elements and always uses the data from the current active instance as the data source. You should declare the initial value in the data option of the component.
Additionally, the V-Model internally uses different properties for different input elements and throws different events:
- Text and Textarea elements are used
value
The property andinput
Events; - Checkbox and radio elements
check
The property andchange
Events; - The select element will
value
As prop and willchange
As an event.
As you already know, you can use the V-model directive to create two-way data binding on forms ,
Second, use v-Model on components
Suppose you want to define a custom-input component and use the V-Model directive on it to implement bidirectional binding. Before implementing this function, let’s use the Vue 3 Template Explorer online tool to see the Template compiled:
<custom-input v-model="searchText"></custom-input>
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode,
openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_custom_input = _resolveComponent("custom-input")
return (_openBlock(), _createBlock(_component_custom_input, {
modelValue: searchText,
"onUpdate:modelValue": $event => (searchText = $event)
}, null.8 /* PROPS */["modelValue"."onUpdate:modelValue"))}}Copy the code
By looking at the rendering function above, we can see that the V-model directive is applied to the custom-Input component, which, when compiled by the compiler, generates an input property named modelValue and a custom event name named Update :modelValue. If you are not familiar with the inner workings of custom events, you can read Vue 3.0 Advanced Custom Event Exploration. With this in mind, we are ready to implement the custom-input component:
<div id="app">
<custom-input v-model="searchText"></custom-input>
<p>Search: {{searchText}}</p>
</div>
<script>
const { createApp } = Vue
const app = createApp({
data() {
return {
searchText: "Po Boy"
}
}
})
app.component('custom-input', {
props: ['modelValue'].emits: ['update:modelValue'].template: ` `
})
app.mount('#app')
</script>
Copy the code
The ability to implement bidirectional binding in custom components allows you to define getters and setters using the ability to evaluate properties in addition to using custom events. Here, I will not introduce you. If you are interested, you can read Vue 3’s official website – component basics.
Pay attention to “the road of full stack Repair fairy” read 4 free e-books of Po Ge original (total download 30,000 +) and 9 advanced series of Vue 3 tutorials.
Third, Po Ge has something to say
3.1 How do I Change the Default PROP name and Event name of the V-Model?
By default, V-models on components use modelValue as prop and Update :modelValue as events. We can modify these names by passing parameters to the V-Model directive:
<custom-input v-model:name="searchText"></custom-input>
Copy the code
The above template, compiled by the compiler, produces the following result:
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode,
openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_custom_input = _resolveComponent("custom-input")
return (_openBlock(), _createBlock(_component_custom_input, {
name: searchText,
"onUpdate:name": $event => (searchText = $event)
}, null.8 /* PROPS */["name"."onUpdate:name"))}}Copy the code
By looking at the generated render function, we can see that the custom custom-Input component receives a name input property and contains a custom event named Update :name:
app.component('custom-input', {
props: {
name: String
},
emits: ['update:name'].template: ` `
})
Copy the code
As to why the custom event name is “onUpdate:name”, you can find the emit function described in Vue 3.0 Advanced Custom Event Exploration.
3.2 Can I use more than one V-Model instruction on a component?
In some scenarios, we want to use multiple V-Model directives on a component, each bound to a different data. An example is a user-name component that allows the user to enter firstName and lastName. This component is expected to be used as follows:
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
Copy the code
Again, using the Vue 3 Template Explorer online tool, let’s take a look at the result of compiling the above Template:
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode,
openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_user_name = _resolveComponent("user-name")
return (_openBlock(), _createBlock(_component_user_name, {
"first-name": firstName,
"onUpdate:first-name": $event => (firstName = $event),
"last-name": lastName,
"onUpdate:last-name": $event => (lastName = $event)
}, null.8 /* PROPS */["first-name"."onUpdate:first-name"."last-name"."onUpdate:last-name"))}}Copy the code
By observing the above output results, we can see that v-model:first-name and V-Model :last-name both generate corresponding prop properties and custom events. Attribute names in HTML are case insensitive, so the browser interprets all uppercase characters as lowercase characters. This means that when you use templates in the DOM, camelCase prop names need to be named using their kebab-case equivalents. Such as:
<! -- kebab-case in HTML -->
<blog-post post-title="hello!"></blog-post>
app.component('blog-post', {
props: ['postTitle'].template: '<h3>{{ postTitle }}</h3>'
})
Copy the code
In contrast, for first-name and last-name attribute names, we will use the firstName and lastName hump naming scheme when defining the user-name component.
<div id="app">
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName">
</user-name>
Your name: {{firstName}} {{lastName}}
</div>
<script>
const { createApp } = Vue
const app = createApp({
data() {
return {
firstName: "".lastName: ""
}
}
})
app.component('user-name', {
props: {
firstName: String.lastName: String
},
emits: ['update:firstName'.'update:lastName'].template: ` `
})
app.mount('#app')
</script>
Copy the code
In the above code, the custom attributes and event names used by the user-name component are humped. Does the user-name component above work? The answer is yes, because for custom events, the event name is converted from camelCase to kebab-case via hyphenate inside the emit function, i.e. hyphenate(event) :
// packages/runtime-core/src/componentEmits.ts
export function emit(
instance: ComponentInternalInstance,
event: string. rawArgs:any[]
) {
// Omit some code
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
if(! handler && isModelListener) { handlerName = toHandlerKey(hyphenate(event)) handler = props[handlerName] }if (handler) {
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
}
Copy the code
The implementation of hyphenate functions is also simple, as shown below:
// packages/shared/src/index.ts
const hyphenateRE = /\B([A-Z])/g
// The cacheStringFunction function provides caching
export const hyphenate = cacheStringFunction((str: string) = >
str.replace(hyphenateRE, '- $1').toLowerCase()
)
Copy the code
3.3 How Can I Add a Custom Modifier to the V-Model?
The built-in modifiers for the V-model were introduced earlier:.trim,.number, and.lazy. But in some scenarios, you may want to customize the modifiers. Before we show you how to customize modifiers, let’s use the Vue 3 Template Explorer online tool again to see the results of Template compilation using the built-in modifiers in v-Model:
<custom-input v-model.lazy.number="searchText"></custom-input>
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode,
openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_custom_input = _resolveComponent("custom-input")
return (_openBlock(), _createBlock(_component_custom_input, {
modelValue: searchText,
"onUpdate:modelValue": $event => (searchText = $event),
modelModifiers: { lazy: true.number: true}},null.8 /* PROPS */["modelValue"."onUpdate:modelValue"))}}Copy the code
By looking at the generated render function, we can see that the.lazy and.number modifiers added to the V-model are compiled into the modelModifiers Prop property. Suppose we want to customize a capitalize modifier that capitalizes the first letter of the V-Model binding string:
<custom-input v-model.capitalize="searchText"></custom-input>
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { resolveComponent: _resolveComponent, createVNode: _createVNode,
openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_custom_input = _resolveComponent("custom-input")
return (_openBlock(), _createBlock(_component_custom_input, {
modelValue: searchText,
"onUpdate:modelValue": $event => (searchText = $event),
modelModifiers: { capitalize: true}},null.8 /* PROPS */["modelValue"."onUpdate:modelValue"))}}Copy the code
Apparently the.Capitalize modifier on the V-Model is also compiled into the modelModifiers Prop property. With this in mind, we can implement the above modifiers as follows:
<div id="app">
<custom-input v-model.capitalize="searchText"></custom-input>
<p>Search: {{searchText}}</p>
</div>
<script>
const { createApp } = Vue
const app = createApp({
data() {
return {
searchText: ""
}
}
})
app.component('custom-input', {
props: {
modelValue: String.modelModifiers: {
default: () = >({})}},emits: ['update:modelValue'].methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)}this.$emit('update:modelValue', value)
}
},
template: ``
})
app.mount('#app')
</script>
Copy the code
This article introduces the concept of bidirectional binding and the principle behind bidirectional binding in Vue 3. In order to enable you to master the v-model more in-depth knowledge, Po elder brother from the perspective of source code analysis of vModelText instruction internal implementation. In addition, Arbogo explained how to use multiple V-Model directives in components and how to add custom modifiers to v-Models.
Vue 3.0 advanced series of articles is still updated, currently updated to the ninth, want to learn Vue 3.0 partners can add a Pokemon wechat – Semlinker.
Iv. Reference resources
- Vue 3 官网 – custom commands
- Vue 3 official website – Custom events