双向绑定
双向绑定是表单场景中的常见需求——子组件修改值后,需要同步通知父组件更新。Vitarx 通过 useModel 提供了一种优雅的双向绑定方案。
useModel() — 创建双向绑定属性引用
useModel 创建一个 ModelRef 对象,它代理了 props 中某个属性的读写操作:
- 读取
model.value时,返回props[propName]的当前值 - 设置
model.value时,自动调用props['onUpdate:propName']通知父组件
typescript
declare function useModel<T extends AnyProps, K extends keyof T>(
props: T,
propName: K,
defaultValue?: T[K]
): ModelRef<T, K>tsx
import { useModel } from 'vitarx'
function MyInput(props: { value?: string; 'onUpdate:value'?: (val: string) => void }) {
const model = useModel(props, 'value')
return (
<input
value={model.value}
onInput={(e) => {
model.value = e.target.value
}}
/>
)
}命名约定
Vitarx 的双向绑定遵循以下命名约定:
| 属性名 | 更新事件名 | 说明 |
|---|---|---|
value | onUpdate:value | 通用值绑定 |
modelValue | onUpdate:modelValue | v-model 默认绑定 |
customProp | onUpdate:customProp | 自定义属性绑定 |
当你在子组件中通过 useModel(props, 'value') 修改值时,框架会自动调用 props['onUpdate:value'] 将新值传递给父组件。
WithModelEvent 类型工具
在 TypeScript 中手动声明 onUpdate:xxx 事件类型比较繁琐。WithModelEvent 类型工具可以自动为指定属性生成对应的更新事件类型:
typescript
type WithModelEvent<T extends AnyProps, K extends keyof T> = T & {
[key in K extends string ? `onUpdate:#123;K}` : never]?: (value: T[K]) => void
}使用示例:
tsx
import { useModel, type WithModelEvent } from 'vitarx'
// 不用 WithModelEvent — 需要手动声明更新事件
interface MyInputProps1 {
value?: string
'onUpdate:value'?: (val: string) => void
title?: string
'onUpdate:title'?: (val: string) => void
}
// 使用 WithModelEvent — 自动生成更新事件类型
type MyInputProps2 = WithModelEvent<
{
value?: string
title?: string
},
'value' | 'title'
>
function MyInput(props: MyInputProps2) {
const valueModel = useModel(props, 'value')
const titleModel = useModel(props, 'title')
return (
<div>
<input
value={titleModel.value}
onInput={(e) => {
titleModel.value = e.target.value
}}
/>
<input
value={valueModel.value}
onInput={(e) => {
valueModel.value = e.target.value
}}
/>
</div>
)
}v-model 语法糖
在 JSX 中,Vitarx 提供了 v-model 属性作为语法糖,仅支持 v-model <-> modelValue 的约定:
tsx
import { ref } from 'vitarx'
function App() {
const text = ref('')
// 使用 v-model 语法糖
return <MyInput v-model={text} />
// 等效于:
// return <MyInput modelValue={text} onUpdate:modelValue={(v) => { text.value = v }} />
}
v-model仅支持与modelValue属性的绑定,不支持v-model:propName形式。自定义属性的双向绑定请使用useModel。
自定义 v-model 示例
下面展示如何创建一个支持双向绑定的自定义组件:
tsx
import { ref, useModel, type WithModelEvent } from 'vitarx'
// 定义 props 类型,使用 WithModelEvent 自动生成更新事件
type SearchInputProps = WithModelEvent<
{
modelValue?: string
placeholder?: string
},
'modelValue'
>
function SearchInput(props: SearchInputProps) {
const model = useModel(props, 'modelValue')
const isFocused = ref(false)
return (
<div class={`search-input #123;isFocused.value ? 'focused' : ''}`}>
<span class="search-icon">🔍</span>
<input
value={model.value}
placeholder={props.placeholder || '搜索...'}
onInput={(e) => {
model.value = e.target.value
}}
onFocus={() => {
isFocused.value = true
}}
onBlur={() => {
isFocused.value = false
}}
/>
{model.value && (
<button
class="clear-btn"
onClick={() => {
model.value = ''
}}
>
×
</button>
)}
</div>
)
}
// 使用
function App() {
const keyword = ref('')
return (
<div>
<SearchInput v-model={keyword} placeholder="输入关键词" />
<p>搜索关键词:{keyword}</p>
</div>
)
}useModel 的默认值
useModel 的第三个参数可以为属性设置默认值。当 props[propName] 为 undefined 时,会使用默认值:
tsx
import { useModel } from 'vitarx'
function Toggle(props: { visible?: boolean; 'onUpdate:visible'?: (v: boolean) => void }) {
// 当 props.visible 为 undefined 时,默认值为 false
const visible = useModel(props, 'visible', false)
return (
<button
onClick={() => {
visible.value = !visible.value
}}
>
{visible.value ? '开启' : '关闭'}
</button>
)
}巧用 useModel 绕过 props 只读限制
由于 useModel 在设置值时会通过事件通知父组件更新,你可以用它来"修改"props 的值,而不会违反 props 只读的规则:
tsx
import { useModel } from 'vitarx'
function Dialog(props: { show?: boolean; 'onUpdate:show'?: (v: boolean) => void }) {
// 通过 useModel 代理 show 属性,实现"修改"效果
const visible = useModel(props, 'show', false)
return (
<div v-show={visible}>
<p>对话框内容</p>
<button
onClick={() => {
visible.value = false
}}
>
关闭
</button>
</div>
)
}完整示例
下面是一个完整的表单组件示例,展示多种双向绑定场景:
tsx
import { ref, useModel, type WithModelEvent } from 'vitarx'
// ========== 输入框组件 ==========
type TextInputProps = WithModelEvent<
{
modelValue?: string
label?: string
type?: string
},
'modelValue'
>
function TextInput(props: TextInputProps) {
const model = useModel(props, 'modelValue')
return (
<div class="form-item">
{props.label && <label>{props.label}</label>}
<input
type={props.type || 'text'}
value={model.value}
onInput={(e) => {
model.value = e.target.value
}}
/>
</div>
)
}
// ========== 开关组件 ==========
type SwitchProps = WithModelEvent<
{
modelValue?: boolean
label?: string
},
'modelValue'
>
function Switch(props: SwitchProps) {
const model = useModel(props, 'modelValue', false)
return (
<div class="form-item switch-item">
{props.label && <label>{props.label}</label>}
<button
class={`switch #123;model.value ? 'on' : 'off'}`}
onClick={() => {
model.value = !model.value
}}
>
{model.value ? '开启' : '关闭'}
</button>
</div>
)
}
// ========== 选择器组件 ==========
type SelectProps = WithModelEvent<
{
modelValue?: string
options: Array<{ label: string; value: string }>
label?: string
},
'modelValue'
>
function Select(props: SelectProps) {
const model = useModel(props, 'modelValue')
return (
<div class="form-item">
{props.label && <label>{props.label}</label>}
<select
value={model.value}
onChange={(e) => {
model.value = e.target.value
}}
>
{props.options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
)
}
// ========== 注册表单 ==========
function RegisterForm() {
const username = ref('')
const password = ref('')
const agree = ref(false)
const role = ref('user')
const handleSubmit = () => {
if (!username.value || !password.value) {
alert('请填写用户名和密码')
return
}
if (!agree.value) {
alert('请同意用户协议')
return
}
console.log('提交:', {
username: username.value,
password: password.value,
agree: agree.value,
role: role.value
})
}
return (
<div class="register-form">
<h2>注册账号</h2>
<TextInput v-model={username} label="用户名" />
<TextInput v-model={password} label="密码" type="password" />
<Select
v-model={role}
label="角色"
options={[
{ label: '普通用户', value: 'user' },
{ label: '管理员', value: 'admin' }
]}
/>
<Switch v-model={agree} label="同意用户协议" />
<button onClick={handleSubmit}>注册</button>
</div>
)
}