Text Interpolation
Use curly braces to output reactive text content.
Basic Interpolation
<!-- Single expression -->
<p>Hello, { name }!</p>
<!-- Expressions -->
<span>Total: { price * quantity }</span>
<!-- Optional chaining -->
<div>{ user.profile?.bio || 'No bio' }</div>
<!-- Method calls -->
<p>{ formatDate(created) }</p>
HTML Interpolation
<!-- Double braces for raw HTML -->
<div class="content">{{ htmlContent }}</div>
<!-- Use with rendered markdown -->
<article>{{ markdownToHtml(post.body) }}</article>
Warning: Never use {{ }} with user-provided content without sanitization.
Attribute Binding
Use :attr to dynamically bind attributes.
<!-- Basic binding -->
<img :src="imageUrl" :alt="imageAlt" />
<a :href="link.url">{ link.text }</a>
<!-- Input values -->
<input :value="searchTerm" :placeholder="placeholder" />
<!-- Boolean attributes -->
<button :disabled="isLoading">Submit</button>
<input :checked="isChecked" type="checkbox" />
<select :multiple="allowMultiple">...</select>
<!-- Computed values -->
<div :id="'item-' + item.id">{ item.name }</div>
<input :maxlength="maxChars" />
Spread Props with :bind
Spread an object as attributes:
<input :bind="inputProps" />
<!-- Where inputProps = { type: 'email', placeholder: 'Enter email', required: true } -->
Class Binding
Dynamically apply CSS classes with :class.
Object Syntax
<!-- Conditional classes -->
<div :class="{
'active': isActive,
'disabled': isDisabled,
'bg-blue-500': isPrimary,
'text-white': isPrimary
}">
Content
</div>
String/Ternary Syntax
<!-- String expression -->
<div :class="currentTheme">...</div>
<!-- Ternary -->
<button :class="isLoading ? 'opacity-50' : ''">
Submit
</button>
<!-- Combined with static classes -->
<button class="btn" :class="variant">
Method Calls Are Reactive
Method calls inside :class track their dependencies:
<!-- isActiveTab() reads this.currentTab, so class updates automatically -->
<button :class="'tab ' + (isActiveTab('home') ? 'active' : '')">Home</button>
Style Binding
Apply inline styles dynamically with :style.
<!-- Object syntax -->
<div :style="{
backgroundColor: bgColor,
fontSize: fontSize + 'px',
transform: 'rotate(' + rotation + 'deg)'
}">
Styled content
</div>
<!-- String syntax -->
<div :style="'color: ' + textColor + '; padding: ' + padding">
...
</div>
<!-- Conditional -->
<div :style="{ display: isVisible ? 'block' : 'none' }">
Conditional Rendering
Use :if, :else-if, and :else to conditionally render elements.
<!-- Simple conditional -->
<div :if="isLoggedIn">Welcome back!</div>
<div :else>Please log in</div>
<!-- Multiple conditions -->
<div :if="status === 'loading'">
<span class="spinner"></span> Loading...
</div>
<div :else-if="status === 'error'">
<span class="text-red-500">{ errorMessage }</span>
</div>
<div :else-if="items.length === 0">
No items found.
</div>
<div :else>
<ul>
<li :for="item of items">{ item.name }</li>
</ul>
</div>
<!-- With method calls (reactive) -->
<div :if="isAdmin()">Admin Panel</div>
<div :else>User View</div>
Important Notes
:else-ifand:elsemust immediately follow:ifor:else-if- Elements are removed from DOM when condition is false (not just hidden)
- Method calls in conditions are reactive - state changes trigger re-evaluation
List Rendering
Use :for to render lists. It automatically detects both array changes and nested object mutations.
✅ Smart Deep Watching (v0.9.31+)
The :for directive now automatically detects nested object mutations. No more `:foreach` or array reassignment tricks needed!
Basic Usage
<!-- Basic iteration -->
<ul>
<li :for="item of items">{ item.name }</li>
</ul>
<!-- With index -->
<tr :for="user, index of users">
<td>{ index + 1 }</td>
<td>{ user.name }</td>
</tr>
<!-- Destructuring -->
<div :for="{ name, email } of users">
<h3>{ name }</h3>
<p>{ email }</p>
</div>
Mutating Items (Just Works!)
<!-- Toggle settings -->
<div :for="setting of settings">
<span>{ setting.label }</span>
<button @click="toggle(setting)"
:class="setting.on ? 'bg-green-500' : 'bg-gray-300'">
{ setting.on ? 'On' : 'Off' }
</button>
</div>
// ✅ This just works - no tricks needed!
toggle(setting) {
setting.on = !setting.on
// UI updates automatically
}
<!-- Destructuring -->
<div :for="{ name, email } of users">
<h3>{ name }</h3>
<p>{ email }</p>
</div>
<!-- With :key for better performance -->
<div :for="item of items" :key="item.id">
{ item.name }
</div>
<!-- Nested loops -->
<div :for="category of categories">
<h2>{ category.name }</h2>
<ul>
<li :for="item of category.items">{ item.name }</li>
</ul>
</div>
Two-Way Binding
Use :model for convenient two-way data binding on form elements. :bind is an alias that works the same way.
<!-- Text input -->
<input :model="username" placeholder="Username" />
<!-- With .number modifier -->
<input :model.number="quantity" type="number" />
<!-- With .trim modifier -->
<input :model.trim="email" type="email" />
<!-- Checkbox -->
<input :model="rememberMe" type="checkbox" />
<!-- Select -->
<select :model="selectedCountry">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>
<!-- Textarea -->
<textarea :model="message"></textarea>
:model Modifiers
.number- Parse input as number.trim- Trim whitespace.change- Update on change (not input)
What :model Does
:model="prop" expands to:
:value="prop" @input="prop = $event.target.value"
When to Use :model vs :value + emit()
Pages (own state) → Use :model
<!-- page: ticket-form.noun -->
<input :model="form.title" />
<select :model="form.priority">...</select>
Direct two-way binding when the component owns the state.
Child Components (parent owns state) → Use :value + emit()
<!-- component: CustomInput.noun -->
<input :value="value" @input="handleInput" />
// In script:
handleInput(e) {
this.emit('change', e.target.value)
}
Parent owns state, child emits events to request changes.
Key principle: Data flows down (props), events flow up (emit). Child components should never mutate props directly.
Event Handling
Use @event to bind event handlers.
Basic Events
<!-- Method name -->
<button @click="handleClick">Click</button>
<!-- Inline expression -->
<button @click="count++">+</button>
<!-- With event object -->
<input @input="value = $event.target.value" />
<!-- Method with arguments -->
<button @click="deleteItem(item.id)">Delete</button>
Event Modifiers
<!-- Prevent default -->
<form @submit.prevent="handleSubmit">
<!-- Stop propagation -->
<div @click.stop="handleClick">
<!-- Both -->
<a @click.prevent.stop="navigate">
<!-- Only on target element -->
<div @click.self="handleClick">
<!-- Run once -->
<button @click.once="initialize">
Key Modifiers
<input @keyup.enter="search" />
<input @keydown.esc="clearInput" />
<input @keyup.space="togglePlay" />
<input @keydown.delete="handleDelete" />
<div @keydown.up="moveUp" />
<div @keydown.down="moveDown" />
<div @keydown.left="moveLeft" />
<div @keydown.right="moveRight" />
| Modifier | Description |
|---|---|
.prevent | Call event.preventDefault() |
.stop | Call event.stopPropagation() |
.self | Only trigger if event target is the element itself |
.once | Only trigger handler once |
.enter | Enter key |
.esc | Escape key |
.space | Space key |
.up/.down/.left/.right | Arrow keys |
Element References
Use ref to get direct access to DOM elements.
<!-- Template -->
<input ref="emailInput" type="email" />
<button @click="focusEmail">Focus</button>
<div ref="output"></div>
class MyComponent extends Component {
focusEmail() {
// Access via $refs
this.$refs.emailInput.focus()
}
mounted() {
// Refs are available after mount
console.log(this.$refs.output)
}
}
Slots
Use <slot/> to pass content into child components. Slot content is evaluated in the parent's scope.
Parent Component
<user-card :user="user">
<span class="badge">Premium</span>
</user-card>
UserCard Component
<div class="card">
<h3>{ user.name }</h3>
<slot/> <!-- Renders: Premium badge -->
</div>
Named Slots - Component Template
<div class="modal">
<div class="header"><slot name="header"/></div>
<div class="body"><slot/></div>
<div class="footer"><slot name="footer"/></div>
</div>
Named Slots - Usage
<my-modal>
<div slot="header">Modal Title</div>
<p>Body content goes here</p>
<div slot="footer">
<button @click="close">Close</button>
</div>
</my-modal>
Fallback Content
<!-- Shows "Untitled" if no header provided -->
<slot name="title">Untitled</slot>
<slot>No content provided</slot>
Slot Scoping
<!-- Expressions use parent's scope -->
<counter-display>
<p>Count: { count }</p>
<button @click="increment">Add</button>
</counter-display>
Quick Reference
| Syntax | Description | Example |
|---|---|---|
{ expr } |
Text interpolation | { user.name } |
{{ expr }} |
HTML interpolation | {{ htmlContent }} |
:attr |
Dynamic attribute | :src="imageUrl" |
:class |
Dynamic classes | :class="{ active: isActive }" |
:style |
Dynamic styles | :style="{ color: textColor }" |
:if/:else-if/:else |
Conditionals | :if="isVisible" |
:for |
List rendering (with smart deep watching) | :for="item of items" |
:model |
Two-way binding | :model="username" |
@event |
Event handler | @click="handleClick" |
ref |
Element reference | ref="inputEl" |