Lightweight AJAX Page Navigation Library – µJS

Lightweight AJAX Page Navigation Library – µJS
Lightweight AJAX Page Navigation Library – µJS
µJS is a lightweight AJAX navigation library that intercepts link clicks and form submissions to fetch page content via the browser’s fetch API. The fetched HTML replaces part or all of the current page to prevent full browser refreshes.

The library works with any server-side stack (PHP, Python, Go, Ruby), or anything that returns HTML. It’s ideal for developers who want SPA-like navigation speed on a traditionally server-rendered site.

Features:

  • Pure vanilla JavaScript with no external requirements. No bundler or compile step needed.
  • Update multiple independent DOM fragments from a single server response.
  • Full HTTP verb support: GET, POST, PUT, PATCH, and DELETE on links, buttons, and forms.
  • Real-time DOM updates through a persistent EventSource connection.
  • Preserves DOM state (input focus, scroll position, video playback) via optional idiomorph integration.
  • Animated page transitions in supported browsers, with a silent fallback on older ones.
  • Fetches the target page after a 50ms hover delay for near-instant perceived navigation.
  • Custom triggers: change, blur, focus, or load — with optional debounce and polling intervals.
  • Top-of-page progress indicator.
  • Supports HTML5 validation, file uploads, quit-page confirmation, and custom validator functions.

How To Use It:

1. Download the package and place both script tags at the end of <body>, after your HTML content. µJS scans the DOM on init, so the body must be fully parsed before the call.

<script src="/path/to/dist/mu.min.js"></script>
<script>mu.init();</script>

2. After mu.init(), all internal links (URLs beginning with /) are intercepted automatically. µJS fetches the target page and replaces the full <body> by default.

<body>
  <nav>
    <!-- µJS handles all internal links automatically -->
    <a href="/">Home</a>
    <a href="/posts">Posts</a>
    <a href="/about">About</a>
  </nav>

  <main id="main-content">
    <p>Post list appears here.</p>
  </main>

  <!-- External URLs are left alone by µJS -->
  <a href="https://github.com">GitHub</a>

  <!-- Links with target="_blank" are ignored -->
  <a href="/archive" target="_blank">Open archive in new tab</a>

  <!-- Opt a single link out of µJS interception -->
  <a href="/special-page" mu-disabled>Bypass µJS for this link</a>

  <script src="/path/to/dist/mu.min.js"></script>
  <script>mu.init();</script>
</body>

3. To replace only a fragment instead of the full body, set matching mu-target and mu-source selectors:

<!-- Fetch /posts, extract #main-content from the response,
and replace the current #main-content with it -->
<a href="/posts" mu-target="#main-content" mu-source="#main-content">Posts</a>

4. The mu-mode attribute controls how fetched content lands in the DOM. The default is replace.

  • replace (string): Replaces the target node with the source node. Default mode.
  • update (string): Replaces the inner HTML of the target with the inner HTML of the source.
  • prepend (string): Inserts the source at the beginning of the target element.
  • append (string): Appends the source at the end of the target element.
  • before (string): Inserts the source immediately before the target in the DOM.
  • after (string): Inserts the source immediately after the target in the DOM.
  • remove (string): Removes the target from the DOM. The source is ignored.
  • none (string): Fires lifecycle events and executes scripts, but makes no DOM changes.
  • patch (string): Processes multiple annotated fragments from the response. See Patch Mode below.
<!-- Refresh only the notification badge count in place -->
<a href="/api/notifications/count"
   mu-mode="update"
   mu-target="#badge-count"
   mu-source="#badge-count">
  Inbox
</a>

5. Patch mode lets one request update multiple independent parts of the page. The server responds with standard HTML elements annotated with mu-patch-target. µJS extracts each annotated element and applies it to the matching DOM node.

Trigger a patch request:

<button mu-url="/api/cart/add/12"
        mu-method="post"
        mu-mode="patch">
  Add to Cart
</button>

Server response — multiple independent fragments in one HTML payload:

<!-- Append the new item to the cart list -->
<div class="cart-row" mu-patch-target="#cart-items" mu-patch-mode="append">
  <span>Wireless Keyboard — $49.99</span>
</div>

<!-- Update the subtotal display -->
<span mu-patch-target="#cart-subtotal">$124.97</span>

<!-- Update the header cart badge -->
<span mu-patch-target="#cart-badge-count">3</span>

<!-- Remove a promo banner that's no longer relevant -->
<div mu-patch-target="#promo-banner" mu-patch-mode="remove"></div>

µJS processes each annotated node in order and applies its mu-patch-mode to the corresponding DOM element.

To push the URL to browser history after a patch:

<a href="/products?category=audio" mu-mode="patch" mu-patch-history="true">Audio</a>

6. µJS intercepts form submissions automatically. It runs reportValidity() before sending any request.

<!-- GET form: input data serializes to a query string -->
<form action="/search" method="get"
      mu-target="#search-results" mu-source="#search-results">
  <input type="text" name="q" placeholder="Search articles...">
  <button type="submit">Search</button>
</form>

<!-- POST form: sends as application/x-www-form-urlencoded -->
<form action="/api/newsletter/subscribe" method="post">
  <input type="email" name="email" placeholder="your@email.com">
  <button type="submit">Subscribe</button>
</form>

<!-- PUT form via mu-method override -->
<form action="/api/user/profile" mu-method="put">
  <input type="text" name="display_name" value="Alex">
  <button type="submit">Update Profile</button>
</form>

<!-- DELETE form — no body data needed -->
<form action="/api/comment/88" mu-method="delete">
  <button type="submit">Delete Comment</button>
</form>

<!-- Warn the user before leaving with unsaved changes -->
<form action="/api/article/draft" method="post" mu-confirm-quit>
  <input type="text" name="title" placeholder="Article title">
  <textarea name="body"></textarea>
  <button type="submit">Save Draft</button>
</form>

Custom validator:

<form action="/api/post/publish" method="post" mu-validate="validatePublishForm">
  <input type="text" id="post-title" name="title" placeholder="Post title">
  <button type="submit">Publish</button>
</form>

<script>
function validatePublishForm(form) {
  // Return true to proceed, false to block the submission
  return form.querySelector('#post-title').value.trim().length >= 5;
}
</script>

POST form with patch response:

<form action="/api/comment/post" method="post" mu-mode="patch">
  <textarea name="body" placeholder="Write a comment..."></textarea>
  <button type="submit">Post Comment</button>
</form>

Server response:

<!-- Append the new comment to the thread -->
<div class="comment" mu-patch-target="#comment-thread" mu-patch-mode="append">
  <p>Great article, thanks for writing it up.</p>
</div>

<!-- Replace the form with a fresh blank version -->
<form action="/api/comment/post" method="post" mu-patch-target="#comment-form-wrapper">
  <textarea name="body" placeholder="Write a comment..."></textarea>
  <button type="submit">Post Comment</button>
</form>

6. Any element with a mu-url attribute can trigger a fetch request. The default trigger for <a> and <button> is click. For <form> it is submit. For <input>, <textarea>, and <select> it is change.

<!-- Live search: fires 400ms after the user stops typing -->
<input type="text" name="q"
       mu-trigger="change" mu-debounce="400"
       mu-url="/api/search"
       mu-target="#search-results" mu-source="#search-results"
       mu-mode="update">

<!-- Load city suggestions when the field gains focus -->
<input type="text" name="city"
       mu-trigger="focus"
       mu-url="/api/city-hints"
       mu-target="#city-suggestions" mu-mode="update">

<!-- Auto-save field content when the user tabs away -->
<input type="text" name="page-title"
       mu-trigger="blur"
       mu-url="/api/draft/autosave" mu-method="put"
       mu-target="#autosave-indicator" mu-mode="update">

<!-- Load a content block immediately when the element renders -->
<div mu-trigger="load"
     mu-url="/api/recommended-posts"
     mu-target="#recommendations" mu-mode="update">
</div>

<!-- Poll for new notifications every 8 seconds -->
<div mu-trigger="load" mu-repeat="8000"
     mu-url="/api/notifications/latest"
     mu-target="#notification-tray" mu-mode="update">
</div>

The polling interval clears automatically when the element leaves the DOM. No manual cleanup needed.

7. Set mu-method="sse" to open a persistent EventSource connection.

<!-- Open an SSE connection on page load and patch the DOM with each message -->
<div mu-trigger="load"
     mu-url="/stream/live-activity"
     mu-mode="patch"
     mu-method="sse">
</div>

The server sends HTML fragments, identical in format to patch responses:

event: message
data: <li mu-patch-target="#activity-feed" mu-patch-mode="prepend">New signup: alice@example.com</li>

event: message
data: <span mu-patch-target="#active-user-count">47</span>

µJS closes the EventSource automatically when the element is removed from the DOM.

SSE limitations to keep in mind:

  • EventSource does not support custom request headers. Pass authentication tokens as query parameters: mu-url="/stream/feed?token=abc123".
  • HTTP/1.1 browsers cap open SSE connections at six per domain. HTTP/2 removes this limit.

8. All configuration options.

  • processLinks (bool): Intercept <a> tag clicks. Default: true.
  • processForms (bool): Intercept <form> submissions. Default: true.
  • history (bool): Push the URL to browser history on each navigation. Default: true.
  • mode (string): Default injection mode for all requests. Default: "replace".
  • target (string): Default CSS selector for the DOM node to update. Default: "body".
  • source (string): Default CSS selector for the node to extract from the fetched page. Default: "body".
  • title (string): CSS selector for the element used to update the page title. Supports "selector/attribute" syntax. Default: "title".
  • scroll (bool|null): Scroll-to-top behavior after render. null means auto based on mode and context. Default: null.
  • urlPrefix (string|null): Prefix prepended to all fetched URLs. Default: null.
  • progress (bool): Show the built-in progress bar during requests. Default: true.
  • prefetch (bool): Prefetch target pages on link hover. Default: true.
  • prefetchTtl (number): Prefetch cache expiry in milliseconds. Default: 3000.
  • morph (bool): Use idiomorph or a registered custom function for DOM updates. Default: true.
  • transition (bool): Use the View Transitions API when the browser supports it. Default: true.
  • confirmQuitText (string): Text shown in the unsaved-changes confirmation dialog. Default: "Are you sure you want to leave this page?".
mu.init({
  target: "#main-content",
  source: "#main-content",
  progress: true, 
  prefetch: true,
  morph: true
});

9. All attributes accept both mu-* and data-mu-* formats.

  • mu-disabled (boolean): Opts this element out of µJS processing entirely.
  • mu-mode (string): Injection mode for this element. Overrides the global default.
  • mu-target (string): CSS selector of the DOM node to update.
  • mu-source (string): CSS selector of the node to extract from the fetched HTML.
  • mu-url (string): URL to fetch. Overrides href or action on the element.
  • mu-prefix (string): URL prefix applied to this specific request.
  • mu-title (string): Selector for the element used to update the page title.
  • mu-history (bool): Push the URL to browser history for this navigation. Overrides global setting.
  • mu-scroll (bool): Scroll to the top after rendering for this navigation.
  • mu-morph (bool): Set "false" to skip morphing for this specific element.
  • mu-transition (bool): Set "false" to skip the View Transition for this navigation.
  • mu-prefetch (bool): Set "false" to disable hover prefetch for this link.
  • mu-method (string): HTTP method for this element: get, post, put, patch, delete, or sse.
  • mu-trigger (string): Event that fires the request: click, submit, change, blur, focus, or load.
  • mu-debounce (number): Debounce delay in milliseconds before the request fires.
  • mu-repeat (number): Polling interval in milliseconds after the first load trigger.
  • mu-confirm (string): Show a browser confirmation dialog before sending the request.
  • mu-confirm-quit (boolean): Prompt the user before leaving if the form has unsaved changes.
  • mu-validate (string): Name of a global JavaScript function to call before form submission.
  • mu-patch-target (string): On patch fragments — the CSS selector of the target node in the current page.
  • mu-patch-mode (string): On patch fragments — injection mode for this fragment.
  • mu-patch-history (bool): Set "true" to push the URL to browser history after a patch. Default: "false".

10. API methods.

// Load a URL programmatically with optional config overrides.
// This bypasses the click/submit event path.
mu.load("/dashboard", { history: false, target: "#workspace", mode: "update" });

// Get the URL of the last page µJS navigated to.
// Returns null if no navigation has occurred yet.
const current = mu.getLastUrl();    // "/dashboard"

// Get the URL of the page before the current one.
const previous = mu.getPreviousUrl();  // "/home"

// Block navigation and page unload when the user has unsaved changes.
mu.setConfirmQuit(true);

// Remove the navigation block (e.g., after a successful form save).
mu.setConfirmQuit(false);

// Register a custom DOM morphing function.
// The function receives (targetNode, htmlString, options).
mu.setMorph(function(target, html, opts) {
  MyDiffLib.morph(target, html, opts);
});

11. Events.

// Fires after mu.init() completes. Not cancelable.
document.addEventListener("mu:init", function(e) {
  console.log("µJS initialized. Starting URL:", e.detail.lastUrl);
});

// Fires before any fetch request is sent.
// Call e.preventDefault() to abort the navigation.
document.addEventListener("mu:before-fetch", function(e) {
  if (e.detail.url.startsWith("/admin") && !userIsAdmin) {
    e.preventDefault();
    window.location.href = "/login";
  }
});

// Fires after the fetch, before the DOM is updated.
// Modify e.detail.html to transform the response before injection.
document.addEventListener("mu:before-render", function(e) {
  // Replace a server-side placeholder with a client-side value
  e.detail.html = e.detail.html.replace("%%BUILD%%", APP_BUILD_ID);
});

// Fires after the DOM has been updated. Use this to re-initialize
// any third-party widgets that the new content requires.
document.addEventListener("mu:after-render", function(e) {
  console.log("Rendered:", e.detail.finalUrl);
  Prism.highlightAll();
  initTooltips();
});

// Fires on network failure or a non-2xx HTTP response.
document.addEventListener("mu:fetch-error", function(e) {
  if (e.detail.status === 404) {
    document.querySelector("#main-content").innerHTML =
      "<p>That page doesn't exist.</p>";
  }
  if (e.detail.status === 500) {
    console.error("Server error at:", e.detail.url);
  }
});

12. Morphing preserves input focus, textarea content, scroll positions, video playback state, and CSS transitions across page updates. This makes µJS viable for pages with rich interactive components that a raw innerHTML swap would reset.

<script src="/path/to/dist/idiomorph.min.js"></script>
<script src="/path/to/dist/mu.min.js"></script>
<script>mu.init();</script>

13. The built-in progress bar uses the ID #mu-progress. Override it with CSS:

/* Red, slightly taller progress bar */
#mu-progress {
  background: #e63946 !important;
  height: 4px !important;
}

Alternatives

  • Turbo (Hotwire): The server-rendered SPA framework from 37signals.
  • HTMX: More powerful for complex interaction patterns, but significantly larger (~14 KB gzipped).

The post Lightweight AJAX Page Navigation Library – µJS appeared first on CSS Script.


Discover more from RSS Feeds Cloud

Subscribe to get the latest posts sent to your email.

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading