v1.6.1 · 4.0 KB gzip · zero runtime deps

HTML fragments,
not a framework.

Minu fetches server-rendered HTML and puts it where it belongs. Forms, polling, out-of-band updates, and SSE without a SPA, a router, or a build step.

npm install minu
fragment exchange live

request

<div
  minu-get="/widgets/price"
  minu-trigger="every 5s"
  minu-target="#price">
</div>

response

Loading fragment…
server-rendered no client templates
0runtime dependencies
4.0 KBminified + gzipped
18tests passing
MITopen source

The small surface

A fragment loader only needs three decisions: what to request, where to put it, and what to do when it changes.

Three moves, then get out of the way.

01

Request

One attribute sets the verb and URL. Forms serialize themselves. Extra values, headers, timeout, and credentials stay available without a config layer.

02

Swap

Point the response at a selector and choose one of eight swap modes. Extract a fragment, update a cart out of band, or do nothing and only fire events.

03

React

Listen with normal DOM events. Poll, reveal, debounce, cancel, and stream with SSE, all through the same target and swap pipeline.

Reference

Every attribute, event, and method.

Start with install, then scan the request attributes or jump straight to the event and method tables.

Install

npm

npm install minu
const minu = require('minu');   // CommonJS
// or: import minu from 'minu'; // ESM
minu.init();

CDN

<script src="https://cdn.jsdelivr.net/npm/minu@1.6.1/dist/minu.min.js"
        integrity="sha384-bvb7Fc/vjU8Hm+hZrH6DCXG2zwSdtAR52XhVqqXL0xGLeY8xcpKmijAxMAzcFVhP"
        crossorigin="anonymous"></script>
<script>minu.init();</script>

Also available from unpkg. Pin a version in production.

ESM from CDN

import minu from 'https://cdn.jsdelivr.net/npm/minu@1.6.1/dist/minu.esm.mjs';
minu.init();

Secure build

The default build strips scripts, inline handlers, and javascript: URLs. It is not an XSS boundary. For untrusted HTML, use DOMPurify and the secure entry.

npm install minu dompurify
import minu from 'minu/secure';
minu.init();
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/minu@1.6.1/dist/minu.secure.min.js"></script>
<script>minu.init();</script>

Initialize

Call minu.init() once after the DOM is ready. It finds elements matching the default selector and binds their triggers. Repeated calls do not double-bind.

minu.init();
// or a narrower scope:
minu.init('#account [minu-get]');

The default selector is:

[minu-get],[minu-post],[minu-put],[minu-patch],[minu-delete],[minu-sse]

The DOM can change after init. Call minu.init(selector) for new elements, or call minu.init() again.

Requests

One verb attribute selects the request. If several are present, the first in this order wins: get, post, put, patch, delete.

AttributeBehavior
minu-getGET the URL.
minu-postPOST the URL.
minu-putPUT the URL.
minu-patchPATCH the URL.
minu-deleteDELETE the URL.
minu-triggerEvent or trigger expression. Default load.
minu-targetCSS selector for the swap target. this means the trigger element.
minu-swapSwap mode. Default innerHTML.
minu-valsJSON object merged into request values.
minu-includeSelector or selectors of extra fields/forms to serialize.
minu-headersJSON object merged into request headers.
minu-credentialsomit, same-origin, or include.
minu-timeoutTimeout in milliseconds. Overrides config.timeout.
minu-confirmText shown through window.confirm before sending.
minu-preventSet to false to allow the native click or submit.
minu-selectExtract one CSS-selected fragment from the response.

Every request sends X-Minu-Request: true. Server code can use that to return a fragment instead of a full page.

<button minu-post="/cart/add"
        minu-trigger="click"
        minu-target="#cart"
        minu-swap="innerHTML"
        minu-confirm="Add this item?">
    Add to cart
</button>

Targets & swaps

Target resolution runs in this order:

  1. minu-target, if present.
  2. A [minu-content] inside the trigger element.
  3. The single [minu-content] on the page.
  4. The trigger element itself.
minu-swapResult
innerHTML (default)Replace the target's children.
outerHTMLReplace the target itself.
beforebeginInsert before the target.
afterbeginInsert as the first child.
beforeendInsert as the last child.
afterendInsert after the target.
deleteRemove the target. The response body is ignored.
noneFetch, fire events, do not swap.

A missing explicit target falls back through the resolution order. If you need a hard failure, validate the target selector in your own minu:beforeRequest listener.

<div id="list"></div>
<button minu-get="/items/next"
        minu-trigger="click"
        minu-target="#list"
        minu-swap="beforeend">
    Load one more
</button>

Forms & values

When the trigger is inside a form, or is the form, Minu serializes the form automatically. Files produce multipart/form-data; the rest are application/x-www-form-urlencoded. GET values are appended to the query string.

<form minu-post="/contact" minu-trigger="submit">
    <input name="name" required>
    <input name="email" type="email" required>
    <textarea name="message"></textarea>
    <button type="submit">Send</button>
</form>

minu-vals adds values that are not in the form. Nested objects are stringified.

<button minu-post="/save"
        minu-vals='{"source":"demo","nested":{"level":2}}'
        minu-headers='{"X-Demo-Token":"abc"}'>
    Save
</button>

minu-include pulls in other controls or forms by selector. It accepts a comma-separated CSS selector.

<input id="search" name="q" value="coffee">
<button minu-get="/search"
        minu-include="#search"
        minu-target="#results">
    Search
</button>

Triggers

minu-trigger is a DOM event name plus optional modifiers. The first token is the event. The remaining tokens configure it.

TokenMeaning
loadRun on init. This is the default.
click, submit, change, keyup, …Any DOM event.
every 5sPoll. Skips ticks while the document is hidden.
revealedLoad once when the element enters the viewport.
intersectLoad each time the element enters the viewport.
onceRemove the listener after the first event.
changedSkip events where the field value did not change.
delay:300msDebounce.
throttle:500msThrottle.
from:#otherListen on another element.
<input minu-get="/search"
       minu-trigger="keyup changed delay:300ms"
       minu-target="#results">
<div id="results"></div>

every 5s does not queue. A new tick aborts the in-flight request for that element and starts another. The last response wins. The first tick runs after the interval, not immediately.

There is no key-filter grammar such as “Enter only.” Bind your own keydown listener and call minu.reloadComponents('#search') when the key matches.

revealed and intersect use IntersectionObserver. Browsers without it load immediately. destroy() disconnects observers and clears intervals.

UI state

AttributeBehavior
minu-contentDefault target inside the trigger element.
minu-loaderLegacy loader. Display is toggled during the request.
minu-indicatorSelector or selectors receiving minu-requesting.
minu-disableSelector or selectors disabled during the request. self and this work.

Prefer minu-indicator with CSS. It does not touch layout properties.

.spinner { display: none; }
.spinner.minu-requesting { display: inline-flex; }
<button minu-post="/save"
        minu-indicator="#spinner"
        minu-disable="#save">
    Save
</button>
<span class="spinner" id="spinner">Saving…</span>

Overlapping requests share refcounted state. A superseded request releases its own loader, indicator, and disabled-state claims; it does not release the request that replaced it.

Events

All Minu events bubble, are composed, and carry detail. minu:beforeRequest is cancelable.

EventWhenDetail
minu:contentSetupElement initialized.{ url, trigger }
minu:beforeRequestBefore the fetch. Cancelable.{ url, method, element }
minu:contentLoadedResponse parsed and swapped.{ html, url, method }
minu:contentErrorNetwork, HTTP, or timeout failure.{ error, url }
minu:afterRequestAfter cleanup. Not fired when beforeRequest cancels.{ url, method, ok }
minu:sseOpenSSE connection opens.{ url }
minu:sseMessageSSE message received and processed.{ data, event, url }
minu:sseErrorSSE connection error.{ error, url }
minu:trigger:<name>From a Minu-Trigger response header.{ name, url }
document.addEventListener('minu:beforeRequest', (event) => {
    if (!confirm('Send?')) event.preventDefault();
});

document.addEventListener('minu:contentLoaded', (event) => {
    console.log(event.detail.url, event.detail.method);
});

A request that is aborted by a newer request does not fire minu:afterRequest. Timeouts and HTTP errors do.

Methods

MethodDescription
minu.init(selector?)Initialize matching elements. Idempotent. Default selector covers all verb attributes and minu-sse.
minu.reloadComponents(selector | string[])Re-fetch every matching element that has a URL.
minu.reloadComponent(...)Deprecated alias for reloadComponents.
minu.config({ headers, timeout, credentials })Merge global request defaults. Per-element headers override global headers.
minu.destroy()Abort in-flight requests, remove listeners, clear timers and observers, close SSE streams, restore disabled controls.
minu.closeStream(element?)Close one SSE stream, or all streams when omitted.
minu.config({
    headers: {
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
    },
    timeout: 10000,
    credentials: 'same-origin',
});

minu.reloadComponents(['#price', '#cart']);
minu.closeStream(document.querySelector('[minu-sse]'));
minu.destroy();

Server protocol

Response headerEffect
Minu-Trigger: refreshDispatch minu:trigger:refresh on the requestor. Comma-separated names are allowed.
Minu-Redirect: /loginNavigate with location.assign.
Minu-Refresh: trueReload the page.

Headers are applied before the body swap. 204 and 205 responses skip the swap.

Out-of-band updates

An OOB element is applied to another selector and removed from the main payload. The default mode is outerHTML.

<!-- response -->
<div id="cart" minu-oob="#cart">3 items</div>
<div class="toast" minu-oob="beforeend:#toasts">Saved</div>
<p>Main panel content.</p>

minu-oob="#cart" replaces the target. Use mode:selector for another mode, for example beforeend:#toasts or afterbegin:#list. minu-oob="true" targets the element's own id.

An OOB-only response updates its targets and leaves the main target untouched. The response is sanitized before OOB elements are applied; the secure build preserves Minu protocol attributes through DOMPurify.

Server-sent events

<div minu-sse="/events/prices"
     minu-sse-event="price"
     minu-target="#price"
     minu-swap="innerHTML"></div>

<div id="price">$0.00</div>
<div id="ticker">waiting…</div>
event: price
data: <span class="price">$42.00</span><div id="ticker" minu-oob="#ticker">42 ms</div>
AttributeBehavior
minu-sseURL opened with native EventSource on init.
minu-sse-eventComma-separated event names. Default message.
minu-target, minu-swapSame target and swap pipeline as requests.

EventSource reconnects automatically. retry: and id: work as usual. Cross-origin streams need CORS. Disable proxy buffering with X-Accel-Buffering: no.

Chat sends use normal minu-post; only the receive side is a stream. WebSocket support is not included.

JSON boundary

JSON is supported for attribute configuration, not as a wire format.

UseSupportedNotes
minu-valsYesParsed with JSON.parse, merged into form values. Nested objects are stringified.
minu-headersYesParsed with JSON.parse, merged into request headers.
JSON request bodyNoBodies are form-encoded or multipart.
JSON response renderingNoResponses are read as text and treated as HTML fragments.

For a JSON API, render HTML on the server from the JSON, or use native fetch plus your own renderer.

Security

minu.js strips <script> and <style> blocks, inline event handlers, and javascript: URLs. Regex sanitization is not an XSS boundary.

For untrusted HTML, use minu/secure with DOMPurify. The secure build throws at load if DOMPurify is missing. It also allows Minu's protocol attributes through DOMPurify, because OOB and nested triggers are part of the response format.

The default build is appropriate when the server is trusted. The secure build is the production path for content you do not control.

TypeScript

Types are included. Minu augments DocumentEventMap, HTMLElementEventMap, and WindowEventMap, so event.detail is inferred.

import minu from 'minu';

minu.init();

document.addEventListener('minu:contentLoaded', (event) => {
    const html: string = event.detail.html;
    console.log(html);
});

Examples

The runnable gallery covers twelve scenarios: basic loading, forms, tabs, search, polling, lazy reveal, swap modes, OOB, response headers, events, JSON, and SSE.

Need the source repository, issues, or contribution rules? GitHub · Contributing · Security policy