Toast

Bus-driven notification stack. Any component can call Micra.emit('toast', { … }) and a single container subscribes and renders the queue. Keyed data-each + an id per toast lets CSS animations fire only for new entries.

Live preview

Markup

<!-- Anywhere on the page: the renderer -->
<div class="toast-stack" data-component="toast-stack">
  <template data-each="toasts" data-key="id">
    <div class="toast" data-bind="class:item.severityClass">
      <div class="toast-body">
        <div class="toast-title"   data-text="item.title"></div>
        <div class="toast-message" data-text="item.message"></div>
      </div>
      <button @click="dismiss" data-bind="data-id:item.id">×</button>
    </div>
  </template>
</div>

<!-- Anywhere else: trigger toasts via the bus -->
<div data-component="toast-trigger">
  <button @click="success">Saved</button>
</div>

Definition

Micra.define('toast-stack', {
  state: { toasts: [] },

  onCreate() {
    this._timers = new Map()
    this.on('toast', (payload) => this.push(payload))
  },

  onDestroy() {
    this._timers.forEach(clearTimeout)
    this._timers.clear()
  },

  push({ title, message, severity = 'info', duration = 4000 }) {
    const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
    const toast = {
      id, title, message,
      severityClass: 'toast toast-' + severity,
    }
    this.state.toasts = [...this.state.toasts, toast]

    const timer = setTimeout(() => this.remove(id), duration)
    this._timers.set(id, timer)
  },

  remove(id) {
    this.state.toasts = this.state.toasts.filter(t => t.id !== id)
    clearTimeout(this._timers.get(id))
    this._timers.delete(id)
  },

  dismiss(e) {
    this.remove(e.currentTarget.dataset.id)
  },
})

Micra.define('toast-trigger', {
  info()    { Micra.emit('toast', { title: 'Heads up',  message: 'Just so you know…',     severity: 'info'    }) },
  success() { Micra.emit('toast', { title: 'Saved',     message: 'Changes were persisted', severity: 'success' }) },
  error()   { Micra.emit('toast', { title: 'Failed',    message: 'Server returned 500.',   severity: 'error'   }) },
  warning() { Micra.emit('toast', { title: 'Watch out', message: 'Trial expires Friday.',  severity: 'warning' }) },
})

Integration

  • Decoupled emit. Any code on the page can emit 'toast' — Micra components, vanilla JS handlers, fetch error handlers. One renderer mounted once is enough.
  • Timers are tracked per id in a Map. Dismissing manually clears the auto-dismiss timer — otherwise it would try to remove an already-removed toast.
  • Severity → class is stored as severityClass on each toast, consumed by data-bind="class:…". Keeping it on the item (not computed) makes diffing cheaper.
  • Animations stay smooth because keyed data-each only inserts the new row — existing toasts aren't re-rendered.

Pitfalls

  • Don't mount multiple toast-stack instances unless you want duplicate notifications — both will receive the bus event.
  • Don't store the timer id on the toast object itself — diffing churns and you lose them across renders. Keep them in a separate Map on the instance.
  • Don't forget onDestroy to clear pending timers — otherwise they fire after the container unmounts and silently no-op on a dead instance.