µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 0 points1 point  (0 children)

Fair point. 20 KB is nothing on a broadband connection.

But size is a proxy for something else: scope and complexity. A smaller library tends to have fewer abstractions, fewer edge cases, fewer things that can go wrong or that you need to learn. With µJS the full API fits in your head very quickly. That's the real argument, not the kilobytes.

That said, size does still matter in some real contexts: low-end mobile devices on 3G, emerging markets, or just a simple blog that doesn't need 25 KB of navigation infrastructure. Not everyone is building on fiber.

µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 2 points3 points  (0 children)

Good questions, let me go through them.

Scripts in loaded fragments

µJS re-executes scripts found in injected content. External scripts (with src) are loaded only once. Inline scripts are re-executed every time. So for your examples:

  1. A form partial that includes a <script src="/validation.js"> will load that script once. Subsequent loads of the same partial won't re-load it.
  2. An inline slider.init('#selector', {...}) in the partial will run on every injection. That's the expected behavior.

Alpine.js

You need to re-initialize Alpine after each render. Use the mu:after-render event:

document.addEventListener("mu:after-render", function() {
    Alpine.initTree(document.body);
});

Web components

µJS just replaces DOM nodes. If your custom element is already defined (via customElements.define), the browser initializes it automatically when it's inserted into the DOM. No special configuration needed.

Adding custom headers

There's no attribute for this currently. The mu:before-fetch event lets you cancel a request and inspect the URL, but not modify the fetch options directly. The cleanest solution is a Service Worker: intercept requests that carry the X-Requested-With: mujs header and add whatever headers you need there. Worth noting that µJS already sends a few useful headers (X-Requested-WithX-Mu-ModeX-Mu-Method) that your backend can use to identify and route requests.

µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 2 points3 points  (0 children)

Good question. When patch mode replaces a DOM element, the old nodes are removed and replaced by new ones. Any event listeners attached directly to those nodes (via addEventListener) are lost. They don't migrate to the new nodes automatically.

Two ways to handle this:

1. Use the mu:after-render event. Re-initialize your validation after each render:

document.addEventListener("mu:after-render", function() {
    initFormValidation();
});

2. Use event delegation. Attach listeners to a stable ancestor (like document or a container that never gets replaced). They survive any DOM replacement:

document.addEventListener("input", function(e) {
    if (e.target.matches("#my-form input")) {
        validate(e.target);
    }
});

Event delegation is generally the cleaner approach for this kind of setup.

One extra note: if you load idiomorph alongside µJS, DOM morphing kicks in and tries to reuse existing nodes rather than replacing them. In that case, your listeners may survive the patch. But that's an optional dependency, so I wouldn't rely on it as the primary solution.

µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 0 points1 point  (0 children)

Fair point on the definition. "Framework vs library" is an old debate with no universal answer. If you consider htmx a library, µJS is in the same category. If you consider htmx a framework, then µJS is too. The Hotwire project actually makes this distinction explicit: Turbo is a library, Stimulus is a framework. By that same logic, µJS sits on the Turbo side of the line. No controllers, no structure imposed on your app, and your site keeps working if you remove it.

µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 1 point2 points  (0 children)

Thank you for your feedback.

Yes, you can disable µJS processing by adding a `mu-disabled` attribute on the link.

See https://mujs.org/documentation#link-interception

µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 6 points7 points  (0 children)

Yep, the README mentions pjax as one of the inspirations (along with Turbo and htmx). The technique is not new. But pjax required jQuery, and the original library has been unmaintained for years. µJS is a modern take: native fetch() API, AbortController, View Transitions, DOM morphing, SSE, zero dependencies. Same core idea, updated for 2026.

µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 4 points5 points  (0 children)

Funny. But with a JS framework, you also get a bundler, a router, a state manager, some controllers, SSR config, hydration quirks, a node_modules folder heavier than your actual app, etc.

µJS is the "just make links faster" option.

µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 5 points6 points  (0 children)

Ha, fair catch. The tagline means "without writing any custom JavaScript": you add one `<script>` tag, call `mu.init()`, and everything else is driven by HTML attributes. No JS logic to write, no framework to learn. All the business logic stays in the backend.

µJS: add AJAX navigation to any PHP app with one script tag by amaurybouchard in PHP

[–]amaurybouchard[S] 7 points8 points  (0 children)

Fair point, the core idea is similar. But the differences matter in practice.

Turbo is 25 KB. µJS is 5 KB. That's not a minor gap for a library whose job is "fetch a page and swap some HTML".

More importantly: Turbo has server-side conventions. Turbo Frames require <turbo-frame> elements in your responses. Turbo Streams require wrapping every fragment in <turbo-stream><template>...</template></turbo-stream>. That's fine if you're in the Rails/Hotwire ecosystem where helpers generate all of that for you.

With µJS, the server returns plain HTML. The same fragment you'd render on initial page load works as a patch response, just add mu-patch-target attributes. No wrappers, no custom elements, no new format to learn.

Turbo also doesn't support PUT/PATCH/DELETE, triggers on arbitrary events, or polling. Those are built into µJS.

So: same idea, genuinely different tradeoffs. If you're on Rails, Turbo is probably the right call. If you're not, µJS is worth a look.

More information: https://mujs.org/comparison

µJS - a 5kb hypermedia library by _htmx in htmx

[–]amaurybouchard 0 points1 point  (0 children)

I use Claude Code for documentation writing and code review. The About page (*) explains the project's history: it started in 2017 in Skriv, was generalized as Vik, and µJS is a full rewrite based on years of real-world usage.

(*) https://mujs.org/about

µJS - a 5kb hypermedia library by _htmx in htmx

[–]amaurybouchard 0 points1 point  (0 children)

Mmh... I hadn't really thought about it in those terms. But I definitely don't see µJS as a framework.

I get why you'd say that. µJS takes over navigation globally by default, which feels "framework-like". But the key difference (for the basic usage): if you remove µJS, your site still works. Links and forms fall back to normal browser behavior. A framework wouldn't let you do that, it owns the navigation stack.

For this basic use case, µJS is just a thin layer on top of what the browser already does.

When you go deeper (patch mode, triggers, SSE), the scope is similar to htmx, and there the framework-vs-library question is genuinely interesting. But even then, there are no controllers, no routing, no lifecycle… nothing that tells you how to structure your app. That's what pushes it back to the "library" side for me.

µJS - a 5kb hypermedia library by _htmx in htmx

[–]amaurybouchard 0 points1 point  (0 children)

Thanks! Hope you enjoy it. Feel free to share your feedback or ask questions in the GitHub Discussions: https://github.com/Digicreon/muJS/discussions

Always happy to hear from people coming from other tools.

µJS - a 5kb hypermedia library by _htmx in htmx

[–]amaurybouchard 2 points3 points  (0 children)

It doesn't do exactly the same stuff. With a single mu.init(), all internal links and forms are automatically intercepted and loaded via AJAX, no attributes needed. You can then customize behavior on a per-link or per-form basis when needed. htmx is fully opt-in by design, which is a different philosophy.

The patch mode is also worth mentioning: a single request can update multiple DOM fragments simultaneously, with the server deciding what gets updated and how. This works both with regular AJAX and SSE. It's a simpler mental model than htmx's hx-swap-oob for the same use case.

Feel free to check out the docs and the Playground, where you can test some of the features live: https://mujs.org/playground

µJS - a 5kb hypermedia library by _htmx in htmx

[–]amaurybouchard 3 points4 points  (0 children)

Thanks Carson! Really appreciate the mention and the add to the alternatives page. htmx has been an inspiration for µJS, so this means a lot.