v-model 与受控组件
一、v-model
v-model 用于在表单输入元素或组件上创建双向绑定。
1、基本用法
v-model 的使用范围:
<input>
<select>
<textarea>
components
举个例子:
<input
:value="text"
@input="event => text = event.target.value"
/>
<input v-model="text" />
注意:v-model 会忽略任何表单元素上的 value
、checked
或 selected
属性。
2、v-model 的修饰符
.lazy
: 监听 change 事件而不是 input;.number
: 将输入的合法符串转为数字;.trim
: 移除输入内容两端空格。
2-1、.lazy
默认情况下,v-model 会在每次 input 事件后更新数据。添加 lazy 修饰符可以改为在每次 change 事件后更新数据:
举个例子:
<template>
<p>age: {{ age }}</p>
<input v-model.lazy="age" />
</template>
<script setup>
import { ref } from 'vue'
const age = ref('')
</script>
实现效果:
2-2、.number
number 修饰符可以让用户输入自动转为数字。
举个例子:
<template>
<p>age: {{ age }}</p>
<input v-model.number="age" />
</template>
<script setup>
import { ref } from 'vue'
const age = ref('')
</script>
实现效果:
2-3、.trim
trim 修饰符可以自动去除用户输入内容中两端的空格。
举个例子:
<template>
<p>age: {{ age }}</p>
<input v-model.trim="age" />
</template>
<script setup>
import { ref } from 'vue'
const age = ref('')
</script>
实现效果:
3、在组件上使用 v-model
<MyInput v-model="age" />
在自定义组件上使用 v-model,相当于:
<MyInput
:modelValue="age"
@update:modelValue="newVal => (age = newVal)"
/>
因此,要使组件能使用 v-model,组件内部需要做两件事:
- 在内部原生元素的
value
上绑定 modelValue; - 输入新值时在元素上触发
update:modelValue
事件并传递新值。
具体例子,封装组件:
- Vue-Composition
- Vue-JSX
<template>
<input
class="my-input"
:value="modelValue"
@input="handleInput"
/>
</template>
<script setup lang="ts">
interface InputProps {
/**
* 输入框值
*/
modelValue?: string | number
}
interface InputEmits {
/**
* 输入框输入时触发
*/
(type: 'update:modelValue', val: string | number): void
}
const {
modelValue = undefined,
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()
const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
emit('update:modelValue', newVal)
}
</script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Input',
props: {
/**
* 输入框值
*/
modelValue: {
type: [String, Number],
default: undefined,
},
},
emits: {
/**
* 输入框输入时触发
*/
'update:modelValue': (val: string | number) => true,
},
setup(props, { emit }) {
const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value;
emit('update:modelValue', newVal);
};
return () => {
return (
<input
class="my-input"
value={props.modelValue}
onInput={handleInput}
/>
);
};
},
});
使用组件:
<template>
<p>age: {{ age }}</p>
<MyInput v-model="age" />
</template>
<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'
// import MyInput from './MyInput.tsx'
const age = ref(123)
</script>
实现效果:
4、组件绑定多个 v-model
默认情况下,v-model 在组件上使用 modelValue
作为 prop,并以 update:modelValue
作为对应的事件。可以给 v-model 指定一个参数来更改这些名字,使用组件时通过 : + 参数
的方式控制对应的 v-model,例如:
- 组件封装
- 组件使用
<template>
<input
class="my-input"
:value="name"
@input="handleInput"
/>
</template>
<script setup lang="ts">
interface InputProps {
/**
* 输入框值
*/
name?: string | number
}
interface InputEmits {
/**
* 输入框输入时触发
*/
(type: 'update:name', val: string | number): void
}
const {
modelValue = undefined,
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()
const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
emit('update:name', newVal)
}
</script>
<template>
<p>msg: {{ msg }}</p>
<MyInput v-model:name="msg" />
</template>
<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'
const msg = ref(123)
</script>
通过 v-model 参数的特性,可以给组件添加多个 v-model:
- 组件封装
- 组件使用
<template>
<input
:value="name1"
@input="handleInput1"
/>
<input
:value="name2"
@input="handleInput2"
/>
</template>
<script setup lang="ts">
interface InputProps {
/**
* 输入框1值
*/
name1?: string | number
/**
* 输入框2值
*/
name2?: string | number
}
interface InputEmits {
/**
* 输入框1输入时触发
*/
(type: 'update:name1', val: string | number): void
/**
* 输入框2输入时触发
*/
(type: 'update:name2', val: string | number): void
}
const {
modelValue = undefined,
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()
const handleInput1 = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
emit('update:name1', newVal)
}
const handleInput2 = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
emit('update:name2', newVal)
}
</script>
<template>
<p>msg1: {{ msg1 }}</p>
<p>msg2: {{ msg2 }}</p>
<MyInput
v-model:name1="msg1"
v-model:name2="msg2"
/>
</template>
<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'
const msg1 = ref('111')
const msg2 = ref('222')
</script>
实现效果:
二、Vue 受控组件
1、一般受控
一般情况下,组件通过传入 value
作为受控固定值,传入 defaultValue
作为非受控默认值;
其中 value
的值一旦固定,组件内部关联的变量将随之而固定,这就是所谓的受控;
而 defaultValue
即使固定不变,组件内部关联的变量也不受其影响,依然可以改变,这就是非受控。
- Vue-Composition
- Vue-JSX
<template>
<button @click="handleSwitch">
{{ innerValue ? '开' : '关' }}
</button>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface SwitchProps {
/**
* 开关固定值(受控)
*/
value?: boolean
/**
* 开关默认值(非受控)
* @default false
*/
defaultValue?: boolean
}
interface SwitchEmits {
/**
* 切换开关时触发
*/
(type: 'change', val: boolean): void
}
const {
value = undefined, // 注意这里传入的受控值需主动声明为 undefined
defaultValue = false
} = defineProps<SwitchProps>()
const emit = defineEmits<SwitchEmits>()
const _switchValue = ref(defaultValue)
const innerValue = computed(() => value ?? _switchValue.value)
const handleSwitch = () => {
const newVal = !innerValue.value
_switchValue.value = newVal
emit('change', newVal) // 这里需要主动调用外部传入的 change 函数
}
</script>
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
name: 'Switch',
props: {
/**
* 开关固定值(受控)
*/
value: {
type: Boolean,
default: undefined // 注意这里传入的受控值需主动声明为 undefined
},
/**
* 开关默认值(非受控)
* @default false
*/
defaultValue: {
type: Boolean,
default: false
}
},
emits: {
/**
* 切换开关时触发
*/
'change': (val: boolean) => true,
},
setup(props, { emit }) {
const _switchValue = ref(props.defaultValue)
const innerValue = computed(() => props.value ?? _switchValue.value)
const handleSwitch = () => {
const newVal = !innerValue.value
_switchValue.value = newVal
emit('change', newVal) // 这里需要主动调用外部传入的 change 函数
}
return () => {
return (
<button
onClick={handleSwitch}
>
{innerValue.value ? '开' : '关'}
</button>
);
};
},
});
2、input 受控
在 React 中,input
/ textarea
设置了 value 属性则为受控组件,可通过 onChange 事件配合 setState()
改变 value
值来更新输入框的值。
而在 Vue 中,input
/ textarea
设置 value 值,当 value 发生变化时,输入框中的值并不会跟着变化,而且即使 value 属性定死,输入框仍可以自由输入。另外,Vue 中 onInput 事件相当于 React 的 onChange 事件。
Vue 的 input 受控需要在 onInput 中通过 nextTick 单独处理。
- Vue-Composition
- Vue-JSX
<template>
<input
ref="inputRef"
:value="innerValue"
@input="handleInput"
/>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
interface InputProps {
/**
* 输入框固定值(受控)
*/
value?: string | number
/**
* 输入框默认值(非受控)
* @default false
*/
defaultValue?: string | number
}
interface InputEmits {
/**
* 输入框输入时触发
*/
(type: 'input', val: string | number, ev: Event): void
}
const {
value = undefined, // 注意这里传入的受控值需主动声明为 undefined
defaultValue = ''
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()
const _inputValue = ref(defaultValue)
const innerValue = computed(() => value ?? _inputValue.value)
const inputRef = ref<HTMLInputElement>()
const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
_inputValue.value = newVal
emit('input', newVal, e)
// 受控处理
nextTick(() => {
if (inputRef.value && innerValue.value !== inputRef.value.value) {
inputRef.value.value = innerValue.value as string
}
})
}
</script>
import { computed, defineComponent, ref, nextTick } from 'vue';
export default defineComponent({
name: 'Input',
props: {
/**
* 输入框固定值(受控)
*/
value: {
type: [String, Number],
default: undefined // 注意这里传入的受控值需主动声明为 undefined
},
/**
* 输入框默认值(非受控)
* @default ''
*/
defaultValue: {
type: [String, Number],
default: ''
}
},
emits: {
/**
* 输入框输入时触发
*/
'input': (val: string | number, ev: Event) => true,
},
setup(props, { emit }) {
const _inputValue = ref(props.defaultValue)
const innerValue = computed(() => props.value ?? _inputValue.value)
const inputRef = ref<HTMLInputElement>()
const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
_inputValue.value = newVal
emit('input', newVal, e)
// 受控处理
nextTick(() => {
if (inputRef.value && innerValue.value !== inputRef.value.value) {
inputRef.value.value = innerValue.value as string
}
})
}
return () => {
return (
<input
ref={inputRef}
value={innerValue.value}
onInput={handleInput}
/>
);
};
},
});
3、与 v-model 结合使用
在 Vue 中,还多了 v-model 语法糖来辅助组件受控,可通过以下方式进行受控组件封装:
- Vue-Composition
- Vue-JSX
<template>
<input
ref="inputRef"
:value="innerValue"
@input="handleInput"
/>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
interface InputProps {
/**
* 输入框固定值(受控)
*/
modelValue?: string | number
/**
* 输入框默认值(非受控)
* @default false
*/
defaultValue?: string | number
}
interface InputEmits {
/**
* 输入框输入时触发 v-model
*/
(type: 'update:modelValue', val: string | number): void
/**
* 输入框输入时触发
*/
(type: 'input', val: string | number, ev: Event): void
}
const {
modelValue = undefined, // 注意这里传入的受控值需主动声明为 undefined
defaultValue = ''
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()
const _inputValue = ref(defaultValue)
const innerValue = computed(() => modelValue ?? _inputValue.value)
const inputRef = ref<HTMLInputElement>()
const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
_inputValue.value = newVal
emit('input', newVal, e)
emit('update:modelValue', newVal)
// 受控处理
nextTick(() => {
if (inputRef.value && innerValue.value !== inputRef.value.value) {
inputRef.value.value = innerValue.value as string
}
})
}
</script>
import { computed, defineComponent, ref, nextTick } from 'vue';
export default defineComponent({
name: 'Input',
props: {
/**
* 输入框固定值(受控)
*/
modelValue: {
type: [String, Number],
default: undefined // 注意这里传入的受控值需主动声明为 undefined
},
/**
* 输入框默认值(非受控)
* @default ''
*/
defaultValue: {
type: [String, Number],
default: ''
}
},
emits: {
/**
* 输入框输入时触发 v-model
*/
'update:modelValue': (val: string | number) => true,
/**
* 输入框输入时触发
*/
'input': (val: string | number, ev: Event) => true,
},
setup(props, { emit }) {
const _inputValue = ref(props.defaultValue)
const innerValue = computed(() => props.modelValue ?? _inputValue.value)
const inputRef = ref<HTMLInputElement>()
const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
_inputValue.value = newVal
emit('input', newVal, e)
emit('update:modelValue', newVal)
// 受控处理
nextTick(() => {
if (inputRef.value && innerValue.value !== inputRef.value.value) {
inputRef.value.value = innerValue.value as string
}
})
}
return () => {
return (
<input
ref={inputRef}
value={innerValue.value}
onInput={handleInput}
/>
);
};
},
});