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 +12 -0
- package/README.md +188 -0
- package/htmt.d.ts +4 -0
- package/htmt.js +79 -0
- package/package.json +25 -0
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
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
|
+
}
|