Template Syntax

Expressive, reactive templates with familiar syntax

Text Interpolation

Use curly braces to output reactive text content.

Basic Interpolation

Template
<!-- 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

Template
<!-- 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.

Dynamic 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.

Style Binding
<!-- 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.

Conditional Directives
<!-- 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-if and :else must immediately follow :if or :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>
Component
// ✅ This just works - no tricks needed!
toggle(setting) {
  setting.on = !setting.on
  // UI updates automatically
}
More :for Examples
<!-- 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.

:model Directive
<!-- 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

Keyboard Events
<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
.preventCall event.preventDefault()
.stopCall event.stopPropagation()
.selfOnly trigger if event target is the element itself
.onceOnly trigger handler once
.enterEnter key
.escEscape key
.spaceSpace key
.up/.down/.left/.rightArrow keys

Element References

Use ref to get direct access to DOM elements.

Using Refs
<!-- Template -->
<input ref="emailInput" type="email" />
<button @click="focusEmail">Focus</button>
<div ref="output"></div>
Component
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"