xhtmlx 0.1.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dkropachev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,384 @@
1
+ # xhtmlx
2
+
3
+ A lightweight, zero-dependency JavaScript library for building dynamic UIs with REST APIs using declarative HTML attributes.
4
+
5
+ Like [htmx](https://htmx.org), but instead of receiving HTML from the server, xhtmlx receives **JSON** and renders UI **client-side** using templates.
6
+
7
+ ## Quick Start
8
+
9
+ ```html
10
+ <script src="xhtmlx.js"></script>
11
+
12
+ <div xh-get="/api/users" xh-trigger="load" xh-template="/templates/user-list.html">
13
+ <span class="xh-indicator">Loading...</span>
14
+ </div>
15
+ <div id="results"></div>
16
+ ```
17
+
18
+ ```html
19
+ <!-- /templates/user-list.html -->
20
+ <div xh-each="users">
21
+ <div class="user">
22
+ <span xh-text="name"></span>
23
+ <span xh-text="email"></span>
24
+ </div>
25
+ </div>
26
+ ```
27
+
28
+ Server returns:
29
+ ```json
30
+ { "users": [{ "name": "Alice", "email": "alice@example.com" }] }
31
+ ```
32
+
33
+ xhtmlx fetches the JSON, loads the template, renders it with the data, and swaps it into the DOM. No JavaScript needed.
34
+
35
+ ## Installation
36
+
37
+ Drop the script into your page:
38
+
39
+ ```html
40
+ <script src="xhtmlx.js"></script>
41
+ ```
42
+
43
+ No build step. No dependencies.
44
+
45
+ ## API Reference
46
+
47
+ ### REST Verbs
48
+
49
+ | Attribute | Description |
50
+ |-----------|-------------|
51
+ | `xh-get` | Issue a GET request to the URL |
52
+ | `xh-post` | Issue a POST request |
53
+ | `xh-put` | Issue a PUT request |
54
+ | `xh-delete` | Issue a DELETE request |
55
+ | `xh-patch` | Issue a PATCH request |
56
+
57
+ ```html
58
+ <button xh-get="/api/users">Load Users</button>
59
+ <button xh-post="/api/users" xh-vals='{"name": "Bob"}'>Create User</button>
60
+ <button xh-delete="/api/users/{{id}}">Delete</button>
61
+ ```
62
+
63
+ ### Templates
64
+
65
+ **External template file:**
66
+ ```html
67
+ <div xh-get="/api/users" xh-template="/templates/user-list.html"></div>
68
+ ```
69
+
70
+ **Inline template:**
71
+ ```html
72
+ <div xh-get="/api/users">
73
+ <template>
74
+ <span xh-text="name"></span>
75
+ </template>
76
+ </div>
77
+ ```
78
+
79
+ Templates can contain xhtmlx attributes, enabling nested API calls and template composition:
80
+
81
+ ```html
82
+ <!-- /templates/user-card.html -->
83
+ <div class="card">
84
+ <h2 xh-text="name"></h2>
85
+ <div xh-get="/api/users/{{id}}/posts"
86
+ xh-trigger="load"
87
+ xh-template="/templates/post-list.html">
88
+ </div>
89
+ </div>
90
+ ```
91
+
92
+ ### Data Binding
93
+
94
+ | Attribute | Description |
95
+ |-----------|-------------|
96
+ | `xh-text` | Set element's textContent from data field |
97
+ | `xh-html` | Set element's innerHTML from data field (use with caution) |
98
+ | `xh-attr-*` | Set any attribute from data field |
99
+
100
+ ```html
101
+ <span xh-text="user.name"></span>
102
+ <div xh-html="user.bio"></div>
103
+ <img xh-attr-src="user.avatar" xh-attr-alt="user.name">
104
+ <a xh-attr-href="user.profile_url" xh-text="user.name"></a>
105
+ ```
106
+
107
+ ### Iteration
108
+
109
+ `xh-each` repeats the element for each item in an array:
110
+
111
+ ```html
112
+ <ul>
113
+ <li xh-each="items">
114
+ <span xh-text="name"></span> - <span xh-text="price"></span>
115
+ </li>
116
+ </ul>
117
+ ```
118
+
119
+ For data `{ "items": [{ "name": "A", "price": 10 }, { "name": "B", "price": 20 }] }`, this renders two `<li>` elements.
120
+
121
+ Access the iteration index with `$index`:
122
+ ```html
123
+ <li xh-each="items">
124
+ <span xh-text="$index"></span>. <span xh-text="name"></span>
125
+ </li>
126
+ ```
127
+
128
+ ### Conditionals
129
+
130
+ | Attribute | Description |
131
+ |-----------|-------------|
132
+ | `xh-if` | Render element only if field is truthy |
133
+ | `xh-unless` | Render element only if field is falsy |
134
+
135
+ ```html
136
+ <span xh-if="is_admin" class="badge">Admin</span>
137
+ <span xh-unless="verified" class="warning">Unverified</span>
138
+ ```
139
+
140
+ ### Triggers
141
+
142
+ `xh-trigger` specifies what event fires the request:
143
+
144
+ ```html
145
+ <div xh-get="/api/data" xh-trigger="load">Auto-load on page load</div>
146
+ <input xh-get="/api/search" xh-trigger="keyup changed delay:300ms">
147
+ <div xh-get="/api/feed" xh-trigger="every 5s">Polling</div>
148
+ <div xh-get="/api/more" xh-trigger="revealed">Load when scrolled into view</div>
149
+ <button xh-get="/api/data" xh-trigger="click once">Load once</button>
150
+ ```
151
+
152
+ **Default triggers:**
153
+ - `click` — buttons, links, and general elements
154
+ - `submit` — forms
155
+ - `change` — inputs, selects, textareas
156
+
157
+ **Modifiers:**
158
+ - `once` — fire only once
159
+ - `changed` — only fire if value changed
160
+ - `delay:Nms` — debounce before firing
161
+ - `throttle:Nms` — throttle firing rate
162
+ - `from:selector` — listen on a different element
163
+
164
+ ### Targeting
165
+
166
+ | Attribute | Description |
167
+ |-----------|-------------|
168
+ | `xh-target` | CSS selector for where to place the rendered result |
169
+ | `xh-swap` | How to insert the content |
170
+
171
+ ```html
172
+ <button xh-get="/api/users" xh-target="#user-list" xh-swap="innerHTML">
173
+ Load Users
174
+ </button>
175
+ <div id="user-list"></div>
176
+ ```
177
+
178
+ **Swap modes:**
179
+
180
+ | Mode | Behavior |
181
+ |------|----------|
182
+ | `innerHTML` | Replace target's children (default) |
183
+ | `outerHTML` | Replace target itself |
184
+ | `beforeend` | Append inside target |
185
+ | `afterbegin` | Prepend inside target |
186
+ | `beforebegin` | Insert before target |
187
+ | `afterend` | Insert after target |
188
+ | `delete` | Remove target |
189
+ | `none` | Don't swap (fire-and-forget) |
190
+
191
+ ### URL Interpolation
192
+
193
+ Use `{{field}}` in URLs to insert values from the current data context:
194
+
195
+ ```html
196
+ <div xh-each="users">
197
+ <button xh-get="/api/users/{{id}}/profile"
198
+ xh-template="/templates/profile.html">
199
+ View Profile
200
+ </button>
201
+ </div>
202
+ ```
203
+
204
+ Supports dot notation: `{{user.address.city}}`
205
+
206
+ ### Indicators
207
+
208
+ Show a loading element while a request is in-flight:
209
+
210
+ ```html
211
+ <button xh-get="/api/data" xh-indicator="#spinner">Load</button>
212
+ <span id="spinner" class="xh-indicator">Loading...</span>
213
+ ```
214
+
215
+ The library adds the `xh-request` class to the indicator during requests. Default CSS is injected automatically to show/hide `.xh-indicator` elements.
216
+
217
+ ### Request Data
218
+
219
+ | Attribute | Description |
220
+ |-----------|-------------|
221
+ | `xh-vals` | JSON string of values to send with the request |
222
+ | `xh-headers` | JSON string of custom headers |
223
+
224
+ ```html
225
+ <button xh-post="/api/users"
226
+ xh-vals='{"name": "Alice", "role": "admin"}'
227
+ xh-headers='{"X-Custom": "value"}'>
228
+ Create User
229
+ </button>
230
+ ```
231
+
232
+ For forms, form fields are automatically serialized:
233
+
234
+ ```html
235
+ <form xh-post="/api/users" xh-template="/templates/success.html">
236
+ <input name="name" type="text">
237
+ <input name="email" type="email">
238
+ <button type="submit">Create</button>
239
+ </form>
240
+ ```
241
+
242
+ ### Error Handling
243
+
244
+ Specify templates for error responses:
245
+
246
+ ```html
247
+ <div xh-get="/api/users"
248
+ xh-template="/templates/user-list.html"
249
+ xh-error-template="/templates/error.html"
250
+ xh-error-template-404="/templates/not-found.html"
251
+ xh-error-template-4xx="/templates/client-error.html"
252
+ xh-error-target="#error-area">
253
+ </div>
254
+ ```
255
+
256
+ **Resolution order:**
257
+ 1. `xh-error-template-{exact code}` on the element (e.g. `xh-error-template-404`)
258
+ 2. `xh-error-template-{class}` on the element (e.g. `xh-error-template-4xx`)
259
+ 3. `xh-error-template` on the element (generic fallback)
260
+ 4. Nearest ancestor `xh-error-boundary` (see below)
261
+ 5. `xhtmlx.config.defaultErrorTemplate` (global fallback)
262
+ 6. No template: adds `xh-error` CSS class and emits `xh:responseError` event
263
+
264
+ #### Error Boundaries
265
+
266
+ Wrap a section of your page with `xh-error-boundary` to catch errors from any child widget that doesn't have its own error template:
267
+
268
+ ```html
269
+ <div xh-error-boundary
270
+ xh-error-template="/templates/error.html"
271
+ xh-error-target="#section-errors">
272
+ <div id="section-errors"></div>
273
+
274
+ <!-- If this fails and has no error template, the boundary catches it -->
275
+ <div xh-get="/api/widget-a" xh-trigger="load">
276
+ <template><span xh-text="data"></span></template>
277
+ </div>
278
+
279
+ <!-- This has its own error template, so the boundary is skipped -->
280
+ <div xh-get="/api/widget-b" xh-trigger="load"
281
+ xh-error-template="/templates/widget-error.html">
282
+ <template><span xh-text="data"></span></template>
283
+ </div>
284
+ </div>
285
+ ```
286
+
287
+ Boundaries support the same status-specific attributes: `xh-error-template-404`, `xh-error-template-4xx`, etc.
288
+
289
+ Boundaries nest — the nearest ancestor boundary catches the error:
290
+
291
+ ```html
292
+ <div xh-error-boundary xh-error-template="/templates/page-error.html">
293
+ <div xh-error-boundary xh-error-template="/templates/section-error.html">
294
+ <!-- Errors here go to section-error, not page-error -->
295
+ <div xh-get="/api/data" xh-trigger="load">...</div>
296
+ </div>
297
+ </div>
298
+ ```
299
+
300
+ #### Global Error Config
301
+
302
+ Set a page-wide default for widgets without any error handling:
303
+
304
+ ```html
305
+ <script>
306
+ xhtmlx.config.defaultErrorTemplate = "/templates/error.html";
307
+ xhtmlx.config.defaultErrorTarget = "#global-error";
308
+ </script>
309
+ <div id="global-error"></div>
310
+ ```
311
+
312
+ Any widget that errors without an element-level template or boundary will use this global fallback.
313
+
314
+ **Error data context:**
315
+ ```json
316
+ {
317
+ "status": 422,
318
+ "statusText": "Unprocessable Entity",
319
+ "body": { "error": "validation_failed", "fields": [...] }
320
+ }
321
+ ```
322
+
323
+ Use it in templates like any other data:
324
+ ```html
325
+ <!-- /templates/validation-error.html -->
326
+ <div class="error">
327
+ <h3>Error <span xh-text="status"></span></h3>
328
+ <ul xh-each="body.fields">
329
+ <li><strong xh-text="name"></strong>: <span xh-text="message"></span></li>
330
+ </ul>
331
+ </div>
332
+ ```
333
+
334
+ ### Events
335
+
336
+ xhtmlx emits custom DOM events for programmatic control:
337
+
338
+ | Event | When | Cancelable |
339
+ |-------|------|------------|
340
+ | `xh:beforeRequest` | Before fetch fires | Yes |
341
+ | `xh:afterRequest` | After fetch completes | No |
342
+ | `xh:beforeSwap` | Before DOM swap | Yes |
343
+ | `xh:afterSwap` | After DOM swap | No |
344
+ | `xh:responseError` | On HTTP error response | No |
345
+
346
+ ```javascript
347
+ document.body.addEventListener("xh:responseError", function(e) {
348
+ console.log(e.detail.status);
349
+ console.log(e.detail.body);
350
+ });
351
+ ```
352
+
353
+ ### Data Context
354
+
355
+ Data flows through nested templates via a context chain. Child templates can access parent data:
356
+
357
+ ```html
358
+ <!-- Parent: fetches user -->
359
+ <div xh-get="/api/users/1" xh-trigger="load" xh-template="/templates/user.html"></div>
360
+
361
+ <!-- /templates/user.html: can access user fields, fetches posts -->
362
+ <h1 xh-text="name"></h1>
363
+ <div xh-get="/api/users/{{id}}/posts" xh-trigger="load">
364
+ <template>
365
+ <!-- Each post can access its own fields AND parent user fields via $parent -->
366
+ <div xh-each="posts">
367
+ <p><span xh-text="title"></span> by <span xh-text="$parent.name"></span></p>
368
+ </div>
369
+ </template>
370
+ </div>
371
+ ```
372
+
373
+ **Special variables:**
374
+ - `$index` — current iteration index (inside `xh-each`)
375
+ - `$parent` — parent data context
376
+ - `$root` — topmost data context
377
+
378
+ ## Browser Support
379
+
380
+ xhtmlx uses `fetch()`, `Promise`, `WeakMap`, and `IntersectionObserver`. Works in all modern browsers (Chrome, Firefox, Safari, Edge). No IE support.
381
+
382
+ ## License
383
+
384
+ MIT
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "xhtmlx",
3
+ "version": "0.1.0",
4
+ "description": "Declarative HTML attributes for REST API driven UIs. Like htmx, but with JSON APIs and client-side rendering.",
5
+ "main": "xhtmlx.js",
6
+ "types": "xhtmlx.d.ts",
7
+ "browser": "xhtmlx.js",
8
+ "unpkg": "xhtmlx.min.js",
9
+ "jsdelivr": "xhtmlx.min.js",
10
+ "files": [
11
+ "xhtmlx.js",
12
+ "xhtmlx.d.ts",
13
+ "xhtmlx.min.js",
14
+ "xhtmlx.min.js.map",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "test": "jest --verbose",
20
+ "test:unit": "jest tests/unit --verbose",
21
+ "test:integration": "jest tests/integration --verbose",
22
+ "lint": "eslint xhtmlx.js tests/ --ext .js",
23
+ "check": "node --check xhtmlx.js",
24
+ "build": "npx terser xhtmlx.js -o xhtmlx.min.js --compress --mangle --source-map \"filename='xhtmlx.min.js.map',url='xhtmlx.min.js.map'\"",
25
+ "prepublishOnly": "npm run check && npm run lint && npm test && npm run build",
26
+ "examples": "node examples/server.js"
27
+ },
28
+ "keywords": [
29
+ "htmx",
30
+ "rest",
31
+ "api",
32
+ "declarative",
33
+ "html",
34
+ "attributes",
35
+ "template",
36
+ "client-side",
37
+ "rendering",
38
+ "json",
39
+ "ajax",
40
+ "fetch",
41
+ "spa",
42
+ "lightweight",
43
+ "no-build"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/teryxjs/xhtmlx.git"
48
+ },
49
+ "homepage": "https://github.com/teryxjs/xhtmlx#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/teryxjs/xhtmlx/issues"
52
+ },
53
+ "author": "dkropachev",
54
+ "license": "MIT",
55
+ "devDependencies": {
56
+ "eslint": "^8.56.0",
57
+ "express": "^4.18.2",
58
+ "jest": "^29.7.0",
59
+ "jest-environment-jsdom": "^29.7.0",
60
+ "terser": "^5.27.0"
61
+ }
62
+ }