cotomy 0.3.4 → 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
@@ -36,10 +36,9 @@ The View layer provides thin wrappers around DOM elements and window events.
36
36
  - `new CotomyElement({ html, css? })` — Creates from HTML and injects scoped CSS
37
37
  - `new CotomyElement({ tagname, text?, css? })`
38
38
  - Scoped CSS
39
- - `scopeId: string` Unique attribute injected into the element for scoping
40
- - `scopedSelector: string` Selector string like `[__cotomy_scope__...]`
41
- - `[scope]` placeholder in provided CSS is replaced by the element’s scope
42
- - `stylable: boolean` — False for tags like `script`, `style`, `link`, `meta`
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`
43
42
  - Static helpers
44
43
  - `CotomyElement.encodeHtml(text)`
45
44
  - `CotomyElement.first(selector, type?)`
@@ -76,7 +75,7 @@ The View layer provides thin wrappers around DOM elements and window events.
76
75
  - `append(child): this` / `prepend(child): this` / `appendAll(children): this`
77
76
  - `insertBefore(sibling): this` / `insertAfter(sibling): this`
78
77
  - `appendTo(target): this` / `prependTo(target): this`
79
- - `clone(type?): CotomyElement` Returns a deep-cloned element, optionally typed
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
80
79
  - `clear(): this` — Removes all descendants and text
81
80
  - `remove(): void`
82
81
  - Geometry & visibility
@@ -121,6 +120,17 @@ panel.onSubTree("click", ".ok", () => console.log("clicked!"));
121
120
  document.body.appendChild(panel.element);
122
121
  ```
123
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
+
124
134
  ### CotomyMetaElement
125
135
 
126
136
  - `CotomyMetaElement.get(name): CotomyMetaElement`
@@ -220,6 +230,28 @@ The Form layer builds on `CotomyElement` for common form flows.
220
230
  - `filler(type, (input, value))` — Register fillers; defaults provided for `datetime-local`, `checkbox`, `radio`
221
231
  - Fills non-array, non-object fields by matching input/select/textarea `name`
222
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
+
223
255
  #### Array binding
224
256
 
225
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]`).
@@ -916,7 +916,6 @@ class CotomyElement {
916
916
  }
917
917
  constructor(element) {
918
918
  this._parentElement = null;
919
- this._scopeId = null;
920
919
  if (element instanceof HTMLElement) {
921
920
  this._element = element;
922
921
  }
@@ -944,7 +943,7 @@ class CotomyElement {
944
943
  }
945
944
  }
946
945
  if (!this.instanceId) {
947
- this.attribute("data-cotomy-instance", `__cotomy_instance_${cuid_default()()}`);
946
+ this.attribute("data-cotomy-instance", cuid_default()());
948
947
  this.removed(() => {
949
948
  this._element = CotomyElement.createHTMLElement(`<div data-cotomy-invalidated style="display: none;"></div>`);
950
949
  EventRegistry.instance.clear(this);
@@ -963,14 +962,10 @@ class CotomyElement {
963
962
  return this.attribute("data-cotomy-instance");
964
963
  }
965
964
  get scopeId() {
966
- if (!this._scopeId) {
967
- this._scopeId = `__cotomy_scope__${cuid_default()()}`;
968
- this.attribute(this._scopeId, "");
965
+ if (!this.hasAttribute("data-cotomy-scopeid")) {
966
+ this.attribute("data-cotomy-scopeid", cuid_default()());
969
967
  }
970
- return this._scopeId;
971
- }
972
- get scopedSelector() {
973
- return `[${this.scopeId}]`;
968
+ return this.attribute("data-cotomy-scopeid");
974
969
  }
975
970
  get stylable() {
976
971
  return !["script", "style", "link", "meta"].includes(this.tagname);
@@ -983,7 +978,7 @@ class CotomyElement {
983
978
  const cssid = this.scopedCssElementId;
984
979
  CotomyElement.find(`#${cssid}`).forEach(e => e.remove());
985
980
  const element = document.createElement("style");
986
- const writeCss = css.replace(/\[scope\]/g, `[${this.scopeId}]`);
981
+ const writeCss = css.replace(/\[scope\]/g, `[data-cotomy-scopeid="${this.scopeId}"]`);
987
982
  const node = document.createTextNode(writeCss);
988
983
  element.appendChild(node);
989
984
  element.id = cssid;
@@ -1014,7 +1009,10 @@ class CotomyElement {
1014
1009
  }
1015
1010
  clone(type) {
1016
1011
  const ctor = (type ?? CotomyElement);
1017
- return new ctor(this.element.cloneNode(true));
1012
+ const cloned = new ctor(this.element.cloneNode(true));
1013
+ cloned.attribute("data-cotomy-scopeid", null);
1014
+ cloned.find("[data-cotomy-scopeid]").forEach(e => e.attribute("data-cotomy-scopeid", null));
1015
+ return cloned;
1018
1016
  }
1019
1017
  get tagname() {
1020
1018
  return this.element.tagName.toLowerCase();
@@ -2220,6 +2218,10 @@ class CotomyViewRenderer {
2220
2218
  this._renderers[type] = callback;
2221
2219
  return this;
2222
2220
  }
2221
+ get renderers() {
2222
+ this.initialize();
2223
+ return this._renderers;
2224
+ }
2223
2225
  get initialized() {
2224
2226
  return this._builded;
2225
2227
  }
@@ -2259,6 +2261,15 @@ class CotomyViewRenderer {
2259
2261
  }
2260
2262
  }
2261
2263
  });
2264
+ this.renderer("date", (element, value) => {
2265
+ if (value) {
2266
+ const date = new Date(value);
2267
+ if (!isNaN(date.getTime())) {
2268
+ const format = element.attribute("data-cotomy-format") ?? "YYYY/MM/DD";
2269
+ element.text = dayjs_min_default()(date).format(format);
2270
+ }
2271
+ }
2272
+ });
2262
2273
  this._builded = true;
2263
2274
  }
2264
2275
  return this;
@@ -2269,8 +2280,8 @@ class CotomyViewRenderer {
2269
2280
  console.debug(`Binding data to element [data-cotomy-bind="${propertyName}"]:`, value);
2270
2281
  }
2271
2282
  const type = element.attribute("data-cotomy-bindtype")?.toLowerCase();
2272
- if (type && this._renderers[type]) {
2273
- this._renderers[type](element, value);
2283
+ if (type && this.renderers[type]) {
2284
+ this.renderers[type](element, value);
2274
2285
  }
2275
2286
  else {
2276
2287
  element.text = String(value ?? "");
@@ -2310,9 +2321,6 @@ class CotomyViewRenderer {
2310
2321
  }
2311
2322
  }
2312
2323
  async applyAsync(respose) {
2313
- if (!this.initialized) {
2314
- this.initialize();
2315
- }
2316
2324
  if (!respose.available) {
2317
2325
  throw new Error("Response is not available.");
2318
2326
  }