# fixijs **Repository Path**: mirrors/fixijs ## Basic Information - **Project Name**: fixijs - **Description**: Fixi.js 是轻量级的前端交互框架,用极简代码实现高效动态页面更新 - **Primary Language**: HTML/CSS - **License**: Not specified - **Default Branch**: master - **Homepage**: https://www.oschina.net/p/fixi-js - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-02-18 - **Last Updated**: 2026-01-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README

🚲 fixi.js - it ain't much...

[fixi.js](https://swag.htmx.org/products/fixi-js-tee) is an experimental, minimalist implementation of [generalized hypermedia controls](https://dl.acm.org/doi/fullHtml/10.1145/3648188.3675127) The fixi [api](#api) consists of six [HTML attributes](#attributes), nine [events](#events) & two [properties](#properties) Here is an example: ```html ``` When this fixi-powered `button` is clicked it will issue an HTTP `GET` request to the `/content` [relative URL](https://www.w3.org/TR/WD-html40-970917/htmlweb.html#h-5.1.2) and swap the HTML content of the response inside the `output` tag below it. ## Minimalism Philosophically, fixi is [scheme](https://scheme.org/) to [htmx](https://htmx.org)'s [common lisp](https://lisp-lang.org/): it is designed to be as [lean as possible](https://ia601608.us.archive.org/8/items/pdfy-PeRDID4QHBNfcH7s/LeanSoftware_text.pdf) while still being useful for real world projects. As such, it doesn't have many of the features found in htmx, including: * [request queueing & synchronization](https://htmx.org/attributes/hx-sync/) * [extended selector support](https://htmx.org/docs/#extended-css-selectors) * [extended event support](https://htmx.org/docs/#special-events) * [attribute inheritance](https://htmx.org/docs/#inheritance) * [request indicators](https://htmx.org/docs/#indicators) * [CSS transitions](https://htmx.org/docs/#css_transitions) * [history support](https://htmx.org/docs/#history) fixi takes advantage of some modern JavaScript features not used by htmx: * [`async` functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) * The [`fetch()` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) * The use of [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) for monitoring when new content is added * The [View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) (used by htmx, but the sole mechanism for transitions in fixi) A hard constraint on the project is that the _unminified_, _uncompressed_ size must be less than that of the minified & compressed version of the (excellent) [preact library](https://bundlephobia.com/package/preact) (currently 4.6Kb). The current uncompressed size is `3292` bytes, the gzipped size is `1398` bytes and the brotli'd size is `1181` bytes, as determined by: ```bash ls -l fixi.js | awk '{print "raw:", $5}'; gzip -k fixi.js; ls -l fixi.js.gz | awk '{print "gzipped:", $5}'; rm fixi.js.gz; brotli fixi.js; ls -l fixi.js.br | awk '{print "brotlid:", $5}'; rm fixi.js.br ``` Another goal is that users should be able to [debug](https://developer.chrome.com/docs/devtools/javascript/) fixi easily, since it is small enough to use unminified. The code style is [dense](fixi.js), but the statements are structured for debugging. Like a fixed-gear bike, fixi has very few moving parts: * No dependencies (including test and development) * No JavaScript API (beyond the [events](#events)) * No minified `fixi.min.js` file * No `package.json` * No build step The fixi project consists of three files: * [`fixi.js`](fixi.js), the code for the library * [`test.html`](test.html), the test suite for the library * This [`README.md`](README.md), which is the documentation [`test.html`](test.html) is a stand-alone HTML file that implements its own visual testing infrastructure, mocking for `fetch()`, etc. and that can be opened using the `file:` protocol for easy testing. ## Installing fixi is designed to be easily [vendored](https://htmx.org/essays/vendoring/), that is, copied, into your project: ```bash curl https://raw.githubusercontent.com/bigskysoftware/fixi/refs/tags/0.9.2/fixi.js >> fixi-0.9.2.js ``` The SHA256 of v0.9.2 is `0957yKwrGW4niRASx0/UxJxBY/xBhYK63vDCnTF7hH4=` generated by the following command line script: ```bash cat fixi.js | openssl sha256 -binary | openssl base64 ``` Alternatively can download the source from here: You can also use the JSDelivr CDN for local development or testing: ```html ``` Finally, fixi is available on NPM as the [`fixi-js`](https://www.npmjs.com/package/fixi-js) package. ## Support You can get support for fixi via: * [Github Issues](https://github.com/bigskysoftware/fixi/issues) * [The htmx Discord `#fixi` channel](https://htmx.org/discord) ## API ### Attributes
attribute description example
fx-action The URL to which an HTTP request will be issued, required fx-action='/demo'
fx-method The HTTP Method that will be used for the request (case-insensitive), defaults to GET fx-method='DELETE'
fx-target A CSS selector specifying where to place the response HTML in the DOM, defaults to the current element fx-target='#a-div'
fx-swap A string specifying how the content should be swapped into the DOM, can be one of innerHTML, outerHTML, beforebegin, afterbegin, beforeend, afterend, none, or any valid property on the element (e.g. `className` or `value`), defaults to outerHTML fx-swap='innerHTML'
fx-trigger The event that will trigger a request, defaults to submit for form elements, change for input-like elements & click for all other elements fx-trigger='click'
fx-ignore Any element with this attribute on it or on an ancestor will not be processed for fx-* attributes
#### Modus Operandi fixi works in a straight-forward manner & I encourage you to look at [the source](fixi.js) as you read through this. The three components of fixi are: * [Processing](#processing) elements in the DOM (or added to the DOM) * Issuing HTTP [requests](#requests) in response to events * [Swapping](#swapping) new HTML content into the DOM ##### Processing The main entry point is found at the bottom of [fixi.js](fixi.js): on the `DOMContentLoaded` event fixi does two things: * It has a MutationObserver begin to watch for newly added content with fixi-powered elements * It processes any existing fixi-powered elements fixi-powered elements are elements with the `fx-action` attribute on them. When fixi finds one it will establish an event listener on that element that will dispatch an AJAX request via `fetch()` to the URL specified by `fx-action`. fixi will ignore any elements that have the `fx-ignore` attribute on them or on an ancestor. The event that will trigger the request is determined by the `fx-trigger` attribute. If that attribute is not present, the trigger defaults to: * `submit` for `form` elements * `change` for `input:not([type=button])`, `select` & `textarea` elements * `click` for everything else. ##### Requests When a request is triggered, the [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) of the request will be determined by the `fx-method` attribute. If this attribute is not present, it will default to `GET`. This attribute is case-insensitive. fixi sends the [request header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) `FX-Request`, with the value `true`. You can add or remove headers using the `evt.detail.cfg.headers` object, see the [`fx:config`](#fxconfig) event below. If an element is within a form element or has a `form` attribute, the values of that form will be included with the request. Otherwise, if the element has a `name`, its `name` & `value` will be sent with the request. You can add or remove values using the `evt.detail.cfg.form` `FormData` object in the [`fx:config`](#fxconfig) event. `GET` & `DELETE` requests will include values via query parameters, other request types will submit them as a form encoded body. Before a request is sent, the aforementioned [`fx:config`](#fxconfig) event is triggered, which can be used to configure aspects of the request. If `preventDefault()` is invoked in this event, the request will not be sent. The `evt.detail.cfg.drop` property will be [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy) if there is an existing outstanding request associated with the element, otherwise the value will be [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy). More specifically, the value is equal to the number of outstanding requests associated with the element. If the value is truthy after the last `fx:config` handler has ran to completion, the request will be dropped (i.e. not issued). This implies, if you do not [customize the behavior](#replace-existing-requests-in-flight), an element will drop all new requests whilst there is an outstanding request associated with it. In the [`fx:config`](#fxconfig) event you can also set the `evt.detail.cfg.confirm` property to a no-argument function. This function can return a Promise and can be used to asynchronously confirm that the request should be issued: ```js function showAsynConfirmDialog() { //... a Promise-based confirmation dialog... } document.addEventListener("fx:config", (evt) => { evt.detail.cfg.confirm = showAsynConfirmDialog; }) ``` Note that confirmation will only occur if the [`fx:config`](#fxconfig) event is not canceled and the request is not dropped. After the configuration step and the confirmation, if any, the [`fx:before`](#fxbefore) event will be triggered, and then a `fetch()` will be issued. The `evt.detail.cfg` object from the events above is passed to the `fetch()` function as the second [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) argument. When fixi receives a response it triggers the [`fx:after`](#fxafter) event. In this event there are two more properties available on `evt.detail.cfg`: * `response` the fetch [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object * `text` the text of the response These can be inspected, and the `text` value can be changed if you want to transform it in some way. If a network error occurs the [`fx:error`](#fxerror) event will be triggered instead of `fx:after`, and nothing will be swapped. Note that `fetch()` only triggers errors [when a request fails due to a bad URL or network error](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch), so valid HTTP responses with non-`200` response codes will not trigger an error. If you wish to handle non-200 reponses differently you should check the response code in the `fx:after` event: ```js document.addEventListener("fx:after", (evt)=>{ // rewire 404s to the body, remove current head so new head can replace it if (evt.detail.cfg.response.status == 404){ document.head.remove() evt.detail.cfg.target = document.body evt.detail.cfg.swap = 'outerHTML' } }) ``` The [`fx:finally`](#fxfinally) event will be triggered regardless if an error occurs or not. ##### Swapping fixi then swaps the response text into the DOM using the mechanism specified by `fx-swap`, targeting the element specified by `fx-target`. If the `fx-swap` attribute is not present, fixi will use `outerHTML`. If the `fx-target` attribute is not present, it will target the element making the request. The swap mechanism and target can be changed in the request-related fixi events. You can implement a custom swapping mechanism by setting a function into the `evt.detail.cfg.swap` property in one of the request related events. This function should take one argument that will be set to the fixi request config itself. On that object you can access the `target`, `text`, `request`, etc. You can see an [example below](#custom-swapping-algorithms) showing how to do this. By default, swapping will occur in a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) if they are available. If you don't want this to occur, you can set the `evt.detail.cfg.transition` property to false in one of the request-related events. Finally, when the swap and any associated View Transitions have completed, the `fx:swapped` event will be triggered on the element. If the element has been removed from the DOM, the event will also be triggered on `document`, which allows you to listen for this event on `document` and receive it after every swap. ###### Notes on Targeting the Document Element (`html`) Note that if you want to replace the entire document (that is, target the `html` element) you _must_ use an `innerHTML` swap, because the default `outerHTML` swap will fail with a [`NoModificationAllowed`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException#nomodificationallowederror) error. If you wish to default the swap mechanism to `innerHTML` when targeting the `html` you can use this code: ```js document.addEventListener("fx:config", (evt) => { if (evt.detail.cfg.target === document.documentElement){ evt.detail.cfg.swap = "innerHTML" } }) ``` Note also that if you replace the `head` tag, due to the [quirks of the DOM API specification](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#security_considerations), styles _will_ be updated, but _scripts_ will not be executed. You must therefore manually manage and execute any script tags: ```js document.addEventListener("fx:config", (evt) => { if (evt.detail.cfg.target === document.documentElement){ let scripts = document.head.querySelectorAll("script"); for(let script of scripts) { let newScript = document.createRange().createContextualFragment(script.outerHTML) document.head.insertBefore(newScript, script) script.remove() } } }) ``` For inline scripts where [Locality of Behavior](https://htmx.org/essays/locality-of-behaviour) is desired. The target scripts need to be replaced in order to execute. This will automatically execute all script tags that are swapped in: ```js document.addEventListener('fx:swapped', (evt) => { evt.detail.cfg.target.querySelectorAll('script').forEach(s => s.replaceWith(Object.assign(document.createElement('script'),{textContent:s.textContent})) ) }) ``` However, these simple approaches may fail if you have scripts that, for example, create global variables with `let`, etc. For this reason we broadly recommend loading all your scripts up front or simply using anchor tags for full page navigations, unless you want to get into the weeds of dealing with these issues. #### Complete Example Here is a complete example using all the attributes available in fixi: ```html -- ``` In this example, the button will issue a `GET` request to `/demo` and put the resulting HTML into the `innerHTML` of the output element with the id `output`. Because the `output` element is marked as `fx-ignore`, any `fx-action` attributes in the new content will be ignored. ### Events fixi fires the following events, broken into two categories:
category event description
initialization fx:init triggered on elements that have a fx-action attribute and are about to be initialized by fixi
fx:inited triggered on elements have been initialized by fixi (does not bubble)
fx:process fixi listens on the document object for this event and will process (that is, enable any fixi-powered elements) within that element.
fetch fx:config triggered on an element immediately when a request has been triggered, allowing users to configure the request
fx:before triggered on an element just before a fetch() request is made
fx:after triggered on an element just after a fetch() request finishes normally but before content is swapped
fx:error triggered on an element if something is thrown from a fetch()
fx:finally triggered on an element after a request no matter what
fx:swapped triggered after the swap and any associated View Transition has completed
#### Initialization Events ##### `fx:init` The `fx:init` event is triggered when fixi is processing a node with an `fx-action` attribute. One property is available in `evt.detail`: * `options` - An [Options Object](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options) that will be passed to `addEventListener()` If this event is cancelled via `preventDefault()`, the element will not be initialized by fixi. ##### `fx:inited` The `fx:inited` event is triggered when fixi finished processing a node with an `fx-action` attribute. Unlike other fixi events, this event does not bubble. ##### `fx:process` fixi listens for the `fx:process` event on the `document` and will enable any unprocessed fixi-powered elements within the element as well as the element itself. #### Fetch Events fixi uses the [`fetch()` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to issue HTTP requests. It triggers events around this call that allow users to configure the request. ##### `fx:config` The first event triggered is `fx:config`. This event can be used to configure the arguments passed to `fetch()` via the fixi config object, which can be found at `evt.detail.cfg`. This config object has the following properties: * `trigger` - The event that triggered the request * `method` - The HTTP Method that is going to be used * `action` - The URL that the request is going to be issued to * `headers` - An Object of name/value pairs to be sent as HTTP Request Headers * `target` - The target element that will be swapped when the response is processed * `swap` - The mechanism by which the element will be swapped * `body` - The body of the request, if present, a FormData object that holds the data of the form associated with the request * `drop` - Whether this request will be dropped, defaults to the number of outstanding requests associated with the element * `transition` - The View Transition function, if it is available. Set to `false` if you don't want a transition to occur * `preventTrigger` - A boolean (defaults to `true`) that, if true, will call `preventDefault()` on the triggering event * `signal` - The AbortSignal of the related AbortController for the request * `abort()` - A function that can be invoked to abort the pending fetch request * `fetch()` - The fetch() function that will be used for the request, can be used for [mocking](#mocking) requests Mutating the `method`, etc. properties of the `cfg` object will change the behavior of the request dynamically. Note that the `cfg` object is passed to `fetch()` as the second argument of type `RequestInit`, so any properties you want to set on the `RequestInit` may be set on the `cfg` object (e.g. `credentials`). Another property available on the `detail` of this event is `requests`, which will be a [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) of any existing outstanding requests for the element. ###### replace existing requests in flight fixi does not implement request queuing like htmx does, but you can implement a simple "replace existing requests in flight" rule with the following JavaScript: ```js document.addEventListener("fx:config", (evt) => { evt.detail.cfg.drop = 0; // allow this request to be issued even if there are other requests evt.detail.requests.forEach((cfg) => cfg.abort()); // abort all existing requests }) ``` If you call `preventDefault()` on this event, no request will be issued. ##### `fx:before` The `fx:before` event is triggered just before a `fetch()` is issued. The config will again be available in the `evt.detail.cfg` property, but after any confirmation is done. The requests will also be available in `evt.detail.requests` and will include the current request. If you call `preventDefault()` on this event, no request will be issued. ##### `fx:after` The `fx:after` event is triggered after a `fetch()` successfully completes. The config will again be available in the `evt.detail.cfg` property, and will have two additional properties: * `response` - The response object from the `fetch()` call * `text` - The text of the response At this point you may still mutate the `swap`, etc. attributes to affect swapping, and you may mutate the `text` if you want to modify it is some way before it is swapped. Calling `preventDefault()` on this event will prevent swapping from occurring. ##### `fx:error` The `fx:error` event is triggered when a network error occurs or when the request is aborted using the `abort` function on the config. In this case the `evt.detail.cfg` object is available for modification and `cfg.response` and `cfg.text` will not be present. The `evt.detail.error` property contains the thrown value. If you receive this event, swapping will not occur, the processing is terminated early. ##### `fx:finally` The `fx:finally` event is triggered regardless if an error occurs or not and can be used to clean up after a request. Again the `evt.detail.cfg` object is available for modification. ##### `fx:swapped` The `fx:swapped` event is triggered once the swap and any associated View Transitions complete. The `evt.detail.cfg` object is available. ### Properties fixi adds two properties to elements in the DOM
property description
document.__fixi_mo The MutationObserver that fixi uses to watch for new content to process new fixi-powered elements.
elt.__fixi The event handler created by fixi on fixi-powered elements
#### `document.__fixi_mo` fixi stores the Mutation Observer that it uses to watch for new content in the `__fixi_mo` property on the `document`. You can use this property to temporarily disable mutation observation for performance reasons: ```js // disable processing document.__fixi_mo.disconnect() /* ... heavy mutation code that should not be processed by fixi */ // reenable processing document.__fixi_mo.observe(document.body, {childList:true, subtree:true}) ``` Similar code can be used to adjust the MutationObserver to listen for mutations in some subset of the document. Finally, you can also switch to entirely manual processing using the [`fx:after`](#fxafter), [`fx:swapped`](#fxswapped) & [`fx:process`](#fxprocess) events: ```js document.__fixi_mo.disconnect() document.addEventListener("fx:after", (evt)=>{ // capture the parent element of the target in the config before swapping evt.detail.cfg.parent = evt.detail.cfg.target.parentElement }) document.addEventListener("fx:swapped", (evt)=>{ // reprocess the parent evt.detail.cfg.parent.dispatchEvent(new CustomEvent("fx:process"), {bubbles:true}) }) ``` #### `elt.__fixi` The `__fixi` property will be added to any element that has an `fx-action` attribute on it assuming that the element or an ancestor is not marked `fx-ignore`. The value of the property will be the event listener that is added to the element. It also has two properties: * `evt` - the string event name that will trigger the handler * `requests` - the config values of any open requests (may be `null`) This property can be used to remove the fixi-generated event handler like so: ```js elt.removeEventListener(elt.__fixi.evt, elt.__fixi) ``` If you want to reprocess the element you will need to remove the property entirely and trigger the [`fx:process`](#fxprocess) event on it: ```js elt.removeEventListener(elt.__fixi.evt, elt.__fixi) delete elt.__fixi elt.dispatchEvent(new CustomEvent("fx:process"), {bubbles:true}) ``` You can also use this property to store extension-related information. See the [polling example](#polling) below. ## Mocking It is easy to mock `fetch()` requests in fixi by replacing the `evt.detail.cfg.fetch` property with a mocking function. The function can take the same arguments as [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) (or not if they are not needed) and should return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) compatible object or a Promise that resolves to one. For the simple case, the return object need only implement the `.text()` method. Here is an example that mocks responses using `template` elements ```js document.addEventListener("fx:config", (evt) => { const template = document.getElementById(evt.detail.cfg.action) if (template) { evt.detail.cfg.fetch = ()=>({text: ()=>template.innerHTML}) // note the parens to make {} an object } }) ``` ```html ``` ## Examples Here are some basic examples of fixi in action ### Click To Edit The htmx [click to edit example](https://htmx.org/examples/click-to-edit/) can be easily ported to fixi: ```html
: Joe
: Blow
: joe@blow.com
``` ### Delete Row The [delete row example](https://htmx.org/examples/delete-row/) from htmx can be implemented in fixi like so: ```html Angie MacDowell angie@macdowell.org Active ``` Note that this version does not have a confirmation prompt, you would need to implement that yourself using the [`fx:config`](#fxconfig) event. ### Lazy Loading The htmx [lazy loading](https://htmx.org/examples/lazy-load/) example can be ported to fixi using the [`fx:inited`](#fxinited) event: ```html
Content Loading...
``` ## Extensions Because fixi is minimalistic the user is responsible for implementing many behaviors they want via events. We have already seen how to abort an existing request that is already in flight. A suggested convention when you are adding fixi extension attributes is to use the `ext-fx` prefix, and to process the extension in the `fx:init` method. You may find it useful to use the `__fixi` property on the element to store values necessary for the extension to work. Another suggested convention is keeping your fixi extensions in a single file called `ext-fixi.js` alongside your `fixi-.js` file. Here are some examples of useful fixi extensions implemented using events. ### Disabling an Element During A Request Here is an example that will use attributes to disable an element when a request is in flight: ```js // fixi disable elements extension document.addEventListener("fx:init", (evt)=>{ if (evt.target.matches("[ext-fx-disable]")){ var disableSelector = evt.target.getAttribute('ext-fx-disable') evt.target.addEventListener('fx:before', ()=>{ let disableTarget = disableSelector == "" ? evt.target : document.querySelector(disableSelector) disableTarget.disabled = true evt.target.addEventListener('fx:after', (afterEvt)=>{ if (afterEvt.target == evt.target){ disableTarget.disabled = false } }) }) } }) ``` ```html ``` ### Showing an Indicator During A Request Here is an example that will use attributes a `fixi-request-in-flight` class to show an indicator of some kind: ```js // fixi request indicator extension document.addEventListener("fx:init", (evt)=>{ if (evt.target.matches("[ext-fx-indicator]")){ var disableSelector = evt.target.getAttribute("ext-fx-indicator") evt.target.addEventListener("fx:before", ()=>{ let disableTarget = disableSelector == "" ? evt.target : document.querySelector(disableSelector) disableTarget.classList.add("fixi-request-in-flight") evt.target.addEventListener("fx:after", (afterEvt)=>{ if (afterEvt.target == evt.target){ disableTarget.classList.remove("fixi-request-in-flight") } }) }) } }) ``` ```html ``` This example can be modified to use classes or other mechanisms for showing indicators as well. ### Debouncing A Request The following extension allows you to [debounce](https://www.geeksforgeeks.org/debouncing-in-javascript/) the triggering event for a fixi-powered element. It does this by removing the initial listener installed by fixi and wiring in a new listener for the same event that delegates to the fixi handler if no other events occur in the given time period. The debouncing time is specified via the `ext-fx-debounce` attribute, which specified the number of milliseconds to wait before triggering the request. ```js // fixi event debouncing extension document.addEventListener("fx:init", (evt)=>{ let target = evt.target // if this element has the debounce extention if (target.hasAttribute("ext-fx-debounce")){ // add a listener for the fx:inited event, when the __fixi property is available target.addEventListener("fx:inited", ()=>{ // remove the default listener target.removeEventListener(target.__fixi.evt, target.__fixi) let debounceTime = parseInt(target.getAttribute("ext-fx-debounce")) let timeout = null // install a debounced version that delegates to the default listener target.addEventListener(target.__fixi.evt, (evt)=>{ clearTimeout(timeout) timeout = setTimeout(()=>target.__fixi(evt), debounceTime) }) }) } }) ``` Here is an implementation of the [active search](https://htmx.org/examples/active-search/) example from the htmx website using this extension: ```html
... ...
``` ### Polling htmx-style polling can be implemented in the following manner: ```js // fixi polling extension document.addEventListener("fx:init", (evt)=>{ let elt = evt.target if (elt.matches("[ext-fx-poll-interval]")){ // wait for the non-bubbling fx:inited event on the element so the __fixi property is available elt.addEventListener("fx:inited", ()=>{ // squirrel away in case we want to call clearInterval() later elt.__fixi.pollInterval = setInterval(()=>{ elt.dispatchEvent(new CustomEvent("poll")) }, parseInt(elt.getAttribute("ext-fx-poll-interval"))) }) } }) ``` ```html

Live News

... initial content ...
``` ### Server Sent Events [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)-based swaps can be implemented in the following manner: ```js const evtSource = new EventSource("/sse"); evtSource.addEventListener("fixi", (event) => { const { target, swap, text } = JSON.parse(event.data); document.querySelectorAll(target).forEach(async (ele) => { if (ele) { const cfg = { target: ele, swap: swap, text: text, transition: document.startViewTransition?.bind(document), }; let doSwap = () => { if (/(before|after)(begin|end)/.test(cfg.swap)) cfg.target.insertAdjacentHTML(cfg.swap, cfg.text); else if (cfg.swap in cfg.target) cfg.target[cfg.swap] = cfg.text; else throw cfg.swap; }; if (cfg.transition) await cfg.transition(doSwap).finished; else await doSwap(); } }); }); ``` #### Example Event On the server side, a fixi event type can be sent with a stringified object containing target, swap, and text. ``` event: fixi data: {"target":"#clock","swap":"innerHTML","text":"Mon Jun 30 2025 14:23:58 GMT-0400 (Eastern Daylight Time)"} ``` ### Confirmation This extension implements a simple `confirm()` based confirmation if the `ext-fx-confirm` attribute is found. Note that it does not use a Promise, just the regular old blocking `confirm()` function ```js // fixi confirmation extension document.addEventListener("fx:config", (evt)=>{ var confirmationMessage = evt.target.getAttribute("ext-fx-confirm") if (confirmationMessage){ evt.detail.cfg.confirm = ()=>confirm(confirmationMessage) } }) ``` ```html ``` ### Relative Selectors This extension implements relative selectors for the `fx-target` attribute. ```js // fixi relative selectors extension document.addEventListener('fx:config', (evt)=>{ console.log("here") var target = evt.target.getAttribute("fx-target") || "" if (target.indexOf("closest ") == 0){ evt.detail.cfg.target = evt.target.closest(target.substring(8)) } else if (target.indexOf("find ") == 0){ evt.detail.cfg.target = evt.target.querySelector(target.substring(5)) } else if (target.indexOf("next ") == 0){ var matches = Array.from(document.querySelectorAll(target.substring(5))) evt.detail.cfg.target = matches.find((elt) => evt.target.compareDocumentPosition(elt) === Node.DOCUMENT_POSITION_FOLLOWING) } else if (target.indexOf("previous ") == 0){ var matches = Array.from(document.querySelectorAll(target.substring(9))).reverse() evt.detail.cfg.target = matches.find((elt) => evt.target.compareDocumentPosition(elt) === Node.DOCUMENT_POSITION_PRECEDING) } }) ``` ```html ``` ### Intersection Events fixi does not trigger events when elements become visible like htmx does, but you can implement this behavior with the following extension. It adds an [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) for every element with the `intersect` trigger and wires it in to trigger that event once the element intersects the viewport. ```js // fixi intersection events extension document.addEventListener("fx:init", (evt) => { let trigger = evt.target.getAttribute("fx-trigger") if(trigger === "intersect") { let obs = evt.target.__fixi_ob = new IntersectionObserver((entries)=>{ for(const entry of entries) { if (entry.isIntersecting){ // done observing, remove obs.unobserve(evt.target) evt.target.__fixi_ob = null; // trigger event evt.target.dispatchEvent(new CustomEvent("intersect")) return; } } }) obs.observe(evt.target) } }) ``` With this extension it is possible to implement the [infinite scroll](https://htmx.org/examples/infinite-scroll/) example: ```html Agent Smith void29@null.org 55F49448C0 ``` ### Custom Swapping Algorithms You can implement a custom swap strategies using the [`fx:config`](#fxconfig) event, and wiring in a function for the `evt.detail.cfg.swap` property. Here is an example that allows you to use [Idiomorph](https://github.com/bigskysoftware/idiomorph), a morphing algorithm, with the `morph` & `innerMorph` values in `fx-swap`: ```js // fixi morphing extension document.addEventListener("fx:config", (evt) => { function morph(cfg, style) { Idiomorph.morph(cfg.target, cfg.text, { morphStyle: style }).forEach((n) => { // process nodes as morphing existing nodes will not trigger fixi MutationObserver n.dispatchEvent(new CustomEvent("fx:process", { bubbles: true })); }); } if (evt.detail.cfg.swap == "morph") evt.detail.cfg.swap = (cfg) => morph(cfg, "outerHTML"); if (evt.detail.cfg.swap == "innerMorph") evt.detail.cfg.swap = (cfg) => morph(cfg, "innerHTML"); }); ``` ```html

Morph

``` ### Implementing Attribute Inheritance fixi does not implement [attribute inheritance](https://htmx.org/docs/#inheritance) like htmx does, but you can modify the fixi source to do so easily. Simply change this line: ```js let attr = (elt, name, defaultVal)=>elt.getAttribute(name) || defaultVal ``` to this: ```js let attr = (elt, name, defaultVal)=>elt.closest(`[${name}]`)?.getAttribute(name) || defaultVal ``` ### Implementing History Support fixi does not implement history support, but you can add rudimentary support like so: ```js function initJS() { // initialize javascript things here } document.addEventListener("DOMContentLoaded", (evt)=>{ initJS(); }); document.addEventListener("fx:after", (evt)=>{ if (evt.target.hasAttribute("ext-fx-push")){ history.replaceState({fixi:true, url:location.href}, "", location.href) history.pushState({fixi:true, url:evt.detail.cfg.response.url}, "", evt.detail.cfg.response.url) } }) window.addEventListener("popstate", async(evt)=>{ if (evt.state.fixi){ let historyResp = await fetch(evt.state.url) document.documentElement.innerHTML = await historyResp.text() document.dispatchEvent(new CustomEvent("fx:process")) initJS() } }) ``` This adds an event listener for the `fx:after` event, and if the element has the `ext-fx-push` attribute it uses the [JavaScript History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) to update the URL in the browser. Note that, like with htmx, this mechanism expects that remote systems will return full pages when the given URL is requested, so that things like refresh will work properly. Note also that scripts in head tags that are swapped in by fixi are _not_ executed by default, which is the browser standard for whatever reason. With this extension, you can write code like this: ```html Example Fixi-Powered Link ``` And fixi will handle the click on this link, and the URL of the site will properly update, assuming JavaScript is enabled. More sophisticated History handling (in particular, `head` tag handling) is left as an exercise for the reader. ## LICENCE ``` Zero-Clause BSD ============= Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLEs FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ```

💀 Memento Mori

```js /** * Adding a single line to this file requires great internal reflection * and thought. You must ask yourself if your one line addition is so * important, so critical to the success of the company, that it warrants * a slowdown for every user on every page load. Adding a single letter * here could cost thousands of man hours around the world. * * That is all. */ ``` -- [A comment](https://www.youtube.com/watch?v=wHlyLEPtL9o&t=1528s) at the beginning of [Primer](https://gist.github.com/makinde/376039)