Examples

1. Counter

HTML

<div data-component="counter">
  <button @click="decrement">-</button>
  <strong data-text="count"></strong>
  <button @click="increment">+</button>
  <button @click="reset">Reset</button>
</div>

JS

import * as Micra from 'micra.js'

Micra.define('counter', {
  state: { count: 0 },

  increment() { this.state.count++ },
  decrement() { this.state.count-- },
  reset() { this.state.count = 0 },
})

Micra.start()

2. User table with search and pagination

HTML

<div data-component="users-table">
  <input data-model="query" placeholder="Search users">

  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Email</th>
      </tr>
    </thead>
    <tbody>
      <template data-each="pagedUsers" data-key="id">
        <tr>
          <td data-text="item.id"></td>
          <td data-text="item.name"></td>
          <td data-text="item.email"></td>
        </tr>
      </template>
    </tbody>
  </table>

  <p data-text="summary()"></p>

  <button data-bind="disabled:page <= 1" @click="prevPage">Previous</button>
  <button data-bind="disabled:endIndex() >= filteredUsers().length" @click="nextPage">Next</button>
</div>

JS

import * as Micra from 'micra.js'

Micra.define('users-table', {
  state: {
    query: '',
    page: 1,
    perPage: 5,
    users: [
      { id: 1, name: 'Ada Lovelace', email: 'ada@example.com' },
      { id: 2, name: 'Linus Torvalds', email: 'linus@example.com' },
      { id: 3, name: 'Grace Hopper', email: 'grace@example.com' },
      { id: 4, name: 'Margaret Hamilton', email: 'margaret@example.com' },
      { id: 5, name: 'Edsger Dijkstra', email: 'edsger@example.com' },
      { id: 6, name: 'Barbara Liskov', email: 'barbara@example.com' },
    ],
  },

  filteredUsers() {
    const q = this.state.query.toLowerCase()
    return this.state.users.filter(user =>
      user.name.toLowerCase().includes(q) ||
      user.email.toLowerCase().includes(q),
    )
  },

  startIndex() {
    return (this.state.page - 1) * this.state.perPage
  },

  endIndex() {
    return this.startIndex() + this.state.perPage
  },

  pagedUsers() {
    return this.filteredUsers().slice(this.startIndex(), this.endIndex())
  },

  summary() {
    return `Page ${this.state.page} · ${this.filteredUsers().length} matching users`
  },

  nextPage() {
    if (this.endIndex() < this.filteredUsers().length) this.state.page++
  },

  prevPage() {
    if (this.state.page > 1) this.state.page--
  },
})

Micra.start()

3. Async data fetch with loading state

HTML

<div data-component="user-loader">
  <button @click="load">Reload</button>
  <p data-if="loading">Loading...</p>
  <pre data-if="!loading" data-text="prettyUser()"></pre>
</div>

JS

import * as Micra from 'micra.js'

Micra.define('user-loader', {
  state: {
    loading: false,
    user: null as null | { id: number; name: string; email: string },
  },

  async onCreate() {
    await this.load()
  },

  async load() {
    this.state.loading = true
    try {
      this.state.user = await this.fetch('/api/user') as { id: number; name: string; email: string }
    } finally {
      this.state.loading = false
    }
  },

  prettyUser() {
    return JSON.stringify(this.state.user, null, 2)
  },
})

Micra.start()

4. Modal with event bus

HTML

<button data-component="modal-button" @click="open">Open modal</button>

<div data-component="modal" data-if="open">
  <div class="backdrop" @click.self="close">
    <div class="dialog">
      <h2>Modal</h2>
      <p>Hello from Micra.</p>
      <button @click="close">Close</button>
    </div>
  </div>
</div>

JS

import * as Micra from 'micra.js'

Micra.define('modal-button', {
  open() {
    this.emit('modal:open')
  },
})

Micra.define('modal', {
  state: { open: false },

  onCreate() {
    this.on('modal:open', () => {
      this.state.open = true
    })
  },

  close() {
    this.state.open = false
  },
})

Micra.start()

5. Multiple dropdowns (same definition, independent state)

One definition, many instances — each with its own data-* props.

HTML

<div id="sort-dd"   data-component="dropdown" data-label="Sort by"></div>
<div id="status-dd" data-component="dropdown" data-label="Status"></div>
<div id="per-page-dd" data-component="dropdown" data-label="Per page"></div>
<!-- The shared dropdown template structure (inside each root) -->
<button @click="toggle" data-text="label"></button>
<ul data-if="open">
  <template data-each="options" data-key="value">
    <li @click="select" data-bind="data-value:item.value" data-text="item.label"></li>
  </template>
</ul>

JS

import * as Micra from 'micra.js'

Micra.define('dropdown', {
  state: {
    open: false,
    label: 'Choose...',
    options: [] as { label: string; value: string }[],
  },

  onCreate() {
    this.state.label = this.prop('label', 'Choose...')
    // Close on outside click
    this._outside = (e: MouseEvent) => {
      if (!this.$el.contains(e.target as Node)) this.state.open = false
    }
    document.addEventListener('click', this._outside)
  },

  onDestroy() {
    document.removeEventListener('click', this._outside)
  },

  toggle() {
    this.state.open = !this.state.open
  },

  select(e: Event) {
    const value = (e.currentTarget as HTMLElement).dataset['value'] ?? ''
    this.state.label = value
    this.state.open  = false
    // Publish so other components can react
    this.emit('dropdown:change', { id: this.$el.id, value })
  },
})

Micra.start()

Reacting to dropdown changes from another component

Micra.define('user-table', {
  state: { users: [], statusFilter: 'all' },

  onCreate() {
    this.on('dropdown:change', ({ id, value }: { id: string; value: string }) => {
      if (id === 'status-dd') {
        this.state.statusFilter = value
      }
    })
  },
})

6. Tabs

<div data-component="tabs" data-default-tab="overview">
  <nav>
    <button @click="select" data-bind="data-tab:'overview'"
            data-class="active:tab === 'overview'">Overview</button>
    <button @click="select" data-bind="data-tab:'billing'"
            data-class="active:tab === 'billing'">Billing</button>
    <button @click="select" data-bind="data-tab:'security'"
            data-class="active:tab === 'security'">Security</button>
  </nav>

  <section data-if="tab === 'overview'">Overview content</section>
  <section data-if="tab === 'billing'">Billing content</section>
  <section data-if="tab === 'security'">Security content</section>
</div>
Micra.define('tabs', {
  state: { tab: 'overview' },

  onCreate() {
    this.state.tab = this.prop('defaultTab', 'overview')
  },

  select(e: Event) {
    this.state.tab = (e.currentTarget as HTMLElement).dataset['tab'] ?? 'overview'
  },
})

7. Autocomplete / search input

<div data-component="autocomplete">
  <input data-model="query" @input="search" placeholder="Search users…">
  <ul data-if="suggestions.length > 0">
    <template data-each="suggestions" data-key="id">
      <li @click="pick" data-bind="data-id:item.id, data-email:item.email"
          data-text="item.name"></li>
    </template>
  </ul>
  <p data-if="selected" data-text="'Selected: ' + selected"></p>
</div>
Micra.define('autocomplete', {
  state: {
    query: '',
    suggestions: [] as { id: number; name: string; email: string }[],
    selected: '',
  },

  async search() {
    const q = this.state.query.trim()
    if (q.length < 2) { this.state.suggestions = []; return }
    this.state.suggestions = await this.fetch('/api/users', { q }) as typeof this.state.suggestions
  },

  pick(e: Event) {
    const el = e.currentTarget as HTMLElement
    this.state.selected  = el.dataset['email'] ?? ''
    this.state.query     = el.textContent ?? ''
    this.state.suggestions = []
    this.emit('user:selected', { id: Number(el.dataset['id']), email: this.state.selected })
  },
})

8. Inline edit (click to edit)

<div data-component="inline-edit" data-value="Alice Liddell">
  <span data-if="!editing" data-text="value" @click="startEdit"></span>
  <input data-ref="input" data-if="editing" data-model="value" @blur="save" @keydown="onKey">
</div>

This example branches on e.key inside onKey, but since 2.5 you can also use the @keydown.enter / @keydown.escape modifiers directly.

Micra.define('inline-edit', {
  state: { value: '', editing: false },

  onCreate() {
    this.state.value = this.prop('value', '')
  },

  startEdit() {
    this.state.editing = true
    // focus the input after render
    setTimeout(() => this.refs['input']?.focus(), 0)
  },

  onKey(e: KeyboardEvent) {
    if (e.key === 'Enter') this.save()
  },

  save() {
    this.state.editing = false
    this.emit('inline-edit:saved', { el: this.$el, value: this.state.value })
  },
})

9. Charts with data-ref

Use data-ref to get a direct DOM reference for imperative APIs (canvas, third-party charts).

<div data-component="revenue-chart" data-endpoint="/api/revenue">
  <canvas data-ref="canvas" width="600" height="300"></canvas>
  <p data-if="loading">Loading chart…</p>
</div>
declare const Chart: { new(el: HTMLCanvasElement, cfg: unknown): { destroy(): void } }

Micra.define('revenue-chart', {
  state: { loading: true },
  _chart: null as unknown,

  async onCreate() {
    const endpoint = this.prop('endpoint', '/api/revenue')
    const data = await this.fetch(endpoint)
    this._chart = new Chart(this.refs['canvas'] as HTMLCanvasElement, {
      type: 'line',
      data,
    })
    this.state.loading = false
  },

  onDestroy() {
    (this._chart as { destroy(): void } | null)?.destroy()
  },
})