Menu

🚲 fixi.js: turbocharged HTML

Overview

fixi.js(3.4kb raw / 1.2kb brotli) is a miniature version of htmx and offers similar, simplified functionality. It makes it possible for any element to issue HTTP requests in response to any event, and place the response HTML anywhere in the document.

Like htmx, fixi uses attributes to add behaviors to HTML elements.

A Simple Demo

Here is a fixi-powered button that loads a fragment into a panel:

<button fx-action="/profile" 
        fx-target="#panel" 
        fx-swap="innerHTML">
    Load profile
</button>
<output id="panel"></output>

Clicking this button issues a GET /profile request & whatever HTML comes back is placed inside #panel.

This demonstrates the core idea of fixi: an element issues a request, gets some HTML back & places it somewhere in the DOM.

Attributes

The five core fixi attributes are:

attribute description default
fx-action the URL to issue a request to required
fx-method the HTTP verb to use GET
fx-trigger the DOM event that fires the request a sensible value based on element type
fx-target a CSS selector for the element to swap into the element itself
fx-swap how to insert the response (outerHTML, innerHTML, beforeend, etc.) outerHTML

Events

fixi dispatches a set of events during its lifecycle

event when
fx:init just before fixi wires up an element. Cancelable: preventDefault() skips this element.
fx:inited after the element is fully wired up. Does not bubble.
fx:process listened for on document; processes evt.target and its descendants. Dispatch this after manual DOM changes to pick up new fixi-powered nodes.
fx:config the request has been triggered but not yet sent. Mutate evt.detail.cfg to inject headers, set a confirm callback, rewrite the URL, etc.
fx:before immediately before fetch() is called.
fx:after the response has arrived, but before the swap.
fx:swapped the response has been swapped in (and any View Transition has finished).
fx:error the fetch() threw an exception
fx:finally after every request, success or failure.

The most commonly used events are fx:config, to customize the request before it goes out, and fx:swapped, to react to freshly inserted content.

Modifying The Request Config

Each fixi request is configured through an object exposed on the fx:config event’s evt.detail.cfg.

Listening for that event and mutating cfg is how you customize a request before it is sent.

field controls
cfg.action the URL fixi will fetch
cfg.method the HTTP verb
cfg.headers a plain object of headers to send
cfg.body the request body (FormData, string, etc.)
cfg.target the element the response will be swapped into
cfg.swap the swap mode (outerHTML, innerHTML, morph, etc.)
cfg.confirm an optional () => boolean callback fixi awaits before sending
cfg.transition the View Transition function, or false to disable
cfg.fetch the fetch implementation (replace for mocking)

Per-Element Configuration

Attach an on-fx:config handler with moxi (or a plain addEventListener) to customize a single element’s requests.

moxi exposes every key on event.detail as a bare name, so cfg resolves directly:

<button fx-action="/things/42" fx-method="DELETE"
        on-fx:config="cfg.confirm = () => confirm('Delete this thing?')">
    delete
</button>

Global Configuration

Listen on document to apply the same modification to every fixi request. This is the right place for auth headers, request-ID injection, or a uniform URL prefix:

document.addEventListener('fx:config', (e) => {
    e.detail.cfg.headers.Authorization = `Bearer ${getToken()}`
})

Default Configuration via window.fixiCfg

For values you want to set once at page load, fixi reads window.fixiCfg for default swap, transition, and headers:

<script>
    window.fixiCfg = {
        swap: "morph",
        headers: { "X-CSRF-Token": "abc123" },
    }
</script>
<script src="fixi.js"></script>

Per-element listeners always win over these defaults.

Examples

Below are some fixi-only examples to show what you can do with it.

Inline Editing

A common fixi pattern is to click a row (or div) and to swap in an edit form, then submit to swap back to a display row.

Both of these are plain fixi swaps:

<tr id="row-42">
    <td>Ada</td>
    <td>
        <button fx-action="/contacts/42/edit"
                fx-target="#row-42"
                fx-swap="outerHTML">edit</button>
    </td>
</tr>

The server returns either the display row or the edit form depending on the URL. The edit form posts back to the same row id; the response replaces it with a fresh display row.

Form Submission

When the fixi-powered element is a <form>, the submit event triggers the request & fixi includes the form’s inputs in the request body (or URL):

<form fx-action="/signup" fx-method="POST"
      fx-target="#status" fx-swap="innerHTML">
    <input name="email" type="email" required placeholder="email">
    <input name="password" type="password" required minlength="8" placeholder="password">
    <button type="submit">Sign up</button>
</form>
<output id="status"></output>

On submit, fixi POSTs the email and password as form data and swaps whatever HTML comes back into #status.

Click to Delete

fx-method="DELETE" issues a DELETE request: if the server responds with an empty body, the default outerHTML swap removes the targeted element from the DOM.

<li id="todo-42">
    Buy milk
    <button fx-action="/todos/42" fx-method="DELETE"
            fx-target="#todo-42" fx-swap="outerHTML">x</button>
</li>

The button targets its containing <li> so the whole row vanishes when the server responds.

Load More

The Load More button sits in the last row of a table, spanning all columns. By targeting that row and relying on the default outerHTML swap, clicking the button replaces the whole row with more data rows plus a fresh “Load more” row:

<table>
    <tbody>
        <tr><td>Ada</td><td>ada@example.com</td></tr>
        <tr><td>Grace</td><td>grace@example.com</td></tr>
        <tr id="more">
            <td colspan="2">
                <button fx-action="/people?page=2" fx-target="#more">Load more</button>
            </td>
        </tr>
    </tbody>
</table>

The server responds with the next page of <tr> rows followed by a fresh <tr id="more"> row pointing at ?page=3. When the pages run out, the server omits the placeholder row and the pagination ends naturally.

Lazy Loading

Any fixi event can be used as an fx-trigger, including the fx:init event that fires when fixi wires an element up.

That makes a “load this content asynchronously” simple to implement:

<div fx-action="/expensive-data"
     fx-trigger="fx:init">
    loading...
</div>

When fixi initializes the div it dispatches fx:init the element issues GET /expensive-data, and the response replaces the element.

Reference

For a complete fixi.js reference see the README on GitHub.

Next up is moxi.js, which makes fixi much more powerful.