{"id":49298158,"url":"https://github.com/bigskysoftware/moxi","last_synced_at":"2026-05-02T07:01:14.781Z","repository":{"id":353833308,"uuid":"1220945753","full_name":"bigskysoftware/moxi","owner":"bigskysoftware","description":"moxi.js - a companion to fixi.js","archived":false,"fork":false,"pushed_at":"2026-04-25T18:54:30.000Z","size":21,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-25T20:32:00.540Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://fixiproject.org","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bigskysoftware.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-25T14:50:04.000Z","updated_at":"2026-04-25T18:54:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bigskysoftware/moxi","commit_stats":null,"previous_names":["bigskysoftware/moxi"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/bigskysoftware/moxi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fmoxi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fmoxi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fmoxi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fmoxi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bigskysoftware","download_url":"https://codeload.github.com/bigskysoftware/moxi/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fmoxi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32286271,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"online","status_checked_at":"2026-04-26T02:00:05.962Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-26T05:00:51.287Z","updated_at":"2026-05-01T06:01:16.034Z","avatar_url":"https://github.com/bigskysoftware.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1\u003e\u0026#x1F94A; moxi.js - \u003ci\u003ejust a little more...\u003c/i\u003e\u003c/h1\u003e\n\nmoxi.js is an experimental, minimalist companion to [fixi.js](https://github.com/bigskysoftware/fixi) that\nlets you put small bits of behavior directly on HTML elements: event handlers, reactive\nexpressions \u0026 a compact query helper.\n\nmoxi is part of the [fixi project](https://fixiproject.org): fixi handles the network and swapping, moxi handles local\ninteractivity.\n\nThe two are designed to be used together, but moxi has no dependency on fixi and works fine on its own.\n\nThe moxi api consists of two [attributes](#attributes), nine [event modifiers](#event-modifiers),\nthree [globals plus a handler scope](#scope), and four [events](#events).\n\nHere is an example:\n\n```html\n\u003cinput id=\"name\" placeholder=\"name\"\u003e\n\u003coutput live=\"this.innerText = 'hello ' + q('#name').value\"\u003e\u003c/output\u003e\n\u003cbutton on-click=\"q('#name').value = ''\"\u003eclear\u003c/button\u003e\n```\n\nWhen a user types into the `input`, the `output` updates automatically because the `live`\nattribute re-runs when the DOM or form state changes.\n\nThe `button` clears the input when clicked, and the `output` updates again in response.\n\n## Installing\n\nmoxi is designed to be easily [vendored](https://htmx.org/essays/vendoring/), that is, copied, into your project:\n\n```bash\ncurl https://raw.githubusercontent.com/bigskysoftware/moxi/refs/tags/0.1.0/moxi.js \u003e\u003e moxi-0.1.0.js\n```\n\nThe SHA256 of v0.1.0 is\n\n`mrpYW3yY45ec7RlIyDVCDx2/NnrZOQNB4v+OavwQo7Q=`\n\ngenerated by the following command line script:\n\n```bash\ncat moxi.js | openssl sha256 -binary | openssl base64\n```\n\nAlternatively can download the source from here:\n\n\u003chttps://github.com/bigskysoftware/moxi/archive/refs/tags/0.1.0.zip\u003e\n\nYou can also use the JSDelivr CDN for local development or testing:\n\n```html\n\n\u003cscript src=\"https://cdn.jsdelivr.net/gh/bigskysoftware/moxi@0.1.0/moxi.js\"\n        crossorigin=\"anonymous\"\n        integrity=\"sha256-mrpYW3yY45ec7RlIyDVCDx2/NnrZOQNB4v+OavwQo7Q=\"\u003e\u003c/script\u003e\n```\n\nFinally, moxi is available on NPM as the [\n`@bigskysoftware/moxi-js`](https://www.npmjs.com/package/@bigskysoftware/moxi-js) package.\n\n## API\n\n### Attributes\n\n| attribute    | description                                                                                                                                        | example                                    |\n|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|\n| `on-\u003cevent\u003e` | Binds a handler for `\u003cevent\u003e` on this element. Colons are allowed in the event name (e.g. `on-fx:after`).                                          | `on-click=\"q('#out').innerText = 'hi'\"`    |\n| `on-init`    | Special case - runs once at bind time rather than registering an event listener. Useful for setup code that lives on the element itself.           | `on-init=\"this.dataset.ready = true\"`      |\n| `live`       | An expression that is evaluated at bind time and re-evaluated whenever the DOM or form state changes. Great for reactive output.                   | `live=\"this.innerText = q('#name').value\"` |\n| `mx-ignore`  | Any element with this attribute on it or on an ancestor will be skipped during processing - no `on-*` or `live` attributes on it will be wired up. |                                            |\n\n### Event Modifiers\n\nModifiers are dot-separated and composable. They live between the event name and the `=`.\nFor example, `on-click.prevent.stop=\"...\"` will both `preventDefault()` and `stopPropagation()`\nbefore the body runs.\n\n| modifier   | description                                                                                                                                                                                                                                                             |\n|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `.prevent` | Calls `event.preventDefault()` before the handler body runs.                                                                                                                                                                                                            |\n| `.stop`    | Calls `event.stopPropagation()` before the handler body runs.                                                                                                                                                                                                           |\n| `.halt`    | Equivalent to `.prevent.stop` - a shorthand for the common case.                                                                                                                                                                                                        |\n| `.once`    | Removes the listener after the first successful fire. Plays correctly with `.self` and `.outside` - skipped invocations don't consume the listener.                                                                                                                     |\n| `.self`    | Skips the handler when `event.target !== this`. Ignores bubbled events from children.                                                                                                                                                                                   |\n| `.capture` | Passes `{capture: true}` to `addEventListener`.                                                                                                                                                                                                                         |\n| `.passive` | Passes `{passive: true}` to `addEventListener`. Required for smooth scroll/touch handlers.                                                                                                                                                                              |\n| `.outside` | Attaches the listener to `document` instead of `this`, and only fires when the event happened outside the element. Useful for dismissing menus and modals.                                                                                                              |\n| `.cc`      | Camel-cases the event name. `on-my-event.cc` listens for `myEvent`. Useful when consuming custom events from libraries or web components that dispatch camelCase names, since HTML attribute names are lowercased by the parser and can't otherwise express mixed case. |\n\n### Scope\n\nmoxi exposes three helpers on `globalThis`:\n\n| name             | type          | description                                                                                                                             |\n|------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------|\n| `q(x)`           | fn -\u003e proxy   | Query helper. `x` can be a selector string, a single element, or any iterable of elements. See [The `q()` Helper](#the-q-helper) below. |\n| `wait(x)`        | fn -\u003e Promise | If `x` is a number, resolves after `x` milliseconds. If it's a string, resolves with the event the next time an event named `x` fires.  |\n| `transition(fn)` | fn            | Wraps `fn` in `document.startViewTransition()`, with a fallback if unsupported.                                                         |\n\nInside `on-*` and `live` bodies, four additional bindings are in scope:\n\n| name                             | type          | description                                                                                                                                              |\n|----------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `this`                           | `Element`     | The element the attribute is on.                                                                                                                         |\n| `event`                          | `Event`       | Available in `on-*` handlers; undefined for `on-init` and `live`.                                                                                        |\n| `trigger(type, detail, bubbles)` | fn            | Dispatches a cancelable `CustomEvent` from `this`. `bubbles` defaults to `true`. From outside a handler, use `q(elt).trigger(...)` on the proxy instead. |\n| `debounce(ms)`                   | fn -\u003e Promise | Per-handler debouncer - superseded calls never resolve. Use with `await`. Handler-scope only because it carries per-handler state.                       |\n\n`q()` directionals (`next`, `prev`, `closest`, `in this`) and `wait(\"event\")` are\ncontext-aware: in a handler they resolve relative to `this`; called globally they\nresolve relative to `document.documentElement`.\n\nHandler bodies are compiled as **async functions** (via the `AsyncFunction` constructor), so\n`await` works anywhere.\n\n#### Bare-name access to `event.detail`\n\nFor `on-*` handlers, every key on `event.detail` is also exposed as a top-level\nvariable inside the handler body. So instead of writing\n\n```html\n\n\u003cbutton on-fx:config=\"event.detail.cfg.confirm = () =\u003e confirm('Delete?')\"\u003edelete\u003c/button\u003e\n```\n\nyou can drop the `event.detail.` prefix and write\n\n```html\n\n\u003cbutton on-fx:config=\"cfg.confirm = () =\u003e confirm('Delete?')\"\u003edelete\u003c/button\u003e\n```\n\nReads, mutations (`cfg.foo = ...`), and even reassignments (`cfg = {...}`) all hit the\nunderlying `event.detail` object. If a handler updates `cfg.confirm` inside an\n`fx:config` listener, fixi sees the change. This is implemented with a `with` block\naround the handler body, so:\n\n* If `event.detail` is missing or null (e.g., a plain non-`CustomEvent`), nothing\n  is injected and the handler still runs.\n* Names that aren't on `event.detail` resolve normally to `this`, `event`, `trigger`,\n  `debounce`, the global helpers (`q`, `wait`, `transition`), or any other binding.\n* Assignments to a name that *isn't* already a property of `event.detail` fall\n  through to the outer scope, so they don't accidentally pollute `detail`.\n\n### The `q()` Helper\n\n`q(x)` returns a proxy over matched elements. `x` is most often a selector string, but\ncan also be a single `Element` (wrapped) or any iterable of elements (e.g. a `NodeList`\nor `Array`). When given a string, the grammar is:\n\n```\n[\u003cdirection\u003e ]\u003ccss-selector\u003e[ in (this | \u003cscope-selector\u003e)]\n```\n\n#### Directions\n\n| direction   | result                                                                       |\n|-------------|------------------------------------------------------------------------------|\n| _(none)_    | All elements matching the selector in the scope (default scope: `document`). |\n| `next X`    | The first `X` after `this` in document order.                                |\n| `prev X`    | The last `X` before `this` in document order.                                |\n| `closest X` | The same as `this.closest(X)`.                                               |\n| `first X`   | The first `X` in the scope.                                                  |\n| `last X`    | The last `X` in the scope.                                                   |\n\n#### Scoping with `in`\n\n* `q('.row in this')` - scopes the query to `this`\n* `q('.row in #panel')` - scopes the query to the element matching `#panel`\n* If the scope selector matches nothing, `q` returns an empty proxy (no throw).\n\n#### The Proxy\n\nThe object returned by `q()` is a `Proxy` that fans reads, writes, and method calls across\nevery matched element:\n\n| operation                               | behavior                                                                                                                                                                                |\n|-----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `q(...).prop = v`                       | Sets `prop = v` on every match.                                                                                                                                                         |\n| `q(...).method(...)`                    | Calls `method` on every match. Returns the result from the first match - so value-returning methods like `checkValidity()` or `getAttribute()` work naturally.                          |\n| `q(...).prop` (object)                  | Returns a new proxy over `[e1.prop, e2.prop, ...]`, so nested access like `q('.row').classList.add('sel')` and `q('.row').style.color = 'red'` works.                                   |\n| `q(...).prop` (primitive or function)   | Returns the value from the first match.                                                                                                                                                 |\n| `q(...).count`                          | Returns the number of matched elements.                                                                                                                                                 |\n| `q(...).arr()`                          | Returns the matched elements as a plain `Array`, so you can chain `.filter()`, `.map()`, etc. without spreading.                                                                        |\n| `q(...).trigger(type, detail, bubbles)` | Dispatches the event from every matched element. `bubbles` defaults to `true`.                                                                                                          |\n| `q(...).take(cls, from)`                | Removes `cls` from every element matching `from` (a selector string or iterable of elements), then adds it to every matched element. Perfect for active-tab / active-nav patterns.      |\n| `q(...).insert(pos, html)`              | Parses `html` and inserts it at every matched element. `pos` is one of `'before'` / `'start'` / `'end'` / `'after'` - a friendlier spelling of the four `insertAdjacentHTML` positions. |\n| `for (let e of q(...))` / `[...q(...)]` | Iterates over the raw matched elements.                                                                                                                                                 |\n\n### Events\n\nmoxi fires three lifecycle events. All are dispatched on the element being processed; listen\non the `document` for global hooks.\n\n| event        | description                                                                                                                                                                                                                                                                        |\n|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `mx:init`    | Fired just before moxi initializes an element. Cancelable - calling `preventDefault()` will skip binding that element.                                                                                                                                                             |\n| `mx:inited`  | Fired after the element has been fully initialized. Does not bubble.                                                                                                                                                                                                               |\n| `mx:process` | moxi listens for this event on the `document` and will process the `evt.target` and its descendants. Dispatch this to force re-scanning after manual DOM changes.                                                                                                                  |\n| `refresh`    | moxi listens for this bubbling event on the `document` and re-runs every `live` expression. Dispatch it (e.g. via `trigger('refresh')` from a handler or `document.dispatchEvent(new Event('refresh'))`) when state outside the DOM changes and you want live blocks to recompute. |\n\n### Properties\n\n| property             | description                                                                                                                                                                       |\n|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `document.__moxi_mo` | The `MutationObserver` that moxi uses to auto-process newly added elements and to drive reactivity. You can `disconnect()` it temporarily for performance during large mutations. |\n| `elt.__moxi`         | An object mapping event names to the handlers moxi wired up on this element. Useful for debugging and for manually removing listeners.                                            |\n\n## Modus Operandi\n\nmoxi's entry point is at the bottom of [moxi.js](moxi.js). On `DOMContentLoaded` it:\n\n1. Starts a `MutationObserver` watching the document for added nodes, attribute changes,\n   character data changes, and text child changes.\n2. Adds capturing document-level listeners for `input` and `change` to drive reactivity.\n3. Processes the existing body.\n\n### Discovery\n\nmoxi finds elements using a single XPath query:\n\n```\ndescendant-or-self::*[@live or @*[starts-with(name(),'on-')]]\n```\n\nThat is - anything with a `live` attribute, or any attribute name starting with `on-`. XPath\nmeans moxi only visits elements it actually needs to wire up, rather than iterating every\ndescendant.\n\n### `on-*` Handlers\n\nFor each `on-\u003cevent\u003e[.\u003cmod\u003e...]` attribute, moxi compiles the attribute value into an async\nfunction with the handler scope described above, then attaches it as an event listener. The\nattribute name after the `on-` prefix is the event name (colons allowed), optionally followed\nby dot-separated modifiers.\n\nIf the event name is the literal string `init`, moxi invokes the function immediately instead\nof registering a listener.\n\n### `live` Expressions\n\nFor each `live` attribute, moxi compiles the value into an async function, runs it once, and\nadds it to a global set of reactive expressions. Whenever the `MutationObserver` sees a\nchange, or the capturing `input`/`change` listener fires, every live expression is re-run.\n\nTo avoid runaway self-mutation cycles, moxi guards recompute behind a `pending` flag cleared\non the next macrotask - so a live expression writing to the DOM will, at worst, settle in two\nticks rather than cycle forever.\n\nLive expressions whose element has been removed from the DOM are removed from the run set on\nthe next invocation (they detect `!elt.isConnected` and clean up).\n\n### Pairing with fixi\n\nmoxi and [fixi](https://github.com/bigskysoftware/fixi) compose cleanly. Because moxi listens\nfor events via `on-*`, you can react to fixi's lifecycle events with an ordinary handler:\n\n```html\n\n\u003cdiv fx-action=\"/data\" on-fx:after=\"q('closest section').classList.add('loaded')\"\u003e\n    ...\n\u003c/div\u003e\n```\n\nor trigger a fixi request from a moxi handler by dispatching from the proxy:\n\n```html\n\n\u003cbutton on-click=\"q('#target').trigger('refresh')\"\u003eReload\u003c/button\u003e\n\u003cdiv id=\"target\" fx-action=\"/data\" fx-trigger=\"refresh\"\u003e...\u003c/div\u003e\n```\n\n## Examples\n\n### Reactive Output\n\n```html\n\u003cinput id=\"name\" placeholder=\"type something\"\u003e\n\u003coutput live=\"this.innerText = 'hello ' + (q('#name').value || 'stranger')\"\u003e\u003c/output\u003e\n```\n\n### Click Counter\n\n```html\n\n\u003cbutton on-init=\"this.count = 0\"\n        on-click=\"this.count++; q('next output').value = this.count\"\u003eclick me\n\u003c/button\u003e\n\u003coutput\u003e0\u003c/output\u003e\n```\n\n### Active Tab With `take()`\n\n```html\n\n\u003cnav\u003e\n    \u003cbutton class=\"tab active\" on-click=\"q(this).take('active', '.tab')\"\u003eOne\u003c/button\u003e\n    \u003cbutton class=\"tab\" on-click=\"q(this).take('active', '.tab')\"\u003eTwo\u003c/button\u003e\n    \u003cbutton class=\"tab\" on-click=\"q(this).take('active', '.tab')\"\u003eThree\u003c/button\u003e\n\u003c/nav\u003e\n```\n\n### Debounced Search\n\n```html\n\u003cinput on-input=\"await debounce(250); q('next output').innerText = 'searching ' + this.value\"\u003e\n\u003coutput\u003e\u003c/output\u003e\n```\n\n### View Transition on Toggle\n\n```html\n\u003cbutton on-click=\"transition(() =\u003e q('#panel').classList.toggle('open'))\"\u003etoggle\u003c/button\u003e\n\u003cdiv id=\"panel\"\u003e...\u003c/div\u003e\n```\n\n### Click-Outside-To-Dismiss\n\n```html\n\n\u003cbutton on-click=\"q('#menu').hidden = false\"\u003eopen menu\u003c/button\u003e\n\u003cdiv id=\"menu\" hidden on-click.outside=\"this.hidden = true\"\u003e\n    Menu contents...\n\u003c/div\u003e\n```\n\n### Parent-Listens-For-Child-Emits\n\n```html\n\n\u003cdialog on-confirm=\"alert('confirmed: ' + event.detail)\"\u003e\n    \u003cbutton on-click=\"trigger('confirm', 'yes')\"\u003eyes\u003c/button\u003e\n    \u003cbutton on-click=\"trigger('confirm', 'no')\"\u003eno\u003c/button\u003e\n\u003c/dialog\u003e\n```\n\n## LICENCE\n\n```\nZero-Clause BSD\n=============\n\nPermission to use, copy, modify, and/or distribute this software for\nany purpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL\nWARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE\nFOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY\nDAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN\nAN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\nOF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbigskysoftware%2Fmoxi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbigskysoftware%2Fmoxi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbigskysoftware%2Fmoxi/lists"}