Skip to content

Latest commit

 

History

History
311 lines (236 loc) · 15.2 KB

File metadata and controls

311 lines (236 loc) · 15.2 KB

CLAUDE.md — µJS project context

Project overview

µJS (muJS) is a lightweight JavaScript library for AJAX navigation, similar to pjax, Turbo (Hotwire) and HTMX. It intercepts link clicks and form submissions to load pages via fetch() and inject content into the current page without full browser reload.

Repository structure

mujs/
├── lib/
│   └── mu.js          # Source file (single file, fully documented)
├── dist/
│   └── mu.min.js      # Minified version (generated by make)
├── Makefile           # Build: minify with terser
├── package.json       # npm package definition
├── README.md          # User-facing documentation
├── LICENSE            # MIT license
└── CLAUDE.md          # This file

Coding conventions

These conventions come from the original Vik codebase and MUST be followed strictly:

  • Indentation: tabs (not spaces)
  • Return style: return (value); with parentheses
  • Private members: prefixed with _ (e.g. this._cfg, mu._morph)
  • Section separators: /* ********** SECTION NAME ********** */
  • Documentation: JSDoc on every object, method, function and variable
  • No ES6+ syntax: no arrow functions, no let/const, no template literals, no destructuring. Use var, function(){}, string concatenation. The code targets broad browser compatibility without transpilation.
  • Global variable: the library exposes a single var mu global
  • Architecture: single new function() pattern (same as Vik)
  • Self-reference: methods use mu. (not this.) to avoid context issues in callbacks
  • No dependencies: zero runtime dependencies. Idiomorph is optional and auto-detected.
  • No build system: no webpack, rollup, vite. Just make + terser for minification.

Architecture decisions

Event delegation

µJS uses a single click listener and a single submit listener on document for click/submit triggers. No per-element binding for those, no re-binding after page load. For other triggers (change, blur, focus, load), per-element listeners are attached by _initTriggers() after each render. This replaces Vik's approach of iterating over all links/forms and adding onclick/onsubmit attributes, then re-running init() after each page load.

URL resolution (_resolveUrl)

_resolveUrl(url) resolves any URL to a local path using new URL(url, document.baseURI). Returns the pathname+search+hash for same-origin URLs, or null for external/invalid/hash-only URLs. Uses document.baseURI (not window.location.href) so that the <base> tag is respected when present. Note: only the <base> tag from the initially loaded page is used; <base> tags in dynamically fetched pages are not applied.

Link filtering (_shouldProcess)

µJS skips elements that should not be intercepted. _shouldProcess() returns false for:

  • mu-disabled or data-mu-disabled attribute present
  • mu="false" or data-mu="false" attribute
  • target attribute present (e.g. target="_blank")
  • download attribute present (file downloads)
  • <a> with onclick attribute
  • <form> with onsubmit attribute
  • External URLs (resolved via _resolveUrl)

Modifier keys (ctrl, meta, shift, alt) on click are also ignored, allowing native browser behavior (open in new tab, etc.).

Attributes: mu-* with data-mu-* fallback

All HTML attributes use the mu- prefix (e.g. mu-mode, mu-target). The data-mu- variant is also supported for W3C validation. The helper function _attr(el, name) checks both.

Fetch API

Uses native fetch() with AbortController for request cancellation. Replaces Vik's XMLHttpRequest. In-flight requests are automatically aborted when a new navigation starts.

No callbacks in config

Vik had preCallback/postCallback in config + data-vik-pre-callback attributes + vik:preLoad/vik:postLoad events (3 redundant mechanisms). µJS uses only CustomEvent on document. Events: mu:init, mu:before-fetch, mu:before-render, mu:after-render, mu:fetch-error.

Removed utilities

  • overloadObject() → replaced by Object.assign()
  • onDocumentReady() → unnecessary (event delegation)
  • fetchHttp() → replaced by native fetch()
  • execCallback() / createMergedArray() → removed (no more callback config)

Injection modes (mu-mode)

The attribute mu-mode controls how content is injected. Default: replace.

Mode DOM operation
replace target.replaceWith(source) (default)
update target.innerHTML = source.innerHTML
prepend target.prepend(source)
append target.append(source)
before target.before(source)
after target.after(source)
remove target.remove()
none no DOM change
patch multi-fragment mode (see below)

Naming rationale: replace/update/prepend/append/before/after/remove from Turbo Streams. The attribute name mu-mode was chosen over mu-swap (HTMX), strategy (Vik), action (Turbo) and method (too close to HTTP method).

replace is the default because it's the fastest (single DOM operation) and the most predictable behavior.

Patch mode

Patch mode (mu-mode="patch") allows a single response to update multiple DOM targets. The server returns HTML where each fragment has a mu-patch-target attribute (CSS selector) and an optional mu-patch-mode attribute.

Key design decisions:

  • No special <mu-stream> custom element. Fragments are regular HTML elements.
  • The node carrying mu-patch-target IS the element that gets injected (not its children).
  • mu-patch-* attributes are NOT removed after injection (useful for debugging).
  • Default patch mode is replace.
  • Patch does not modify browser history by default (mu-patch-history defaults to false). Use mu-patch-history="true" to enable history.
  • Patch does not scroll to top.
  • Page title, CSS, scripts can be updated via patch by targeting title, head, etc.

Morphing

DOM morphing (preserving focus, scroll, video state during replace/update) is:

  • Enabled by default in config (morph: true)
  • Auto-detected: init() checks for window.Idiomorph.morph
  • Fallback: if no morph lib is loaded, falls back to direct DOM replacement silently
  • Overridable: mu.setMorph(fn) for custom morph libraries
  • Disablable: morph: false in config or mu-morph="false" per-element
  • Morphing only applies to replace and update modes (not append/prepend/before/after)

View Transitions

  • Enabled by default in config (transition: true)
  • Uses document.startViewTransition() when available
  • Falls back silently on unsupported browsers
  • Disablable: transition: false in config or mu-transition="false" per-element

Scripts

  • <head> scripts: merged additively by _mergeHead(). Already-present scripts (matched by _elKey) are not re-added. New scripts are added and executed once.
  • <body> scripts: re-executed by _runScripts() on each navigation. External scripts (with src) are tracked in _jsIncludes and loaded only once. Inline scripts are re-executed every time.
  • mu-disabled on a <script> tag prevents _runScripts from re-executing it
  • Third-party analytics (Plausible, GA) in <head> are never re-executed — they detect SPA navigations via their own pushState interception

Forms

  • HTML5 validation via reportValidity() before any fetch
  • Optional custom validation via mu-validate="functionName"
  • GET forms: data serialized as query string, behaves like a link
  • Non-GET forms (POST, PUT, PATCH, DELETE): data sent as URLSearchParams (application/x-www-form-urlencoded) by default, or FormData (multipart/form-data) if the form has enctype="multipart/form-data". History disabled by default
  • mu-method attribute overrides the form's method attribute (supports put, patch, delete)
  • Submit button name/value: included in form data via e.submitter (modern browsers) with fallback to mu._submitter (tracked via click event delegation) for older browsers
  • Quit-page confirmation via mu-confirm-quit attribute on <form> (uses input event delegation)

History & Scroll

mu-history controls whether the URL is added to browser history. mu-scroll controls whether the page scrolls to top (independent from mu-history).

Defaults depend on mode and context:

  • Modes replace/update + GET link/form: history=true, scroll=true
  • Modes replace/update + POST/PUT/PATCH/DELETE form: history=false, scroll=true
  • Modes replace/update + triggers (change/blur/focus/load): history=false, scroll=false
  • Modes append/prepend/before/after/remove/none: history=false, scroll=false
  • Patch mode: history=false, scroll=false (override with mu-patch-history="true")
  • Redirections always add the URL to browser history
  • mu-history="false" on links/forms, or history: false in config

HTTP methods (mu-method)

  • mu-method attribute sets the HTTP method: get, post, put, patch, delete, sse
  • If absent on a link/button: defaults to get
  • If absent on a form: uses the form's method attribute (default get)
  • Non-GET methods send X-Mu-Method header
  • For forms, non-GET methods send FormData as body (same as POST)
  • For non-form elements (buttons, divs), non-GET methods send an empty body
  • sse is a special value that opens an EventSource instead of using fetch

Prefetch

  • Enabled by default (prefetch: true)
  • GET only: elements with mu-method other than get are never prefetched
  • Triggered on mouseover (event delegation), with 50ms delay to filter accidental hover-throughs
  • One entry per URL in _prefetchCache (Map)
  • Cache is consumed and deleted on click (no persistent cache)
  • Low-priority fetch (priority: "low")
  • Disablable: prefetch: false in config or mu-prefetch="false" per-link

HTTP headers sent by µJS

  • X-Requested-With: XMLHttpRequest — identifies AJAX requests (de facto standard, compatible with Rails, Laravel, Django, etc.)
  • X-Mu-Mode: <mode> — current injection mode
  • X-Mu-Method: <METHOD> — HTTP method, sent for non-GET requests (POST, PUT, PATCH, DELETE)
  • X-Mu-Prefetch: 1 — sent on prefetch requests (server can return lighter content)

Progress bar

  • Thin bar (3px) at top of page, id mu-progress
  • Styled inline (no external CSS needed), customizable via CSS
  • Animates to 70% during fetch, 100% on completion, then resets
  • Disablable: progress: false in config

Versioning

Even minor version numbers (1.2, 1.4, 1.6…) indicate stable releases. Odd minor versions (1.3, 1.5…) are reserved for development/unstable.

Build

  • make or make dist — minify with terser
  • make size — show file sizes (source, minified, gzipped)
  • make hash — compute SRI hash (sha384) of dist/mu.min.js
  • make check — dry-run npm pack
  • make publish — minify + npm publish
  • make clean — remove dist/

Scroll restoration

Implemented. Before each navigation, _saveScroll() stores scrollX/scrollY in the current history.state via replaceState. On popstate (back/forward), the stored position is restored after rendering instead of scrolling to top.

Triggers (mu-trigger)

Implemented in v1.2. Allows any element with mu-url to trigger a fetch on events beyond click/submit. Supports debounce and polling.

New attributes

Attribute Values Description
mu-method get, post, put, patch, delete, sse HTTP method
mu-trigger click, submit, change, blur, focus, load Event trigger
mu-repeat number (ms) Polling interval
mu-debounce number (ms) Debounce delay

Default triggers (when mu-trigger is absent)

Element Default trigger
<a> click
<form> submit
<input>, <textarea>, <select> change
Any other element click

Trigger mapping to browser events

Trigger Browser event(s)
click click (event delegation)
submit submit (event delegation)
change input
blur change + blur (deduplicated via timestamp)
focus focus
load fires immediately when rendered

Key functions

  • _getTrigger(el) — returns effective trigger for an element
  • _debounce(fn, delay) — classic debounce utility
  • _triggerAction(el) — common action handler for non-delegation triggers
  • _initTriggers(container) — scans and binds per-element listeners
  • _cleanupTriggers(container) — clears intervals and SSE before DOM replacement

Design decisions

  • click and submit remain handled by event delegation (no per-element binding)
  • Other triggers (change, blur, focus, load) use per-element listeners attached by _initTriggers(), called after each render
  • Elements marked with _mu_bound = true to prevent duplicate binding
  • Polling via setInterval, stored as el._mu_interval, cleaned up by _cleanupTriggers
  • Non-click/submit triggers default to history=false, scroll=false

Server-Sent Events (SSE)

Implemented in v1.2 via mu-method="sse". Opens an EventSource connection. Incoming messages are parsed as HTML and rendered via _renderPatch or _renderPage.

Key function

  • _openSSE(url, el, cfg) — creates EventSource, handles messages and errors

Design decisions

  • Each element with mu-method="sse" gets its own EventSource connection
  • Connection stored as el._mu_sse, closed by _cleanupTriggers
  • EventSource does not support custom headers — documented limitation
  • Browser limit: ~6 SSE connections per domain in HTTP/1.1
  • WebSocket support not implemented (SSE covers most real-time use cases)

TODO / future improvements

  • Publish to npm — published as @digicreon/mujs
  • Create mujs.org website — live at https://mujs.org
  • Document how to install and use idiomorph with µJS (CDN, npm, local file) on mujs.org − live at https://mujs.org
  • mu-trigger attribute — implemented in v1.2
  • SSE integration for server-pushed patches — implemented in v1.2
  • Consider: mu-indicator attribute (see details below)
  • Consider: WebSocket integration (complement to SSE)
  • Consider: revealed trigger via IntersectionObserver (infinite scroll)
  • Write unit tests
  • Write integration tests with a simple PHP backend

mu-indicator (not yet implemented)

Per-element loading indicator. The global progress bar (#mu-progress) already covers most cases. mu-indicator would add per-element feedback for pages with multiple independently-loadable zones.

Possible syntax:

<a href="/page" mu-indicator="#spinner">
    Load
    <span id="spinner" style="display:none"></span>
</a>

Behavior: µJS shows the indicator (display="") on fetch start, hides it (display="none") after render. Simple to implement but low priority — the global progress bar covers 90% of use cases.