cotomy 0.1.72 → 0.2.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 CHANGED
@@ -1,20 +1,20 @@
1
- The MIT License (MIT)
2
- Copyright (c) 2025 Yasuhiro Arakawa
3
-
4
- Permission is hereby granted, free of charge, to any person obtaining a copy
5
- of this software and associated documentation files (the "Software"), to deal
6
- in the Software without restriction, including without limitation the rights
7
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- copies of the Software, and to permit persons to whom the Software is
9
- furnished to do so, subject to the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be included in all
12
- copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
- SOFTWARE.
1
+ The MIT License (MIT)
2
+ Copyright (c) 2025 Yasuhiro Arakawa
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md CHANGED
@@ -1,22 +1,22 @@
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
-
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
20
  Cotomy will continue to expand with more detailed usage instructions and code examples added to the README in the future.
21
21
  For the latest updates, please check the official documentation or repository regularly.
22
22
 
@@ -75,6 +75,7 @@ The View layer provides thin wrappers around DOM elements and window events.
75
75
  - `append(child): this` / `prepend(child): this` / `appendAll(children): this`
76
76
  - `insertBefore(sibling): this` / `insertAfter(sibling): this`
77
77
  - `appendTo(target): this` / `prependTo(target): this`
78
+ - `clone(type?): CotomyElement` — Returns a deep-cloned element, optionally typed
78
79
  - `clear(): this` — Removes all descendants and text
79
80
  - `remove(): void`
80
81
  - Geometry & visibility
@@ -170,8 +171,8 @@ The Form layer builds on `CotomyElement` for common form flows.
170
171
  - `initialized: boolean` — Set after `initialize()`
171
172
  - `submitAsync(): Promise<void>` — Abstract in base
172
173
  - Routing & reload
173
- - `method(): string` — Defaults to `get` in base; specialized in subclasses
174
- - `actionUrl(): string` — Defaults to `action` attribute or current path
174
+ - `method: string` — Getter that defaults to `get` in base; specialized in subclasses
175
+ - `actionUrl: string` — Getter that defaults to the `action` attribute or current path
175
176
  - `reloadAsync(): Promise<void>` — Page reload using `CotomyWindow`
176
177
  - `autoReload: boolean` — Backed by `data-cotomy-autoreload` (default true)
177
178
 
@@ -184,8 +185,8 @@ The Form layer builds on `CotomyElement` for common form flows.
184
185
 
185
186
  - API integration
186
187
  - `apiClient(): CotomyApi` — Override to inject a client; default creates a new one
187
- - `actionUrl(): string` — Uses `action` attribute
188
- - `method(): string` — Defaults to `post`
188
+ - `actionUrl: string` — Uses `action` attribute
189
+ - `method: string` — Defaults to `post`
189
190
  - `formData(): FormData` — Builds from form, converts `datetime-local` to ISO (UTC offset)
190
191
  - `submitAsync()` — Calls `submitToApiAsync(formData)`
191
192
  - `submitToApiAsync(formData): Promise<CotomyApiResponse>` — Uses `CotomyApi.submitAsync`
@@ -199,8 +200,8 @@ The Form layer builds on `CotomyElement` for common form flows.
199
200
  - Surrogate key flow
200
201
  - `data-cotomy-entity-key` — Holds the entity identifier if present
201
202
  - `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`
202
- - `actionUrl()` — Appends the key to the base `action` when present; otherwise normalizes trailing slash for collection URL
203
- - `method()` — `put` when key exists; otherwise `post` (unless `method` attribute is explicitly set)
203
+ - `actionUrl` — Appends the key to the base `action` when present; otherwise normalizes trailing slash for collection URL
204
+ - `method` — `put` when key exists; otherwise `post` (unless `method` attribute is explicitly set)
204
205
 
205
206
  ### CotomyEntityFillApiForm
206
207
 
@@ -208,14 +209,21 @@ The Form layer builds on `CotomyElement` for common form flows.
208
209
  - `initialize()` — Adds default fillers and triggers `loadAsync()` on `CotomyWindow.ready`
209
210
  - `reloadAsync()` — Alias to `loadAsync()`
210
211
  - `loadAsync(): Promise<CotomyApiResponse>` — Calls `CotomyApi.getAsync` when `canLoad()` is true
211
- - `loadActionUrl(): string` — Defaults to `actionUrl()`; override for custom endpoints
212
+ - `loadActionUrl(): string` — Defaults to `actionUrl`; override for custom endpoints
212
213
  - `canLoad(): boolean` — Defaults to `hasEntityKey`
213
214
  - Naming & binding
214
215
  - `bindNameGenerator(): ICotomyBindNameGenerator` — Defaults to `CotomyBracketBindNameGenerator` (`user[name]`)
215
216
  - `renderer(): CotomyViewRenderer` — Applies `[data-cotomy-bind]` to view elements
216
- - `filler(type, (input, value))` — Register fillers; defaults provided for `datetime-local`, `checkbox`, `radio`
217
+ - `filler(type, (input, value))` — Register fillers; defaults provided for `datetime-local`, `checkbox`, `radio`
217
218
  - Fills non-array, non-object fields by matching input/select/textarea `name`
218
219
 
220
+ #### Array binding
221
+
222
+ - 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]`).
223
+ - 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.
224
+ - 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.
225
+ - 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.
226
+
219
227
  Example:
220
228
 
221
229
  ```ts
@@ -228,69 +236,69 @@ form.submitFailed(e => console.warn("Submit failed", e.response.status));
228
236
  ```
229
237
 
230
238
  ### Entity API forms
231
-
232
- `CotomyEntityApiForm` targets REST endpoints that identify records with a single surrogate key.
233
- 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.
234
- On `201 Created`, the form reads the `Location` header and stores the generated key back into `data-cotomy-entity-key`, enabling subsequent `PUT` submissions.
235
- 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.
236
- 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.
237
-
238
- The core of Cotomy is `CotomyElement`, which is constructed as a wrapper for `Element`.
239
- By passing HTML and CSS strings to the constructor, it is possible to generate Element designs with a limited scope.
240
-
241
- ```typescript
242
- const ce = new CotomyElement({
243
- html: /* html */`
244
- <div>
245
- <p>Text</p>
246
- </div>
247
- `,
248
- css: /* css */`
249
- [scope] {
250
- display: block;
251
- }
252
- [scope] > p {
253
- text-align: center;
254
- }
255
- `
256
- });
257
- ```
258
-
259
- - `"display HTML in character literals with color coding"` → `"syntax highlighting for embedded HTML"`
260
- - `"generate Element designs with a limited scope"` → `"generate scoped DOM elements with associated styles"`
261
-
262
- ## Development
263
-
264
- Cotomy ships with both ESM (`dist/esm`) and CommonJS (`dist/cjs`) builds, plus generated type definitions in `dist/types`.
265
- 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).
266
- Include the minified build like so:
267
-
268
- ```html
269
- <script src="https://unpkg.com/cotomy/dist/browser/cotomy.min.js"></script>
270
- <script>
271
- const el = new Cotomy.CotomyElement("<div>Hello</div>");
272
- document.body.appendChild(el.element);
273
- </script>
274
- ```
275
-
276
- Run the build to refresh every target bundle:
277
-
278
- ```bash
279
- npm install
280
- npm run build
281
- ```
282
-
283
- The Vitest-based test suite can be executed via:
284
-
285
- ```bash
286
- npx vitest run
287
- ```
288
-
289
- ## License
290
-
291
- This project is licensed under the [MIT License](LICENSE).
292
-
293
- ## Contact
294
-
295
- You can reach out to me at: [yshr1920@gmail.com](mailto:yshr1920@gmail.com)
296
- GitHub repository: [https://github.com/yshr1920/cotomy](https://github.com/yshr1920/cotomy)
239
+
240
+ `CotomyEntityApiForm` targets REST endpoints that identify records with a single surrogate key.
241
+ 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.
242
+ On `201 Created`, the form reads the `Location` header and stores the generated key back into `data-cotomy-entity-key`, enabling subsequent `PUT` submissions.
243
+ 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.
244
+ 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.
245
+
246
+ The core of Cotomy is `CotomyElement`, which is constructed as a wrapper for `Element`.
247
+ By passing HTML and CSS strings to the constructor, it is possible to generate Element designs with a limited scope.
248
+
249
+ ```typescript
250
+ const ce = new CotomyElement({
251
+ html: /* html */`
252
+ <div>
253
+ <p>Text</p>
254
+ </div>
255
+ `,
256
+ css: /* css */`
257
+ [scope] {
258
+ display: block;
259
+ }
260
+ [scope] > p {
261
+ text-align: center;
262
+ }
263
+ `
264
+ });
265
+ ```
266
+
267
+ - `"display HTML in character literals with color coding"` → `"syntax highlighting for embedded HTML"`
268
+ - `"generate Element designs with a limited scope"` → `"generate scoped DOM elements with associated styles"`
269
+
270
+ ## Development
271
+
272
+ Cotomy ships with both ESM (`dist/esm`) and CommonJS (`dist/cjs`) builds, plus generated type definitions in `dist/types`.
273
+ 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).
274
+ Include the minified build like so:
275
+
276
+ ```html
277
+ <script src="https://unpkg.com/cotomy/dist/browser/cotomy.min.js"></script>
278
+ <script>
279
+ const el = new Cotomy.CotomyElement("<div>Hello</div>");
280
+ document.body.appendChild(el.element);
281
+ </script>
282
+ ```
283
+
284
+ Run the build to refresh every target bundle:
285
+
286
+ ```bash
287
+ npm install
288
+ npm run build
289
+ ```
290
+
291
+ The Vitest-based test suite can be executed via:
292
+
293
+ ```bash
294
+ npx vitest run
295
+ ```
296
+
297
+ ## License
298
+
299
+ This project is licensed under the [MIT License](LICENSE).
300
+
301
+ ## Contact
302
+
303
+ You can reach out to me at: [yshr1920@gmail.com](mailto:yshr1920@gmail.com)
304
+ GitHub repository: [https://github.com/yshr1920/cotomy](https://github.com/yshr1920/cotomy)
@@ -174,7 +174,7 @@ module.exports = cuid;
174
174
  /***/ 826:
175
175
  /***/ (function(module) {
176
176
 
177
- !function(t,i){ true?module.exports=i():0}(this,(function(){"use strict";var t="minute",i=/[+-]\d\d(?::?\d\d)?/g,e=/([+-]|\d\d)/g;return function(s,f,n){var u=f.prototype;n.utc=function(t){var i={date:t,utc:!0,args:arguments};return new f(i)},u.utc=function(i){var e=n(this.toDate(),{locale:this.$L,utc:!0});return i?e.add(this.utcOffset(),t):e},u.local=function(){return n(this.toDate(),{locale:this.$L,utc:!1})};var r=u.parse;u.parse=function(t){t.utc&&(this.$u=!0),this.$utils().u(t.$offset)||(this.$offset=t.$offset),r.call(this,t)};var o=u.init;u.init=function(){if(this.$u){var t=this.$d;this.$y=t.getUTCFullYear(),this.$M=t.getUTCMonth(),this.$D=t.getUTCDate(),this.$W=t.getUTCDay(),this.$H=t.getUTCHours(),this.$m=t.getUTCMinutes(),this.$s=t.getUTCSeconds(),this.$ms=t.getUTCMilliseconds()}else o.call(this)};var a=u.utcOffset;u.utcOffset=function(s,f){var n=this.$utils().u;if(n(s))return this.$u?0:n(this.$offset)?a.call(this):this.$offset;if("string"==typeof s&&(s=function(t){void 0===t&&(t="");var s=t.match(i);if(!s)return null;var f=(""+s[0]).match(e)||["-",0,0],n=f[0],u=60*+f[1]+ +f[2];return 0===u?0:"+"===n?u:-u}(s),null===s))return this;var u=Math.abs(s)<=16?60*s:s;if(0===u)return this.utc(f);var r=this.clone();if(f)return r.$offset=u,r.$u=!1,r;var o=this.$u?this.toDate().getTimezoneOffset():-1*this.utcOffset();return(r=this.local().add(u+o,t)).$offset=u,r.$x.$localOffset=o,r};var h=u.format;u.format=function(t){var i=t||(this.$u?"YYYY-MM-DDTHH:mm:ss[Z]":"");return h.call(this,i)},u.valueOf=function(){var t=this.$utils().u(this.$offset)?0:this.$offset+(this.$x.$localOffset||this.$d.getTimezoneOffset());return this.$d.valueOf()-6e4*t},u.isUTC=function(){return!!this.$u},u.toISOString=function(){return this.toDate().toISOString()},u.toString=function(){return this.toDate().toUTCString()};var l=u.toDate;u.toDate=function(t){return"s"===t&&this.$offset?n(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate():l.call(this)};var c=u.diff;u.diff=function(t,i,e){if(t&&this.$u===t.$u)return c.call(this,t,i,e);var s=this.local(),f=n(t).local();return c.call(s,f,i,e)}}}));
177
+ !function(t,i){ true?module.exports=i():0}(this,(function(){"use strict";var t="minute",i=/[+-]\d\d(?::?\d\d)?/g,e=/([+-]|\d\d)/g;return function(s,f,n){var u=f.prototype;n.utc=function(t){var i={date:t,utc:!0,args:arguments};return new f(i)},u.utc=function(i){var e=n(this.toDate(),{locale:this.$L,utc:!0});return i?e.add(this.utcOffset(),t):e},u.local=function(){return n(this.toDate(),{locale:this.$L,utc:!1})};var o=u.parse;u.parse=function(t){t.utc&&(this.$u=!0),this.$utils().u(t.$offset)||(this.$offset=t.$offset),o.call(this,t)};var r=u.init;u.init=function(){if(this.$u){var t=this.$d;this.$y=t.getUTCFullYear(),this.$M=t.getUTCMonth(),this.$D=t.getUTCDate(),this.$W=t.getUTCDay(),this.$H=t.getUTCHours(),this.$m=t.getUTCMinutes(),this.$s=t.getUTCSeconds(),this.$ms=t.getUTCMilliseconds()}else r.call(this)};var a=u.utcOffset;u.utcOffset=function(s,f){var n=this.$utils().u;if(n(s))return this.$u?0:n(this.$offset)?a.call(this):this.$offset;if("string"==typeof s&&(s=function(t){void 0===t&&(t="");var s=t.match(i);if(!s)return null;var f=(""+s[0]).match(e)||["-",0,0],n=f[0],u=60*+f[1]+ +f[2];return 0===u?0:"+"===n?u:-u}(s),null===s))return this;var u=Math.abs(s)<=16?60*s:s,o=this;if(f)return o.$offset=u,o.$u=0===s,o;if(0!==s){var r=this.$u?this.toDate().getTimezoneOffset():-1*this.utcOffset();(o=this.local().add(u+r,t)).$offset=u,o.$x.$localOffset=r}else o=this.utc();return o};var h=u.format;u.format=function(t){var i=t||(this.$u?"YYYY-MM-DDTHH:mm:ss[Z]":"");return h.call(this,i)},u.valueOf=function(){var t=this.$utils().u(this.$offset)?0:this.$offset+(this.$x.$localOffset||this.$d.getTimezoneOffset());return this.$d.valueOf()-6e4*t},u.isUTC=function(){return!!this.$u},u.toISOString=function(){return this.toDate().toISOString()},u.toString=function(){return this.toDate().toUTCString()};var l=u.toDate;u.toDate=function(t){return"s"===t&&this.$offset?n(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate():l.call(this)};var c=u.diff;u.diff=function(t,i,e){if(t&&this.$u===t.$u)return c.call(this,t,i,e);var s=this.local(),f=n(t).local();return c.call(s,f,i,e)}}}));
178
178
 
179
179
  /***/ })
180
180
 
@@ -804,12 +804,12 @@ class HandlerRegistory {
804
804
  }
805
805
  }
806
806
  class EventRegistry {
807
- static get instance() {
808
- return this._instance ?? (this._instance = new EventRegistry());
809
- }
810
807
  constructor() {
811
808
  this._registry = new Map();
812
809
  }
810
+ static get instance() {
811
+ return this._instance ?? (this._instance = new EventRegistry());
812
+ }
813
813
  map(target) {
814
814
  const scopeId = target.scopeId;
815
815
  let registry = this._registry.get(scopeId);
@@ -994,6 +994,10 @@ class CotomyElement {
994
994
  get element() {
995
995
  return this._element;
996
996
  }
997
+ clone(type) {
998
+ const ctor = (type ?? CotomyElement);
999
+ return new ctor(this.element.cloneNode(true));
1000
+ }
997
1001
  get tagname() {
998
1002
  return this.element.tagName.toLowerCase();
999
1003
  }
@@ -1023,6 +1027,14 @@ class CotomyElement {
1023
1027
  }
1024
1028
  return true;
1025
1029
  }
1030
+ match(selector) {
1031
+ try {
1032
+ return this.element.matches(selector);
1033
+ }
1034
+ catch {
1035
+ return false;
1036
+ }
1037
+ }
1026
1038
  get empty() {
1027
1039
  const nonEmptyTags = new Set([
1028
1040
  "input", "select", "textarea", "img", "video", "audio", "br", "hr",
@@ -1339,6 +1351,36 @@ class CotomyElement {
1339
1351
  return undefined;
1340
1352
  }
1341
1353
  }
1354
+ previousSibling(selector = "*", type) {
1355
+ const element = this.element.previousElementSibling;
1356
+ if (element !== null && element instanceof HTMLElement) {
1357
+ const ctor = (type ?? CotomyElement);
1358
+ const ce = new ctor(element);
1359
+ return ce.match(selector) ? ce : ce.previousSibling(selector, type);
1360
+ }
1361
+ else {
1362
+ return undefined;
1363
+ }
1364
+ }
1365
+ nextSibling(selector = "*", type) {
1366
+ const element = this.element.nextElementSibling;
1367
+ if (element !== null && element instanceof HTMLElement) {
1368
+ const ctor = (type ?? CotomyElement);
1369
+ const ce = new ctor(element);
1370
+ return ce.match(selector) ? ce : ce.nextSibling(selector, type);
1371
+ }
1372
+ else {
1373
+ return undefined;
1374
+ }
1375
+ }
1376
+ siblings(selector = "*", type) {
1377
+ const parent = this.element.parentElement;
1378
+ if (!parent)
1379
+ return [];
1380
+ const ctor = (type ?? CotomyElement);
1381
+ return Array.from(parent.children).filter((e) => e instanceof HTMLElement
1382
+ && e !== this.element).map(e => new ctor(e)).filter(e => e.match(selector));
1383
+ }
1342
1384
  find(selector, type) {
1343
1385
  const elements = Array.from(this.element.querySelectorAll(selector));
1344
1386
  return elements.map(e => new (type ?? CotomyElement)(e));
@@ -2078,11 +2120,17 @@ class CotomyBracketBindNameGenerator {
2078
2120
  create(name, parent) {
2079
2121
  return parent ? `${parent}[${name}]` : name;
2080
2122
  }
2123
+ createIndex(parent, index) {
2124
+ return this.create(String(index), parent);
2125
+ }
2081
2126
  }
2082
2127
  class CotomyDotBindNameGenerator {
2083
2128
  create(name, parent) {
2084
2129
  return parent ? `${parent}.${name}` : name;
2085
2130
  }
2131
+ createIndex(parent, index) {
2132
+ return parent ? `${parent}[${index}]` : `[${index}]`;
2133
+ }
2086
2134
  }
2087
2135
  class CotomyViewRenderer {
2088
2136
  constructor(element, bindNameGenerator) {
@@ -2146,30 +2194,50 @@ class CotomyViewRenderer {
2146
2194
  }
2147
2195
  return this;
2148
2196
  }
2197
+ bindPrimitiveValue(propertyName, value) {
2198
+ this.element.find(`[data-cotomy-bind="${propertyName}" i]`).forEach(element => {
2199
+ if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Bind)) {
2200
+ console.debug(`Binding data to element [data-cotomy-bind="${propertyName}"]:`, value);
2201
+ }
2202
+ const type = element.attribute("data-cotomy-bindtype")?.toLowerCase();
2203
+ if (type && this._renderers[type]) {
2204
+ this._renderers[type](element, value);
2205
+ }
2206
+ else {
2207
+ element.text = String(value ?? "");
2208
+ }
2209
+ });
2210
+ }
2211
+ async applyArrayAsync(values, propertyName) {
2212
+ for (let index = 0; index < values.length; index++) {
2213
+ const item = values[index];
2214
+ const itemName = this.bindNameGenerator.createIndex(propertyName, index);
2215
+ if (Array.isArray(item)) {
2216
+ await this.applyArrayAsync(item, itemName);
2217
+ continue;
2218
+ }
2219
+ if (item && typeof item === "object") {
2220
+ await this.applyObjectAsync(item, itemName);
2221
+ continue;
2222
+ }
2223
+ this.bindPrimitiveValue(itemName, item);
2224
+ }
2225
+ }
2149
2226
  async applyObjectAsync(target, propertyName = undefined) {
2150
2227
  if (!propertyName) {
2151
2228
  this.element.find("[data-cotomy-bind]").forEach(e => e.clear());
2152
2229
  }
2153
2230
  for (const [key, value] of Object.entries(await target)) {
2154
2231
  const pname = this.bindNameGenerator.create(key, propertyName);
2155
- if (Array.isArray(value))
2232
+ if (Array.isArray(value)) {
2233
+ await this.applyArrayAsync(value, pname);
2156
2234
  continue;
2235
+ }
2157
2236
  if (value && typeof value === "object") {
2158
2237
  await this.applyObjectAsync(value, pname);
2159
2238
  continue;
2160
2239
  }
2161
- this.element.find(`[data-cotomy-bind="${pname}" i]`).forEach(element => {
2162
- if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Bind)) {
2163
- console.debug(`Binding data to element [data-cotomy-bind="${pname}"]:`, value);
2164
- }
2165
- const type = element.attribute("data-cotomy-bindtype")?.toLowerCase();
2166
- if (type && this._renderers[type]) {
2167
- this._renderers[type](element, value);
2168
- }
2169
- else {
2170
- element.text = String(value ?? "");
2171
- }
2172
- });
2240
+ this.bindPrimitiveValue(pname, value);
2173
2241
  }
2174
2242
  }
2175
2243
  async applyAsync(respose) {
@@ -2370,10 +2438,10 @@ class CotomyForm extends CotomyElement {
2370
2438
  generateId(prefix = "__cotomy_form__") {
2371
2439
  return super.generateId(prefix);
2372
2440
  }
2373
- method() {
2441
+ get method() {
2374
2442
  return this.attribute("method") ?? "get";
2375
2443
  }
2376
- actionUrl() {
2444
+ get actionUrl() {
2377
2445
  return this.attribute("action") ?? location.pathname + location.search;
2378
2446
  }
2379
2447
  async reloadAsync() {
@@ -2406,11 +2474,11 @@ class CotomyForm extends CotomyElement {
2406
2474
  }
2407
2475
  }
2408
2476
  class CotomyQueryForm extends CotomyForm {
2409
- method() {
2477
+ get method() {
2410
2478
  return "get";
2411
2479
  }
2412
2480
  async submitAsync() {
2413
- const url = this.actionUrl();
2481
+ const url = this.actionUrl;
2414
2482
  const queryParams = {};
2415
2483
  const queryString = url.split("?")[1];
2416
2484
  if (queryString) {
@@ -2452,7 +2520,7 @@ class CotomyApiForm extends CotomyForm {
2452
2520
  apiClient() {
2453
2521
  return new CotomyApi();
2454
2522
  }
2455
- actionUrl() {
2523
+ get actionUrl() {
2456
2524
  return this.attribute("action");
2457
2525
  }
2458
2526
  apiFailed(handle) {
@@ -2479,7 +2547,7 @@ class CotomyApiForm extends CotomyForm {
2479
2547
  console.error("Submit failed:", response);
2480
2548
  }
2481
2549
  }
2482
- method() {
2550
+ get method() {
2483
2551
  return this.attribute("method") ?? "post";
2484
2552
  }
2485
2553
  formData() {
@@ -2508,8 +2576,8 @@ class CotomyApiForm extends CotomyForm {
2508
2576
  const api = this.apiClient();
2509
2577
  try {
2510
2578
  const response = await api.submitAsync({
2511
- method: this.method(),
2512
- action: this.actionUrl(),
2579
+ method: this.method,
2580
+ action: this.actionUrl,
2513
2581
  body: formData,
2514
2582
  });
2515
2583
  return response;
@@ -2533,7 +2601,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
2533
2601
  get hasEntityKey() {
2534
2602
  return !!this.entityKey;
2535
2603
  }
2536
- actionUrl() {
2604
+ get actionUrl() {
2537
2605
  const action = this.attribute("action");
2538
2606
  const normalized = action.replace(/\/+$/, "");
2539
2607
  if (!this.entityKey) {
@@ -2541,7 +2609,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
2541
2609
  }
2542
2610
  return `${normalized}/${encodeURIComponent(this.entityKey)}`;
2543
2611
  }
2544
- method() {
2612
+ get method() {
2545
2613
  if (this.hasAttribute("method") && this.attribute("method") !== "") {
2546
2614
  return this.attribute("method");
2547
2615
  }
@@ -2583,7 +2651,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
2583
2651
  this.attribute("data-cotomy-entity-key", addedParts[0]);
2584
2652
  }
2585
2653
  else {
2586
- const msg = `Location does not contain a single entity key segment.
2654
+ const msg = `Location does not contain a single entity key segment.
2587
2655
  action="${baseAction}", location="${locPath}", added=["${addedParts.join('","')}"]`;
2588
2656
  throw new Error(msg);
2589
2657
  }
@@ -2641,7 +2709,7 @@ class CotomyEntityFillApiForm extends CotomyEntityApiForm {
2641
2709
  await this.loadAsync();
2642
2710
  }
2643
2711
  loadActionUrl() {
2644
- return this.actionUrl();
2712
+ return this.actionUrl;
2645
2713
  }
2646
2714
  bindNameGenerator() {
2647
2715
  return new CotomyBracketBindNameGenerator();
@@ -2672,32 +2740,52 @@ class CotomyEntityFillApiForm extends CotomyEntityApiForm {
2672
2740
  throw error;
2673
2741
  }
2674
2742
  }
2743
+ applyValueToInputs(pname, value) {
2744
+ this.find(`input[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple]),
2745
+ textarea[name="${pname}" i]:not([data-cotomy-fill="false"]),
2746
+ select[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple])`).forEach(input => {
2747
+ if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Fill)) {
2748
+ console.debug(`Filling input[name="${pname}"] with value:`, value);
2749
+ }
2750
+ const type = input.attribute("type")?.toLowerCase();
2751
+ if (type && this._fillers[type]) {
2752
+ this._fillers[type](input, value);
2753
+ }
2754
+ else {
2755
+ input.value = String(value || "");
2756
+ }
2757
+ });
2758
+ }
2759
+ async fillArrayAsync(bindNameGenerator, values, propertyName) {
2760
+ for (let index = 0; index < values.length; index++) {
2761
+ const item = values[index];
2762
+ const itemName = bindNameGenerator.createIndex(propertyName, index);
2763
+ if (Array.isArray(item)) {
2764
+ await this.fillArrayAsync(bindNameGenerator, item, itemName);
2765
+ continue;
2766
+ }
2767
+ if (item && typeof item === "object") {
2768
+ await this.fillObjectAsync(bindNameGenerator, item, itemName);
2769
+ continue;
2770
+ }
2771
+ this.applyValueToInputs(itemName, item);
2772
+ }
2773
+ }
2675
2774
  async fillObjectAsync(bindNameGenerator, target, propertyName = undefined) {
2676
2775
  for (const [key, value] of Object.entries(target)) {
2677
2776
  if (key.endsWith('[]')) {
2678
2777
  continue;
2679
2778
  }
2680
2779
  const pname = bindNameGenerator.create(key, propertyName);
2681
- if (Array.isArray(value))
2780
+ if (Array.isArray(value)) {
2781
+ await this.fillArrayAsync(bindNameGenerator, value, pname);
2682
2782
  continue;
2783
+ }
2683
2784
  if (value && typeof value === "object") {
2684
2785
  await this.fillObjectAsync(bindNameGenerator, value, pname);
2685
2786
  continue;
2686
2787
  }
2687
- this.find(`input[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple]),
2688
- textarea[name="${pname}" i]:not([data-cotomy-fill="false"]),
2689
- select[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple])`).forEach(input => {
2690
- if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Fill)) {
2691
- console.debug(`Filling input[name="${pname}"] with value:`, value);
2692
- }
2693
- const type = input.attribute("type")?.toLowerCase();
2694
- if (type && this._fillers[type]) {
2695
- this._fillers[type](input, value);
2696
- }
2697
- else {
2698
- input.value = String(value || "");
2699
- }
2700
- });
2788
+ this.applyValueToInputs(pname, value);
2701
2789
  }
2702
2790
  }
2703
2791
  async fillAsync(response) {