Directives reference

A complete tour of the directives you can use in ichigo.js templates, with their modifiers and examples. The syntax is Vue-like, so Vue users will feel at home.

Directives are written as element attributes. In addition to the v- forms, the two most common have shorthands:

Full Shorthand Meaning
v-bind:href="url" :href="url" attribute binding
v-on:click="fn" @click="fn" event listener

Live, runnable demos are linked from the Examples page.

Text interpolation {{ }}

Embed an expression with double curly braces. The result is inserted as text and HTML is escaped.

<p>Hello, {{ user.name }} ({{ items.length }} items)</p>
<p>Total: {{ (price * quantity).toLocaleString() }}</p>

Interpolations accept JavaScript expressions (not statements) and can reference data / computed / methods as well as helpers like $refs.

v-text / v-html

v-text replaces the element's textContent with the expression result. Unlike {{ }}, it overwrites the whole content, which is handy to avoid a flash of un-interpolated text.

<span v-text="message"></span>
<!-- Same result as {{ message }} (HTML is escaped) -->

v-html sets innerHTML, rendering raw HTML.

<div v-html="htmlContent"></div>

⚠️ Security: v-html is an easy path to XSS. Use it only on trusted content and never on user input. For plain text use v-text or {{ }}.

v-bind (:)

Bind attributes and properties to an expression.

<img :src="imageUrl" :alt="imageAlt">
<a :href="`/users/${user.id}`">Profile</a>
<button :disabled="isLoading">Submit</button>

Binding class

Object syntax (apply a class when its value is truthy) and array syntax are both supported, and merge with any static class.

<!-- Object syntax -->
<div class="card" :class="{ active: isActive, 'is-done': todo.completed }"></div>

<!-- Array syntax -->
<div :class="['badge', isError ? 'badge-error' : 'badge-ok']"></div>

Binding style

<div :style="{ height: value * 2 + 'px', color: theme.ink }"></div>

v-if / v-else-if / v-else

Conditional rendering. A false branch is not present in the DOM (it is created and destroyed).

<div v-if="count > 10">Greater than 10</div>
<div v-else-if="count > 5">Greater than 5</div>
<div v-else>5 or less</div>

v-else-if / v-else must immediately follow the matching v-if element.

v-show

v-show always renders the element and only toggles display. For elements you toggle frequently, v-show is cheaper than v-if (which creates/destroys).

<div v-show="isVisible">Can be toggled</div>

v-for

Render lists from arrays, objects, numbers, or strings. Add :key for stable diffing (recommended).

<ul>
  <li v-for="(item, index) in items" :key="item.id">
    {{ index }}: {{ item.name }}
  </li>
</ul>

Wrapper-free repetition with <template>

To repeat multiple nodes without an extra wrapper element, place v-for on a <template>.

<dl>
  <template v-for="row in rows" :key="row.id">
    <dt>{{ row.term }}</dt>
    <dd>{{ row.description }}</dd>
  </template>
</dl>

A <template> supports either v-for or v-if, but not both on the same element. When you need both, nest them.

<template v-for="item in items" :key="item.id">
  <div v-if="item.visible">{{ item.name }}</div>
</template>

Combining v-for and v-if

On a regular (non-<template>) element you can combine v-for and v-if. v-for is evaluated first, then v-if per iteration.

<li v-for="item in items" v-if="item.visible" :key="item.id">{{ item.name }}</li>

v-on (@)

Listen for events. Handlers can be a method name or an inline expression.

<button @click="handleClick">Click</button>
<button @click="count++">Inline expression</button>
<form @submit.prevent="handleSubmit">Submit</form>

Handler context

Every event handler receives the event as the first argument and $ctx as the second.

methods: {
  handleClick(event, $ctx) {
    // event        … the DOM event
    // $ctx.element  … the DOM element
    // $ctx.vnode    … the VNode instance
    // $ctx.userData … Proxy-free storage
  }
}

Event modifiers

Modifier Effect
.stop stopPropagation()
.prevent preventDefault()
.capture listen in the capture phase
.self only when the event's target is the element itself
.once run at most once
<div @click.stop="onClick">Stop propagation</div>
<a @click.prevent="onNav">Prevent default</a>

Key modifiers (KeyboardEvent)

.enter .tab .delete (Delete / Backspace) .esc (.escape) .space .up .down .left .right

<input @keyup.enter="submit" @keyup.esc="cancel">

Mouse button modifiers (MouseEvent)

.left .middle .right

<a @mousedown.middle="openInNewTab">Middle-click</a>
<div @contextmenu.prevent="onMenu">Suppress the context menu</div>

System modifier keys

.shift .ctrl .alt .meta. Add .exact to require that no other modifier keys are held.

<button @click.shift="onShiftClick">Shift+Click</button>
<button @click.ctrl.exact="onCtrlOnly">Ctrl-only click</button>

v-model

Two-way binding for form elements.

<input v-model="message">
<textarea v-model="bio"></textarea>

Modifiers

Modifier Effect
.lazy sync on change instead of input
.number cast to a number
.trim trim surrounding whitespace
<input v-model.trim="username">
<input v-model.number="age" type="number">

Per-element behavior

<!-- Checkbox (boolean) -->
<input type="checkbox" v-model="isChecked">

<!-- Custom true/false values -->
<input type="checkbox" v-model="status" :true-value="'yes'" :false-value="'no'">

<!-- Bound to an array (multi-select) -->
<input type="checkbox" value="a" v-model="selected">
<input type="checkbox" value="b" v-model="selected">

<!-- Radio -->
<input type="radio" value="a" v-model="picked">
<input type="radio" value="b" v-model="picked">

<!-- Select (re-applies the selection even when options are generated with v-for) -->
<select v-model="choice">
  <option v-for="o in options" :key="o.value" :value="o.value">{{ o.label }}</option>
</select>
  • Checkbox — binds to a boolean, to a :true-value / :false-value pair, or to an array (the value is added/removed)
  • Radio — binds to the selected value (:value)
  • Select — binds to the selected option's value

A writable computed ({ get, set }) can also be a v-model target — see the Core guide.

v-focus

Manage focus declaratively. Focus is deferred via requestAnimationFrame, so elements revealed just before the directive runs (e.g. inside a v-if) still receive focus reliably.

<!-- Focus once after mount -->
<input v-focus>

<!-- Focus + select all -->
<input v-focus.select>

<!-- Focus + caret at the end -->
<input v-focus.cursor-end value="prefilled">

<!-- Conditional: fires on the falsy → truthy edge -->
<input v-focus="isEditing">
  • Without an expression — focused exactly once after mount
  • With an expression — focuses only on the falsy → truthy edge (never re-focuses on every update)
  • Modifiers .select (select all) / .cursor-end (caret at the end)

Lifecycle hooks

Run code at each stage of an element's lifecycle. Subscribe with @ (v-on); each hook receives $ctx (element / vnode / userData). Use them to integrate third-party libraries or drive animations.

<div v-if="show"
     @mount="onMount"
     @mounted="onMounted"
     @update="onUpdate"
     @updated="onUpdated"
     @unmount="onUnmount"
     @unmounted="onUnmounted">
  Content
</div>
Hook When
@mount before mounting to the DOM
@mounted after mounting
@update before an update
@updated after an update
@unmount before teardown begins (element still in the DOM)
@unmounted after teardown ($ctx.element still available)

userData and automatic cleanup

$ctx.userData is a Map storage that is not touched by the reactive proxy — ideal for holding third-party instances (Chart.js, etc.). Objects with a close() method are cleaned up automatically on teardown.

methods: {
  onMounted($ctx) {
    const chart = new Chart($ctx.element.querySelector('canvas'), { /* ... */ });
    $ctx.userData.set('chart', chart);
  },
  onUpdated($ctx) {
    $ctx.userData.get('chart')?.update();
  },
  onUnmount($ctx) {
    $ctx.userData.get('chart')?.destroy();
  }
}

The teardown order is:

  1. @unmount fires (element still in the DOM)
  2. userData auto-cleanup (close() called)
  3. child nodes destroyed recursively
  4. dependencies unregistered
  5. directive manager cleanup
  6. @unmounted fires (removed from the DOM, but $ctx.element still available)

Hooks also fire when toggled by v-if and for each v-for item. See the live Lifecycle Hooks demo.

Observer directives

Use the browser's native Observer APIs declaratively. Each one is automatically disconnected on teardown, and handlers receive $ctx. Pass options with :options.<kind> or the generic :options.

v-resize

Monitor element size changes with ResizeObserver.

<div v-resize="onResize" :options.resize="{ box: 'border-box' }">
  {{ width }}px × {{ height }}px
</div>
methods: {
  onResize(entries, $ctx) {
    const r = entries[0].contentRect;
    this.width = Math.round(r.width);
    this.height = Math.round(r.height);
  }
}

→ Live demo: Resize Observer

v-intersection

Detect visibility with IntersectionObserver — perfect for lazy loading, infinite scroll, and scroll-triggered effects.

<div v-intersection="onIntersection" :options.intersection="{ threshold: 0.5 }">
  {{ isVisible ? 'Visible' : 'Not visible' }}
</div>
methods: {
  onIntersection(entries, $ctx) {
    this.isVisible = entries[0].isIntersecting;
  }
}

→ Live demo: Intersection Observer

v-performance

Observe performance entries with PerformanceObserver. The handler signature is (entries, observer, options, $ctx).

<div v-performance="onPerformance" :options.performance="{ entryTypes: ['measure', 'mark'] }">
  Monitored
</div>
methods: {
  onPerformance(entries, observer, options, $ctx) {
    entries.getEntries().forEach(e => console.log(`${e.name}: ${e.duration}ms`));
  }
}

→ Live demo: Performance Observer

Next steps