🚲 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.