cotomy 0.1.73 → 0.2.1

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
 
@@ -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
  }
@@ -1739,7 +1743,7 @@ class CotomyElement {
1739
1743
  }
1740
1744
  return this;
1741
1745
  }
1742
- changelayout(handle) {
1746
+ changeLayout(handle) {
1743
1747
  this.listenLayoutEvents();
1744
1748
  if (handle) {
1745
1749
  this.element.addEventListener("cotomy:changelayout", async (e) => await handle(e));
@@ -2116,11 +2120,17 @@ class CotomyBracketBindNameGenerator {
2116
2120
  create(name, parent) {
2117
2121
  return parent ? `${parent}[${name}]` : name;
2118
2122
  }
2123
+ createIndex(parent, index) {
2124
+ return this.create(String(index), parent);
2125
+ }
2119
2126
  }
2120
2127
  class CotomyDotBindNameGenerator {
2121
2128
  create(name, parent) {
2122
2129
  return parent ? `${parent}.${name}` : name;
2123
2130
  }
2131
+ createIndex(parent, index) {
2132
+ return parent ? `${parent}[${index}]` : `[${index}]`;
2133
+ }
2124
2134
  }
2125
2135
  class CotomyViewRenderer {
2126
2136
  constructor(element, bindNameGenerator) {
@@ -2184,30 +2194,50 @@ class CotomyViewRenderer {
2184
2194
  }
2185
2195
  return this;
2186
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
+ }
2187
2226
  async applyObjectAsync(target, propertyName = undefined) {
2188
2227
  if (!propertyName) {
2189
2228
  this.element.find("[data-cotomy-bind]").forEach(e => e.clear());
2190
2229
  }
2191
2230
  for (const [key, value] of Object.entries(await target)) {
2192
2231
  const pname = this.bindNameGenerator.create(key, propertyName);
2193
- if (Array.isArray(value))
2232
+ if (Array.isArray(value)) {
2233
+ await this.applyArrayAsync(value, pname);
2194
2234
  continue;
2235
+ }
2195
2236
  if (value && typeof value === "object") {
2196
2237
  await this.applyObjectAsync(value, pname);
2197
2238
  continue;
2198
2239
  }
2199
- this.element.find(`[data-cotomy-bind="${pname}" i]`).forEach(element => {
2200
- if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Bind)) {
2201
- console.debug(`Binding data to element [data-cotomy-bind="${pname}"]:`, value);
2202
- }
2203
- const type = element.attribute("data-cotomy-bindtype")?.toLowerCase();
2204
- if (type && this._renderers[type]) {
2205
- this._renderers[type](element, value);
2206
- }
2207
- else {
2208
- element.text = String(value ?? "");
2209
- }
2210
- });
2240
+ this.bindPrimitiveValue(pname, value);
2211
2241
  }
2212
2242
  }
2213
2243
  async applyAsync(respose) {
@@ -2408,10 +2438,10 @@ class CotomyForm extends CotomyElement {
2408
2438
  generateId(prefix = "__cotomy_form__") {
2409
2439
  return super.generateId(prefix);
2410
2440
  }
2411
- method() {
2441
+ get method() {
2412
2442
  return this.attribute("method") ?? "get";
2413
2443
  }
2414
- actionUrl() {
2444
+ get actionUrl() {
2415
2445
  return this.attribute("action") ?? location.pathname + location.search;
2416
2446
  }
2417
2447
  async reloadAsync() {
@@ -2444,11 +2474,11 @@ class CotomyForm extends CotomyElement {
2444
2474
  }
2445
2475
  }
2446
2476
  class CotomyQueryForm extends CotomyForm {
2447
- method() {
2477
+ get method() {
2448
2478
  return "get";
2449
2479
  }
2450
2480
  async submitAsync() {
2451
- const url = this.actionUrl();
2481
+ const url = this.actionUrl;
2452
2482
  const queryParams = {};
2453
2483
  const queryString = url.split("?")[1];
2454
2484
  if (queryString) {
@@ -2490,7 +2520,7 @@ class CotomyApiForm extends CotomyForm {
2490
2520
  apiClient() {
2491
2521
  return new CotomyApi();
2492
2522
  }
2493
- actionUrl() {
2523
+ get actionUrl() {
2494
2524
  return this.attribute("action");
2495
2525
  }
2496
2526
  apiFailed(handle) {
@@ -2517,7 +2547,7 @@ class CotomyApiForm extends CotomyForm {
2517
2547
  console.error("Submit failed:", response);
2518
2548
  }
2519
2549
  }
2520
- method() {
2550
+ get method() {
2521
2551
  return this.attribute("method") ?? "post";
2522
2552
  }
2523
2553
  formData() {
@@ -2546,8 +2576,8 @@ class CotomyApiForm extends CotomyForm {
2546
2576
  const api = this.apiClient();
2547
2577
  try {
2548
2578
  const response = await api.submitAsync({
2549
- method: this.method(),
2550
- action: this.actionUrl(),
2579
+ method: this.method,
2580
+ action: this.actionUrl,
2551
2581
  body: formData,
2552
2582
  });
2553
2583
  return response;
@@ -2571,7 +2601,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
2571
2601
  get hasEntityKey() {
2572
2602
  return !!this.entityKey;
2573
2603
  }
2574
- actionUrl() {
2604
+ get actionUrl() {
2575
2605
  const action = this.attribute("action");
2576
2606
  const normalized = action.replace(/\/+$/, "");
2577
2607
  if (!this.entityKey) {
@@ -2579,7 +2609,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
2579
2609
  }
2580
2610
  return `${normalized}/${encodeURIComponent(this.entityKey)}`;
2581
2611
  }
2582
- method() {
2612
+ get method() {
2583
2613
  if (this.hasAttribute("method") && this.attribute("method") !== "") {
2584
2614
  return this.attribute("method");
2585
2615
  }
@@ -2621,7 +2651,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
2621
2651
  this.attribute("data-cotomy-entity-key", addedParts[0]);
2622
2652
  }
2623
2653
  else {
2624
- const msg = `Location does not contain a single entity key segment.
2654
+ const msg = `Location does not contain a single entity key segment.
2625
2655
  action="${baseAction}", location="${locPath}", added=["${addedParts.join('","')}"]`;
2626
2656
  throw new Error(msg);
2627
2657
  }
@@ -2679,7 +2709,7 @@ class CotomyEntityFillApiForm extends CotomyEntityApiForm {
2679
2709
  await this.loadAsync();
2680
2710
  }
2681
2711
  loadActionUrl() {
2682
- return this.actionUrl();
2712
+ return this.actionUrl;
2683
2713
  }
2684
2714
  bindNameGenerator() {
2685
2715
  return new CotomyBracketBindNameGenerator();
@@ -2710,32 +2740,52 @@ class CotomyEntityFillApiForm extends CotomyEntityApiForm {
2710
2740
  throw error;
2711
2741
  }
2712
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
+ }
2713
2774
  async fillObjectAsync(bindNameGenerator, target, propertyName = undefined) {
2714
2775
  for (const [key, value] of Object.entries(target)) {
2715
2776
  if (key.endsWith('[]')) {
2716
2777
  continue;
2717
2778
  }
2718
2779
  const pname = bindNameGenerator.create(key, propertyName);
2719
- if (Array.isArray(value))
2780
+ if (Array.isArray(value)) {
2781
+ await this.fillArrayAsync(bindNameGenerator, value, pname);
2720
2782
  continue;
2783
+ }
2721
2784
  if (value && typeof value === "object") {
2722
2785
  await this.fillObjectAsync(bindNameGenerator, value, pname);
2723
2786
  continue;
2724
2787
  }
2725
- this.find(`input[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple]),
2726
- textarea[name="${pname}" i]:not([data-cotomy-fill="false"]),
2727
- select[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple])`).forEach(input => {
2728
- if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Fill)) {
2729
- console.debug(`Filling input[name="${pname}"] with value:`, value);
2730
- }
2731
- const type = input.attribute("type")?.toLowerCase();
2732
- if (type && this._fillers[type]) {
2733
- this._fillers[type](input, value);
2734
- }
2735
- else {
2736
- input.value = String(value || "");
2737
- }
2738
- });
2788
+ this.applyValueToInputs(pname, value);
2739
2789
  }
2740
2790
  }
2741
2791
  async fillAsync(response) {