htmt 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ # Zero-Clause BSD
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for
4
+ any purpose with or without fee is hereby granted.
5
+
6
+ THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
7
+ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
8
+ OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
9
+ FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
10
+ DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
11
+ AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
12
+ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # HyperText Markup Targets (HTMT)
2
+
3
+ **HyperText Markup Targets (HTMT)** brings dynamic partial-page loading to plain HTML. Add a `target` attribute to links and forms, and HTMT handles the rest—no build tools, no framework, just declarative markup.
4
+
5
+ It's built on web standards: regular `<a>` and `<form>` elements, standard HTML attributes, and just enough JavaScript to orchestrate the behavior. Your HTML remains semantic, searchable, and resilient.
6
+
7
+ **Tiny footprint:** 794 bytes gzipped, 686 bytes minified + gzipped.
8
+
9
+ ## Quick Start
10
+
11
+ 1. Load the HTMT script as an ES module and initialize it on your document:
12
+
13
+ ```html
14
+ <script type="module">
15
+ import { htmt } from "./htmt.js";
16
+ htmt(document.body, true);
17
+ </script>
18
+ ```
19
+
20
+ 2. Add `target` attributes to your links and forms:
21
+
22
+ ```html
23
+ <span id="results">Loading...</span>
24
+
25
+ <a href="/search?q=example" target="results"> Search </a>
26
+ ```
27
+
28
+ 3. Serve partial HTML responses with a matching target element:
29
+
30
+ ```html
31
+ <span id="results"> Found 42 results for "example" </span>
32
+ ```
33
+
34
+ That's it. HTMT loads the response into a hidden iframe and swaps the target element.
35
+
36
+ ## Usage
37
+
38
+ ### Setup Parameters
39
+
40
+ ```javascript
41
+ htmt(root, install);
42
+ ```
43
+
44
+ - `root` – the DOM subtree to scan for enhanced elements. Usually `document.body`.
45
+ - `install` – when `true`, automatically wires up global `click` and `submit` handlers to track interactions.
46
+
47
+ ### Targeted Links
48
+
49
+ Link with a `target` attribute to swap an element:
50
+
51
+ ```html
52
+ <span id="results">Click to load</span>
53
+
54
+ <a href="/api/search" target="results"> Load Results </a>
55
+ ```
56
+
57
+ Server response:
58
+
59
+ ```html
60
+ <span id="results"> Here are your search results. </span>
61
+ ```
62
+
63
+ HTMT creates a hidden iframe, loads the response there, and replaces the target element in the main document.
64
+
65
+ ### Forms
66
+
67
+ The same pattern works with forms (GET or POST):
68
+
69
+ ```html
70
+ <form method="post" action="/api/subscribe" target="message">
71
+ <input type="email" name="email" />
72
+ <button>Subscribe</button>
73
+ <span id="message"></span>
74
+ </form>
75
+ ```
76
+
77
+ Server response:
78
+
79
+ ```html
80
+ <span id="message"> Thanks for subscribing! </span>
81
+ ```
82
+
83
+ ### Controlling Multiple Requests
84
+
85
+ Use the optional `frame` attribute to name the iframe. Multiple links/forms with the same frame name will share a single iframe—newer requests automatically cancel pending ones:
86
+
87
+ ```html
88
+ <a href="/api/page-1" target="results" frame="pages">Page 1</a>
89
+ <a href="/api/page-2" target="results" frame="pages">Page 2</a>
90
+
91
+ <div id="results"></div>
92
+ ```
93
+
94
+ If `frame` is omitted, HTMT uses the `target` value as the frame name.
95
+
96
+ ### Executing Scripts in the Main Document
97
+
98
+ Wrap scripts in a `<template>` in the response `<head>` to delay execution until after the content is inserted into the main page:
99
+
100
+ ```html
101
+ <head>
102
+ <template>
103
+ <span id="results">
104
+ Content with
105
+ <script>
106
+ console.log("Running in main frame");
107
+ </script>
108
+ </span>
109
+ </template>
110
+ </head>
111
+ ```
112
+
113
+ HTMT extracts templates, moves their content to the main document, and runs any scripts there.
114
+
115
+ ### Works Without JavaScript
116
+
117
+ Your links and forms remain fully functional HTML. Without HTMT loaded, users can still click a link with a `target` attribute—it opens the partial response in a new page or tab, just like normal.
118
+
119
+ This means you can respond with a complete HTML document. Include stylesheets, scripts, images—whatever makes the partial content self-contained. The same response works both as a partial swap (with HTMT) and as a standalone page (without HTMT).
120
+
121
+ ```html
122
+ <!DOCTYPE html>
123
+ <html>
124
+ <head>
125
+ <link rel="stylesheet" href="/styles.css" />
126
+ <script src="/interactive.js"></script>
127
+ </head>
128
+ <body>
129
+ <span id="results">
130
+ Your partial content here, complete with styles and scripts.
131
+ </span>
132
+ </body>
133
+ </html>
134
+ ```
135
+
136
+ ## Attributes Reference
137
+
138
+ ### Required
139
+
140
+ - `target` – the `id` of the element to replace with the response content.
141
+
142
+ ### Optional
143
+
144
+ - `frame` – names the internal iframe. Links/forms sharing the same frame name reuse a single iframe. If omitted, `target` is used as the frame name.
145
+ - `busy` – comma-separated list of element IDs to set `aria-busy=true` during the request (e.g., `busy="submit-btn,status"`)
146
+ - `disabled` – comma-separated list of element IDs to set `disabled` and `aria-disabled` during the request
147
+
148
+ Special values for `busy` and `disabled`:
149
+
150
+ - `_self` – the element that triggered the request
151
+ - `_submitter` – the button that submitted the form (form elements only)
152
+
153
+ Example:
154
+
155
+ ```html
156
+ <form
157
+ method="post"
158
+ action="/api/save"
159
+ target="status"
160
+ busy="_self"
161
+ disabled="submit-btn,form-input"
162
+ >
163
+ <input id="form-input" type="text" />
164
+ <button id="submit-btn">Save</button>
165
+ <div id="status"></div>
166
+ </form>
167
+ ```
168
+
169
+ ## How It Works
170
+
171
+ When you initialize HTMT with `install: true`, it sets up global event listeners for clicks and form submissions. When it detects an element with a `target` attribute:
172
+
173
+ 1. Creates or reuses a hidden iframe (named by the `frame` attribute, or `target` if not specified)
174
+ 2. Directs the browser to load the URL into that iframe
175
+ 3. When the response loads, HTMT extracts any `<template>` elements from the response `<head>` and queues them for injection
176
+ 4. Locates the element matching the `target` ID in both the response and the main document
177
+ 5. Replaces the main document's element with the response element
178
+ 6. Executes any queued templates and their scripts in the main document context
179
+
180
+ ## Philosophy
181
+
182
+ HTMT is intentionally minimal. It adds just enough JavaScript to coordinate iframe-based loading and element swapping. The patterns are declarative—all behavior flows from your HTML structure. Your links and forms remain valid, semantic HTML that works without JavaScript; HTMT progressively enhances them.
183
+
184
+ This aligns with the web's foundational principle: start with semantic markup, layer on behavior. No build step required, no framework lock-in.
185
+
186
+ ## Status
187
+
188
+ This project is experimental. The API may evolve as we explore these patterns.
package/htmt.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function htmt(
2
+ root: HTMLElement | Document | DocumentFragment,
3
+ installGlobalHandlers?: boolean
4
+ ): void;
package/htmt.js ADDED
@@ -0,0 +1,79 @@
1
+ let forTargets = (def, sub, attr, cb) =>
2
+ def
3
+ ?.getAttribute(attr)
4
+ ?.split(",")
5
+ .forEach((id) => {
6
+ let target =
7
+ id === "_self"
8
+ ? def
9
+ : id === "_submitter"
10
+ ? sub
11
+ : document.getElementById(id);
12
+ if (target) cb(target);
13
+ });
14
+
15
+ let mkFrame = (name) => {
16
+ let frame = document.createElement("iframe");
17
+ frame._ = [];
18
+ frame.name = name;
19
+ frame.hidden = true;
20
+ frame.onload = () => load(frame);
21
+ document.body.append(frame);
22
+ frame.contentWindow.onbeforeunload = (_, [def, sub] = frame._) => {
23
+ forTargets(def, sub, "busy", (el) => el.setAttribute("aria-busy", "true"));
24
+ forTargets(def, sub, "disabled", (el) => {
25
+ if ("disabled" in el) el.disabled = true;
26
+ el.setAttribute("aria-disabled", "true");
27
+ });
28
+ };
29
+ };
30
+
31
+ let load = (frame) => {
32
+ let {
33
+ contentDocument: cd,
34
+ name,
35
+ _: [def, sub],
36
+ } = frame;
37
+ if (cd.URL == "about:blank") return;
38
+ let f = document.createDocumentFragment();
39
+
40
+ forTargets(def, sub, "busy", (el) => el.removeAttribute("aria-busy"));
41
+ forTargets(def, sub, "disabled", (el) => {
42
+ if ("disabled" in el) el.disabled = false;
43
+ el.removeAttribute("aria-disabled");
44
+ });
45
+
46
+ cd.querySelectorAll("head>template").forEach((t) => {
47
+ t.id
48
+ ? document.getElementById(t.id)?.replaceWith(t.content)
49
+ : f.append(t.content);
50
+ });
51
+ f.append(...cd.body.childNodes);
52
+
53
+ frame.remove();
54
+ mkFrame(name);
55
+
56
+ setTimeout(() => (htmt(f), document.getElementById(name)?.replaceWith(f)));
57
+ };
58
+
59
+ let htmt = (root, install) => {
60
+ if (install) {
61
+ onclick = onsubmit = (e) => {
62
+ let el = e.target,
63
+ name = el?.getAttribute("frame") || el?.getAttribute("target"),
64
+ iframe = name && document.querySelector(`iframe[name="${name}"]`);
65
+ if (iframe) iframe._ = [el, e.submitter ?? el];
66
+ };
67
+ }
68
+ root.querySelectorAll("a[target],form[target]").forEach((el) => {
69
+ let name = el?.getAttribute("frame") || el?.getAttribute("target");
70
+ if (
71
+ name &&
72
+ name[0] !== "_" &&
73
+ !document.querySelector(`iframe[name="${name}"]`)
74
+ )
75
+ mkFrame(name);
76
+ });
77
+ };
78
+
79
+ export { htmt };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "htmt",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "HyperText Markup Targets",
6
+ "packageManager": "pnpm@10.27.0",
7
+ "license": "0BSD",
8
+ "scripts": {
9
+ "build": "vite build --config ./site/vite.config.ts",
10
+ "dev": "vite dev --config ./site/vite.config.ts",
11
+ "start": "vite preview --config ./site/vite.config.ts",
12
+ "measure": "echo 'src' && cat ./htmt.js | gzip -c | wc -c && echo 'minified' && cat ./htmt.js | terser | gzip -c | wc -c",
13
+ "test": "playwright test --config=./tests/playwright.config.ts"
14
+ },
15
+ "devDependencies": {
16
+ "@playwright/test": "^1.57.0",
17
+ "terser": "^5.44.1",
18
+ "vite": "^7.3.1"
19
+ },
20
+ "files": [
21
+ "htmt.d.ts",
22
+ "htmt.js",
23
+ "README.md"
24
+ ]
25
+ }