A lightweight, modern fork of fixi - progressive enhancement for HTML with declarative AJAX.
npm install @mypolis.eu/fixiImport the library and it automatically enhances your HTML:
import "@mypolis.eu/fixi/core";Then add fx-* attributes to your HTML:
<button
fx-action="/api/like"
fx-target="#likes"
fx-swap="innerHTML"
>
Like
</button>
<div id="likes">0 likes</div>The URL to fetch when triggered.
<button fx-action="/api/data">Load Data</button>CSS selector for the element to update. Defaults to the element itself.
<button
fx-action="/api/update"
fx-target="#result"
>
Update
</button>
<div id="result">Old content</div>How to swap the content. Options: innerHTML, outerHTML, beforebegin, afterbegin, beforeend, afterend, none, or a custom function.
<!-- Replace inner content -->
<div
fx-action="/api/content"
fx-swap="innerHTML"
>
Loading...
</div>
<!-- Replace entire element -->
<div
fx-action="/api/card"
fx-swap="outerHTML"
>
Loading...
</div>
<!-- Insert adjacent HTML -->
<div
fx-action="/api/item"
fx-swap="beforeend"
>
Items:
</div>Event that triggers the action. Defaults: click (button/div), submit (form), change (input/select/textarea).
<!-- Trigger on hover -->
<div
fx-action="/api/preview"
fx-trigger="mouseenter"
>
Hover me
</div>
<!-- Trigger on intersection (lazy loading) -->
<div
fx-action="/api/lazy"
fx-trigger="intersect"
>
Load when visible
</div>
<!-- Custom threshold and root margin for intersection -->
<div
fx-action="/api/lazy"
fx-trigger="intersect"
fx-intersect-threshold="0.5"
fx-intersect-root-margin="100px"
>
Load when 50% visible with 100px margin
</div>HTTP method. Default: GET
<form
fx-action="/api/create"
fx-method="POST"
>
<input name="title" />
<button type="submit">Create</button>
</form>Show loading state during requests.
<!-- Toggle fx-requesting class on the button -->
<button
fx-action="/api/save"
fx-indicator
>
Save
</button>
<!-- Toggle custom class -->
<button
fx-action="/api/save"
fx-indicator
fx-indicator-class="is-loading"
>
Save
</button>
<!-- Toggle multiple attributes for accessibility -->
<button
fx-action="/api/save"
fx-indicator
fx-indicator-attr="disabled,aria-busy,aria-disabled"
>
Save
</button>
<!-- Use external indicator element -->
<button
fx-action="/api/save"
fx-indicator="#spinner"
>
Save
</button>
<span
id="spinner"
class="hidden"
>Loading...</span
>The library dispatches events throughout the request lifecycle:
Fired when an element is first processed.
document.addEventListener("fx:init", (e) => {
console.log("Element initialized:", e.target);
});Intercept and modify configuration before the request.
element.addEventListener("fx:config", (e) => {
// Add custom headers
e.cfg.headers["X-Custom"] = "value";
// Use custom fetch
e.cfg.fetch = myCustomFetch;
});Fired before the request starts. Cancelable.
element.addEventListener("fx:before", (e) => {
if (!confirm("Are you sure?")) {
e.preventDefault(); // Cancel the request
}
});Fired after successful response. Cancelable.
element.addEventListener("fx:after", (e) => {
console.log("Response received:", e.cfg.text);
});Fired after DOM has been updated.
element.addEventListener("fx:swapped", (e) => {
// Re-initialize any JS that depends on the new DOM
initTooltips();
});Fired when request fails.
element.addEventListener("fx:error", (e) => {
console.error("Request failed:", e.cfg.error);
});Always fired after request completes (success or error).
If you add elements dynamically, manually trigger processing:
import {fixi} from "@mypolis.eu/fixi/core";
// After adding new element to DOM
const newElement = document.createElement("div");
newElement.setAttribute("fx-action", "/api/data");
document.body.appendChild(newElement);
fixi.process(newElement);<script>
element.addEventListener("fx:config", (e) => {
e.cfg.swap = async (cfg) => {
// Custom logic to handle the response
const data = JSON.parse(cfg.text);
cfg.target.innerHTML = renderTemplate(data);
};
});
</script>The library automatically uses document.startViewTransition when available:
// Check if browser supports view transitions
if (document.startViewTransition) {
console.log("Smooth transitions enabled!");
}Use fx-ignore to prevent processing of child elements:
<div fx-ignore>
<button fx-action="/api/ignored">This won't be enhanced</button>
</div>Global event types are automatically available when importing:
import type {FxConfigEvent, FxBeforeEvent, FxAfterEvent} from "@mypolis.eu/fixi/core";
element.addEventListener("fx:config", (e: FxConfigEvent) => {
e.cfg.fetch = customFetch;
});interface FixiConfig {
trigger: Event; // The triggering event
action: string; // URL to fetch
method: string; // HTTP method
target: Element; // Element to update
swap: string | Function; // Swap strategy
body: FormData | null; // Request body
headers: Record<string, string>;
fetch: typeof fetch; // Fetch function to use
confirm?: () => Promise<boolean>;
// ... more options
}| Browser | Version | Year |
|---|---|---|
| Chrome | 74+ | 2019 |
| Firefox | 90+ | 2021 |
| Safari | 14.1+ | 2021 |
| Edge | 79+ | 2020 |
All modern browsers supporting ES2022.
Note: View Transitions (for smooth animations) require Chrome 111+, but the library works perfectly without them in older browsers.
MIT - Based on fixi by Big Sky Software