x-model 双向绑定 #

什么是 x-model? #

x-model 指令用于在表单元素和数据之间创建双向绑定。当用户修改表单时,数据会自动更新;当数据变化时,表单也会自动更新。

基本语法 #

html
<input x-model="属性名">

基本用法 #

文本输入框 #

html
<div x-data="{ name: '' }">
    <input type="text" x-model="name" placeholder="输入你的名字">
    <p>Hello, <span x-text="name"></span>!</p>
</div>

多行文本框 #

html
<div x-data="{ message: '' }">
    <textarea x-model="message" placeholder="输入消息"></textarea>
    <p>消息: <span x-text="message"></span></p>
</div>

表单元素类型 #

复选框(单个) #

html
<div x-data="{ agreed: false }">
    <label>
        <input type="checkbox" x-model="agreed">
        同意条款
    </label>
    <p x-text="agreed ? '已同意' : '未同意'"></p>
</div>

复选框(多个) #

html
<div x-data="{ 
    selectedFruits: [],
    fruits: ['Apple', 'Banana', 'Orange']
}">
    <template x-for="fruit in fruits">
        <label>
            <input 
                type="checkbox" 
                :value="fruit" 
                x-model="selectedFruits"
            >
            <span x-text="fruit"></span>
        </label>
    </template>
    
    <p>已选择: <span x-text="selectedFruits.join(', ')"></span></p>
</div>

单选按钮 #

html
<div x-data="{ color: 'red' }">
    <label>
        <input type="radio" value="red" x-model="color"> 红色
    </label>
    <label>
        <input type="radio" value="green" x-model="color"> 绿色
    </label>
    <label>
        <input type="radio" value="blue" x-model="color"> 蓝色
    </label>
    
    <p>选择的颜色: <span x-text="color"></span></p>
</div>

下拉选择(单选) #

html
<div x-data="{ 
    selectedCity: '',
    cities: ['Beijing', 'Shanghai', 'Guangzhou']
}">
    <select x-model="selectedCity">
        <option value="">请选择城市</option>
        <template x-for="city in cities">
            <option :value="city" x-text="city"></option>
        </template>
    </select>
    
    <p>选择的城市: <span x-text="selectedCity"></span></p>
</div>

下拉选择(多选) #

html
<div x-data="{ 
    selectedCities: [],
    cities: ['Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen']
}">
    <select x-model="selectedCities" multiple>
        <template x-for="city in cities">
            <option :value="city" x-text="city"></option>
        </template>
    </select>
    
    <p>选择的城市: <span x-text="selectedCities.join(', ')"></span></p>
</div>

修饰符 #

.lazy #

默认情况下,x-modelinput 事件时同步。使用 .lazy 修饰符改为在 change 事件时同步:

html
<div x-data="{ name: '' }">
    <input type="text" x-model.lazy="name" placeholder="失去焦点后更新">
    <p x-text="name"></p>
</div>

.number #

自动将输入值转换为数字:

html
<div x-data="{ age: '' }">
    <input type="number" x-model.number="age" placeholder="输入年龄">
    <p>类型: <span x-text="typeof age"></span></p>
    <p>值: <span x-text="age"></span></p>
</div>

.debounce #

防抖处理,延迟更新:

html
<div x-data="{ search: '' }">
    <input 
        type="text" 
        x-model.debounce.500ms="search" 
        placeholder="搜索..."
    >
    <p>搜索词: <span x-text="search"></span></p>
</div>

.throttle #

节流处理,限制更新频率:

html
<div x-data="{ scroll: 0 }">
    <input 
        type="range" 
        x-model.throttle.100ms="scroll"
        min="0" 
        max="100"
    >
    <p>值: <span x-text="scroll"></span></p>
</div>

组合修饰符 #

html
<input x-model.lazy.number="value">
<input x-model.debounce.300ms.number="value">

自定义值绑定 #

复选框自定义值 #

html
<div x-data="{ status: 'off' }">
    <label>
        <input 
            type="checkbox" 
            x-model="status"
            true-value="on"
            false-value="off"
        >
        开关状态
    </label>
    <p>状态: <span x-text="status"></span></p>
</div>

单选按钮自定义值 #

html
<div x-data="{ answer: null }">
    <label>
        <input 
            type="radio" 
            x-model="answer"
            :value="{ text: 'Yes', code: 1 }"
        >
        是
    </label>
    <label>
        <input 
            type="radio" 
            x-model="answer"
            :value="{ text: 'No', code: 0 }"
        >
        否
    </label>
    
    <p x-show="answer">答案: <span x-text="answer?.text"></span></p>
</div>

实用示例 #

登录表单 #

html
<div x-data="{
    form: {
        email: '',
        password: '',
        remember: false
    },
    errors: {},
    submit() {
        this.errors = {}
        if (!this.form.email) this.errors.email = '请输入邮箱'
        if (!this.form.password) this.errors.password = '请输入密码'
        
        if (Object.keys(this.errors).length === 0) {
            console.log('提交:', this.form)
        }
    }
}" @submit.prevent="submit()">
    <div>
        <label>邮箱</label>
        <input type="email" x-model="form.email">
        <span x-show="errors.email" x-text="errors.email" style="color: red"></span>
    </div>
    
    <div>
        <label>密码</label>
        <input type="password" x-model="form.password">
        <span x-show="errors.password" x-text="errors.password" style="color: red"></span>
    </div>
    
    <div>
        <label>
            <input type="checkbox" x-model="form.remember">
            记住我
        </label>
    </div>
    
    <button type="submit">登录</button>
</div>

搜索表单 #

html
<div x-data="{
    search: '',
    category: 'all',
    results: [],
    async searchItems() {
        if (this.search.length < 2) {
            this.results = []
            return
        }
        const res = await fetch(`/api/search?q=${this.search}&cat=${this.category}`)
        this.results = await res.json()
    }
}">
    <input 
        type="text" 
        x-model.debounce.300ms="search" 
        @input="searchItems()"
        placeholder="搜索..."
    >
    
    <select x-model="category" @change="searchItems()">
        <option value="all">全部分类</option>
        <option value="products">产品</option>
        <option value="articles">文章</option>
    </select>
    
    <ul>
        <template x-for="result in results" :key="result.id">
            <li x-text="result.title"></li>
        </template>
    </ul>
</div>

设置表单 #

html
<div x-data="{
    settings: {
        theme: 'light',
        language: 'zh-CN',
        notifications: {
            email: true,
            push: false,
            sms: false
        },
        privacy: {
            profile: 'public',
            activity: 'friends'
        }
    },
    save() {
        console.log('保存设置:', this.settings)
    }
}">
    <h3>外观设置</h3>
    <label>
        主题:
        <select x-model="settings.theme">
            <option value="light">浅色</option>
            <option value="dark">深色</option>
            <option value="auto">自动</option>
        </select>
    </label>
    
    <h3>通知设置</h3>
    <label>
        <input type="checkbox" x-model="settings.notifications.email">
        邮件通知
    </label>
    <label>
        <input type="checkbox" x-model="settings.notifications.push">
        推送通知
    </label>
    <label>
        <input type="checkbox" x-model="settings.notifications.sms">
        短信通知
    </label>
    
    <h3>隐私设置</h3>
    <label>
        个人资料:
        <select x-model="settings.privacy.profile">
            <option value="public">公开</option>
            <option value="friends">仅好友</option>
            <option value="private">私密</option>
        </select>
    </label>
    
    <button @click="save()">保存设置</button>
</div>

动态表单 #

html
<div x-data="{
    fields: [
        { id: 1, label: '姓名', type: 'text', value: '' },
        { id: 2, label: '邮箱', type: 'email', value: '' },
        { id: 3, label: '电话', type: 'tel', value: '' }
    ],
    addField() {
        this.fields.push({
            id: Date.now(),
            label: '新字段',
            type: 'text',
            value: ''
        })
    },
    removeField(id) {
        this.fields = this.fields.filter(f => f.id !== id)
    }
}">
    <template x-for="field in fields" :key="field.id">
        <div>
            <label x-text="field.label"></label>
            <input :type="field.type" x-model="field.value">
            <button @click="removeField(field.id)">删除</button>
        </div>
    </template>
    
    <button @click="addField()">添加字段</button>
    
    <pre x-text="JSON.stringify(fields, null, 2)"></pre>
</div>

文件上传 #

html
<div x-data="{
    files: [],
    handleFiles(e) {
        this.files = Array.from(e.target.files).map(file => ({
            name: file.name,
            size: (file.size / 1024).toFixed(2) + ' KB',
            type: file.type
        }))
    }
}">
    <input 
        type="file" 
        multiple 
        @change="handleFiles($event)"
    >
    
    <ul>
        <template x-for="file in files" :key="file.name">
            <li>
                <span x-text="file.name"></span>
                <small x-text="file.size"></small>
            </li>
        </template>
    </ul>
</div>

范围滑块 #

html
<div x-data="{
    price: {
        min: 0,
        max: 1000
    },
    value: [0, 500]
}">
    <div>
        <label>最低价格: <span x-text="value[0]"></span></label>
        <input 
            type="range" 
            x-model.number="value[0]"
            min="0" 
            max="1000"
            step="10"
        >
    </div>
    
    <div>
        <label>最高价格: <span x-text="value[1]"></span></label>
        <input 
            type="range" 
            x-model.number="value[1]"
            min="0" 
            max="1000"
            step="10"
        >
    </div>
    
    <p>价格范围: <span x-text="`${value[0]} - ${value[1]}`"></span></p>
</div>

注意事项 #

1. IME 输入法 #

在使用中文等 IME 输入法时,x-model 可能在输入过程中频繁更新。使用 .lazy 修饰符解决:

html
<input x-model.lazy="text">

2. 表单重置 #

使用 x-model 时,表单的 reset() 方法不会重置数据。需要手动重置:

html
<div x-data="{
    form: { name: '', email: '' },
    reset() {
        this.form = { name: '', email: '' }
    }
}">
    <form @reset.prevent="reset()">
        <input x-model="form.name">
        <input x-model="form.email">
        <button type="reset">重置</button>
    </form>
</div>

3. 初始值 #

x-model 会忽略表单元素的初始 value 属性:

html
<input x-model="name" value="Initial">

应该在 x-data 中设置初始值:

html
<div x-data="{ name: 'Initial' }">
    <input x-model="name">
</div>

小结 #

x-model 指令要点:

  • 实现表单元素与数据的双向绑定
  • 支持所有表单元素类型
  • 提供多种修饰符控制更新行为
  • 支持自定义值绑定
  • 注意 IME 输入法问题

下一章,我们将学习 x-textx-html 指令进行内容绑定。

最后更新:2026-03-28