Cookbook

Small, copyable patterns.

Set text

An effect that reads st.n writes el.textContent when that key changes.

example
<button moo="{ click: 'st.label_n = (st.label_n ?? 0) + 1' }">Add</button>
<output moo="el.textContent = st.label_n ?? 0">0</output>
LIVE
0

Hide and show

Write el.hidden from one flag. That is the whole recipe.

example
<button moo="{ click: 'st.help_open = !st.help_open' }">Help</button>
<section moo="el.hidden = !st.help_open">A little help.</section>
LIVE
A little help, right when you need it.

Toggle a class

classList.toggle takes a boolean. The class is the only thing this effect writes.

example
<button moo="{ click: 'st.loud = !st.loud' }">Toggle</button>
<p moo="el.classList.toggle('text-primary', !!st.loud)">A line of text.</p>
LIVE

A line of text.

Nested state and arrays

Nest when the value is a list you mutate. A flat key cannot replace push.

example
<div moo="{ init: 'st.cart ??= { items: [] }' }">
  <button moo="{ click: 'st.cart.items.push({ title: `Seed` })' }">Add</button>
  <output moo="el.textContent = `${st.cart.items.length} items`">0 items</output>
</div>
LIVE
0 items

Two-way text input

The event writes state. The effect copies a different value back (a reset, a patch). The !== guard skips a no-op assign.

example
<input
  autocomplete="off"
  placeholder="Type here"
  moo="{
    init: 'st.echo ??= ``',
    input: 'st.echo = el.value',
    effect: 'if (el.value !== st.echo) el.value = st.echo'
  }"
>
<output moo="el.textContent = st.echo || '…'"></output>
LIVE

Assigning value moves the caret

The jump shows when the assigned string differs. Current browsers often leave the caret alone if you write the same string back.

example
<input
  autocomplete="off"
  moo="{
    init: 'st.jump ??= `edit here`',
    input: 'st.jump = el.value',
    effect: 'el.value = (st.jump ?? ``).toUpperCase()'
  }"
>
LIVE

Click in the middle, then type a letter. The field uppercases and the caret goes to the end.

Return clearTimeout from the event. That disposer runs before the next input. See Reference: Lifecycle.

example
<input type="search" moo="{
  input: `
    const q = el.value
    const t = setTimeout(() => { st.search_query = q }, 200)
    return () => clearTimeout(t)
  `
}">
<output moo="el.textContent = st.search_query || '…'"></output>
LIVE

Debounce the input, then ox.get. The server patches the hit list.

example
<input type="search" moo="{
  input: `
    const q = el.value
    const t = setTimeout(() => {
      st.search_abort?.()
      const request = ox.get('/ox-cookbook/search', { query: { q } })
      st.search = request.status
      st.search_abort = request.abort
    }, 200)
    return () => clearTimeout(t)
  `
}">
<ul id="ox-search-hits" class="m-0 list-disc ps-5">
  <li>Type to search.</li>
</ul>
LIVE
  • Type to search.

Throttle with a lock

Keep the lock on the element. Do not return clearTimeout: the next click would cancel the unlock.

example
<button moo="{
  click: `
    if (el._busy) return
    el._busy = true
    st.lock_n = (st.lock_n ?? 0) + 1
    setTimeout(() => { el._busy = false }, 2000)
  `
}">Add</button>
<output moo="el.textContent = st.lock_n ?? 0">0</output>
LIVE
0

Close on outside click

Put outside on the wrapper that includes the Open button. A click on Open is otherwise outside the panel and closes it again.

example
<div moo="{ click: { outside: true, fn: 'st.menu_open = false' } }">
  <button moo="{ click: 'st.menu_open = true' }">Open</button>
  <aside moo="el.hidden = !st.menu_open">Menu</aside>
</div>
LIVE

Listen on window

target: "window" or "document". Ox removes the listener when the element unbinds.

example
<output moo="{
  init: 'st.viewport_width = innerWidth',
  resize: { target: 'window', fn: 'st.viewport_width = innerWidth' },
  effect: 'el.textContent = `${st.viewport_width}px`'
}"></output>
LIVE

Resize the window.

Keydown on window

Listen on window. Read evt.key. Call preventDefault only for the keys you handle. prevent: true on window would stop typing in every field.

example
<output moo="{
  keydown: {
    target: 'window',
    fn: `
      if (evt.key !== '?') return
      evt.preventDefault()
      st.help_keys = (st.help_keys ?? 0) + 1
    `
  },
  effect: 'el.textContent = `${st.help_keys ?? 0} ? presses`'
}"></output>
LIVE
0 ? presses

Press ? anywhere on the page.

Prevent default

prevent: true calls preventDefault(). The link does not navigate.

example
<a href="#prevent-default" moo="{
  click: { prevent: true, fn: 'st.stayed = true' }
}">Go</a>
<p moo="el.textContent = st.stayed ? 'Stayed on the page.' : 'Not clicked yet.'"></p>
LIVE
Go

Not clicked yet.

Run once

once: true is passed to addEventListener. The handler runs one time.

example
<button moo="{ click: { once: true, fn: 'st.once_n = (st.once_n ?? 0) + 1' } }">
  Once
</button>
<output moo="el.textContent = st.once_n ?? 0">0</output>
LIVE
0

Own a timer safely

Return a disposer from init. Ox runs it when the element unbinds. See Reference: Lifecycle.

example
<output moo="{
  init: `
    st.clock ??= 0
    const id = setInterval(() => st.clock++, 1000)
    return () => clearInterval(id)
  `,
  effect: 'el.textContent = st.clock'
}">0</output>
LIVE
0

Read without subscribing

ox.peek must receive a function. Writes to peeked keys do not rerun the effect.

example
<output moo="el.textContent = (st.visible ?? 0) + ox.peek(() => st.ignored ?? 0)">0</output>
<button moo="{ click: 'st.visible = (st.visible ?? 0) + 1' }">Visible</button>
<button moo="{ click: 'st.ignored = (st.ignored ?? 0) + 1' }">Ignored</button>
LIVE
0

Render a local list

Prefer server HTML when rows need their own moo= bindings. Morph those from SSE.

example
<button moo="{ click: 'st.items ??= []; st.items.push(`Item ${st.items.length + 1}`)' }">Add</button>
<ul moo="{
  effect: `
    el.replaceChildren(...(st.items ?? []).map(item => {
      const li = document.createElement('li')
      li.textContent = item
      return li
    }))
  `
}"></ul>
LIVE

    Submit a form

    Native submit. prevent stops navigation. Read values from el.elements like a normal form.

    example
    <form moo="{
      submit: {
        prevent: true,
        fn: `
          const request = ox.post('/ox-cookbook/todos', {
            body: { title: el.elements.title.value }
          })
          st.create = request.status
          return request.abort
        `
      }
    }">
      <input name="title" required>
      <button>Add todo</button>
    </form>
    <p moo="el.textContent = st.create?.phase ?? 'idle'"></p>
    LIVE

    idle

    Post collected fields

    Each input writes a key on st.draft. The request body is that object. No form, and no effect back into the field.

    example
    <div moo="{ init: 'st.draft ??= { title: ``, note: `` }' }">
      <input
        autocomplete="off"
        placeholder="Title"
        moo="{ input: 'st.draft.title = el.value' }"
      >
      <input
        autocomplete="off"
        placeholder="Note"
        moo="{ input: 'st.draft.note = el.value' }"
      >
      <button moo="{
        click: `
          const request = ox.post('/ox-cookbook/save', { body: st.draft })
          st.draft_save = request.status
          return request.abort
        `
      }">Save draft</button>
    </div>
    <pre moo="el.textContent = JSON.stringify(st.draft ?? {}, null, 2)"></pre>
    LIVE
    {}

    Show request status

    Bind request.status once. Effects that read phase or active update as the request moves.

    example
    <button moo="{
      click: `
        const request = ox.post('/ox-cookbook/save', { body: { ok: true } })
        st.save = request.status
        return request.abort
      `
    }">Save</button>
    <p moo="el.textContent = `${st.save?.phase ?? 'idle'} · active ${!!st.save?.active}`"></p>
    LIVE

    idle · active false

    Abort a request

    Keep abort() and call it from another control. The status phase becomes aborted.

    example
    <button moo="{
      click: `
        const request = ox.get('/ox-cookbook/feed')
        st.feed = request.status
        st.feed_abort = request.abort
        return request.abort
      `
    }">Start</button>
    <button moo="{ click: 'st.feed_abort?.()' }">Abort</button>
    <p moo="el.textContent = st.feed?.phase ?? 'idle'"></p>
    LIVE

    idle

    Finish with 204

    A 204 ends with no stream. reconnect defaults to never.

    example
    <button moo="{
      click: `
        const request = ox.get('/ox-cookbook/empty')
        st.empty = request.status
        return request.abort
      `
    }">Get</button>
    <p moo="el.textContent = st.empty?.phase ?? 'idle'"></p>
    LIVE

    idle

    Replace HTML

    Outer morph needs one target and one root with the same id.

    example
    <ul id="replaced" class="m-0 list-disc ps-5"><li>Old</li></ul>
    <button moo="{ click: `ox.morph('#replaced',
      '<ul id=replaced class="m-0 list-disc ps-5"><li>Fresh</li></ul>')` }">
      Replace
    </button>
    LIVE
    • Old

    Append HTML

    mode: append adds roots as children of the target.

    example
    <ul id="appended"><li>Old</li></ul>
    <button moo="{ click: `ox.morph('#appended', '<li>More</li>', { mode: 'append' })` }">
      Append
    </button>
    LIVE
    • Old

    Click to edit

    Edit asks the server for a form. Save morphs the row back. Cancel restores the last title.

    example
    <article id="ox-edit-row" moo="{ init: 'st.edit_title ??= "Alpha"' }">
      <p>Alpha</p>
      <button moo="{
        click: 'ox.get("/ox-cookbook/edit", { query: { title: st.edit_title ?? "Alpha" } })'
      }">Edit</button>
    </article>
    LIVE

    Alpha

    Load more

    ox.get sends the cursor. The server appends rows and patches more_after.

    example
    <ul id="ox-more-list" class="m-0 list-disc ps-5">
      <li>Alpha</li>
      <li>Beta</li>
    </ul>
    <button moo="{
      click: `
        const request = ox.get('/ox-cookbook/more', {
          query: { after: st.more_after ?? 2 }
        })
        st.more = request.status
        return request.abort
      `,
      effect: 'el.hidden = (st.more_after ?? 2) >= 6'
    }">Load more</button>
    LIVE
    • Alpha
    • Beta

    That is the full list.

    Confirm, then delete

    The confirm step is local. The delete posts an id. The server morphs that row away.

    example
    <li id="ox-del-alpha">
      Alpha
      <button moo="{ click: 'st.del_alpha = true' }">Delete</button>
      <span moo="el.hidden = !st.del_alpha">
        <button moo="{
          click: 'ox.post(`/ox-cookbook/delete`, { body: { id: `alpha` } })'
        }">Confirm</button>
        <button moo="{ click: 'st.del_alpha = false' }">Keep</button>
      </span>
    </li>
    LIVE
    • Alpha
    • Beta

    Bind a later tree

    Call ox.init only for markup you insert yourself. Skip it after ox.morph or ox.post.

    example
    <div id="later-host"></div>
    <button moo="{ click: `
      const host = document.getElementById('later-host')
      const out = document.createElement('output')
      out.setAttribute('moo', 'el.textContent = st.later ?? 0')
      host.replaceChildren(out)
      ox.init(host)
      st.later = (st.later ?? 0) + 1
    ` }">Insert and bind</button>
    LIVE
    Theme

    Made by Adam with Stario