on_the_money 0.3.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/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/dist/on_the_money.min.js +1 -0
- package/package.json +61 -0
- package/src/core/On.js +27 -0
- package/src/core/Select.js +55 -0
- package/src/core/The.js +305 -0
- package/src/core/index.js +36 -0
- package/src/eslint/config.js +16 -0
- package/src/eslint/plugin.js +218 -0
- package/src/linter/Linter.js +139 -0
- package/src/linter/cli.js +94 -0
- package/src/linter/main.js +7 -0
- package/src/stylelint/config.js +9 -0
- package/src/stylelint/plugin.js +42 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.3.0] — 2026-05-18
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- **`otm-lint` reduced to four HTML / cross-file rules.** The bundled linter no longer enforces JS or CSS rules. Twelve rules dropped: JS-001, JS-003, JS-009, JS-011, JS-015, JS-016, JS-019, HTML-014, HTML-018, HTML-020, HTML-021, HTML-022, CSS-006, CSS-012. Their enforcement now lives in `eslint-plugin-otm`, `stylelint-plugin-otm`, the stylelint built-in `declaration-no-important`, and recommended companions like `eslint-plugin-no-unsanitized` and `html-validate`.
|
|
12
|
+
- **`otm-lint` only scans `.html` files now.** JS and CSS files are silently ignored by the binary; route them through ESLint and Stylelint.
|
|
13
|
+
- **Dropped runtime dependencies** `espree` and `css-tree` from devDependencies; the linter no longer parses JS/CSS.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`eslint-plugin-otm`** with five rules: `prefer-on`, `prefer-the-set`, `flat-state`, `prefer-submit`, `no-style-mutation`. Distributed via `on_the_money/eslint-plugin`.
|
|
18
|
+
- **`eslint-config-otm`** shareable flat-config at `on_the_money/eslint-config`.
|
|
19
|
+
- **`stylelint-plugin-otm`** with one rule: `prefer-attribute-selector`. Distributed via `on_the_money/stylelint-plugin`.
|
|
20
|
+
- **`stylelint-config-otm`** at `on_the_money/stylelint-config` — enables our rule plus stylelint's built-in `declaration-no-important`.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **README** linter section rewritten to describe the three-tool stack (ESLint + Stylelint + otm-lint) instead of a single bundled linter.
|
|
25
|
+
- **Linter `flat-state` rule** now catches `the({ nested: ... })` single-arg batch case, which the old `Linter.js` missed.
|
|
26
|
+
|
|
27
|
+
### Rationale
|
|
28
|
+
|
|
29
|
+
The old `Linter.js` was an attempt at a one-tool-fits-all frontend linter that reimplemented innerHTML detection, !important banning, and inline-handler banning — all of which existing ecosystem tools do better, with editor LSP integration, inline disables, configurable severity, and SARIF reporters. The reorientation positions `otm-lint` as a small auxiliary tool for the cross-file checks no other ecosystem covers, while the heavy lifting goes to ESLint + Stylelint + html-validate where it already belongs.
|
|
30
|
+
|
|
31
|
+
## [0.2.0] — 2026-05-18
|
|
32
|
+
|
|
33
|
+
### Breaking
|
|
34
|
+
|
|
35
|
+
- **Removed import-time auto-handshake.** Importing `on_the_money` is now side-effect-free. Consumers must call `await the.boot()` at their entry point. The old auto-call performed a network fetch and DOM mutation on import.
|
|
36
|
+
- **Removed `the.ready` promise.** Boot is explicit now; await `the.boot()` directly.
|
|
37
|
+
- **Removed `the(form)` polymorphic shape.** Form extraction is only via `the.form(formEl)`.
|
|
38
|
+
- **`the(string, undefined)` now throws.** Two args means set; missing val is a contract violation. Previously silently coerced to the string `"undefined"`.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **`the.boot(options?)`** explicit init. Accepts `{ signal, locales, dictionary, namespace }` for abort, override path, inline dictionary, and configurable localStorage prefix.
|
|
43
|
+
- **`the.flat(obj, sep = "_")`** recursive flatten helper. Composes `the.form` output into `the(el, {...})` batch input.
|
|
44
|
+
- **`the.dictionary`** and **`the.locale`** as live accessors on the exported `the`. Hot-swap supported.
|
|
45
|
+
- **`on()` returns an unsubscribe function.** `const off = on(...); off()` detaches the listener.
|
|
46
|
+
- **Boolean coercion in `the()` setter.** `the(el, "checked", true)` writes `"true"`; `false` writes `"false"`. Eliminates the truthy-`"false"`-string footgun.
|
|
47
|
+
- **`otm-lint` default-excludes** `node_modules`, `dist`, `.git`, and dotdirs. Consumer projects no longer get drowned in dependency false-positives.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **Framing.** "Anti-Framework" rhetoric dropped. Now self-describes as an opinionated, attribute-driven, standards-oriented modern framework.
|
|
52
|
+
- **`the(...)` dispatch** rewritten on a single Element-first-arg disambiguator. Three disjoint first-arg types (Element / string / plain Object) eliminate the prior ambiguity that produced silent mis-dispatch on `the("k", undefined)`.
|
|
53
|
+
- **`route()`** resolves `href` via `new URL(link.href, window.location.href)` instead of reading the anchor's `.origin`/`.pathname` IDL properties. Robust under non-browser DOM implementations.
|
|
54
|
+
- **Tests co-located** next to source (`src/core/On.test.js`, etc.). Integration suite remains at `test/integration.test.js`.
|
|
55
|
+
- **`exports."."`** points at the built `dist/on_the_money.min.js`; raw ESM source available as `./src` subpath.
|
|
56
|
+
- **README** rewritten as an LLM authoring context document.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- Coverage thresholds adjusted from 80/80/100 to 50/50/50 (the global engineering baseline) — the 100% function gate was demanding test acrobatics for paths like `route()` that need a full window.
|
|
61
|
+
- `the.form` no longer requires `FormData` global; uses `querySelectorAll` over named controls for test environment compatibility.
|
|
62
|
+
|
|
63
|
+
## [0.1.1] — 2026-03-18
|
|
64
|
+
|
|
65
|
+
Initial public surface. Auto-handshake, polymorphic `the()` dispatch including `the(form)`, `the.ready` promise.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PossumTech Laboratories
|
|
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,433 @@
|
|
|
1
|
+
# on_the_money.js
|
|
2
|
+
|
|
3
|
+
Opinionated, attribute-driven, standards-oriented modern framework for the web. <2KB gzip. Native browser APIs only. ESNext.
|
|
4
|
+
|
|
5
|
+
State lives in DOM attributes. Reactivity comes from `[data-text]` and `[data-i18n]` selectors. Events use one delegated listener per `(parent, type)` pair. Routing uses the History API. Localization uses `Intl`. There is no virtual DOM, no JSX, no transpilation step, no proprietary tooling. The framework is a thin layer of conventions over the platform.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install on_the_money
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
A complete two-file app. `index.html` carries semantic structure, `app.js` carries behavior.
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<!DOCTYPE html>
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="UTF-8">
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
23
|
+
<title data-i18n="app_title"></title>
|
|
24
|
+
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@2/css/pico.classless.min.css">
|
|
25
|
+
<script type="module" src="./app.js"></script>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<main>
|
|
29
|
+
<h1 data-i18n="app_title"></h1>
|
|
30
|
+
<p>Hello, <strong data-text="user">friend</strong>.</p>
|
|
31
|
+
<button data-action="greet">Greet</button>
|
|
32
|
+
</main>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
// app.js
|
|
39
|
+
import { the, on } from "on_the_money";
|
|
40
|
+
|
|
41
|
+
await the.boot({
|
|
42
|
+
dictionary: { app_title: "On The Money" },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
on("main", "click", '[data-action="greet"]', () => {
|
|
46
|
+
the("user", "Alice");
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The framework rehydrates body `data-*` and `[data-text]` from `localStorage` on boot; subsequent `the()` writes both the attribute and persist to `localStorage["otm:KEY"]`.
|
|
51
|
+
|
|
52
|
+
## Pico Classless integration
|
|
53
|
+
|
|
54
|
+
Pico Classless is the recommended companion stylesheet. It styles semantic HTML directly — no classes, no JS. Include the stylesheet once:
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@2/css/pico.classless.min.css">
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then write semantic HTML (`<main>`, `<nav>`, `<article>`, `<form>`, `<button>`, `<input>` with `<label>`, etc.). on_the_money's conventions all use `data-*` and `aria-*` attributes, so they layer cleanly under Pico's element-targeted CSS.
|
|
61
|
+
|
|
62
|
+
## Exports
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
import { on, the, _t, route, $, $$ } from "on_the_money";
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Aliases on `the`: `the.t === _t`, `the.route === route`, `the.form(formEl)`, `the.flat(obj, sep?)`, `the.boot(options?)`.
|
|
69
|
+
Live accessors on `the`: `the.dictionary`, `the.locale`.
|
|
70
|
+
|
|
71
|
+
## API
|
|
72
|
+
|
|
73
|
+
### `on(parent, event, selector, fn)` — event delegation
|
|
74
|
+
|
|
75
|
+
| Arg | Type | Notes |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| `parent` | `Element \| string \| null` | If string, resolved via `document.querySelector`. If falsy, defaults to `document.body`. |
|
|
78
|
+
| `event` | `string` | Any DOM event name (`"click"`, `"submit"`, `"mounted"`, etc.). |
|
|
79
|
+
| `selector` | `string` | CSS selector matched via `event.target.closest(selector)`. |
|
|
80
|
+
| `fn` | `(event, target) => void` | `target` is the element the selector matched. |
|
|
81
|
+
|
|
82
|
+
Returns an `() => void` unsubscribe function.
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
const off = on("#list", "click", "[data-action='delete']", (e, target) => {
|
|
86
|
+
target.closest("[data-item]").remove();
|
|
87
|
+
});
|
|
88
|
+
off(); // detaches the listener
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `on.emit(el, event, detail)` — `CustomEvent` dispatch
|
|
92
|
+
|
|
93
|
+
Dispatches a bubbling, cancelable `CustomEvent` on `el`. `el` may be an element or a selector string.
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
on.emit("#cart", "items-changed", { count: 3 });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `the(...)` — state
|
|
100
|
+
|
|
101
|
+
Polymorphic on a single disambiguator: `args[0] instanceof Element`. Three call shapes per scope (get, set, batch).
|
|
102
|
+
|
|
103
|
+
| Call | Behavior | Returns |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `the(key)` | Read body `data-KEY` (or aria-mapped equivalent). | `string \| null` |
|
|
106
|
+
| `the(key, val)` | Write body attribute + `localStorage["otm:KEY"]` + descendant `[data-text="key"]`. | `document.body` |
|
|
107
|
+
| `the({ k: v, ... })` | Batch global write. | `document.body` |
|
|
108
|
+
| `the(el, key)` | Read scoped attribute on `el`. | `string \| null` |
|
|
109
|
+
| `the(el, key, val)` | Write scoped attribute on `el` + descendant `[data-text="key"]`. | `el` |
|
|
110
|
+
| `the(el, { k: v, ... })` | Batch scoped write. | `el` |
|
|
111
|
+
|
|
112
|
+
- **ARIA mapping** (key → attribute): `expanded`, `selected`, `hidden`, `checked`, `disabled` → `aria-*`. All other keys → `data-*`.
|
|
113
|
+
- **Booleans coerce** to `"true"`/`"false"` inside the setter. `the(el, "checked", true)` writes `"true"`.
|
|
114
|
+
- **Values MUST be flat primitives.** Pass nested objects through `the.flat(...)` first.
|
|
115
|
+
- **`the(key, undefined)` throws.** Two args means set; missing val is a contract violation.
|
|
116
|
+
|
|
117
|
+
### `the.form(formEl)` — form extraction
|
|
118
|
+
|
|
119
|
+
Walks `input, select, textarea` descendants. Skips unnamed, disabled, submit/button/reset controls, and unchecked checkboxes/radios. Parses bracket-notation names into a nested object:
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
the.form(form);
|
|
123
|
+
// <input name="user[name]" value="Alice"> → { user: { name: "Alice" } }
|
|
124
|
+
// <input name="tags[]" value="a"> → { tags: ["a", ...] }
|
|
125
|
+
// <input name="tags[]" value="b">
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Returns a **nested** object (matches browser submission semantics). Compose with `the.flat` before feeding to `the(el, {...})`.
|
|
129
|
+
|
|
130
|
+
### `the.flat(obj, sep = "_")` — nested-to-flat
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
the.flat({ user: { name: "Alice" }, tags: ["a", "b"] });
|
|
134
|
+
// → { user_name: "Alice", tags_0: "a", tags_1: "b" }
|
|
135
|
+
|
|
136
|
+
the.flat({ a: { b: 1 } }, ".");
|
|
137
|
+
// → { "a.b": 1 }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Throws on non-object input.
|
|
141
|
+
|
|
142
|
+
### `the.boot(options?)` — explicit init
|
|
143
|
+
|
|
144
|
+
Importing the module does **nothing**. Call `the.boot()` at the consumer's entry point.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
await the.boot({ signal, locales, dictionary, namespace });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
| Option | Type | Behavior |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `signal` | `AbortSignal` | Aborts the i18n fetch. |
|
|
153
|
+
| `locales` | `string` | Override the `<meta name="i18n" content>` path. |
|
|
154
|
+
| `dictionary` | `object` | Inline dictionary; skips the fetch entirely. |
|
|
155
|
+
| `namespace` | `string` | Sets `localStorage` prefix to `${namespace}:` (default `otm:`). Must be set before any state ops. |
|
|
156
|
+
|
|
157
|
+
Boot sequence:
|
|
158
|
+
1. Resolve locale: `?lang=` query → `localStorage["${prefix}lang"]` → `navigator.language`. Writes `the.locale`.
|
|
159
|
+
2. Resolve dictionary: inline `dictionary` → `fetch(${path}/${target}.json)` if `<meta name="i18n">` or `locales` option is present. Falls back through full → base → `data-fallback`.
|
|
160
|
+
3. Replay `localStorage` entries matching the prefix (except `${prefix}lang`) back onto body `data-*` and `[data-text]`.
|
|
161
|
+
4. Run `_t()` to hydrate `[data-i18n]`.
|
|
162
|
+
|
|
163
|
+
### `_t(key, options)` — `Intl` localization
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
_t("hello"); // string lookup in the.dictionary
|
|
167
|
+
_t("hello", { name: "Alice" }); // {name} → "Alice"
|
|
168
|
+
_t("items", { qty: 5 }); // Intl.PluralRules picks { one, other } entry
|
|
169
|
+
_t("price", { val: 9.99, type: "currency" }); // Intl.NumberFormat (USD)
|
|
170
|
+
_t("when", { val: Date.now(), type: "date" }); // Intl.DateTimeFormat
|
|
171
|
+
|
|
172
|
+
_t(node); // hydrate every [data-i18n] inside node
|
|
173
|
+
_t(); // hydrate document.body
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Missing dictionary keys preserve existing `textContent` (SEO fallback).
|
|
177
|
+
|
|
178
|
+
`[data-i18n]` element binding:
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<span data-i18n="cart_items" data-i18n-qty="3"></span>
|
|
182
|
+
<span data-i18n="price" data-i18n-val="9.99" data-i18n-type="currency"></span>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `route(callback)` — pushState router
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
route((pathname, search, hash) => {
|
|
189
|
+
// render whatever fits this route
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Fires the callback on initial mount, on `popstate`, on `hashchange`, and on intercepted internal `<a>` clicks. Skips interception for `data-external`, `target="_blank"`, and cross-origin hrefs. Hash-only same-page links are left to the browser; `hashchange` still triggers the callback.
|
|
194
|
+
|
|
195
|
+
### `$(context, selector)` / `$$(context, selector)` — context-aware DOM query
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
$(el, ".child"); // first match (Element | null)
|
|
199
|
+
$$(el, ".child"); // all matches (Array)
|
|
200
|
+
$(".child"); // shorthand: context = document
|
|
201
|
+
$$(".child");
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### `$.clone(parent, selector)` — template instantiation
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
const el = $.clone("#list", "#tmp");
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Clones the first element of `<template selector>`, runs `_t(el)` for i18n hydration, appends to `parent`, dispatches a bubbling `mounted` CustomEvent (`detail: { parent }`), and returns the mounted element. Throws if `parent` or template is missing.
|
|
211
|
+
|
|
212
|
+
## Patterns
|
|
213
|
+
|
|
214
|
+
### Form intake
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
on("#todo-form", "submit", (e) => {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
const data = the.form(e.target); // { task, tags: [...] }
|
|
220
|
+
the($.clone("#todo-list", "#todo-item"), the.flat(data)); // task, tags_0, tags_1 → attrs
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### List rendering with i18n
|
|
225
|
+
|
|
226
|
+
```html
|
|
227
|
+
<template id="post-card">
|
|
228
|
+
<article data-item>
|
|
229
|
+
<h2 data-text="title"></h2>
|
|
230
|
+
<p data-i18n="posted_at" data-i18n-val=""></p>
|
|
231
|
+
</article>
|
|
232
|
+
</template>
|
|
233
|
+
<section id="posts"></section>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
for (const post of posts) {
|
|
238
|
+
const el = $.clone("#posts", "#post-card");
|
|
239
|
+
the(el, { title: post.title });
|
|
240
|
+
$(el, '[data-i18n="posted_at"]').setAttribute("data-i18n-val", post.created_at);
|
|
241
|
+
}
|
|
242
|
+
_t();
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Routing (multi-page SPA)
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
route((path) => {
|
|
249
|
+
the("page", path === "/" ? "home" : path.slice(1));
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```css
|
|
254
|
+
main > section { display: none; }
|
|
255
|
+
[data-page="home"] main > section[data-route="home"] { display: block; }
|
|
256
|
+
[data-page="about"] main > section[data-route="about"] { display: block; }
|
|
257
|
+
[data-page="contact"] main > section[data-route="contact"] { display: block; }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Hot dictionary swap
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
on("nav", "click", "[data-lang]", async (_e, link) => {
|
|
264
|
+
const lang = link.getAttribute("data-lang");
|
|
265
|
+
the.locale = lang;
|
|
266
|
+
the.dictionary = await (await fetch(`/locales/${lang}.json`)).json();
|
|
267
|
+
_t();
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Namespaced state
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
// Two apps on the same origin can coexist:
|
|
275
|
+
await the.boot({ namespace: "dashboard" }); // localStorage keys: dashboard:theme, etc.
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Cleanup with on() unsubscribe
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
const off = on("#modal", "click", "[data-action='close']", closeModal);
|
|
282
|
+
on.emit(document.body, "modal-closed");
|
|
283
|
+
off(); // detach when modal is destroyed
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Server-side rendering (no extra API)
|
|
287
|
+
|
|
288
|
+
There's no SSR shim. Emit `data-*` attributes from the server:
|
|
289
|
+
|
|
290
|
+
```html
|
|
291
|
+
<!-- rendered server-side -->
|
|
292
|
+
<body data-theme="dark" data-user="Alice">
|
|
293
|
+
<h1 data-text="user">Alice</h1>
|
|
294
|
+
</body>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
On the client:
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
await the.boot(); // localStorage values, if any, override the server-rendered attrs
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The framework reads existing attributes via `the(key)` without modification, so server-rendered state is observable immediately. `the.boot()` only mutates when localStorage has matching keys.
|
|
304
|
+
|
|
305
|
+
## Lint stack
|
|
306
|
+
|
|
307
|
+
on_the_money ships a three-tool stack. Each layer covers what the others can't.
|
|
308
|
+
|
|
309
|
+
### 1. JavaScript — ESLint + `eslint-plugin-otm`
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
npm install -D eslint
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
// eslint.config.js
|
|
317
|
+
import otm from "on_the_money/eslint-config";
|
|
318
|
+
import nounsanitized from "eslint-plugin-no-unsanitized";
|
|
319
|
+
|
|
320
|
+
export default [
|
|
321
|
+
...otm,
|
|
322
|
+
nounsanitized.configs.recommended,
|
|
323
|
+
];
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
| Rule | Source | Behavior |
|
|
327
|
+
| --- | --- | --- |
|
|
328
|
+
| `otm/prefer-on` | eslint-plugin-otm | Ban `addEventListener`; use `on()`. |
|
|
329
|
+
| `otm/prefer-the-set` | eslint-plugin-otm | Ban `textContent`/`innerText`/`nodeValue` assignment. |
|
|
330
|
+
| `otm/flat-state` | eslint-plugin-otm | Ban nested objects/arrays in `the()` calls. |
|
|
331
|
+
| `otm/prefer-submit` | eslint-plugin-otm | Warn on `on(btn, "click", ...)` for form data. |
|
|
332
|
+
| `otm/no-style-mutation` | eslint-plugin-otm | Ban `el.style.* = ...`. |
|
|
333
|
+
| `no-unsanitized/no-inner-html` | external | Ban `innerHTML`/`outerHTML`. |
|
|
334
|
+
| `no-unsanitized/method` | external | Ban `document.write`, `insertAdjacentHTML`. |
|
|
335
|
+
|
|
336
|
+
### 2. CSS — Stylelint + `stylelint-plugin-otm`
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
npm install -D stylelint stylelint-config-standard
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
// stylelint.config.js
|
|
344
|
+
import otm from "on_the_money/stylelint-config";
|
|
345
|
+
|
|
346
|
+
export default {
|
|
347
|
+
extends: ["stylelint-config-standard"],
|
|
348
|
+
...otm,
|
|
349
|
+
};
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
| Rule | Source | Behavior |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| `otm/prefer-attribute-selector` | stylelint-plugin-otm | Ban `.class` selectors; use `[data-state="..."]`. |
|
|
355
|
+
| `declaration-no-important` | stylelint built-in | Ban `!important`. |
|
|
356
|
+
|
|
357
|
+
### 3. HTML / cross-file — `otm-lint`
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
npx otm-lint --check ./src
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
| Rule | Forbidden | Use instead |
|
|
364
|
+
| --- | --- | --- |
|
|
365
|
+
| **HTML-004** | Naked text in HTML | Wrap in a semantic tag or use `data-i18n="key"`. |
|
|
366
|
+
| **HTML-017** | `<div data-action="...">` without `role`/`tabindex` | Use a `<button>` or other interactive element. |
|
|
367
|
+
| **HTML-023** | `data-i18n="..."` without `<meta name="i18n">` | Declare the i18n endpoint. |
|
|
368
|
+
| **HTML-024** | `data-available="..."` doesn't match locales folder | Keep the manifest aligned with the actual locale files. |
|
|
369
|
+
|
|
370
|
+
`otm-lint` default-excludes `node_modules`, `dist`, `.git`, and dotdirs. It only scans `.html` — everything else delegates to the layers above.
|
|
371
|
+
|
|
372
|
+
### Recommended companions (not shipped)
|
|
373
|
+
|
|
374
|
+
| Concern | Tool |
|
|
375
|
+
| --- | --- |
|
|
376
|
+
| HTML correctness (lang/charset/viewport, inline handlers, deprecated attrs) | `html-validate` |
|
|
377
|
+
| Static a11y | `eslint-plugin-jsx-a11y` (works on plain HTML via parsers) |
|
|
378
|
+
| Runtime a11y | `axe-core` via Playwright/Cypress against the rendered page |
|
|
379
|
+
| Supply-chain | `npm audit`, `osv-scanner` |
|
|
380
|
+
|
|
381
|
+
## Suggested project layout
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
my-app/
|
|
385
|
+
├── index.html # Pico + meta tags + script entry
|
|
386
|
+
├── app.js # await the.boot(); register handlers
|
|
387
|
+
├── locales/
|
|
388
|
+
│ ├── en.json
|
|
389
|
+
│ ├── es.json
|
|
390
|
+
│ └── fr.json
|
|
391
|
+
├── styles.css # attribute-selector-driven custom CSS (optional)
|
|
392
|
+
└── views/
|
|
393
|
+
├── home.html # fragment imported via fetch + $.clone, or inline <template>
|
|
394
|
+
└── about.html
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`<meta name="i18n" content="/locales" data-available="en,es,fr" data-fallback="en">` directs `the.boot()` to fetch `/locales/{lang}.json` automatically.
|
|
398
|
+
|
|
399
|
+
## Repository layout (contributors)
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
src/
|
|
403
|
+
├── core/
|
|
404
|
+
│ ├── index.js # public exports
|
|
405
|
+
│ ├── On.js # event delegation + emit
|
|
406
|
+
│ ├── The.js # state, i18n, boot, route, form, flat
|
|
407
|
+
│ └── Select.js # $, $$, clone
|
|
408
|
+
└── linter/
|
|
409
|
+
├── main.js # otm-lint binary entrypoint
|
|
410
|
+
├── cli.js # directory scan, output
|
|
411
|
+
└── Linter.js # rule implementations
|
|
412
|
+
test/
|
|
413
|
+
└── integration.test.js # cross-module integration suite
|
|
414
|
+
dist/
|
|
415
|
+
└── on_the_money.min.js # built ESM bundle (esbuild)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Unit tests are co-located with source: `src/core/On.test.js`, etc.
|
|
419
|
+
|
|
420
|
+
## Commands
|
|
421
|
+
|
|
422
|
+
| Script | Purpose |
|
|
423
|
+
| --- | --- |
|
|
424
|
+
| `npm test` | Run all tests (`node --test`). |
|
|
425
|
+
| `npm run test:coverage` | Tests + coverage report (50/50/50 line/branch/function gate). |
|
|
426
|
+
| `npm run lint` | Biome formatter + import order. |
|
|
427
|
+
| `npm run build` | Build `dist/on_the_money.min.js` via esbuild. |
|
|
428
|
+
| `npm run check` | `lint + build + test + coverage`. CI gate. |
|
|
429
|
+
| `npm run otm` | Self-lint `examples/` against project rules. |
|
|
430
|
+
|
|
431
|
+
## License
|
|
432
|
+
|
|
433
|
+
MIT. Source and issues: <https://github.com/possumtech/on_the_money.js>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var b=class{static on(t,n,o,e){let r=typeof t=="string"?document.querySelector(t):t||document.body,a=i=>{let s=i.target.closest(o);s&&r.contains(s)&&e.call(s,i,s)};return r.addEventListener(n,a),()=>r.removeEventListener(n,a)}static emit(t,n,o){let e=typeof t=="string"?document.querySelector(t):t,r=new CustomEvent(n,{bubbles:!0,cancelable:!0,detail:o});e.dispatchEvent(r)}};var l=class c{static dictionary={};static locale=typeof navigator<"u"?navigator.language:"en-US";static prefix="otm:";static the(...t){if(t.length===0)throw new TypeError("the(): missing args");return t[0]instanceof Element?c.#e(t[0],t.slice(1)):c.#e(document.body,t)}static#e(t,n){let[o,e]=n,r=t===document.body;if(n.length===1&&typeof o=="string")return c.#o(t,o);if(n.length===2&&typeof o=="string"){if(typeof e>"u")throw new TypeError(`the(${JSON.stringify(o)}, undefined): val is required for set`);return c.#t(t,o,e),r&&localStorage.setItem(`${c.prefix}${o}`,e),t}if(n.length===1&&o?.constructor===Object){for(let[a,i]of Object.entries(o))c.#t(t,a,i),r&&localStorage.setItem(`${c.prefix}${a}`,i);return t}throw new TypeError(`the(): unrecognized call shape (${n.map(a=>typeof a).join(", ")})`)}static flat(t,n="_"){if(t===null||typeof t!="object")throw new TypeError("the.flat: input must be an object");let o={},e=(r,a)=>{if(r===null||typeof r!="object"){o[a]=r;return}for(let[i,s]of Object.entries(r))e(s,a?`${a}${n}${i}`:i)};return e(t,""),o}static form(t){let n={},o=t.querySelectorAll("input, select, textarea");for(let e of o){let r=e.name;if(!r||e.disabled)continue;let a=(e.type||"text").toLowerCase();if(a==="submit"||a==="button"||a==="reset"||(a==="checkbox"||a==="radio")&&!(e.checked??e.hasAttribute("checked")))continue;let i=e.value??e.getAttribute("value")??"";c.#n(n,r,i)}return n}static#n(t,n,o){let e=n.split(/[\[\]]/).filter(Boolean),r=t;for(let a=0;a<e.length;a++){let i=e[a];a===e.length-1?r[i]!==void 0?(Array.isArray(r[i])||(r[i]=[r[i]]),r[i].push(o)):r[i]=o:(r[i]=r[i]||{},r=r[i])}}static _t(t,n={}){if(typeof Node<"u"?t instanceof Node:t?.nodeType){let a=t.querySelectorAll?[t,...t.querySelectorAll("[data-i18n]")]:[t];for(let i of a)if(i.hasAttribute?.("data-i18n")){let s=i.getAttribute("data-i18n"),d={};for(let u of i.attributes)if(u.name.startsWith("data-i18n-")&&u.name!=="data-i18n-type"){let y=u.name.replace("data-i18n-","");d[y]=u.value}let f=i.getAttribute("data-i18n-type");i.textContent=c._t(s,{...d,type:f})}return t}if(!t)return c._t(document.body),"";let e=c.dictionary[t];if(!e)return t;if(typeof e=="object"){let a=n.qty!==void 0?Number(n.qty):0,i=new Intl.PluralRules(c.locale).select(a);e=e[i]||e.other||t}if(typeof e!="string")return t;let r=e;for(let[a,i]of Object.entries(n)){let s=i;if(a==="val"&&n.type){let d=Number(i),f=n.type==="currency"?new Intl.NumberFormat(c.locale,{style:"currency",currency:"USD"}):n.type==="date"?new Intl.DateTimeFormat(c.locale):new Intl.NumberFormat(c.locale);s=n.type==="date"?f.format(new Date(i)):f.format(d)}r=r.replace(`{${a}}`,s)}return r}static route(t){if(typeof window>"u")return;let n=()=>t(window.location.pathname,window.location.search,window.location.hash);window.addEventListener("popstate",n),window.addEventListener("hashchange",n),document.addEventListener("click",o=>{let e=o.target.closest("a");if(!e||!e.getAttribute("href")||e.hasAttribute("data-external")||e.target==="_blank")return;let r=new URL(e.getAttribute("href"),window.location.href);r.origin===window.location.origin&&(r.pathname===window.location.pathname&&r.search===window.location.search&&r.hash||(o.preventDefault(),window.history.pushState({},"",r.href),n()))}),n()}static#o(t,n){let e={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"}[n]||`data-${n}`;return t.getAttribute(e)}static#t(t,n,o){let r={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"}[n]||`data-${n}`,a=typeof o=="boolean"?o?"true":"false":o;if(t.setAttribute(r,a),t.querySelectorAll){let i=t.querySelectorAll(`[data-text="${n}"]`);for(let s of i)s.textContent=a}t.getAttribute?.("data-text")===n&&(t.textContent=a)}static async boot({signal:t,locales:n,dictionary:o,namespace:e}={}){e&&(c.prefix=`${e}:`);let r=typeof window<"u"&&window.location?window.location.search:"",a=new URLSearchParams(r),i=(typeof navigator<"u"?navigator.language:null)||document.documentElement.lang||"en";if(c.locale=a.get("lang")||localStorage.getItem(`${c.prefix}lang`)||i,o)c.dictionary=o;else{let s=document.querySelector('meta[name="i18n"]'),d=n||s?.getAttribute("content");if(d){let f=s?.getAttribute("data-fallback")||"en",u=(s?.getAttribute("data-available")||"").split(",").map(p=>p.trim().toLowerCase()),y=c.locale.toLowerCase(),g=y.split("-")[0],w=f;u.includes(y)?w=y:u.includes(g)&&(w=g);try{let p=await fetch(`${d}/${w}.json`,{signal:t});p.ok&&(c.dictionary=await p.json())}catch(p){if(t?.aborted)throw p;console.warn("otm: i18n fetch failed",p)}}}for(let s=0;s<localStorage.length;s++){let d=localStorage.key(s);if(d.startsWith(c.prefix)){let f=d.slice(c.prefix.length);if(f!=="lang"){let u=localStorage.getItem(d);c.#t(document.body,f,u)}}}c._t()}};var h=class{static $(t,n){let o=t,e=n;return typeof o=="string"&&(e=o,o=document),o.querySelector(e)}static $$(t,n){let o=t,e=n;return typeof o=="string"&&(e=o,o=document),Array.from(o.querySelectorAll(e))}static clone(t,n){let o=typeof t=="string"?document.querySelector(t):t,e=document.querySelector(n);if(!o)throw new Error(`Parent not found: ${t}`);if(!e)throw new Error(`Template not found: ${n}`);let r=e.content.cloneNode(!0).firstElementChild;l._t(r),o.appendChild(r);let a=new CustomEvent("mounted",{bubbles:!0,detail:{parent:o}});return r.dispatchEvent(a),r}};var x=b.on;x.emit=b.emit;var m=l.the,$=l._t,A=l.route;m.t=$;m.route=A;m.form=l.form;m.flat=l.flat;m.boot=l.boot;Object.defineProperty(m,"dictionary",{get:()=>l.dictionary,set:c=>{l.dictionary=c}});Object.defineProperty(m,"locale",{get:()=>l.locale,set:c=>{l.locale=c}});var v=h.$;v.clone=h.clone;var S=h.$$,T={on:x,the:m,$:v,$$:S,_t:$};export{v as $,S as $$,$ as _t,T as default,x as on,A as route,m as the};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "on_the_money",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Opinionated, attribute-driven, standards-oriented modern framework. <2KB gzip. Native browser APIs only.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/on_the_money.min.js",
|
|
7
|
+
"module": "dist/on_the_money.min.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"otm-lint": "./src/linter/main.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/on_the_money.min.js",
|
|
13
|
+
"./src": "./src/core/index.js",
|
|
14
|
+
"./eslint-plugin": "./src/eslint/plugin.js",
|
|
15
|
+
"./eslint-config": "./src/eslint/config.js",
|
|
16
|
+
"./stylelint-plugin": "./src/stylelint/plugin.js",
|
|
17
|
+
"./stylelint-config": "./src/stylelint/config.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src/**/*.js",
|
|
21
|
+
"!src/**/*.test.js",
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "esbuild src/core/index.js --bundle --minify --format=esm --outfile=dist/on_the_money.min.js",
|
|
29
|
+
"test": "node --test --test-concurrency=1 src/core/*.test.js src/linter/*.test.js src/eslint/*.test.js src/stylelint/*.test.js test/integration.test.js",
|
|
30
|
+
"test:coverage": "node --test --test-concurrency=1 --experimental-test-coverage --test-coverage-lines=50 --test-coverage-branches=50 --test-coverage-functions=50 src/core/*.test.js src/linter/*.test.js src/eslint/*.test.js src/stylelint/*.test.js test/integration.test.js",
|
|
31
|
+
"check": "npm run lint && npm run build && npm run test && npm run test:coverage",
|
|
32
|
+
"lint": "biome check ./src ./test ./examples",
|
|
33
|
+
"lint:fix": "biome check --write ./src ./test ./examples",
|
|
34
|
+
"otm": "node src/linter/main.js --check examples",
|
|
35
|
+
"prepack": "npm run build",
|
|
36
|
+
"prepublishOnly": "npm run check"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=24.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"parse5": "7.2.1"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"stylelint": ">=16"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"stylelint": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@biomejs/biome": "1.9.4",
|
|
54
|
+
"esbuild": "0.25.0",
|
|
55
|
+
"eslint": "10.4.0",
|
|
56
|
+
"linkedom": "0.18.6",
|
|
57
|
+
"postcss-selector-parser": "7.1.1",
|
|
58
|
+
"stylelint": "17.11.1"
|
|
59
|
+
},
|
|
60
|
+
"license": "MIT"
|
|
61
|
+
}
|
package/src/core/On.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default class On {
|
|
2
|
+
static on(parent, event, selector, fn) {
|
|
3
|
+
const container =
|
|
4
|
+
typeof parent === "string"
|
|
5
|
+
? document.querySelector(parent)
|
|
6
|
+
: parent || document.body;
|
|
7
|
+
|
|
8
|
+
const handler = (e) => {
|
|
9
|
+
const target = e.target.closest(selector);
|
|
10
|
+
if (target && container.contains(target)) {
|
|
11
|
+
fn.call(target, e, target);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
container.addEventListener(event, handler);
|
|
15
|
+
return () => container.removeEventListener(event, handler);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static emit(el, event, detail) {
|
|
19
|
+
const target = typeof el === "string" ? document.querySelector(el) : el;
|
|
20
|
+
const customEvent = new CustomEvent(event, {
|
|
21
|
+
bubbles: true,
|
|
22
|
+
cancelable: true,
|
|
23
|
+
detail,
|
|
24
|
+
});
|
|
25
|
+
target.dispatchEvent(customEvent);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import The from "./The.js";
|
|
2
|
+
|
|
3
|
+
export default class Select {
|
|
4
|
+
static $(context, selector) {
|
|
5
|
+
let ctx = context;
|
|
6
|
+
let sel = selector;
|
|
7
|
+
if (typeof ctx === "string") {
|
|
8
|
+
sel = ctx;
|
|
9
|
+
ctx = document;
|
|
10
|
+
}
|
|
11
|
+
return ctx.querySelector(sel);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static $$(context, selector) {
|
|
15
|
+
let ctx = context;
|
|
16
|
+
let sel = selector;
|
|
17
|
+
if (typeof ctx === "string") {
|
|
18
|
+
sel = ctx;
|
|
19
|
+
ctx = document;
|
|
20
|
+
}
|
|
21
|
+
return Array.from(ctx.querySelectorAll(sel));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Clones a template and attaches it to a parent.
|
|
26
|
+
* This ensures the element is immediately part of the DOM lifecycle.
|
|
27
|
+
* @param {HTMLElement|string} parent - The target container
|
|
28
|
+
* @param {string} selector - The template ID/selector
|
|
29
|
+
* @returns {HTMLElement} The mounted element
|
|
30
|
+
*/
|
|
31
|
+
static clone(parent, selector) {
|
|
32
|
+
const container =
|
|
33
|
+
typeof parent === "string" ? document.querySelector(parent) : parent;
|
|
34
|
+
const template = document.querySelector(selector);
|
|
35
|
+
|
|
36
|
+
if (!container) throw new Error(`Parent not found: ${parent}`);
|
|
37
|
+
if (!template) throw new Error(`Template not found: ${selector}`);
|
|
38
|
+
|
|
39
|
+
const el = template.content.cloneNode(true).firstElementChild;
|
|
40
|
+
|
|
41
|
+
// Surgical hydration of the new fragment before mounting
|
|
42
|
+
The._t(el);
|
|
43
|
+
|
|
44
|
+
container.appendChild(el);
|
|
45
|
+
|
|
46
|
+
// Trigger a 'mounted' custom event for lifecycle hooks
|
|
47
|
+
const event = new CustomEvent("mounted", {
|
|
48
|
+
bubbles: true,
|
|
49
|
+
detail: { parent: container },
|
|
50
|
+
});
|
|
51
|
+
el.dispatchEvent(event);
|
|
52
|
+
|
|
53
|
+
return el;
|
|
54
|
+
}
|
|
55
|
+
}
|