assign-gingerly 0.0.91 → 0.0.93

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.
@@ -50,12 +50,21 @@ export class Infer {
50
50
  }
51
51
  #value;
52
52
  get value() {
53
- return this.#value;
53
+ const element = this.#weakRef.deref();
54
+ if (element === undefined)
55
+ return this.#value;
56
+ const propName = typeof this.#propName === 'string'
57
+ ? this.#propName
58
+ : inferValueProperty(element);
59
+ return coerceElementValue(element, propName);
54
60
  }
55
61
  set value(nv) {
56
62
  this.#value = nv;
57
63
  const { enhancedElement } = this;
58
- enhancedElement[inferValueProperty(enhancedElement)] = nv;
64
+ const propName = typeof this.#propName === 'string'
65
+ ? this.#propName
66
+ : inferValueProperty(enhancedElement);
67
+ enhancedElement[propName] = serializeForProperty(propName, nv);
59
68
  }
60
69
  #display;
61
70
  get display() {
@@ -80,6 +89,16 @@ export class Infer {
80
89
  get valueProperty() {
81
90
  return inferValueProperty(this.enhancedElement);
82
91
  }
92
+ /**
93
+ * The effective language for this element, honoring `lang` / `xml:lang`
94
+ * inheritance up through ancestors and shadow-root hosts, then
95
+ * `<html lang>` and finally `navigator.language`.
96
+ * @returns a BCP-47 tag, or undefined if nothing is resolvable
97
+ */
98
+ get lang() {
99
+ const el = this.#weakRef.deref();
100
+ return el === undefined ? undefined : resolveLang(el);
101
+ }
83
102
  ['|'](itempropAttr, scopeBoundary = '[itemscope]') {
84
103
  return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
85
104
  }
@@ -166,6 +185,108 @@ export function inferValueProperty(element) {
166
185
  }
167
186
  }
168
187
  }
188
+ /**
189
+ * Read the inferred value property off an element and coerce it to a natural
190
+ * JavaScript type, mirroring the legacy be-value-added parsing rules:
191
+ * - `<time>` (dateTime) -> Date (or undefined when empty)
192
+ * - `<input type=number|range>` (valueAsNumber) -> number (undefined when NaN)
193
+ * - `<input type=checkbox|radio>` (checked) -> boolean
194
+ * - schema.org `itemtype` hints (Number/Integer/Float/Boolean/Date/DateTime) are honored
195
+ * - `textContent` is returned verbatim
196
+ * - everything else is JSON-parsed when possible (so `<data value="123">` -> 123,
197
+ * `<data value="true">` -> true), falling back to the raw string
198
+ */
199
+ export function coerceElementValue(element, propName = inferValueProperty(element)) {
200
+ const raw = element[propName];
201
+ switch (propName) {
202
+ case 'valueAsNumber':
203
+ return Number.isNaN(raw) ? undefined : raw;
204
+ case 'valueAsDate':
205
+ return raw ?? undefined;
206
+ case 'checked':
207
+ case 'selectedIndex':
208
+ return raw;
209
+ case 'dateTime': {
210
+ const s = raw == null ? '' : String(raw);
211
+ return s === '' ? undefined : new Date(s);
212
+ }
213
+ }
214
+ if (raw == null)
215
+ return undefined;
216
+ if (typeof raw !== 'string')
217
+ return raw;
218
+ switch (element.getAttribute('itemtype')) {
219
+ case 'https://schema.org/Number': return Number(raw);
220
+ case 'https://schema.org/Integer': return parseInt(raw, 10);
221
+ case 'https://schema.org/Float': return parseFloat(raw);
222
+ case 'https://schema.org/Boolean': return raw === 'true' || raw === 'True';
223
+ case 'https://schema.org/Date':
224
+ case 'https://schema.org/DateTime': return new Date(raw);
225
+ }
226
+ if (propName === 'textContent')
227
+ return raw;
228
+ if (raw === '')
229
+ return undefined;
230
+ try {
231
+ return JSON.parse(raw);
232
+ }
233
+ catch {
234
+ return raw;
235
+ }
236
+ }
237
+ /**
238
+ * Serialize a JS value for assignment to a (usually string-typed) DOM value
239
+ * property, mirroring legacy be-value-added write-back:
240
+ * - DOM-typed properties (`checked`, `valueAsNumber`, `valueAsDate`) take the raw value
241
+ * - a `Date` is written as an ISO string (so `<time>.dateTime` round-trips)
242
+ * - plain objects / arrays are JSON-stringified
243
+ */
244
+ export function serializeForProperty(propName, nv) {
245
+ switch (propName) {
246
+ case 'checked':
247
+ case 'valueAsNumber':
248
+ case 'valueAsDate':
249
+ return nv;
250
+ }
251
+ if (nv instanceof Date)
252
+ return nv.toISOString();
253
+ if (nv !== null && typeof nv === 'object')
254
+ return JSON.stringify(nv);
255
+ return nv;
256
+ }
257
+ /**
258
+ * Resolve the effective language ("computed lang") for an element.
259
+ *
260
+ * There is no DOM accessor for this, so we walk: nearest ancestor element with a
261
+ * `lang` / `xml:lang` attribute, crossing shadow-root boundaries via the host
262
+ * (matching how `:lang()` behaves in modern browsers), then `<html lang>`, then
263
+ * `navigator.language`. Slot reprojection is intentionally not followed — HTML
264
+ * language inheritance is defined over the real node tree, not the flat tree.
265
+ *
266
+ * @returns a BCP-47 tag, or undefined if nothing is resolvable
267
+ */
268
+ export function resolveLang(element) {
269
+ let node = element;
270
+ while (node != null) {
271
+ if (node.nodeType === 1 /* ELEMENT_NODE */) {
272
+ const el = node;
273
+ const lang = el.getAttribute('lang') || el.getAttribute('xml:lang');
274
+ if (lang)
275
+ return lang;
276
+ node = node.parentNode;
277
+ }
278
+ else if (node.nodeType === 11 /* DOCUMENT_FRAGMENT_NODE, incl. ShadowRoot */) {
279
+ node = node.host ?? null;
280
+ }
281
+ else {
282
+ node = null; // Document (9) or detached — stop
283
+ }
284
+ }
285
+ if (typeof document !== 'undefined' && document.documentElement.lang) {
286
+ return document.documentElement.lang;
287
+ }
288
+ return (typeof navigator !== 'undefined' && navigator.language) || undefined;
289
+ }
169
290
  /**
170
291
  * Infer the most appropriate display property for an element
171
292
  * @param element - The element to infer the property for
@@ -65,13 +65,21 @@ export class Infer<TValue = any, TDisplay = any> {
65
65
  #value: TValue | undefined;
66
66
 
67
67
  get value(): TValue | undefined {
68
- return this.#value;
68
+ const element = this.#weakRef.deref();
69
+ if (element === undefined) return this.#value;
70
+ const propName = typeof this.#propName === 'string'
71
+ ? this.#propName
72
+ : inferValueProperty(element);
73
+ return coerceElementValue(element, propName) as TValue | undefined;
69
74
  }
70
-
75
+
71
76
  set value(nv: TValue){
72
77
  this.#value = nv;
73
78
  const {enhancedElement} = this;
74
- (enhancedElement as any)[inferValueProperty(enhancedElement)] = nv;
79
+ const propName = typeof this.#propName === 'string'
80
+ ? this.#propName
81
+ : inferValueProperty(enhancedElement);
82
+ (enhancedElement as any)[propName] = serializeForProperty(propName, nv);
75
83
  }
76
84
 
77
85
  #display: TDisplay | undefined;
@@ -102,6 +110,17 @@ export class Infer<TValue = any, TDisplay = any> {
102
110
  return inferValueProperty(this.enhancedElement);
103
111
  }
104
112
 
113
+ /**
114
+ * The effective language for this element, honoring `lang` / `xml:lang`
115
+ * inheritance up through ancestors and shadow-root hosts, then
116
+ * `<html lang>` and finally `navigator.language`.
117
+ * @returns a BCP-47 tag, or undefined if nothing is resolvable
118
+ */
119
+ get lang(): string | undefined {
120
+ const el = this.#weakRef.deref();
121
+ return el === undefined ? undefined : resolveLang(el);
122
+ }
123
+
105
124
  ['|'](itempropAttr: string, scopeBoundary: string = '[itemscope]'){
106
125
  return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
107
126
  }
@@ -200,6 +219,106 @@ export function inferValueProperty(element: Element): string {
200
219
  }
201
220
  }
202
221
 
222
+ /**
223
+ * Read the inferred value property off an element and coerce it to a natural
224
+ * JavaScript type, mirroring the legacy be-value-added parsing rules:
225
+ * - `<time>` (dateTime) -> Date (or undefined when empty)
226
+ * - `<input type=number|range>` (valueAsNumber) -> number (undefined when NaN)
227
+ * - `<input type=checkbox|radio>` (checked) -> boolean
228
+ * - schema.org `itemtype` hints (Number/Integer/Float/Boolean/Date/DateTime) are honored
229
+ * - `textContent` is returned verbatim
230
+ * - everything else is JSON-parsed when possible (so `<data value="123">` -> 123,
231
+ * `<data value="true">` -> true), falling back to the raw string
232
+ */
233
+ export function coerceElementValue(element: Element, propName: string = inferValueProperty(element)): any {
234
+ const raw = (element as any)[propName];
235
+
236
+ switch (propName) {
237
+ case 'valueAsNumber':
238
+ return Number.isNaN(raw) ? undefined : raw;
239
+ case 'valueAsDate':
240
+ return raw ?? undefined;
241
+ case 'checked':
242
+ case 'selectedIndex':
243
+ return raw;
244
+ case 'dateTime': {
245
+ const s = raw == null ? '' : String(raw);
246
+ return s === '' ? undefined : new Date(s);
247
+ }
248
+ }
249
+
250
+ if (raw == null) return undefined;
251
+ if (typeof raw !== 'string') return raw;
252
+
253
+ switch (element.getAttribute('itemtype')) {
254
+ case 'https://schema.org/Number': return Number(raw);
255
+ case 'https://schema.org/Integer': return parseInt(raw, 10);
256
+ case 'https://schema.org/Float': return parseFloat(raw);
257
+ case 'https://schema.org/Boolean': return raw === 'true' || raw === 'True';
258
+ case 'https://schema.org/Date':
259
+ case 'https://schema.org/DateTime': return new Date(raw);
260
+ }
261
+
262
+ if (propName === 'textContent') return raw;
263
+ if (raw === '') return undefined;
264
+
265
+ try {
266
+ return JSON.parse(raw);
267
+ } catch {
268
+ return raw;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Serialize a JS value for assignment to a (usually string-typed) DOM value
274
+ * property, mirroring legacy be-value-added write-back:
275
+ * - DOM-typed properties (`checked`, `valueAsNumber`, `valueAsDate`) take the raw value
276
+ * - a `Date` is written as an ISO string (so `<time>.dateTime` round-trips)
277
+ * - plain objects / arrays are JSON-stringified
278
+ */
279
+ export function serializeForProperty(propName: string, nv: any): any {
280
+ switch (propName) {
281
+ case 'checked':
282
+ case 'valueAsNumber':
283
+ case 'valueAsDate':
284
+ return nv;
285
+ }
286
+ if (nv instanceof Date) return nv.toISOString();
287
+ if (nv !== null && typeof nv === 'object') return JSON.stringify(nv);
288
+ return nv;
289
+ }
290
+
291
+ /**
292
+ * Resolve the effective language ("computed lang") for an element.
293
+ *
294
+ * There is no DOM accessor for this, so we walk: nearest ancestor element with a
295
+ * `lang` / `xml:lang` attribute, crossing shadow-root boundaries via the host
296
+ * (matching how `:lang()` behaves in modern browsers), then `<html lang>`, then
297
+ * `navigator.language`. Slot reprojection is intentionally not followed — HTML
298
+ * language inheritance is defined over the real node tree, not the flat tree.
299
+ *
300
+ * @returns a BCP-47 tag, or undefined if nothing is resolvable
301
+ */
302
+ export function resolveLang(element: Element): string | undefined {
303
+ let node: Node | null = element;
304
+ while (node != null) {
305
+ if (node.nodeType === 1 /* ELEMENT_NODE */) {
306
+ const el = node as Element;
307
+ const lang = el.getAttribute('lang') || el.getAttribute('xml:lang');
308
+ if (lang) return lang;
309
+ node = node.parentNode;
310
+ } else if (node.nodeType === 11 /* DOCUMENT_FRAGMENT_NODE, incl. ShadowRoot */) {
311
+ node = (node as ShadowRoot).host ?? null;
312
+ } else {
313
+ node = null; // Document (9) or detached — stop
314
+ }
315
+ }
316
+ if (typeof document !== 'undefined' && document.documentElement.lang) {
317
+ return document.documentElement.lang;
318
+ }
319
+ return (typeof navigator !== 'undefined' && navigator.language) || undefined;
320
+ }
321
+
203
322
  /**
204
323
  * Infer the most appropriate display property for an element
205
324
  * @param element - The element to infer the property for
@@ -139,6 +139,18 @@ Plus infrastructure:
139
139
  - `attachInternals()` called in the constructor
140
140
  - Async `fallbackSpawn` for lazy-loading all feature implementations
141
141
 
142
+ ### Injecting a custom or package-local feature
143
+
144
+ `assignFeatures` accepts a `spawn` per feature key — a class, an async loader, or
145
+ an **import-path string**. A string `spawn` overrides the catalog `fallbackSpawn`
146
+ and is dynamically `import()`ed through the page's import map, so `el-maker.json`
147
+ stays pure JSON. Use it to add a feature that isn't in the catalog, or to swap in
148
+ a package-local **subclass** of a catalog feature when the generic one needs
149
+ element-specific logic. See
150
+ [NewHTMLFirstCustomElement.md → "give a shared feature element-specific logic"](./NewHTMLFirstCustomElement.md#how-do-i-give-a-shared-feature-element-specific-logic-penciling-in)
151
+ and the [css-charts](https://github.com/bahrus/css-charts) conversion for a
152
+ worked example ("penciling in" `CSSChartsH2OTable extends H2OTable`).
153
+
142
154
 
143
155
 
144
156
 
@@ -71,10 +71,14 @@ A custom element feature is a class that:
71
71
 
72
72
  ## Step 3: Create Type Definitions
73
73
 
74
+ **All types for the feature live in `types/[project-name]/types.d.ts` — nothing else.** Do not scatter `@typedef {Object} ...` blocks through the `.js` file. That includes the `customData` / injection-config shape, the `detail` payload of any event the feature dispatches, and every internal helper type. The `.js` file only ever *imports* these via `@import`; it never defines them. A reader (or the package that later adopts the feature) should be able to learn the whole type surface from the one `.d.ts`.
75
+
74
76
  Create `types/[project-name]/types.d.ts` with the feature structure:
75
77
 
76
78
  ```typescript
77
- import { SpawnContext } from "../assign-gingerly/types";
79
+ import { SpawnContext, FeatureSpawnContext } from "../assign-gingerly/types";
80
+
81
+ export { FeatureSpawnContext };
78
82
 
79
83
  /**
80
84
  * Configuration/properties that the feature exposes
@@ -96,9 +100,26 @@ export type AP = AllProps;
96
100
  export type PAP = Partial<AP>;
97
101
 
98
102
  /**
99
- * Context passed to the feature constructor
103
+ * The `customData` passed through the injection config — parsed by the
104
+ * constructor into initial state. Keep every configurable knob here.
105
+ */
106
+ export interface CustomData {
107
+ myProp?: string;
108
+ eventType?: string;
109
+ }
110
+
111
+ /**
112
+ * `detail` payload for any CustomEvent the feature dispatches on the host.
100
113
  */
101
- export interface FeatureSpawnContext extends SpawnContext {
114
+ export interface MyFeatureResolvedDetail {
115
+ // ...
116
+ }
117
+ ```
118
+
119
+ If a project's `types/assign-gingerly/types.d.ts` does not yet export `FeatureSpawnContext`, define it locally in this file instead (as `truth-sourcer` and `face-up` do):
120
+
121
+ ```typescript
122
+ export interface FeatureSpawnContext {
102
123
  key: string;
103
124
  optIn: any;
104
125
  injection: any;
@@ -110,7 +131,9 @@ export interface FeatureSpawnContext extends SpawnContext {
110
131
  **Key points:**
111
132
  - `FeatureProps` — the public API of the feature
112
133
  - `AllProps` — includes internal state like a WeakRef to the host element
134
+ - `CustomData` — the injection-config shape; the constructor reads `ctx.injection.customData` and narrows it to this type
113
135
  - The feature class does NOT need to extend any base class
136
+ - The `.js` file imports all of the above with `/** @import {...} from './types/[project-name]/types' */` — it defines no types of its own
114
137
 
115
138
  ## Step 4: Create the Feature Class
116
139
 
@@ -150,7 +173,7 @@ export { MyFeature };
150
173
  - Constructor signature: `(hostElement, ctx, initVals)`
151
174
  - Store host as a `WeakRef` to avoid preventing garbage collection
152
175
  - Apply `initVals` via `Object.assign` in the constructor
153
- - Use `@ts-check` with JSDoc type imports from the `types/` folder
176
+ - Use `@ts-check` with JSDoc type imports from the `types/` folder — all type definitions live in `types/[project-name]/types.d.ts`, never as inline `@typedef` blocks in the `.js`
154
177
  - No compiled TypeScript — ship raw `.js` files
155
178
 
156
179
  ## Step 5: Create imports.html
@@ -679,6 +702,7 @@ customElements.define('my-element', MyElement);
679
702
 
680
703
  - **Call `assignFeatures` before `customElements.define()`** — getters must be on the prototype before instances exist
681
704
  - **Use `@ts-check`** — catches type errors early in `.js` files
705
+ - **Keep all types in `types/[project-name]/types.d.ts`** — including `customData` and event `detail` shapes; the `.js` only `@import`s them, it never declares `@typedef`s
682
706
  - **Store host as WeakRef** — prevents memory leaks
683
707
  - **Keep features focused** — one responsibility per feature class
684
708
  - **Use `validateShape`** — catches injection errors early in development
@@ -7,7 +7,12 @@
7
7
 
8
8
  - **[plus-minus](https://github.com/bahrus/plus-minus)** -- Expand / Collapse component - More robust examples of dynamic DOM manipulation with the help of roundabout configuration. Also demonstrates use of the DX libraries to get typing intellisense help.
9
9
 
10
- - **[side-burger](https://github.com/bahrus/side-burger)** -- Side Drawer component with menu.
10
+ - **[side-burger](https://github.com/bahrus/side-burger)** -- Side Drawer component with menu. Shows integration with many features, including swipe-dismiss, focus-trap-feature, others
11
+
12
+ - **[up-down-counter](https://github.com/bahrus/up-down-counter)** -- Shows use of toLocaleString() formatting.
13
+
14
+ - **[css-charts](https://github.com/bahrus/css-charts)** -- Wrapper around [charts.css](https://chartscss.org/). Demonstrates: reflecting boolean props onto a child element as CSS classes with the multi-invoke (`=*`) operator (see [Step 7 → "toggle a CSS class from a boolean property"](#how-can-i-toggle-a-css-class-from-a-boolean-property)); list rendering with `manageTemplateList`; and **"penciling in"** a package-local feature subclass (`CSSChartsH2OTable extends H2OTable`) via a string `spawn` when a shared feature needs element-specific logic (see [Step 7 → "give a shared feature element-specific logic"](#how-do-i-give-a-shared-feature-element-specific-logic-penciling-in)). Its `Chats/Conversion.md` is a blow-by-blow log of the trans-render → assign-gingerly conversion.
15
+
11
16
 
12
17
  ## Step 4
13
18
 
@@ -453,6 +458,176 @@ const raConfig = {
453
458
 
454
459
  [Please fully digest all the attribute parsing tha assign-gingerly provides before configuring the attributes.](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/withAttrs.md)
455
460
 
461
+ ### How can I display a number with local formatting?
462
+
463
+ **Goal:** the legacy `xtal-element` transform `"% count": "localize"` wrote
464
+ `count.toLocaleString()` (e.g. `30000` → `"30,000"`) into the display element.
465
+ Reproduce it declaratively by calling `toLocaleString` from inside the RHS path
466
+ string of a `merge`.
467
+
468
+ **How it works:** the `assign-gingerly` path evaluator normally *reads* each
469
+ segment as a property. If a segment names a method that you listed in
470
+ `withMethods`, it is *invoked* instead. When that method is the **last** segment,
471
+ has **no `|` suffix**, and is **not followed by an argument segment**, it is
472
+ called with zero arguments and its **return value becomes the resolved value**.
473
+ So `'?.count?.toLocaleString'` resolves to `count.toLocaleString()` → `"30,000"`.
474
+
475
+ > Contrast with the `|` suffix (e.g. `'?.deref|'`): that also calls the method
476
+ > with no arguments but **discards** the return value — use `|` for side effects,
477
+ > omit it when you want the result.
478
+
479
+ **Wiring (in `el-maker.mjs`):**
480
+
481
+ ```JS
482
+ import { akaMethods as m } from 'assign-gingerly/DX/emojis.js';
483
+
484
+ // 1. Register toLocaleString as a callable method for the path evaluator.
485
+ // m['🌐'] is the DX alias for the string 'toLocaleString'.
486
+ const withMethods = [m['🔍'], m['🌐']];
487
+
488
+ const $ = (/** @type {typeof paths<RuntimeProps>} */ (/** @type {any} */(paths)))({ withMethods });
489
+
490
+ const raConfig = {
491
+ assignOptions: {
492
+ akaMethods: {
493
+ '🔍': m['🔍'], // querySelector
494
+ '🌐': m['🌐'], // toLocaleString
495
+ },
496
+ },
497
+ merges: [
498
+ {
499
+ ifKeyIn: ['count'],
500
+ assign: {
501
+ // '?.count?.🌐' works too — the emoji is just an alias for the
502
+ // 'toLocaleString' segment. Both serialize to a plain string.
503
+ '?.countData?.textContent': '?.count?.toLocaleString',
504
+ value: '?.count',
505
+ },
506
+ },
507
+ ],
508
+ defaultPropVals: {
509
+ count: 30000,
510
+ name: '',
511
+ },
512
+ };
513
+ ```
514
+
515
+ **Passing a locale or options:** append the argument as the next segment, e.g.
516
+ `'?.count?.toLocaleString?.en-US'` → `count.toLocaleString('en-US')`.
517
+
518
+ **Serialization:** `render()` emits the segment as a literal string
519
+ (`"?.count?.toLocaleString"`), so the generated `el-maker.json` stays fully
520
+ JSON-serializable — no function is ever embedded.
521
+
522
+ ### How can I toggle a CSS class from a boolean property?
523
+
524
+ **Goal:** the legacy `trans-render` transform `{sa: '.show-primary-axis', o: 'showPrimaryAxis'}`
525
+ means "add class `show-primary-axis` when `showPrimaryAxis` is truthy, remove it
526
+ otherwise." Reproduce it with the assign-gingerly **multi-invoke** (`=*`)
527
+ operator, which calls one `withMethods` method **once per argument-list** on the RHS.
528
+
529
+ **How it works:** `'?.el?.classList?.toggle =*'` calls `el.classList.toggle(...)`
530
+ once for each entry in the RHS array. Each entry is `[className, condition]`. The
531
+ `!!` prefix coerces the condition to a real boolean, so a missing / falsy source
532
+ *removes* the class instead of flipping it — the whole merge is idempotent.
533
+ `toggle` must be listed in `withMethods`.
534
+
535
+ **Wiring (in `el-maker.mjs`):**
536
+
537
+ ```JS
538
+ const raConfig = {
539
+ assignOptions: {
540
+ withMethods: ['querySelector', 'toggle'],
541
+ },
542
+ merges: smoothOver([
543
+ {
544
+ // capture the child once…
545
+ ifAllOf: ['clone'],
546
+ ...doAssign(set($.tableEl).to($.clone.querySelector('table'))),
547
+ },
548
+ {
549
+ // …then (re)run the toggles when the ref appears or any input flips
550
+ ifAllOf: ['tableEl'],
551
+ ifKeyIn: ['tableEl', 'isBar', 'isColumn', 'showLabels', 'showPrimaryAxis'],
552
+ assign: {
553
+ '?.tableEl?.classList?.toggle =*': [
554
+ ['bar', '!!?.isBar'],
555
+ ['column', '!!?.isColumn'],
556
+ ['show-labels', '!!?.showLabels'],
557
+ ['show-primary-axis', '!!?.showPrimaryAxis'],
558
+ ],
559
+ },
560
+ },
561
+ ]),
562
+ };
563
+ ```
564
+
565
+ There is no DX (`$` / `set`) token for `=*` yet — write the key and the
566
+ `[class, '!!?.prop']` pairs as plain strings inside `assign`. `smoothOver` leaves
567
+ them untouched and they serialize straight to JSON.
568
+
569
+ See [multi-invoke.md](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/multi-invoke.md)
570
+ and [css-charts](https://github.com/bahrus/css-charts) (a 10-class example) for more.
571
+
572
+ ### How do I give a shared feature element-specific logic? ("penciling in")
573
+
574
+ Sometimes a reusable el-maker feature does *most* of what you need but not the
575
+ last mile. Example: the `h2o-table` feature scrapes a slotted `<table>` into
576
+ `[{ key, value }, …]`, but [css-charts](https://github.com/bahrus/css-charts) also
577
+ has to scale those values per `chart-type` (`Math.max`, cumulative sums) —
578
+ genuinely imperative code that can't be expressed as a merge.
579
+
580
+ Rather than fork the feature or drop back to a full custom-element class, **pencil
581
+ in a subclass defined inside your package and inject it by string `spawn`:**
582
+
583
+ 1. `MyH2OTable.js` at your package root — override just the hook the base exposes:
584
+
585
+ ```JS
586
+ import { H2OTable } from 'h2o-table/H2OTable.js'; // the generic base
587
+
588
+ export default class MyH2OTable extends H2OTable {
589
+ massageData(rows) {
590
+ const { chartType } = /** @type {any} */ (this.hostElement);
591
+ /* …element-specific transform… */
592
+ return rows;
593
+ }
594
+ }
595
+ ```
596
+
597
+ 2. Point the feature at it in `el-maker.mjs`:
598
+
599
+ ```JS
600
+ const features = {
601
+ assignFeatures: {
602
+ roundabout: { customData: { raConfig }, withAttrs },
603
+ templateMaker: {},
604
+ h2oTable: {
605
+ spawn: 'my-element/MyH2OTable.js', // resolved via the import map
606
+ customData: { itemprops: ['key', 'value'] },
607
+ },
608
+ },
609
+ };
610
+ ```
611
+
612
+ Why it works:
613
+
614
+ - `assignFeatures` uses `featureConfig.spawn` when present and only falls back to
615
+ the catalog `fallbackSpawn` otherwise — so your subclass wins.
616
+ - A string `spawn` is dynamically `import()`ed through the page's import map
617
+ (`"my-element/": "/"`), and the module's `default` export (or first exported
618
+ class) is used. `el-maker.json` stays pure JSON — the string is all that's stored.
619
+ - The base must expose the hook (`massageData()` here) and any state the subclass
620
+ needs as **public** getters — `#private` fields are invisible across the
621
+ subclass boundary.
622
+ - Pull the feature's output into the reactive graph with a merge, gated on
623
+ whatever changes its inputs:
624
+ `{ ifKeyIn: ['slotChangeCount', 'chartType'], assign: { data: '?.h2oTable?.data' } }`.
625
+ The feature re-computes lazily on read; the merge is what makes roundabout
626
+ re-read it.
627
+
628
+ This keeps the generic feature reusable while the element-specific bit lives where
629
+ it belongs. Once the pattern proves out, that logic can be promoted into the
630
+ feature itself (config-driven) or published as its own feature.
456
631
 
457
632
  ## Step 8
458
633
 
@@ -469,7 +644,20 @@ Create a build instruction in package.json:
469
644
 
470
645
  ## Create the Demo Page
471
646
 
472
- For example:
647
+ **Only the first instance of the custom element on the page carries the `imp-h`
648
+ attribute and the `<script type=precede>`.** That first instance does the
649
+ one-time global setup:
650
+
651
+ - `imp-h="<name>/root.html"` imports the declarative shadow-DOM template once.
652
+ - `<script type=precede data-extends=el-maker src="<name>/el-maker.json">`
653
+ registers the element (extends `ElementMaker`, applies the feature JSON) via
654
+ `customElements.define`.
655
+
656
+ Once the element is defined, **every other instance is just the bare tag** —
657
+ repeating `imp-h` or the `precede` script is unnecessary (and wasteful: it
658
+ re-imports and re-registers). Subsequent instances upgrade automatically and
659
+ reuse the same template and configuration; they can still set their own
660
+ attributes.
473
661
 
474
662
  ```html
475
663
  <!DOCTYPE html>
@@ -488,10 +676,16 @@ For example:
488
676
  </head>
489
677
  <body>
490
678
  <be-hive></be-hive>
679
+
680
+ <!-- FIRST instance: imp-h + precede => imports the template and defines the element -->
491
681
  <plus-minus imp-h="plus-minus/root.html">
492
682
  <script type=precede data-extends=el-maker src="plus-minus/el-maker.json"></script>
493
683
  </plus-minus>
494
684
 
685
+ <!-- Every later instance is just the bare tag; attributes still work -->
686
+ <plus-minus></plus-minus>
687
+ <plus-minus expanded></plus-minus>
688
+
495
689
  </body>
496
690
  </html>
497
691
  ```
@@ -30,11 +30,19 @@ export class MyElementElement extends ElementMaker {
30
30
 
31
31
  ## Optional Step 6: Create the Element-Specific Feature (if any)
32
32
 
33
- If your element has unique behavior beyond what the inherited features provide, but the functionality is more than trivial in implementing, consider creating a custom element feature for that functionality, following [NewCustomElementFeature.md](./NewCustomElementFeature.md).
33
+ Most element-specific behavior can and should live directly in the element class (see Step 5). Reserve the custom element feature pattern for behavior that is genuinely reusable across multiple custom element projects.
34
34
 
35
- For example, `time-ticker` has a `TimeTicker.js` feature that provides precise drift-correcting ticking.
35
+ Consider creating a custom element feature **only** when:
36
36
 
37
- If the feature proves useful beyond that one component, it is probably a good idea to move that feature into the el-maker package.
37
+ - The behavior is non-trivial to implement, **and**
38
+ - There is a significant chance that other custom element libraries would want to reuse it, **and**
39
+ - Extracting it into a separate module improves testability or dependency management without adding unnecessary indirection.
40
+
41
+ If the behavior is unique to a single element — for example, rendering chips for a specific `<select multiple>` UI pattern — keep the logic in the element class instead. Follow [NewCustomElementFeature.md](./NewCustomElementFeature.md) when a separate feature is warranted.
42
+
43
+ `time-ticker`, for example, defines a `TimeTicker.js` feature because precise drift-correcting ticking is a generally useful capability.
44
+
45
+ If a feature proves useful beyond the element that first introduced it, consider moving it into the `el-maker` catalog or publishing it as a standalone package.
38
46
 
39
47
  ## Step 7: Create defRef.mjs (Roundabout Configuration)
40
48
 
@@ -360,6 +360,14 @@ export interface IAssignGingerlyOptions {
360
360
  * ]
361
361
  */
362
362
  enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
363
+
364
+ /**
365
+ * Handler implementations scoped to this call, forwarded to `assignFrom` when
366
+ * these options are reused by higher-level features (e.g. roundabout merges).
367
+ * Key: the `do` name referenced in handler configs. Value: a class
368
+ * constructor, an import path, or a `builtIns.*` alias string.
369
+ */
370
+ handlers?: Record<string, AssignFromHandlerConstructor | string>;
363
371
  }
364
372
 
365
373
  /**
@@ -0,0 +1,68 @@
1
+ import { ElementEnhancementGateway, SpawnContext } from "../assign-gingerly/types";
2
+
3
+ export interface EndUserProps {
4
+ /**
5
+ * Intl.NumberFormatOptions / Intl.DateTimeFormatOptions, parsed as JSON from
6
+ * the base attribute (`be-intl='{ ... }'`). The semantic sub-attributes below
7
+ * are folded into a copy of this object by `onFormattingChange`; explicit JSON
8
+ * keys win over the semantic equivalents.
9
+ */
10
+ format?: Intl.NumberFormatOptions | Intl.DateTimeFormatOptions;
11
+
12
+ /** `be-intl-style` — e.g. "currency", "decimal", "percent". */
13
+ style?: string;
14
+ /** `be-intl-currency` — ISO 4217 code, e.g. "EUR". */
15
+ currency?: string;
16
+ /** `be-intl-weekday` — e.g. "long", "short", "narrow". */
17
+ weekday?: string;
18
+ /** `be-intl-year` — e.g. "numeric", "2-digit". */
19
+ year?: string;
20
+ /** `be-intl-month` — e.g. "long", "short", "numeric". */
21
+ month?: string;
22
+ /** `be-intl-day` — e.g. "numeric", "2-digit". */
23
+ day?: string;
24
+
25
+ /**
26
+ * BCP-47 locale tag. When not supplied explicitly it is derived from the
27
+ * enhanced element's `lang` attribute, falling back to the runtime default locale.
28
+ */
29
+ locale?: string;
30
+
31
+ /**
32
+ * When true, re-derive `locale` whenever the enhanced element's `lang` attribute changes.
33
+ * Off by default (the legacy `observeAttr` behavior).
34
+ */
35
+ observeLang?: boolean;
36
+ }
37
+
38
+ export interface AllProps extends EndUserProps {
39
+ enhancedElement: Element & ElementEnhancementGateway;
40
+
41
+ /**
42
+ * The raw value pulled off the enhanced element:
43
+ * a number for `<data>` / `<output>`, a Date for `<time>`.
44
+ */
45
+ value?: number | Date | string;
46
+
47
+ intlDateFormat?: Intl.DateTimeFormat;
48
+ intlNumberFormat?: Intl.NumberFormat;
49
+
50
+ resolved?: boolean;
51
+
52
+ /** Flipped true once `roundabout()` has finished its initial attribute-read pass. */
53
+ initialized?: boolean;
54
+ }
55
+
56
+ export type AP = AllProps;
57
+
58
+ export type PAP = Partial<AP>;
59
+
60
+ export type ProPAP = Promise<PAP>;
61
+
62
+ export interface Actions {
63
+ init(self: AP, enhancedElement: Element & ElementEnhancementGateway, ctx: SpawnContext, initVals: PAP): Promise<void>;
64
+ hydrate(self: AP): ProPAP;
65
+ onFormattingChange(self: AP): PAP;
66
+ formatNumber(self: AP): void;
67
+ formatDate(self: AP): void;
68
+ }
@@ -0,0 +1,71 @@
1
+ import {IIdRefs} from '../id-referencer/types.d.ts';
2
+
3
+ /**
4
+ * Public API properties for the chip-away custom element
5
+ */
6
+ export interface EndUserProps {
7
+ /**
8
+ * Space-separated list of IDs for the select elements to mirror as chips
9
+ */
10
+ for: string;
11
+
12
+ /**
13
+ * When `true`, collapse each referenced `<select>` to a **single** chip
14
+ * whose label is a comma-delimited list of the selected option texts,
15
+ * instead of one chip per selected option. Its delete (✕) clears every
16
+ * selected option for that `<select>`.
17
+ *
18
+ * The boolean `join` attribute seeds the initial value (server-rendered
19
+ * config); after that, set the `join` property to change it at runtime — a
20
+ * `when_join_changes_call_hydrate` compact re-renders. Not a `sourceOfTruth`
21
+ * attribute, so the attribute is not kept in sync with the property.
22
+ */
23
+ join: boolean;
24
+
25
+ /**
26
+ * When `true`, render chips for display only — no per-option delete (✕), no
27
+ * per-`<select>` "clear all", and no joined-chip delete. The referenced
28
+ * `<select>` elements are not otherwise touched.
29
+ *
30
+ * Same wiring as {@link join}: boolean `readonly` attribute seeds the
31
+ * initial value, the `readonly` property is authoritative afterward, and a
32
+ * `when_readonly_changes_call_hydrate` compact re-renders.
33
+ */
34
+ readonly: boolean;
35
+
36
+ /**
37
+ * `join` only: once the number of selected options **exceeds** this, the
38
+ * single summary chip's label becomes `"<n> Selected"` instead of the
39
+ * comma-delimited list. Leave unset for no limit.
40
+ *
41
+ * Seeded from the numeric `max-join` attribute; the `maxJoin` property is
42
+ * authoritative afterward, and a `when_maxJoin_changes_call_hydrate` compact
43
+ * re-renders. See {@link join}.
44
+ */
45
+ maxJoin?: number;
46
+ }
47
+
48
+ /**
49
+ * Full property set including internal state managed by the custom element
50
+ */
51
+ export interface AllProps extends EndUserProps {
52
+ /** `for` split on whitespace into an id list. */
53
+ splitFor: string[];
54
+ }
55
+
56
+ export type AP = AllProps;
57
+
58
+
59
+
60
+ /**
61
+ * Runtime type for the custom element instance, including the lazily-spawned
62
+ * `idRefs` feature.
63
+ */
64
+ export interface RunTimeProps extends AllProps, HTMLElement {
65
+ idRefs: IIdRefs
66
+ }
67
+
68
+ export interface Actions {
69
+ hydrate(self: AP): void;
70
+ temp(self: AP): void;
71
+ }
@@ -3,6 +3,7 @@ import {FontFaceFeatureConfig} from '../font-face-feature/types.js';
3
3
  import {CustomData as TSCD} from '../truth-sourcer/types.js';
4
4
  import {CustomData as FUCD} from '../face-up/types.js';
5
5
  import {AttrPatterns} from '../assign-gingerly/types.js';
6
+ import {IdRefsCustomData} from '../id-referencer/types.js';
6
7
 
7
8
  export interface ElMakerConfig<AllProps = any, TActions = AllProps> {
8
9
  assignFeatures: {
@@ -30,6 +31,21 @@ export interface ElMakerConfig<AllProps = any, TActions = AllProps> {
30
31
  truthSourcer?: {
31
32
  spawn?: string,
32
33
  customData?: TSCD
34
+ },
35
+ idRefs?: {
36
+ spawn?: string,
37
+ customData?: IdRefsCustomData
38
+ },
39
+ focusTrap?: {
40
+ spawn?: string,
41
+ customData?: any
42
+ },
43
+ h2oTable?: {
44
+ spawn?: string,
45
+ customData?: {
46
+ /** `itemprop` names to scrape from each `[itemscope]` row, in order. */
47
+ itemprops: string[]
48
+ }
33
49
  }
34
50
  }
35
- }
51
+ }
@@ -0,0 +1,73 @@
1
+ import { FeatureSpawnContext } from "../assign-gingerly/types";
2
+
3
+ export { FeatureSpawnContext };
4
+
5
+ /**
6
+ * Configuration for the `H2OTable` ("HTML → Object Table") feature, supplied via
7
+ * the injection's `customData`.
8
+ *
9
+ * ```js
10
+ * customElements.assignFeatures(MyElement, {
11
+ * h2oTable: { customData: { itemprops: ['key', 'value'] } }
12
+ * });
13
+ * ```
14
+ */
15
+ export interface CustomData {
16
+ /**
17
+ * The `itemprop` names to read from each light-DOM `[itemscope]` row, in
18
+ * order. Each one becomes a key on the matching {@link DataRecord}.
19
+ */
20
+ itemprops: string[];
21
+ }
22
+
23
+ /**
24
+ * One extracted row: each configured `itemprop` name mapped to the value the
25
+ * assign-gingerly inferencer reads for that element (`<data>`/`<input type=number>`
26
+ * → number, `<time>` → date string, form controls → `value`, anything else →
27
+ * `textContent`). Props whose element is missing from a given row are omitted.
28
+ */
29
+ export type DataRecord = Record<string, any>;
30
+
31
+ /**
32
+ * The members the feature exposes on `host.h2oTable`.
33
+ */
34
+ export interface IH2OTable {
35
+ /**
36
+ * The host's light-DOM `[itemscope]` rows, projected onto plain objects
37
+ * using the configured {@link CustomData.itemprops} and then passed through
38
+ * {@link IH2OTable.massageData}. Re-scraped from the DOM on every read —
39
+ * there is no caching, so it always reflects the current light DOM.
40
+ */
41
+ readonly data: DataRecord[];
42
+
43
+ /**
44
+ * The configured `itemprop` names, in order.
45
+ */
46
+ readonly itemprops: string[];
47
+
48
+ /**
49
+ * Post-process hook, called by {@link IH2OTable.data} with the freshly
50
+ * scraped rows. The base class implements this as the identity function;
51
+ * subclasses override it to add computed columns, filter, sort, etc.
52
+ */
53
+ massageData(data: DataRecord[]): DataRecord[];
54
+ }
55
+
56
+ /**
57
+ * Public prop surface — alias kept for parity with the other el-maker feature
58
+ * type modules (`TemplateMakerProps`, `TruthSourcerProps`, …).
59
+ */
60
+ export interface H2OTableProps extends IH2OTable {}
61
+
62
+ /**
63
+ * Full internal state of an `H2OTable` instance.
64
+ */
65
+ export interface AllProps extends H2OTableProps {
66
+ /** WeakRef back to the host custom element. */
67
+ hostRef: WeakRef<HTMLElement> | null;
68
+ /** Resolved copy of {@link CustomData.itemprops}. */
69
+ itemprops: string[];
70
+ }
71
+
72
+ export type AP = AllProps;
73
+ export type PAP = Partial<AP>;
@@ -0,0 +1,35 @@
1
+ import { FeatureSpawnContext } from "../assign-gingerly/types";
2
+
3
+ export { FeatureSpawnContext };
4
+
5
+ /**
6
+ * Configuration for the `IdRefs` feature, supplied via the injection's
7
+ * `customData`.
8
+ *
9
+ * Note: this local variant is handed its id list directly (via
10
+ * `idRefs.searchFor = string[]`) rather than reading a host attribute, so there is
11
+ */
12
+ export interface IdRefsCustomData {
13
+ /**
14
+ * Event dispatched on the host whenever a DOM-mutation-driven pass changes
15
+ * the resolved element set. Defaults to `'id-referencer:resolved'`.
16
+ */
17
+ eventType?: string;
18
+ }
19
+
20
+ /**
21
+ * `detail` payload of the event named by {@link IdRefsCustomData.eventType}.
22
+ */
23
+ export interface IdRefsResolvedDetail {
24
+ /** The ordered id list currently being resolved. */
25
+ ids: string[];
26
+ /** The resolved, still-connected elements, in id order. */
27
+ elements: Element[];
28
+ }
29
+
30
+ export interface IIdRefs {
31
+
32
+ searchFor: string[];
33
+
34
+ readonly elements: Element[];
35
+ }
@@ -15,14 +15,32 @@ export declare const display: symbol;
15
15
  */
16
16
  export declare class Infer<TValue = any, TDisplay = any> {
17
17
  get enhancedElement(): Element;
18
- constructor(enhancedElement?: Element);
18
+ constructor(enhancedElement?: Element, propName?: string);
19
+ /** Live, type-coerced read of the element's inferred value property. */
19
20
  get value(): TValue | undefined;
20
21
  set value(nv: TValue);
21
22
  get display(): TDisplay | undefined;
22
23
  set display(nv: TDisplay);
23
24
  get eventType(): string;
25
+ /** The inferred value property name (e.g. 'value', 'checked', 'dateTime'). */
26
+ get valueProperty(): string;
27
+ get defaultRemoteBindingPropName(): string;
28
+ /**
29
+ * EventTarget that emits an event named after the changed property.
30
+ * Custom elements with a native propagator return that; otherwise an
31
+ * InferencedPropagator using best-effort change detection.
32
+ */
33
+ getPropagator(): Promise<EventTarget>;
34
+ setDisplay(vm: any): void;
24
35
  }
25
36
 
37
+ /**
38
+ * Read the inferred value property off an element and coerce it to a natural
39
+ * JavaScript type (Date for <time>, number/boolean via JSON parse for <data>,
40
+ * schema.org itemtype hints honored, textContent verbatim).
41
+ */
42
+ export declare function coerceElementValue(element: Element, propName?: string): any;
43
+
26
44
  /**
27
45
  * Registry item for the Infer enhancement
28
46
  */
@@ -103,6 +103,8 @@ export interface ParserOptions {
103
103
  * // With normalizeWhitespace: false -> " First. Second. "
104
104
  */
105
105
  normalizeWhitespace?: boolean;
106
+
107
+ delimiter?: string;
106
108
  }
107
109
 
108
110
  /**
@@ -38,6 +38,20 @@ export interface AllProps extends SwipeDismissProps {
38
38
  /** WeakRef to the host custom element. */
39
39
  hostRef: WeakRef<Element>;
40
40
 
41
+ /** Live gesture progress, refreshed on every pointermove and on release. */
42
+ progressState: {
43
+ /** Always-positive drag magnitude along the axis, clamped to panel size. */
44
+ deltaPx: number;
45
+ /** `deltaPx` as a fraction of panel size (0–1). */
46
+ fraction: number;
47
+ /**
48
+ * `deltaPx` signed for a screen-space `translate`: negative for a
49
+ * left/up drawer, positive for right/down. Feed this straight into
50
+ * `translateX()` / `translateY()` so the panel follows the finger
51
+ * regardless of which edge it is docked to.
52
+ */
53
+ translatePx: number;
54
+ };
41
55
  }
42
56
 
43
57
  export type AP = AllProps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.91",
3
+ "version": "0.0.93",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -360,6 +360,14 @@ export interface IAssignGingerlyOptions {
360
360
  * ]
361
361
  */
362
362
  enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
363
+
364
+ /**
365
+ * Handler implementations scoped to this call, forwarded to `assignFrom` when
366
+ * these options are reused by higher-level features (e.g. roundabout merges).
367
+ * Key: the `do` name referenced in handler configs. Value: a class
368
+ * constructor, an import path, or a `builtIns.*` alias string.
369
+ */
370
+ handlers?: Record<string, AssignFromHandlerConstructor | string>;
363
371
  }
364
372
 
365
373
  /**