cotomy 0.3.5 → 0.3.6

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/README.md CHANGED
@@ -1,317 +1,339 @@
1
- # Cotomy
2
-
3
- > This library targets ES2020+.
4
- > For older browsers (e.g. iOS 13 or IE), you will need a Polyfill such as `core-js`.
5
-
6
- **Cotomy** is a lightweight framework for managing form behavior and page controllers in web applications.
7
- It is suitable for both SPAs (Single Page Applications) and traditional web apps requiring dynamic form operations.
8
-
9
- ⚠️ **Warning**: This project is in early development. APIs may change without notice until version 1.0.0.
10
-
11
-
12
- To install Cotomy in your project, run the following command:
13
-
14
- ```bash
15
- npm i cotomy
16
- ```
17
-
18
- ## Usage
19
-
20
- Cotomy will continue to expand with more detailed usage instructions and code examples added to the README in the future.
21
- For the latest updates, please check the official documentation or repository regularly.
22
-
23
- ## View Reference
24
-
25
- The View layer provides thin wrappers around DOM elements and window events.
26
-
27
- - `CotomyElement` — A wrapper around `HTMLElement` with convenient utilities for scoped CSS, querying, attributes/styles, geometry, and event handling.
28
- - `CotomyMetaElement` — Convenience wrapper for `<meta>` tags.
29
- - `CotomyWindow` — A singleton that exposes window-level events and helpers.
30
-
31
- ### CotomyElement
32
-
33
- - Constructor
34
- - `new CotomyElement(element: HTMLElement)`
35
- - `new CotomyElement(html: string)` — Creates an element from HTML (single root required)
36
- - `new CotomyElement({ html, css? })` — Creates from HTML and injects scoped CSS
37
- - `new CotomyElement({ tagname, text?, css? })`
38
- - Scoped CSS
39
- - `scopeId: string` - Returns the value stored in the element's `data-cotomy-scopeid` attribute
40
- - `[scope]` placeholder in provided CSS is replaced by `[data-cotomy-scopeid="..."]`
41
- - `stylable: boolean` - False for tags like `script`, `style`, `link`, `meta`
42
- - Static helpers
43
- - `CotomyElement.encodeHtml(text)`
44
- - `CotomyElement.first(selector, type?)`
45
- - `CotomyElement.last(selector, type?)`
46
- - `CotomyElement.find(selector, type?)`
47
- - `CotomyElement.contains(selector)` / `CotomyElement.containsById(id)`
48
- - `CotomyElement.byId(id, type?)`
49
- - `CotomyElement.empty(type?)` — Creates a hidden placeholder element
50
- - Identity & matching
51
- - `id: string | null | undefined`
52
- - `generateId(prefix = "__cotomy_elem__"): this`
53
- - `is(selector: string): boolean` — Parent-aware matching helper
54
- - `empty: boolean` — True for tags that cannot have children or have no content
55
- - Attributes, classes, styles
56
- - `attribute(name)` / `attribute(name, value | null): this`
57
- - `hasAttribute(name): boolean`
58
- - `addClass(name): this` / `removeClass(name): this` / `toggleClass(name, force?): this` / `hasClass(name): boolean`
59
- - `style(name)` / `style(name, value | null): this`
60
- - Content & value
61
- - `text: string` (get/set)
62
- - `html: string` (get/set)
63
- - `value: string` — Works for inputs; falls back to `data-cotomy-value` otherwise
64
- - `readonly: boolean` (get/set) — Uses native property if available, otherwise attribute
65
- - `enabled: boolean` (get/set) — Toggles `disabled` attribute
66
- - `setFocus(): void`
67
- - Tree traversal & manipulation
68
- - `parent: CotomyElement`
69
- - `parents: CotomyElement[]`
70
- - `children(selector = "*", type?): T[]` (direct children only)
71
- - `firstChild(selector = "*", type?)`
72
- - `lastChild(selector = "*", type?)`
73
- - `closest(selector, type?)`
74
- - `find(selector, type?)` / `first(selector = "*", type?)` / `last(selector = "*", type?)` / `contains(selector)`
75
- - `append(child): this` / `prepend(child): this` / `appendAll(children): this`
76
- - `insertBefore(sibling): this` / `insertAfter(sibling): this`
77
- - `appendTo(target): this` / `prependTo(target): this`
78
- - `clone(type?): CotomyElement` - Returns a deep-cloned element, optionally typed, and reassigns new `data-cotomy-scopeid` values to the clone and all descendants so scoped CSS and event registries stay isolated
79
- - `clear(): this` — Removes all descendants and text
80
- - `remove(): void`
81
- - Geometry & visibility
82
- - `visible: boolean`
83
- - `width: number` (get/set px)
84
- - `height: number` (get/set px)
85
- - `innerWidth: number` / `innerHeight: number`
86
- - `outerWidth: number` / `outerHeight: number` — Includes margins
87
- - `scrollWidth: number` / `scrollHeight: number` / `scrollTop: number`
88
- - `position(): { top, left }` — Relative to viewport
89
- - `absolutePosition(): { top, left }` — Viewport + page scroll offset
90
- - `screenPosition(): { top, left }`
91
- - `rect(): { top, left, width, height }`
92
- - `innerRect()` — Subtracts padding
93
- - Events
94
- - Generic: `on(eventOrEvents, handler, options?)`, `off(eventOrEvents, handler?, options?)`, `once(eventOrEvents, handler, options?)`, `trigger(event[, Event])` — `eventOrEvents` accepts either a single event name or an array for batch registration/removal. `trigger` emits bubbling events by default and can be customized by passing an `Event`.
95
- - Delegation: `onSubTree(eventOrEvents, selector, handler, options?)` — `eventOrEvents` can also be an array for listening to multiple delegated events at once.
96
- - Mouse: `click`, `dblclick`, `mouseover`, `mouseout`, `mousedown`, `mouseup`, `mousemove`, `mouseenter`, `mouseleave`
97
- - Keyboard: `keydown`, `keyup`, `keypress`
98
- - Inputs: `change`, `input`
99
- - Focus: `focus`, `blur`, `focusin`, `focusout`
100
- - Viewport: `inview`, `outview` (uses `IntersectionObserver`)
101
- - Layout (custom): `resize`, `scroll`, `changelayout` — requires `listenLayoutEvents()` on the element
102
- - Move lifecycle: `cotomy:transitstart`, `cotomy:transitend` — emitted automatically by `append`, `prepend`, `insertBefore/After`, `appendTo`, and `prependTo`. While moving, the element (and its descendants) receive a temporary `data-cotomy-moving` attribute so removal observers know the node is still in transit.
103
- - Removal: `removed` — fired when an element actually leaves the DOM (MutationObserver-backed). Because `cotomy:transitstart`/`transitend` manage the `data-cotomy-moving` flag, `removed` only runs for true detachments, making it safe for cleanup.
104
- - File: `filedrop(handler: (files: File[]) => void)`
105
-
106
- Example (scoped CSS and events):
107
-
108
- ```ts
109
- import { CotomyElement } from "cotomy";
110
-
111
- const panel = new CotomyElement({
112
- html: `<div class="panel"><button class="ok">OK</button></div>`,
113
- css: `
114
- [scope] .panel { padding: 8px; }
115
- [scope] .ok { color: green; }
116
- `,
117
- });
118
-
119
- panel.onSubTree("click", ".ok", () => console.log("clicked!"));
120
- document.body.appendChild(panel.element);
121
- ```
122
-
123
- ## Testing
124
-
125
- The scoped CSS replacement and scope-id isolation logic are covered by `tests/view.spec.ts`. Run the focused specs below to verify the behavior:
126
-
127
- ```bash
128
- npx vitest run tests/view.spec.ts -t "constructs from multiple sources and applies scoped css"
129
- npx vitest run tests/view.spec.ts -t "assigns fresh scope ids when cloning, including descendants"
130
- ```
131
-
132
- The first command ensures `[scope]` expands to `[data-cotomy-scopeid="..."]` in injected styles, while the second confirms that cloning reassigns new `data-cotomy-scopeid` attributes to the cloned tree.
133
-
134
- ### CotomyMetaElement
135
-
136
- - `CotomyMetaElement.get(name): CotomyMetaElement`
137
- - `content: string` — Reads `content` attribute.
138
-
139
- ### CotomyWindow
140
-
141
- - Singleton
142
- - `CotomyWindow.instance`
143
- - `initialized: boolean` — Call `initialize()` once after DOM is ready
144
- - `initialize(): void`
145
- - DOM helpers
146
- - `body: CotomyElement`
147
- - `append(element: CotomyElement)`
148
- - `moveNext(focused: CotomyElement, shift = false)` — Move focus to next/previous focusable
149
- - Window events
150
- - `on(eventOrEvents, handler)` / `off(eventOrEvents, handler?)` / `trigger(event[, Event])` — `eventOrEvents` accepts a single event name or an array. CotomyWindow’s `trigger` also bubbles by default and accepts an `Event` to override the behavior.
151
- - `load(handler)` / `ready(handler)`
152
- - `resize([handler])` / `scroll([handler])` / `changeLayout([handler])` / `pageshow([handler])`
153
- - Window state
154
- - `scrollTop`, `scrollLeft`, `width`, `height`, `documentWidth`, `documentHeight`
155
- - `reload(): void` (sets internal `reloading` flag), `reloading: boolean`
156
-
157
- Quick start:
158
-
159
- ```ts
160
- import { CotomyWindow, CotomyElement } from "cotomy";
161
-
162
- CotomyWindow.instance.initialize();
163
- CotomyWindow.instance.ready(() => {
164
- const el = new CotomyElement("<div>Hello</div>");
165
- CotomyWindow.instance.append(el);
166
- });
167
- ```
168
-
169
- ## Form Reference
170
-
171
- The Form layer builds on `CotomyElement` for common form flows.
172
-
173
- - `CotomyForm` — Base class with submit lifecycle hooks
174
- - `CotomyQueryForm` — Submits to query string (GET)
175
- - `CotomyApiForm` — Submits via `CotomyApi` (handles `FormData`, errors, events)
176
- - `CotomyEntityApiForm` — REST entity helper with surrogate key support
177
- - `CotomyEntityFillApiForm` — Adds automatic field filling and simple view binding
178
-
179
- ### CotomyForm (base)
180
-
181
- - Construction & basics
182
- - Extends `CotomyElement` and expects a `<form>` element
183
- - `initialize(): this` — Wires a `submit` listener that calls `submitAsync()`
184
- - `initialized: boolean` — Set after `initialize()`
185
- - `submitAsync(): Promise<void>` — Abstract in base
186
- - Routing & reload
187
- - `method: string` — Getter that defaults to `get` in base; specialized in subclasses
188
- - `actionUrl: string` — Getter that defaults to the `action` attribute or current path
189
- - `reloadAsync(): Promise<void>` — Page reload using `CotomyWindow`
190
- - `autoReload: boolean` — Backed by `data-cotomy-autoreload` (default true)
191
-
192
- ### CotomyQueryForm
193
-
194
- - Always uses `GET`
195
- - `submitAsync()` merges current query string with form inputs and navigates via `location.href`.
196
-
197
- ### CotomyApiForm
198
-
199
- - API integration
200
- - `apiClient(): CotomyApi` — Override to inject a client; default creates a new one
201
- - `actionUrl: string` — Uses `action` attribute
202
- - `method: string` — Defaults to `post`
203
- - `formData(): FormData` — Builds from form, converts `datetime-local` to ISO (UTC offset)
204
- - `submitAsync()` — Calls `submitToApiAsync(formData)`
205
- - `submitToApiAsync(formData): Promise<CotomyApiResponse>` — Uses `CotomyApi.submitAsync`
206
- - Events
207
- - `apiFailed(handler)` — Listens to `cotomy:apifailed`
208
- - `submitFailed(handler)` — Listens to `cotomy:submitfailed`
209
- - Both events bubble from the form element; payload is `CotomyApiFailedEvent`
210
-
211
- ### CotomyEntityApiForm
212
-
213
- - Surrogate key flow
214
- - `data-cotomy-entity-key` — Holds the entity identifier if present
215
- - `data-cotomy-identify` — Defaults to true; when true and `201 Created` is returned, the form extracts the key from `Location` and stores it in `data-cotomy-entity-key`
216
- - `actionUrl` — Appends the key to the base `action` when present; otherwise normalizes trailing slash for collection URL
217
- - `method` — `put` when key exists; otherwise `post` (unless `method` attribute is explicitly set)
218
-
219
- ### CotomyEntityFillApiForm
220
-
221
- - Data loading and field filling
222
- - `initialize()` — Adds default fillers and triggers `loadAsync()` on `CotomyWindow.ready`
223
- - `reloadAsync()` — Alias to `loadAsync()`
224
- - `loadAsync(): Promise<CotomyApiResponse>` — Calls `CotomyApi.getAsync` when `canLoad()` is true
225
- - `loadActionUrl(): string` — Defaults to `actionUrl`; override for custom endpoints
226
- - `canLoad(): boolean` — Defaults to `hasEntityKey`
227
- - Naming & binding
228
- - `bindNameGenerator(): ICotomyBindNameGenerator` — Defaults to `CotomyBracketBindNameGenerator` (`user[name]`)
229
- - `renderer(): CotomyViewRenderer` — Applies `[data-cotomy-bind]` to view elements
230
- - `filler(type, (input, value))` — Register fillers; defaults provided for `datetime-local`, `checkbox`, `radio`
231
- - Fills non-array, non-object fields by matching input/select/textarea `name`
232
-
233
- #### Array binding
234
-
235
- - Both `CotomyViewRenderer.applyAsync` and `CotomyEntityFillApiForm.fillAsync` resolve array elements by index via the active `ICotomyBindNameGenerator` (dot style → `items[0].name`, bracket style → `items[0][name]`).
236
- - Cotomy does **not** create or clone templates for you. Prepare the necessary DOM (e.g., table rows, list items, individual inputs) ahead of time, then call `fillAsync`/`applyAsync` to populate the values.
237
- - Primitive arrays (strings, numbers, booleans, etc.) are treated the same way—have matching `[data-cotomy-bind]`/`name` attributes ready for every index you want to show.
238
- - If you need dynamic row counts, generate the markup yourself before invoking Cotomy; the framework purposely avoids mutating the structure so it does not get in your way.
239
-
240
- Example:
241
-
242
- ```ts
243
- import { CotomyEntityFillApiForm } from "cotomy";
244
-
245
- const form = new CotomyEntityFillApiForm(document.querySelector("form")!);
246
- form.initialize();
247
- form.apiFailed(e => console.error("API failed", e.response.status));
248
- form.submitFailed(e => console.warn("Submit failed", e.response.status));
249
- ```
250
-
251
- ### Entity API forms
252
-
253
- `CotomyEntityApiForm` targets REST endpoints that identify records with a single surrogate key.
254
- Attach `data-cotomy-entity-key="<id>"` to the form when editing an existing entity; omit the attribute (or leave it empty) to issue a `POST` to the base `action` URL.
255
- On `201 Created`, the form reads the `Location` header and stores the generated key back into `data-cotomy-entity-key`, enabling subsequent `PUT` submissions.
256
- Composite or natural keys are no longer supported—migrate any legacy markup that relied on `data-cotomy-keyindex` or multiple key inputs to the new surrogate-key flow.
257
- When you must integrate with endpoints that still expect natural identifiers, subclass `CotomyEntityApiForm`/`CotomyEntityFillApiForm`, override `canLoad()` to supply your own load condition, and adjust `loadActionUrl()` (plus any submission hooks) to build the appropriate URL fragments.
258
-
259
- The core of Cotomy is `CotomyElement`, which is constructed as a wrapper for `Element`.
260
- By passing HTML and CSS strings to the constructor, it is possible to generate Element designs with a limited scope.
261
-
262
- ```typescript
263
- const ce = new CotomyElement({
264
- html: /* html */`
265
- <div>
266
- <p>Text</p>
267
- </div>
268
- `,
269
- css: /* css */`
270
- [scope] {
271
- display: block;
272
- }
273
- [scope] > p {
274
- text-align: center;
275
- }
276
- `
277
- });
278
- ```
279
-
280
- - `"display HTML in character literals with color coding"` → `"syntax highlighting for embedded HTML"`
281
- - `"generate Element designs with a limited scope"` `"generate scoped DOM elements with associated styles"`
282
-
283
- ## Development
284
-
285
- Cotomy ships with both ESM (`dist/esm`) and CommonJS (`dist/cjs`) builds, plus generated type definitions in `dist/types`.
286
- For direct `<script>` usage, browser-ready bundles are available at `dist/browser/cotomy.js` and `dist/browser/cotomy.min.js` (also served via the npm `unpkg` entry).
287
- Include the minified build like so:
288
-
289
- ```html
290
- <script src="https://unpkg.com/cotomy/dist/browser/cotomy.min.js"></script>
291
- <script>
292
- const el = new Cotomy.CotomyElement("<div>Hello</div>");
293
- document.body.appendChild(el.element);
294
- </script>
295
- ```
296
-
297
- Run the build to refresh every target bundle:
298
-
299
- ```bash
300
- npm install
301
- npm run build
302
- ```
303
-
304
- The Vitest-based test suite can be executed via:
305
-
306
- ```bash
307
- npx vitest run
308
- ```
309
-
310
- ## License
311
-
312
- This project is licensed under the [MIT License](LICENSE).
313
-
314
- ## Contact
315
-
316
- You can reach out to me at: [yshr1920@gmail.com](mailto:yshr1920@gmail.com)
317
- GitHub repository: [https://github.com/yshr1920/cotomy](https://github.com/yshr1920/cotomy)
1
+ # Cotomy
2
+
3
+ > This library targets ES2020+.
4
+ > For older browsers (e.g. iOS 13 or IE), you will need a Polyfill such as `core-js`.
5
+
6
+ **Cotomy** is a lightweight framework for managing form behavior and page controllers in web applications.
7
+ It is suitable for both SPAs (Single Page Applications) and traditional web apps requiring dynamic form operations.
8
+
9
+ ⚠️ **Warning**: This project is in early development. APIs may change without notice until version 1.0.0.
10
+
11
+
12
+ To install Cotomy in your project, run the following command:
13
+
14
+ ```bash
15
+ npm i cotomy
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Cotomy will continue to expand with more detailed usage instructions and code examples added to the README in the future.
21
+ For the latest updates, please check the official documentation or repository regularly.
22
+
23
+ ## View Reference
24
+
25
+ The View layer provides thin wrappers around DOM elements and window events.
26
+
27
+ - `CotomyElement` — A wrapper around `HTMLElement` with convenient utilities for scoped CSS, querying, attributes/styles, geometry, and event handling.
28
+ - `CotomyMetaElement` — Convenience wrapper for `<meta>` tags.
29
+ - `CotomyWindow` — A singleton that exposes window-level events and helpers.
30
+
31
+ ### CotomyElement
32
+
33
+ - Constructor
34
+ - `new CotomyElement(element: HTMLElement)`
35
+ - `new CotomyElement(html: string)` — Creates an element from HTML (single root required)
36
+ - `new CotomyElement({ html, css? })` — Creates from HTML and injects scoped CSS
37
+ - `new CotomyElement({ tagname, text?, css? })`
38
+ - Scoped CSS
39
+ - `scopeId: string` - Returns the value stored in the element's `data-cotomy-scopeid` attribute
40
+ - `[scope]` placeholder in provided CSS is replaced by `[data-cotomy-scopeid="..."]`
41
+ - `stylable: boolean` - False for tags like `script`, `style`, `link`, `meta`
42
+ - Static helpers
43
+ - `CotomyElement.encodeHtml(text)`
44
+ - `CotomyElement.first(selector, type?)`
45
+ - `CotomyElement.last(selector, type?)`
46
+ - `CotomyElement.find(selector, type?)`
47
+ - `CotomyElement.contains(selector)` / `CotomyElement.containsById(id)`
48
+ - `CotomyElement.byId(id, type?)`
49
+ - `CotomyElement.empty(type?)` — Creates a hidden placeholder element
50
+ - Identity & matching
51
+ - `id: string | null | undefined`
52
+ - `generateId(prefix = "__cotomy_elem__"): this`
53
+ - `is(selector: string): boolean` — Parent-aware matching helper
54
+ - `empty: boolean` — True for tags that cannot have children or have no content
55
+ - Attributes, classes, styles
56
+ - `attribute(name)` / `attribute(name, value | null): this`
57
+ - `hasAttribute(name): boolean`
58
+ - `addClass(name): this` / `removeClass(name): this` / `toggleClass(name, force?): this` / `hasClass(name): boolean`
59
+ - `style(name)` / `style(name, value | null): this`
60
+ - Content & value
61
+ - `text: string` (get/set)
62
+ - `html: string` (get/set)
63
+ - `value: string` — Works for inputs; falls back to `data-cotomy-value` otherwise
64
+ - `readonly: boolean` (get/set) — Uses native property if available, otherwise attribute
65
+ - `enabled: boolean` (get/set) — Toggles `disabled` attribute
66
+ - `setFocus(): void`
67
+ - Tree traversal & manipulation
68
+ - `parent: CotomyElement`
69
+ - `parents: CotomyElement[]`
70
+ - `children(selector = "*", type?): T[]` (direct children only)
71
+ - `firstChild(selector = "*", type?)`
72
+ - `lastChild(selector = "*", type?)`
73
+ - `closest(selector, type?)`
74
+ - `find(selector, type?)` / `first(selector = "*", type?)` / `last(selector = "*", type?)` / `contains(selector)`
75
+ - `append(child): this` / `prepend(child): this` / `appendAll(children): this`
76
+ - `insertBefore(sibling): this` / `insertAfter(sibling): this`
77
+ - `appendTo(target): this` / `prependTo(target): this`
78
+ - `clone(type?): CotomyElement` - Returns a deep-cloned element, optionally typed, and reassigns new `data-cotomy-scopeid` values to the clone and all descendants so scoped CSS and event registries stay isolated
79
+ - `clear(): this` — Removes all descendants and text
80
+ - `remove(): void`
81
+ - Geometry & visibility
82
+ - `visible: boolean`
83
+ - `width: number` (get/set px)
84
+ - `height: number` (get/set px)
85
+ - `innerWidth: number` / `innerHeight: number`
86
+ - `outerWidth: number` / `outerHeight: number` — Includes margins
87
+ - `scrollWidth: number` / `scrollHeight: number` / `scrollTop: number`
88
+ - `position(): { top, left }` — Relative to viewport
89
+ - `absolutePosition(): { top, left }` — Viewport + page scroll offset
90
+ - `screenPosition(): { top, left }`
91
+ - `rect(): { top, left, width, height }`
92
+ - `innerRect()` — Subtracts padding
93
+ - Events
94
+ - Generic: `on(eventOrEvents, handler, options?)`, `off(eventOrEvents, handler?, options?)`, `once(eventOrEvents, handler, options?)`, `trigger(event[, Event])` — `eventOrEvents` accepts either a single event name or an array for batch registration/removal. `trigger` emits bubbling events by default and can be customized by passing an `Event`.
95
+ - Delegation: `onSubTree(eventOrEvents, selector, handler, options?)` — `eventOrEvents` can also be an array for listening to multiple delegated events at once.
96
+ - Mouse: `click`, `dblclick`, `mouseover`, `mouseout`, `mousedown`, `mouseup`, `mousemove`, `mouseenter`, `mouseleave`
97
+ - Keyboard: `keydown`, `keyup`, `keypress`
98
+ - Inputs: `change`, `input`
99
+ - Focus: `focus`, `blur`, `focusin`, `focusout`
100
+ - Viewport: `inview`, `outview` (uses `IntersectionObserver`)
101
+ - Layout (custom): `resize`, `scroll`, `changelayout` — requires `listenLayoutEvents()` on the element
102
+ - Move lifecycle: `cotomy:transitstart`, `cotomy:transitend` — emitted automatically by `append`, `prepend`, `insertBefore/After`, `appendTo`, and `prependTo`. While moving, the element (and its descendants) receive a temporary `data-cotomy-moving` attribute so removal observers know the node is still in transit.
103
+ - Removal: `removed` — fired when an element actually leaves the DOM (MutationObserver-backed). Because `cotomy:transitstart`/`transitend` manage the `data-cotomy-moving` flag, `removed` only runs for true detachments, making it safe for cleanup.
104
+ - File: `filedrop(handler: (files: File[]) => void)`
105
+
106
+ Example (scoped CSS and events):
107
+
108
+ ```ts
109
+ import { CotomyElement } from "cotomy";
110
+
111
+ const panel = new CotomyElement({
112
+ html: `<div class="panel"><button class="ok">OK</button></div>`,
113
+ css: `
114
+ [scope] .panel { padding: 8px; }
115
+ [scope] .ok { color: green; }
116
+ `,
117
+ });
118
+
119
+ panel.onSubTree("click", ".ok", () => console.log("clicked!"));
120
+ document.body.appendChild(panel.element);
121
+ ```
122
+
123
+ ## Testing
124
+
125
+ The scoped CSS replacement and scope-id isolation logic are covered by `tests/view.spec.ts`. Run the focused specs below to verify the behavior:
126
+
127
+ ```bash
128
+ npx vitest run tests/view.spec.ts -t "constructs from multiple sources and applies scoped css"
129
+ npx vitest run tests/view.spec.ts -t "assigns fresh scope ids when cloning, including descendants"
130
+ ```
131
+
132
+ The first command ensures `[scope]` expands to `[data-cotomy-scopeid="..."]` in injected styles, while the second confirms that cloning reassigns new `data-cotomy-scopeid` attributes to the cloned tree.
133
+
134
+ ### CotomyMetaElement
135
+
136
+ - `CotomyMetaElement.get(name): CotomyMetaElement`
137
+ - `content: string` — Reads `content` attribute.
138
+
139
+ ### CotomyWindow
140
+
141
+ - Singleton
142
+ - `CotomyWindow.instance`
143
+ - `initialized: boolean` — Call `initialize()` once after DOM is ready
144
+ - `initialize(): void`
145
+ - DOM helpers
146
+ - `body: CotomyElement`
147
+ - `append(element: CotomyElement)`
148
+ - `moveNext(focused: CotomyElement, shift = false)` — Move focus to next/previous focusable
149
+ - Window events
150
+ - `on(eventOrEvents, handler)` / `off(eventOrEvents, handler?)` / `trigger(event[, Event])` — `eventOrEvents` accepts a single event name or an array. CotomyWindow’s `trigger` also bubbles by default and accepts an `Event` to override the behavior.
151
+ - `load(handler)` / `ready(handler)`
152
+ - `resize([handler])` / `scroll([handler])` / `changeLayout([handler])` / `pageshow([handler])`
153
+ - Window state
154
+ - `scrollTop`, `scrollLeft`, `width`, `height`, `documentWidth`, `documentHeight`
155
+ - `reload(): void` (sets internal `reloading` flag), `reloading: boolean`
156
+
157
+ Quick start:
158
+
159
+ ```ts
160
+ import { CotomyWindow, CotomyElement } from "cotomy";
161
+
162
+ CotomyWindow.instance.initialize();
163
+ CotomyWindow.instance.ready(() => {
164
+ const el = new CotomyElement("<div>Hello</div>");
165
+ CotomyWindow.instance.append(el);
166
+ });
167
+ ```
168
+
169
+ ## Form Reference
170
+
171
+ The Form layer builds on `CotomyElement` for common form flows.
172
+
173
+ - `CotomyForm` — Base class with submit lifecycle hooks
174
+ - `CotomyQueryForm` — Submits to query string (GET)
175
+ - `CotomyApiForm` — Submits via `CotomyApi` (handles `FormData`, errors, events)
176
+ - `CotomyEntityApiForm` — REST entity helper with surrogate key support
177
+ - `CotomyEntityFillApiForm` — Adds automatic field filling and simple view binding
178
+
179
+ ### CotomyForm (base)
180
+
181
+ - Construction & basics
182
+ - Extends `CotomyElement` and expects a `<form>` element
183
+ - `initialize(): this` — Wires a `submit` listener that calls `submitAsync()`
184
+ - `initialized: boolean` — Set after `initialize()`
185
+ - `submitAsync(): Promise<void>` — Abstract in base
186
+ - Routing & reload
187
+ - `method: string` — Getter that defaults to `get` in base; specialized in subclasses
188
+ - `actionUrl: string` — Getter that defaults to the `action` attribute or current path
189
+ - `reloadAsync(): Promise<void>` — Page reload using `CotomyWindow`
190
+ - `autoReload: boolean` — Backed by `data-cotomy-autoreload` (default true)
191
+
192
+ ### CotomyQueryForm
193
+
194
+ - Always uses `GET`
195
+ - `submitAsync()` merges current query string with form inputs and navigates via `location.href`.
196
+
197
+ ### CotomyApiForm
198
+
199
+ - API integration
200
+ - `apiClient(): CotomyApi` — Override to inject a client; default creates a new one
201
+ - `actionUrl: string` — Uses `action` attribute
202
+ - `method: string` — Defaults to `post`
203
+ - `formData(): FormData` — Builds from form, converts `datetime-local` to ISO (UTC offset)
204
+ - `submitAsync()` — Calls `submitToApiAsync(formData)`
205
+ - `submitToApiAsync(formData): Promise<CotomyApiResponse>` — Uses `CotomyApi.submitAsync`
206
+ - Events
207
+ - `apiFailed(handler)` — Listens to `cotomy:apifailed`
208
+ - `submitFailed(handler)` — Listens to `cotomy:submitfailed`
209
+ - Both events bubble from the form element; payload is `CotomyApiFailedEvent`
210
+
211
+ ### CotomyEntityApiForm
212
+
213
+ - Surrogate key flow
214
+ - `data-cotomy-entity-key` — Holds the entity identifier if present
215
+ - `data-cotomy-identify` — Defaults to true; when true and `201 Created` is returned, the form extracts the key from `Location` and stores it in `data-cotomy-entity-key`
216
+ - `actionUrl` — Appends the key to the base `action` when present; otherwise normalizes trailing slash for collection URL
217
+ - `method` — `put` when key exists; otherwise `post` (unless `method` attribute is explicitly set)
218
+
219
+ ### CotomyEntityFillApiForm
220
+
221
+ - Data loading and field filling
222
+ - `initialize()` — Adds default fillers and triggers `loadAsync()` on `CotomyWindow.ready`
223
+ - `reloadAsync()` — Alias to `loadAsync()`
224
+ - `loadAsync(): Promise<CotomyApiResponse>` — Calls `CotomyApi.getAsync` when `canLoad()` is true
225
+ - `loadActionUrl(): string` — Defaults to `actionUrl`; override for custom endpoints
226
+ - `canLoad(): boolean` — Defaults to `hasEntityKey`
227
+ - Naming & binding
228
+ - `bindNameGenerator(): ICotomyBindNameGenerator` — Defaults to `CotomyBracketBindNameGenerator` (`user[name]`)
229
+ - `renderer(): CotomyViewRenderer` — Applies `[data-cotomy-bind]` to view elements
230
+ - `filler(type, (input, value))` — Register fillers; defaults provided for `datetime-local`, `checkbox`, `radio`
231
+ - Fills non-array, non-object fields by matching input/select/textarea `name`
232
+
233
+ #### View binding renderers
234
+
235
+ `CotomyViewRenderer` includes a few built-in helpers for `[data-cotomy-bindtype]`:
236
+
237
+ - `mail`, `tel`, `url` Wrap the value in a corresponding anchor tag.
238
+ - `number` Uses `Intl.NumberFormat` with `data-cotomy-locale`/`data-cotomy-currency` inheritance.
239
+ - `utc` — Treats the value as UTC (or appends `Z` when missing) and formats with `data-cotomy-format` (default `YYYY/MM/DD HH:mm`).
240
+ - `date` — Renders local dates with `data-cotomy-format` (default `YYYY/MM/DD`) when the input is a valid `Date` value.
241
+
242
+ Example:
243
+
244
+ ```ts
245
+ const view = new CotomyViewRenderer(
246
+ new CotomyElement(document.querySelector("#profile")!),
247
+ new CotomyBracketBindNameGenerator()
248
+ );
249
+
250
+ await view.applyAsync(apiResponse); // apiResponse is CotomyApiResponse from CotomyApi
251
+ // <span data-cotomy-bind="user.birthday" data-cotomy-bindtype="date" data-cotomy-format="MMM D, YYYY"></span>
252
+ // → renders localized date text if the API payload contains user.birthday
253
+ ```
254
+
255
+ #### Array binding
256
+
257
+ - Both `CotomyViewRenderer.applyAsync` and `CotomyEntityFillApiForm.fillAsync` resolve array elements by index via the active `ICotomyBindNameGenerator` (dot style `items[0].name`, bracket style `items[0][name]`).
258
+ - Cotomy does **not** create or clone templates for you. Prepare the necessary DOM (e.g., table rows, list items, individual inputs) ahead of time, then call `fillAsync`/`applyAsync` to populate the values.
259
+ - Primitive arrays (strings, numbers, booleans, etc.) are treated the same way—have matching `[data-cotomy-bind]`/`name` attributes ready for every index you want to show.
260
+ - If you need dynamic row counts, generate the markup yourself before invoking Cotomy; the framework purposely avoids mutating the structure so it does not get in your way.
261
+
262
+ Example:
263
+
264
+ ```ts
265
+ import { CotomyEntityFillApiForm } from "cotomy";
266
+
267
+ const form = new CotomyEntityFillApiForm(document.querySelector("form")!);
268
+ form.initialize();
269
+ form.apiFailed(e => console.error("API failed", e.response.status));
270
+ form.submitFailed(e => console.warn("Submit failed", e.response.status));
271
+ ```
272
+
273
+ ### Entity API forms
274
+
275
+ `CotomyEntityApiForm` targets REST endpoints that identify records with a single surrogate key.
276
+ Attach `data-cotomy-entity-key="<id>"` to the form when editing an existing entity; omit the attribute (or leave it empty) to issue a `POST` to the base `action` URL.
277
+ On `201 Created`, the form reads the `Location` header and stores the generated key back into `data-cotomy-entity-key`, enabling subsequent `PUT` submissions.
278
+ Composite or natural keys are no longer supported—migrate any legacy markup that relied on `data-cotomy-keyindex` or multiple key inputs to the new surrogate-key flow.
279
+ When you must integrate with endpoints that still expect natural identifiers, subclass `CotomyEntityApiForm`/`CotomyEntityFillApiForm`, override `canLoad()` to supply your own load condition, and adjust `loadActionUrl()` (plus any submission hooks) to build the appropriate URL fragments.
280
+
281
+ The core of Cotomy is `CotomyElement`, which is constructed as a wrapper for `Element`.
282
+ By passing HTML and CSS strings to the constructor, it is possible to generate Element designs with a limited scope.
283
+
284
+ ```typescript
285
+ const ce = new CotomyElement({
286
+ html: /* html */`
287
+ <div>
288
+ <p>Text</p>
289
+ </div>
290
+ `,
291
+ css: /* css */`
292
+ [scope] {
293
+ display: block;
294
+ }
295
+ [scope] > p {
296
+ text-align: center;
297
+ }
298
+ `
299
+ });
300
+ ```
301
+
302
+ - `"display HTML in character literals with color coding"` → `"syntax highlighting for embedded HTML"`
303
+ - `"generate Element designs with a limited scope"` → `"generate scoped DOM elements with associated styles"`
304
+
305
+ ## Development
306
+
307
+ Cotomy ships with both ESM (`dist/esm`) and CommonJS (`dist/cjs`) builds, plus generated type definitions in `dist/types`.
308
+ For direct `<script>` usage, browser-ready bundles are available at `dist/browser/cotomy.js` and `dist/browser/cotomy.min.js` (also served via the npm `unpkg` entry).
309
+ Include the minified build like so:
310
+
311
+ ```html
312
+ <script src="https://unpkg.com/cotomy/dist/browser/cotomy.min.js"></script>
313
+ <script>
314
+ const el = new Cotomy.CotomyElement("<div>Hello</div>");
315
+ document.body.appendChild(el.element);
316
+ </script>
317
+ ```
318
+
319
+ Run the build to refresh every target bundle:
320
+
321
+ ```bash
322
+ npm install
323
+ npm run build
324
+ ```
325
+
326
+ The Vitest-based test suite can be executed via:
327
+
328
+ ```bash
329
+ npx vitest run
330
+ ```
331
+
332
+ ## License
333
+
334
+ This project is licensed under the [MIT License](LICENSE).
335
+
336
+ ## Contact
337
+
338
+ You can reach out to me at: [yshr1920@gmail.com](mailto:yshr1920@gmail.com)
339
+ GitHub repository: [https://github.com/yshr1920/cotomy](https://github.com/yshr1920/cotomy)