Primitives
pagelove/primitives.mjs is the low-level HTTP client that pagelove.mjs uses internally. Most apps do not touch it directly.
When to reach for it
- Tooling that talks to Pagelove without rendering a page.
- Scripts that issue requests outside the declarative binding flow.
- Custom integrations where full control over the request lifecycle is needed.
For everything else, use the Pagelove class and the declarative flow on top.
Loading the library
<script type="module">
import { PLDocument, PLElement } from 'https://pagelove.github.io/beta-js/pagelove/primitives.mjs';
</script>
The module exports two classes: PLDocument and PLElement.
The PLDocument class
A PLDocument represents a Pagelove resource URL and the live DOM associated with it.
Constructor
| Form | Bound document |
|---|---|
new PLDocument() |
window.location.href — bound to window.document |
new PLDocument(url) |
The given URL. If it matches window.location.href, the live document is bound; otherwise the document is fetched and parsed lazily. |
Methods
| Method | Description |
|---|---|
OPTIONS() |
Issues an OPTIONS request with Accept: multipart/mixed, parses the multipart response, and dispatches a PLCapability event on every node matching a declared selector. |
createElement(element) |
Returns a new PLElement wrapping the given DOM node. |
req(method, ...opts) |
Returns a bare Request targeting the document URL. |
Properties
| Property | Type | Description |
|---|---|---|
url |
string |
The URL this document is bound to. |
document |
Promise<Document> |
Resolves to the bound DOM document. Fetches and parses the HTML on first access if no live document is bound. |
A PLDocument bound to a live Document stores a WeakRef to itself on document.pagelove and installs listeners for PLCapability and PLMethodCompleted.
The PLElement class
A PLElement wraps a single DOM node and knows how to issue HTTP requests scoped to that node.
Constructor
| Form | Effect |
|---|---|
new PLElement(url) |
Creates an element bound to the URL with no DOM node attached yet. |
new PLElement(url, element) |
Binds to the given DOM node. The element's ownerDocument becomes the associated document. |
Methods
| Method | Description |
|---|---|
GET() |
Fetches the element fragment, parses the response body as a single HTML node, and returns the new node. |
PUT(body?) |
Replaces the element on the server. Body defaults to element.outerHTML; a Node body is serialized via outerHTML. Returns the raw Response. |
POST(body) |
Posts a child fragment. The response body is parsed as a single HTML node and appended as a child of the wrapped element (unless the supplied body was already a live attached node). Throws if no body is supplied. |
DELETE() |
Deletes the element on the server and removes it from the DOM on success. |
req(method, opts) |
Builds the underlying Request object — sets the Range header, conditionally sets If-Match, and merges the caller's opts. |
Properties
| Property | Type | Description |
|---|---|---|
url |
string |
The document URL the element is scoped against. |
element |
Element |
The wrapped DOM node. Setting it also sets document from the node's ownerDocument. |
document |
Document |
The DOM document the wrapped element belongs to. |
selector |
string |
A stable CSS selector generated from the element. Prefers id, anchors to the nearest ancestor with an id, and falls back through itemprop, class names, role, and :nth-child(...). |
OPTIONS lives on PLDocument, not PLElement — discovery is document-level.
Attached methods on DOM elements
PLDocument listens for PLCapability events on its bound document. The event payload looks like:
{
selector: '<css selector>',
allow: ['GET', 'PUT', 'POST', 'DELETE']
}
For each method named in allow, the document constructs a PLElement for the event target and attaches the matching method directly to the DOM node as a non-writable, configurable property:
Object.defineProperty(target, 'PUT', { value: plElement.PUT.bind(plElement), ... });
After the flow has run, element.GET(), element.PUT(body), element.POST(body), and element.DELETE() are callable on the DOM node itself with no further imports. Only methods the server has authorized appear.
ETag handling
ETags are loaded lazily, only for elements that need them.
- When a
PLCapabilityevent arrives for an element with anidandetag === undefined,PLDocumentregisters the element with a sharedIntersectionObserver(rootMargin: '200px'). - When the element scrolls into view, the observer issues a
HEADrequest withRange: selector=<element selector>and stashes the response'sETagheader on the element aselement.etag. - Successful method requests also update
element.etagfrom the response'sETagheader. After aPOST, the parent's previous ETag is restored and the response ETag is assigned to the new child instead.
Request format
Every request a PLElement issues is built by req(method, opts):
| Header | Value | When |
|---|---|---|
Range |
selector=<generated-selector> |
Always |
If-Match |
<element.etag> |
When element.etag is a string and the method is not POST |
Everything else is a standard fetch() Request. Each request also dispatches PLMethodStarted before sending and PLMethodCompleted after the response arrives. Both events bubble and carry { method, selector }; PLMethodCompleted additionally carries response.
Examples
Calling an attached method
<script type="module">
import { PLDocument } from 'https://pagelove.github.io/beta-js/pagelove/primitives.mjs';
const doc = new PLDocument();
await doc.OPTIONS();
document.addEventListener('click', async (event) => {
const card = event.target.closest('article.note');
if (card && typeof card.DELETE === 'function') {
await card.DELETE();
}
});
</script>
Once OPTIONS() has run, any matching <article class="note"> exposes the methods the server allows.
Manual PLDocument and PLElement
<script type="module">
import { PLDocument } from 'https://pagelove.github.io/beta-js/pagelove/primitives.mjs';
const doc = new PLDocument('/notes.html');
const target = document.getElementById('note-42');
const note = await doc.createElement(target);
await note.PUT('<article id="note-42">Edited</article>');
</script>
The wrapper is constructed by hand and no capability discovery is required. Range: selector=#note-42 is generated from the element's id; If-Match is added automatically once note.element.etag is populated.
See also
- The Pagelove class — the high-level API that sits on top of primitives.
OPTIONSmethod — the multipart capability discovery mechanismOPTIONS()consumes.PUTmethod — theRange: selector=...andIf-Matchheaders in detail.