Data Binding
Data binding is one of the core features of uni-app, allowing you to establish connections between data and views. This guide will introduce various data binding methods and best practices.
Basic Data Binding
Text Interpolation
Use double curly braces for text interpolation:
<template>
<view>
<text>Hello {{ name }}!</text>
<text>Current time: {{ currentTime }}</text>
</view>
</template>
<script>
export default {
data() {
return {
name: 'World',
currentTime: new Date().toLocaleString()
}
}
}
</script>
HTML Content Binding
Use v-html
directive to bind HTML content:
<template>
<view>
<view v-html="htmlContent"></view>
</view>
</template>
<script>
export default {
data() {
return {
htmlContent: '<strong>Bold text</strong>'
}
}
}
</script>
Note: Be cautious when using v-html
to prevent XSS attacks.
Attribute Binding
Basic Attribute Binding
Use v-bind
directive or :
shorthand to bind attributes:
<template>
<view>
<image :src="imageSrc" :alt="imageAlt" />
<button :disabled="isDisabled">{{ buttonText }}</button>
<view :class="containerClass">Container</view>
</view>
</template>
<script>
export default {
data() {
return {
imageSrc: '/static/logo.png',
imageAlt: 'Logo',
isDisabled: false,
buttonText: 'Click me',
containerClass: 'main-container'
}
}
}
</script>
Dynamic Attribute Names
<template>
<view>
<button :[attributeName]="attributeValue">Dynamic attribute</button>
</view>
</template>
<script>
export default {
data() {
return {
attributeName: 'disabled',
attributeValue: true
}
}
}
</script>
Boolean Attributes
For boolean attributes, if the bound value is truthy, the attribute will be included:
<template>
<view>
<button :disabled="isButtonDisabled">Button</button>
<input :readonly="isReadonly" />
</view>
</template>
<script>
export default {
data() {
return {
isButtonDisabled: true, // disabled attribute will be added
isReadonly: false // readonly attribute will not be added
}
}
}
</script>
Class and Style Binding
Class Binding
Object Syntax
<template>
<view>
<view :class="{ active: isActive, 'text-danger': hasError }">
Dynamic classes
</view>
</view>
</template>
<script>
export default {
data() {
return {
isActive: true,
hasError: false
}
}
}
</script>
Array Syntax
<template>
<view>
<view :class="[activeClass, errorClass]">Array classes</view>
<view :class="[isActive ? activeClass : '', errorClass]">
Conditional array classes
</view>
</view>
</template>
<script>
export default {
data() {
return {
activeClass: 'active',
errorClass: 'text-danger',
isActive: true
}
}
}
</script>
Mixed Syntax
<template>
<view>
<view :class="[{ active: isActive }, errorClass]">
Mixed syntax
</view>
</view>
</template>
Style Binding
Object Syntax
<template>
<view>
<view :style="{ color: activeColor, fontSize: fontSize + 'px' }">
Dynamic styles
</view>
</view>
</template>
<script>
export default {
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
}
</script>
Array Syntax
<template>
<view>
<view :style="[baseStyles, overridingStyles]">
Array styles
</view>
</view>
</template>
<script>
export default {
data() {
return {
baseStyles: {
color: 'red',
fontSize: '13px'
},
overridingStyles: {
color: 'blue'
}
}
}
}
</script>
Form Input Binding
Two-way Data Binding with v-model
Text Input
<template>
<view>
<input v-model="message" placeholder="Enter message" />
<text>Message: {{ message }}</text>
</view>
</template>
<script>
export default {
data() {
return {
message: ''
}
}
}
</script>
Textarea
<template>
<view>
<textarea v-model="text" placeholder="Enter text"></textarea>
<text>{{ text }}</text>
</view>
</template>
<script>
export default {
data() {
return {
text: ''
}
}
}
</script>
Checkbox
<template>
<view>
<!-- Single checkbox -->
<label>
<checkbox v-model="checked" />
{{ checked }}
</label>
<!-- Multiple checkboxes -->
<view>
<label v-for="item in items" :key="item.id">
<checkbox :value="item.value" v-model="checkedItems" />
{{ item.label }}
</label>
</view>
<text>Checked: {{ checkedItems }}</text>
</view>
</template>
<script>
export default {
data() {
return {
checked: false,
checkedItems: [],
items: [
{ id: 1, label: 'Option A', value: 'a' },
{ id: 2, label: 'Option B', value: 'b' },
{ id: 3, label: 'Option C', value: 'c' }
]
}
}
}
</script>
Radio
<template>
<view>
<radio-group v-model="picked">
<label v-for="item in options" :key="item.value">
<radio :value="item.value" />
{{ item.label }}
</label>
</radio-group>
<text>Picked: {{ picked }}</text>
</view>
</template>
<script>
export default {
data() {
return {
picked: '',
options: [
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b' },
{ label: 'Option C', value: 'c' }
]
}
}
}
</script>
Select (Picker)
<template>
<view>
<picker
:range="options"
range-key="label"
:value="selectedIndex"
@change="handleChange"
>
<view class="picker">
{{ selectedOption ? selectedOption.label : 'Please select' }}
</view>
</picker>
</view>
</template>
<script>
export default {
data() {
return {
selectedIndex: 0,
options: [
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b' },
{ label: 'Option C', value: 'c' }
]
}
},
computed: {
selectedOption() {
return this.options[this.selectedIndex]
}
},
methods: {
handleChange(e) {
this.selectedIndex = e.detail.value
}
}
}
</script>
v-model Modifiers
.lazy
By default, v-model
syncs the input with the data after each input
event. You can add the lazy
modifier to sync after change
events instead:
<template>
<input v-model.lazy="msg" />
</template>
.number
If you want user input to be automatically typecast as a Number:
<template>
<input v-model.number="age" type="number" />
</template>
.trim
If you want whitespace from user input to be trimmed automatically:
<template>
<input v-model.trim="msg" />
</template>
Computed Properties
Computed properties are cached based on their reactive dependencies and will only re-evaluate when some of their reactive dependencies have changed.
Basic Example
<template>
<view>
<text>Original message: "{{ message }}"</text>
<text>Computed reversed message: "{{ reversedMessage }}"</text>
</view>
</template>
<script>
export default {
data() {
return {
message: 'Hello'
}
},
computed: {
reversedMessage() {
return this.message.split('').reverse().join('')
}
}
}
</script>
Computed vs Methods
<template>
<view>
<!-- Computed property (cached) -->
<text>{{ reversedMessage }}</text>
<text>{{ reversedMessage }}</text>
<!-- Method (called every time) -->
<text>{{ reverseMessage() }}</text>
<text>{{ reverseMessage() }}</text>
</view>
</template>
<script>
export default {
data() {
return {
message: 'Hello'
}
},
computed: {
reversedMessage() {
console.log('Computed called') // Only called once
return this.message.split('').reverse().join('')
}
},
methods: {
reverseMessage() {
console.log('Method called') // Called twice
return this.message.split('').reverse().join('')
}
}
}
</script>
Computed Setter
Computed properties are by default getter-only, but you can also provide a setter:
<template>
<view>
<text>{{ fullName }}</text>
<button @click="changeName">Change Name</button>
</view>
</template>
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName: {
get() {
return this.firstName + ' ' + this.lastName
},
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
},
methods: {
changeName() {
this.fullName = 'Jane Smith'
}
}
}
</script>
Watchers
While computed properties are more appropriate in most cases, there are times when a custom watcher is necessary.
Basic Example
<template>
<view>
<input v-model="question" placeholder="Ask a yes/no question" />
<text>{{ answer }}</text>
</view>
</template>
<script>
export default {
data() {
return {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
}
},
watch: {
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
methods: {
getAnswer() {
this.answer = 'Thinking...'
setTimeout(() => {
this.answer = Math.random() > 0.5 ? 'Yes' : 'No'
}, 1000)
}
}
}
</script>
Deep Watching
To detect nested value changes inside Objects, you need to pass deep: true
:
<script>
export default {
data() {
return {
user: {
name: 'John',
profile: {
age: 25,
city: 'New York'
}
}
}
},
watch: {
user: {
handler(newVal, oldVal) {
console.log('User changed:', newVal)
},
deep: true
}
}
}
</script>
Immediate Execution
If you want the callback to be fired immediately with the current value:
<script>
export default {
data() {
return {
message: 'Hello'
}
},
watch: {
message: {
handler(newVal, oldVal) {
console.log('Message changed:', newVal)
},
immediate: true
}
}
}
</script>
Advanced Data Binding Patterns
Dynamic Component Props
<template>
<view>
<component
:is="currentComponent"
v-bind="componentProps"
@custom-event="handleEvent"
/>
</view>
</template>
<script>
export default {
data() {
return {
currentComponent: 'my-component',
componentProps: {
title: 'Dynamic Title',
data: [1, 2, 3],
config: { theme: 'dark' }
}
}
},
methods: {
handleEvent(payload) {
console.log('Event received:', payload)
}
}
}
</script>
Slot Props
<!-- Parent Component -->
<template>
<my-list :items="items">
<template v-slot:item="{ item, index }">
<view class="custom-item">
{{ index + 1 }}. {{ item.name }} - {{ item.price }}
</view>
</template>
</my-list>
</template>
<!-- Child Component (my-list) -->
<template>
<view>
<view v-for="(item, index) in items" :key="item.id">
<slot name="item" :item="item" :index="index">
<!-- Default content -->
<text>{{ item.name }}</text>
</slot>
</view>
</view>
</template>
Provide/Inject
<!-- Ancestor Component -->
<script>
export default {
provide() {
return {
theme: this.theme,
updateTheme: this.updateTheme
}
},
data() {
return {
theme: 'light'
}
},
methods: {
updateTheme(newTheme) {
this.theme = newTheme
}
}
}
</script>
<!-- Descendant Component -->
<script>
export default {
inject: ['theme', 'updateTheme'],
template: `
<view :class="theme">
<button @click="updateTheme('dark')">Switch to Dark</button>
</view>
`
}
</script>
Performance Optimization
Use Object.freeze for Static Data
For large arrays or objects that won't change, use Object.freeze()
to prevent Vue from making them reactive:
export default {
data() {
return {
// This large list won't be reactive
staticList: Object.freeze([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
// ... many more items
])
}
}
}
Avoid Inline Object/Array Creation in Templates
<template>
<view>
<!-- Avoid: Creates new object on every render -->
<my-component :style="{ color: 'red', fontSize: '14px' }" />
<!-- Better: Use computed property or data -->
<my-component :style="componentStyle" />
</view>
</template>
<script>
export default {
computed: {
componentStyle() {
return {
color: 'red',
fontSize: '14px'
}
}
}
}
</script>
Use v-once for Static Content
For content that will never change, use v-once
directive:
<template>
<view>
<!-- This will only render once -->
<expensive-component v-once :data="staticData" />
</view>
</template>
Common Patterns and Best Practices
Form Validation
<template>
<view>
<form @submit="handleSubmit">
<view class="form-group">
<input
v-model="form.email"
:class="{ error: errors.email }"
placeholder="Email"
@blur="validateEmail"
/>
<text v-if="errors.email" class="error-message">
{{ errors.email }}
</text>
</view>
<view class="form-group">
<input
v-model="form.password"
:class="{ error: errors.password }"
type="password"
placeholder="Password"
@blur="validatePassword"
/>
<text v-if="errors.password" class="error-message">
{{ errors.password }}
</text>
</view>
<button :disabled="!isFormValid" type="submit">
Submit
</button>
</form>
</view>
</template>
<script>
export default {
data() {
return {
form: {
email: '',
password: ''
},
errors: {}
}
},
computed: {
isFormValid() {
return Object.keys(this.errors).length === 0 &&
this.form.email &&
this.form.password
}
},
methods: {
validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!this.form.email) {
this.$set(this.errors, 'email', 'Email is required')
} else if (!emailRegex.test(this.form.email)) {
this.$set(this.errors, 'email', 'Invalid email format')
} else {
this.$delete(this.errors, 'email')
}
},
validatePassword() {
if (!this.form.password) {
this.$set(this.errors, 'password', 'Password is required')
} else if (this.form.password.length < 6) {
this.$set(this.errors, 'password', 'Password must be at least 6 characters')
} else {
this.$delete(this.errors, 'password')
}
},
handleSubmit() {
this.validateEmail()
this.validatePassword()
if (this.isFormValid) {
console.log('Form submitted:', this.form)
}
}
}
}
</script>
Search and Filter
<template>
<view>
<input
v-model="searchQuery"
placeholder="Search items..."
class="search-input"
/>
<view class="filters">
<button
v-for="category in categories"
:key="category"
:class="{ active: selectedCategory === category }"
@click="selectedCategory = category"
>
{{ category }}
</button>
</view>
<view class="items">
<view
v-for="item in filteredItems"
:key="item.id"
class="item"
>
<text>{{ item.name }}</text>
<text>{{ item.category }}</text>
</view>
</view>
<text v-if="filteredItems.length === 0" class="no-results">
No items found
</text>
</view>
</template>
<script>
export default {
data() {
return {
searchQuery: '',
selectedCategory: 'All',
items: [
{ id: 1, name: 'Apple', category: 'Fruit' },
{ id: 2, name: 'Banana', category: 'Fruit' },
{ id: 3, name: 'Carrot', category: 'Vegetable' },
{ id: 4, name: 'Broccoli', category: 'Vegetable' }
]
}
},
computed: {
categories() {
const cats = [...new Set(this.items.map(item => item.category))]
return ['All', ...cats]
},
filteredItems() {
let filtered = this.items
// Filter by category
if (this.selectedCategory !== 'All') {
filtered = filtered.filter(item => item.category === this.selectedCategory)
}
// Filter by search query
if (this.searchQuery) {
filtered = filtered.filter(item =>
item.name.toLowerCase().includes(this.searchQuery.toLowerCase())
)
}
return filtered
}
}
}
</script>
Loading States
<template>
<view>
<button @click="loadData" :disabled="loading">
{{ loading ? 'Loading...' : 'Load Data' }}
</button>
<view v-if="loading" class="loading">
<text>Loading...</text>
</view>
<view v-else-if="error" class="error">
<text>Error: {{ error }}</text>
<button @click="loadData">Retry</button>
</view>
<view v-else-if="data" class="data">
<view v-for="item in data" :key="item.id">
{{ item.name }}
</view>
</view>
<view v-else class="empty">
<text>No data loaded</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
loading: false,
error: null,
data: null
}
},
methods: {
async loadData() {
this.loading = true
this.error = null
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000))
// Simulate random success/failure
if (Math.random() > 0.3) {
this.data = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
} else {
throw new Error('Failed to load data')
}
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
}
}
}
</script>
Summary
Data binding is a fundamental concept in uni-app that enables reactive user interfaces. Key points to remember:
- Use interpolation
for text content
- Use
v-bind
or:
for attribute binding - Use
v-model
for two-way data binding with form inputs - Leverage computed properties for derived data
- Use watchers for side effects and async operations
- Follow performance best practices to ensure smooth user experience
By mastering these data binding techniques, you can build dynamic and interactive applications that respond to user input and data changes efficiently.