Request
One attribute sets the verb and URL. Forms serialize themselves. Extra values, headers, timeout, and credentials stay available without a config layer.
v1.6.1 · 4.0 KB gzip · zero runtime deps
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
request
<div
minu-get="/widgets/price"
minu-trigger="every 5s"
minu-target="#price">
</div>
response
The small surface
A fragment loader only needs three decisions: what to request, where to put it, and what to do when it changes.
One attribute sets the verb and URL. Forms serialize themselves. Extra values, headers, timeout, and credentials stay available without a config layer.
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.
Listen with normal DOM events. Poll, reveal, debounce, cancel, and stream with SSE, all through the same target and swap pipeline.
Reference
Start with install, then scan the request attributes or jump straight to the event and method tables.
No matching sections. Clear the filter to see the whole reference.
npm install minu
const minu = require('minu'); // CommonJS
// or: import minu from 'minu'; // ESM
minu.init();
<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.
import minu from 'https://cdn.jsdelivr.net/npm/minu@1.6.1/dist/minu.esm.mjs';
minu.init();
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>
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.
One verb attribute selects the request. If several are present, the
first in this order wins: get, post,
put, patch, delete.
| Attribute | Behavior |
|---|---|
minu-get | GET the URL. |
minu-post | POST the URL. |
minu-put | PUT the URL. |
minu-patch | PATCH the URL. |
minu-delete | DELETE the URL. |
minu-trigger | Event or trigger expression. Default load. |
minu-target | CSS selector for the swap target. this means the trigger element. |
minu-swap | Swap mode. Default innerHTML. |
minu-vals | JSON object merged into request values. |
minu-include | Selector or selectors of extra fields/forms to serialize. |
minu-headers | JSON object merged into request headers. |
minu-credentials | omit, same-origin, or include. |
minu-timeout | Timeout in milliseconds. Overrides config.timeout. |
minu-confirm | Text shown through window.confirm before sending. |
minu-prevent | Set to false to allow the native click or submit. |
minu-select | Extract 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>
Target resolution runs in this order:
minu-target, if present.[minu-content] inside the trigger element.[minu-content] on the page.minu-swap | Result |
|---|---|
innerHTML (default) | Replace the target's children. |
outerHTML | Replace the target itself. |
beforebegin | Insert before the target. |
afterbegin | Insert as the first child. |
beforeend | Insert as the last child. |
afterend | Insert after the target. |
delete | Remove the target. The response body is ignored. |
none | Fetch, 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>
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>
minu-trigger is a DOM event name plus optional modifiers.
The first token is the event. The remaining tokens configure it.
| Token | Meaning |
|---|---|
load | Run on init. This is the default. |
click, submit, change, keyup, … | Any DOM event. |
every 5s | Poll. Skips ticks while the document is hidden. |
revealed | Load once when the element enters the viewport. |
intersect | Load each time the element enters the viewport. |
once | Remove the listener after the first event. |
changed | Skip events where the field value did not change. |
delay:300ms | Debounce. |
throttle:500ms | Throttle. |
from:#other | Listen 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.
| Attribute | Behavior |
|---|---|
minu-content | Default target inside the trigger element. |
minu-loader | Legacy loader. Display is toggled during the request. |
minu-indicator | Selector or selectors receiving minu-requesting. |
minu-disable | Selector 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.
All Minu events bubble, are composed, and carry detail.
minu:beforeRequest is cancelable.
| Event | When | Detail |
|---|---|---|
minu:contentSetup | Element initialized. | { url, trigger } |
minu:beforeRequest | Before the fetch. Cancelable. | { url, method, element } |
minu:contentLoaded | Response parsed and swapped. | { html, url, method } |
minu:contentError | Network, HTTP, or timeout failure. | { error, url } |
minu:afterRequest | After cleanup. Not fired when beforeRequest cancels. | { url, method, ok } |
minu:sseOpen | SSE connection opens. | { url } |
minu:sseMessage | SSE message received and processed. | { data, event, url } |
minu:sseError | SSE 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.
| Method | Description |
|---|---|
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();
| Response header | Effect |
|---|---|
Minu-Trigger: refresh | Dispatch minu:trigger:refresh on the requestor. Comma-separated names are allowed. |
Minu-Redirect: /login | Navigate with location.assign. |
Minu-Refresh: true | Reload the page. |
Headers are applied before the body swap. 204 and
205 responses skip the swap.
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.
<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>
| Attribute | Behavior |
|---|---|
minu-sse | URL opened with native EventSource on init. |
minu-sse-event | Comma-separated event names. Default message. |
minu-target, minu-swap | Same 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 is supported for attribute configuration, not as a wire format.
| Use | Supported | Notes |
|---|---|---|
minu-vals | Yes | Parsed with JSON.parse, merged into form values. Nested objects are stringified. |
minu-headers | Yes | Parsed with JSON.parse, merged into request headers. |
| JSON request body | No | Bodies are form-encoded or multipart. |
| JSON response rendering | No | Responses 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.
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.
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);
});
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