assign-gingerly 0.0.53 → 0.0.55
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 +847 -5
- package/assignFrom.js +229 -9
- package/assignFrom.ts +338 -9
- package/assignGingerly.js +34 -1
- package/assignGingerly.ts +54 -1
- package/beVigilant.js +73 -0
- package/beVigilant.ts +85 -0
- package/enhanceAll.js +106 -0
- package/enhanceAll.ts +138 -0
- package/handlers/join.js +74 -0
- package/handlers/join.ts +80 -0
- package/handlers/lazyLoad.js +212 -0
- package/handlers/lazyLoad.ts +307 -0
- package/handlers/lazyLoadSwitch.js +58 -0
- package/handlers/lazyLoadSwitch.ts +63 -0
- package/handlers/microDataJoin.js +184 -0
- package/handlers/microDataJoin.ts +270 -0
- package/inferencer/.gitmodules +3 -0
- package/inferencer/.vscode/settings.json +2 -0
- package/inferencer/InferencedPropagator.js +230 -0
- package/inferencer/InferencedPropagator.ts +269 -0
- package/inferencer/LICENSE +21 -0
- package/inferencer/README.md +524 -0
- package/inferencer/Requirements/SupportForPropagator.md +368 -0
- package/inferencer/imports.html +7 -0
- package/inferencer/inferencer.js +254 -0
- package/inferencer/inferencer.ts +292 -0
- package/inferencer/package-lock.json +129 -0
- package/inferencer/package.json +60 -0
- package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
- package/inferencer/playwright-report/index.html +90 -0
- package/inferencer/playwright.config.ts +54 -0
- package/inferencer/test-results/.last-run.json +6 -0
- package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
- package/inferencer/tests/inferencedPropagator.html +428 -0
- package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
- package/inferencer/tests/inferencer.html +355 -0
- package/inferencer/tests/inferencer.spec.ts +19 -0
- package/inferencer/tsconfig.json +19 -0
- package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
- package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
- package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
- package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
- package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
- package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
- package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
- package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
- package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
- package/inferencer/types/LICENSE +21 -0
- package/inferencer/types/NewCustomElement.md +388 -0
- package/inferencer/types/NewCustomElementFeature.md +683 -0
- package/inferencer/types/NewEnhancementInstructions.md +705 -0
- package/inferencer/types/README.md +2 -0
- package/inferencer/types/agrace/types.d.ts +11 -0
- package/inferencer/types/assign-gingerly/types.d.ts +572 -0
- package/inferencer/types/be-a-beacon/types.d.ts +17 -0
- package/inferencer/types/be-bound/types.d.ts +66 -0
- package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
- package/inferencer/types/be-calculating/types.d.ts +54 -0
- package/inferencer/types/be-clonable/types.d.ts +38 -0
- package/inferencer/types/be-committed/types.d.ts +22 -0
- package/inferencer/types/be-consoling/types.d.ts +24 -0
- package/inferencer/types/be-decked-with/types.d.ts +26 -0
- package/inferencer/types/be-delible/types.d.ts +27 -0
- package/inferencer/types/be-dispatching/types.d.ts +34 -0
- package/inferencer/types/be-evanescent/types.d.ts +20 -0
- package/inferencer/types/be-flashy/types.d.ts +21 -0
- package/inferencer/types/be-gone/types.d.ts +25 -0
- package/inferencer/types/be-observing/types.d.ts +55 -0
- package/inferencer/types/be-reflective/types.d.ts +78 -0
- package/inferencer/types/be-reformable/types.d.ts +49 -0
- package/inferencer/types/be-render-neutral/types.d.ts +32 -0
- package/inferencer/types/be-switched/types.d.ts +146 -0
- package/inferencer/types/be-typed/types.d.ts +32 -0
- package/inferencer/types/be-valued/types.d.ts +22 -0
- package/inferencer/types/data-props/types.d.ts +34 -0
- package/inferencer/types/do-inc/types.d.ts +56 -0
- package/inferencer/types/do-invoke/types.d.ts +38 -0
- package/inferencer/types/do-merge/types.d.ts +28 -0
- package/inferencer/types/do-toggle/types.d.ts +31 -0
- package/inferencer/types/face-up/types.d.ts +100 -0
- package/inferencer/types/fetch-for/types.d.ts +36 -0
- package/inferencer/types/folder-picker/types.d.ts +21 -0
- package/inferencer/types/global.d.ts +29 -0
- package/inferencer/types/id-generation/types.d.ts +26 -0
- package/inferencer/types/inferencer/types.d.ts +46 -0
- package/inferencer/types/mount-observer/types.d.ts +363 -0
- package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
- package/inferencer/types/pipe-in/types.d.ts +52 -0
- package/inferencer/types/roundabout/types.d.ts +268 -0
- package/inferencer/types/soak-up/types.d.ts +40 -0
- package/inferencer/types/templ-maker/types.d.ts +43 -0
- package/inferencer/types/time-ticker/types.d.ts +62 -0
- package/inferencer/types/truth-sourcer/types.d.ts +44 -0
- package/inferencer/upSearch.js +27 -0
- package/inferencer/upSearch.ts +26 -0
- package/inferencer/withScopePerimeter.js +27 -0
- package/inferencer/withScopePerimeter.ts +33 -0
- package/inferredAssignments.js +38 -0
- package/inferredAssignments.ts +65 -0
- package/isAllowedImportPath.js +42 -0
- package/isAllowedImportPath.ts +53 -0
- package/package.json +57 -3
- package/paths.js +231 -0
- package/paths.ts +413 -0
- package/processHandlerCommands.js +188 -0
- package/processHandlerCommands.ts +217 -0
- package/resolveIdRef.js +144 -0
- package/resolveIdRef.ts +170 -0
- package/resolveValues.js +41 -2
- package/resolveValues.ts +41 -1
- package/transitionHelper.js +109 -0
- package/transitionHelper.ts +132 -0
- package/types/assign-gingerly/types.d.ts +89 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# assign-gingerly and assign-
|
|
1
|
+
# assign-gingerly, assign-tentatively, and assign-from
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml)
|
|
4
4
|
[](http://badge.fury.io/js/assign-gingerly)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
## Introduction
|
|
11
11
|
|
|
12
|
-
This package starts out innocently enough -- it provides
|
|
12
|
+
This package starts out innocently enough -- it provides the ability to carefully merge one object into another. This is a primitive sorely lacking in the web, and this package is a polyfill for what we (me with a lot of help from AI) would like to see built into the platform. We make no apologies about adding these features directly to the underlying API's, as it is part of a proposal which is sitting there gathering dust, with no apparent alternatives under consideration. In particular the reference:
|
|
13
13
|
|
|
14
14
|
```JavaScript
|
|
15
15
|
import 'assign-gingerly/object-extension.js';
|
|
@@ -17,7 +17,7 @@ import 'assign-gingerly/object-extension.js';
|
|
|
17
17
|
|
|
18
18
|
has the "side effect" of enhancing the platform API in a way that this proposal can only hope the platform chooses to adopt in the future (or some variation).
|
|
19
19
|
|
|
20
|
-
One can achieve the same functionality with a little more work, and "playing nicer" with the platform by importing assign-gingerly.js and/or
|
|
20
|
+
One can achieve the same functionality with a little more work, and "playing nicer" with the platform by importing assign-gingerly.js, assign-tentatively.js, and/or assignFrom.js, which has no such side effects.
|
|
21
21
|
|
|
22
22
|
## Object Extension Pattern
|
|
23
23
|
|
|
@@ -39,7 +39,7 @@ So in our view this package helps fill the void left by not supporting the "is"
|
|
|
39
39
|
|
|
40
40
|
Anyway, let's start out detailing the more innocent features of this package / polyfill.
|
|
41
41
|
|
|
42
|
-
The
|
|
42
|
+
The three utility functions are:
|
|
43
43
|
|
|
44
44
|
## assignGingerly
|
|
45
45
|
|
|
@@ -56,6 +56,22 @@ The second fundamental utility function is:
|
|
|
56
56
|
|
|
57
57
|
assignTentatively provides a far more limited subset of functionality compared to assignGingerly. The tradeoff is that assignTentatively can do something important assignGingerly cannot do -- be "reversed". This can be quite useful for some scenarios. Think of how css "turns on" visual effects while conditions are met, then reverts to how things were before the conditions were met when the conditions are no longer met, as if nothing happened. Another example is allowing user edits to be rolled back as they repeatedly hit "ctrl+z".
|
|
58
58
|
|
|
59
|
+
The third fundamental utility function is:
|
|
60
|
+
|
|
61
|
+
## assignFrom
|
|
62
|
+
|
|
63
|
+
assignFrom builds on assignGingerly by adding a resolution step: RHS values that are `?.`-prefixed path strings are resolved against a source object before assignment. This enables a declarative, data-driven pattern where a view model (or any source) feeds values into a target through path expressions — replacing imperative property lookups with a single configuration object.
|
|
64
|
+
|
|
65
|
+
assignFrom adds support for:
|
|
66
|
+
|
|
67
|
+
1. Resolving RHS path strings against a source object (`from`).
|
|
68
|
+
2. Protocol resolution (`globalThis://`, `localStorage://`, custom protocols).
|
|
69
|
+
3. Handler plugins via the ` =>` operator for custom logic (template instantiation, DOM manipulation, etc.).
|
|
70
|
+
4. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
|
|
71
|
+
5. Spread merging via the `"..."` key.
|
|
72
|
+
|
|
73
|
+
All of assignGingerly's features (nested paths, `withMethods`, `aka`, `@each`, `@eachTime`, registry, etc.) are inherited.
|
|
74
|
+
|
|
59
75
|
## Example 1 - assignGingerly as a "superset" of Object.assign:
|
|
60
76
|
|
|
61
77
|
```TypeScript
|
|
@@ -1115,7 +1131,7 @@ const result = assignGingerly({}, {
|
|
|
1115
1131
|
"[Symbol.for('BqnnTPWRHkWdVGWcGQoAiw')]": true,
|
|
1116
1132
|
style: {
|
|
1117
1133
|
height: '40px'
|
|
1118
|
-
}
|
|
1134
|
+
},
|
|
1119
1135
|
enh: {
|
|
1120
1136
|
mellowYellow?.madAboutFourteen': true
|
|
1121
1137
|
}
|
|
@@ -3425,6 +3441,789 @@ The `"..."` key causes the resolved object to be merged (spread) into the result
|
|
|
3425
3441
|
|
|
3426
3442
|
**Note:** Both `resolveValues` and `assignFrom` are async (return Promises) to support async protocol handlers (e.g., IndexedDB, fetch). For patterns without protocols, the async overhead is negligible.
|
|
3427
3443
|
|
|
3444
|
+
## assignFrom Handlers (` =>` operator)
|
|
3445
|
+
|
|
3446
|
+
`assignFrom` supports a plugin system via the ` =>` operator. When a LHS key ends with ` =>`, instead of normal assignment, a handler class is invoked to perform custom logic (DOM manipulation, template instantiation, etc.).
|
|
3447
|
+
|
|
3448
|
+
### Defining and using a handler
|
|
3449
|
+
|
|
3450
|
+
Handlers are provided via the `handlers` option — scoped to each `assignFrom` call:
|
|
3451
|
+
|
|
3452
|
+
```JavaScript
|
|
3453
|
+
import { assignFrom } from 'assign-gingerly/assignFrom.js';
|
|
3454
|
+
|
|
3455
|
+
class MyListHandler {
|
|
3456
|
+
constructor(config) {
|
|
3457
|
+
this.config = config;
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
async assign(lhsTarget, resolvedParams, options) {
|
|
3461
|
+
const { list, template } = resolvedParams;
|
|
3462
|
+
// Custom logic: e.g., clone template for each item in list
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
await assignFrom(myElement, {
|
|
3467
|
+
'?.querySelector?.tbody =>': {
|
|
3468
|
+
do: 'my-list',
|
|
3469
|
+
resolve: {
|
|
3470
|
+
list: '?.rankings',
|
|
3471
|
+
template: 'globalThis://myTemplate'
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
}, {
|
|
3475
|
+
withMethods: ['querySelector'],
|
|
3476
|
+
from: viewModel,
|
|
3477
|
+
protocols: { globalThis: k => globalThis[k] },
|
|
3478
|
+
handlers: {
|
|
3479
|
+
'my-list': MyListHandler, // class constructor
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
```
|
|
3483
|
+
|
|
3484
|
+
Handlers can also be specified as import paths (dynamically loaded on demand):
|
|
3485
|
+
|
|
3486
|
+
```JavaScript
|
|
3487
|
+
await assignFrom(myElement, {
|
|
3488
|
+
'?.querySelector?.tbody =>': {
|
|
3489
|
+
do: 'my-list',
|
|
3490
|
+
resolve: { list: '?.rankings', template: 'globalThis://myTemplate' }
|
|
3491
|
+
}
|
|
3492
|
+
}, {
|
|
3493
|
+
from: viewModel,
|
|
3494
|
+
withMethods: ['querySelector'],
|
|
3495
|
+
protocols: { globalThis: k => globalThis[k] },
|
|
3496
|
+
handlers: {
|
|
3497
|
+
'my-list': './handlers/my-list.js', // relative path
|
|
3498
|
+
'vendor-chart': 'chart-package/handler.js', // bare specifier (import map)
|
|
3499
|
+
}
|
|
3500
|
+
});
|
|
3501
|
+
```
|
|
3502
|
+
|
|
3503
|
+
Import paths must be local (relative, absolute, or bare specifiers — no cross-domain URLs). The module's default export is checked first; otherwise the first exported class with an `assign` method is used.
|
|
3504
|
+
|
|
3505
|
+
Built-in handlers (`builtIns.lazyLoad`, `builtIns.join`, etc.) auto-load without needing to be listed in `handlers`.
|
|
3506
|
+
|
|
3507
|
+
**How it works:**
|
|
3508
|
+
|
|
3509
|
+
1. Keys ending with ` =>` are separated from normal keys.
|
|
3510
|
+
2. Normal keys are processed via `resolveValues` + `assignGingerly` as usual.
|
|
3511
|
+
3. For handler keys: the LHS path is evaluated (with `withMethods` support) to get the target.
|
|
3512
|
+
4. The `resolve` map is processed through `resolveValues` — paths (`?.`), protocols (`globalThis://`), and literals are all resolved.
|
|
3513
|
+
5. The handler class (looked up via `do` in `options.handlers`, then built-in auto-load) is instantiated with the full config, then `assign(target, resolvedParams, options)` is called.
|
|
3514
|
+
|
|
3515
|
+
**The `resolve` map supports:**
|
|
3516
|
+
- `?.` paths — resolved against `options.from`
|
|
3517
|
+
- Protocol strings — resolved via `options.protocols`
|
|
3518
|
+
- Plain literals — passed through unchanged
|
|
3519
|
+
|
|
3520
|
+
### Built-in handler: `builtIns.lazyLoad`
|
|
3521
|
+
|
|
3522
|
+
Conditionally loads (clones) a template into a target element. Uses comment markers to track inserted content and supports show/hide/remove modes. Built-in handlers are auto-loaded on demand — no explicit import is needed.
|
|
3523
|
+
|
|
3524
|
+
```JavaScript
|
|
3525
|
+
await assignFrom(document.body, {
|
|
3526
|
+
'?.querySelector?..mainView =>': {
|
|
3527
|
+
do: 'builtIns.lazyLoad',
|
|
3528
|
+
resolve: {
|
|
3529
|
+
if: '?.isVisible',
|
|
3530
|
+
instantiate: 'globalThis://myTemplate',
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}, { withMethods: ['querySelector'], from: myVM, protocols: { globalThis: k => globalThis[k] } });
|
|
3534
|
+
```
|
|
3535
|
+
|
|
3536
|
+
**Resolve parameters:**
|
|
3537
|
+
|
|
3538
|
+
| Parameter | Type | Description |
|
|
3539
|
+
|-----------|------|-------------|
|
|
3540
|
+
| `if` | boolean | Show content when truthy, hide/remove when falsy |
|
|
3541
|
+
| `instantiate` | HTMLTemplateElement | The template to clone (typically resolved via globalThis protocol) |
|
|
3542
|
+
| `method` | string | `'appendChild'` (default) or `'prepend'` — where to place markers |
|
|
3543
|
+
| `forget` | boolean | If true, removes nodes entirely when `if` is false (default: hides with `hidden` attribute) |
|
|
3544
|
+
|
|
3545
|
+
**Behavior:**
|
|
3546
|
+
|
|
3547
|
+
- **First load (`if` = true, no existing content):** Clones the template, inserts content between `<!--?start name="X"-->` / `<!--?end-->` comment markers.
|
|
3548
|
+
- **Show (content exists but hidden):** Removes `hidden` attribute from elements between markers.
|
|
3549
|
+
- **Hide (`if` = false, `forget` = false):** Adds `hidden` attribute to elements between markers.
|
|
3550
|
+
- **Remove (`if` = false, `forget` = true):** Removes nodes between markers entirely. Markers persist for re-insertion if `if` becomes true again.
|
|
3551
|
+
|
|
3552
|
+
This is useful for conditional rendering, routing, and lazy-loading views.
|
|
3553
|
+
|
|
3554
|
+
### View Transitions
|
|
3555
|
+
|
|
3556
|
+
Both `builtIns.lazyLoad` and `builtIns.lazyLoadSwitch` support animated transitions via the [View Transition API](https://developer.mozilla.org/docs/Web/API/View_Transition_API). Enable with `transitional: true` in the resolve map:
|
|
3557
|
+
|
|
3558
|
+
```JavaScript
|
|
3559
|
+
await assignFrom(container, {
|
|
3560
|
+
'?.querySelector?..outlet =>': {
|
|
3561
|
+
do: 'builtIns.lazyLoad',
|
|
3562
|
+
resolve: {
|
|
3563
|
+
if: '?.isVisible',
|
|
3564
|
+
instantiate: 'globalThis://myTemplate',
|
|
3565
|
+
transitional: true,
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
}, { withMethods: ['querySelector'], from: vm, protocols: { globalThis: k => globalThis[k] } });
|
|
3569
|
+
```
|
|
3570
|
+
|
|
3571
|
+
**How it works:**
|
|
3572
|
+
|
|
3573
|
+
1. When `transitional: true`, DOM mutations (show/hide/clone) are wrapped in `document.startViewTransition()`.
|
|
3574
|
+
2. Instead of the `hidden` attribute, a CSS class (`.ag-hide` by default) is toggled for visibility.
|
|
3575
|
+
3. The actual animation is entirely CSS-driven — you control timing, easing, and effects via your own styles.
|
|
3576
|
+
4. If `document.startViewTransition` is unavailable (older browsers), the handler falls back to direct DOM mutation with no animation.
|
|
3577
|
+
|
|
3578
|
+
**CSS customization:**
|
|
3579
|
+
|
|
3580
|
+
The handler injects a minimal default style (`.ag-hide { display: none }`) once per rootNode. Override it with your own for animated transitions:
|
|
3581
|
+
|
|
3582
|
+
```css
|
|
3583
|
+
/* Fade + slide transition */
|
|
3584
|
+
.ag-hide {
|
|
3585
|
+
opacity: 0;
|
|
3586
|
+
transform: translateY(-10px);
|
|
3587
|
+
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
/* View transition keyframes (for cross-fade between old/new states) */
|
|
3591
|
+
::view-transition-old(root) {
|
|
3592
|
+
animation: fade-out 0.5s ease;
|
|
3593
|
+
}
|
|
3594
|
+
::view-transition-new(root) {
|
|
3595
|
+
animation: fade-in 0.5s ease;
|
|
3596
|
+
}
|
|
3597
|
+
```
|
|
3598
|
+
|
|
3599
|
+
**Custom hide class:**
|
|
3600
|
+
|
|
3601
|
+
Use the `hideClass` resolve parameter to use a different CSS class name:
|
|
3602
|
+
|
|
3603
|
+
```JavaScript
|
|
3604
|
+
resolve: {
|
|
3605
|
+
if: '?.isVisible',
|
|
3606
|
+
instantiate: 'globalThis://myTemplate',
|
|
3607
|
+
transitional: true,
|
|
3608
|
+
hideClass: 'my-custom-hide', // default: 'ag-hide'
|
|
3609
|
+
}
|
|
3610
|
+
```
|
|
3611
|
+
|
|
3612
|
+
**Concurrency handling:**
|
|
3613
|
+
|
|
3614
|
+
- Rapid toggling (e.g., clicking a button repeatedly) is handled gracefully.
|
|
3615
|
+
- An in-flight transition is cancelled (`skipTransition()`) before starting a new one.
|
|
3616
|
+
- Duplicate show/hide requests while one is pending are ignored (re-entry protection).
|
|
3617
|
+
|
|
3618
|
+
**Routing example with transitions:**
|
|
3619
|
+
|
|
3620
|
+
```JavaScript
|
|
3621
|
+
await assignFrom(container, {
|
|
3622
|
+
'?.querySelector?..routerOutlet =>': [
|
|
3623
|
+
{ do: 'builtIns.lazyLoadSwitch', resolve: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView', transitional: true } },
|
|
3624
|
+
{ do: 'builtIns.lazyLoadSwitch', resolve: { lhs: '?.route', rhs: 'settings', instantiate: 'globalThis://settingsView', transitional: true } },
|
|
3625
|
+
{ do: 'builtIns.lazyLoadSwitch', resolve: { lhs: '?.route', rhs: 'profile', instantiate: 'globalThis://profileView', transitional: true } },
|
|
3626
|
+
]
|
|
3627
|
+
}, { withMethods: ['querySelector'], from: router, protocols: { globalThis: k => globalThis[k] } });
|
|
3628
|
+
```
|
|
3629
|
+
|
|
3630
|
+
**Shared utility for external consumers (`be-switched`, etc.):**
|
|
3631
|
+
|
|
3632
|
+
The transition coordination logic is exported as a reusable utility:
|
|
3633
|
+
|
|
3634
|
+
```JavaScript
|
|
3635
|
+
import { withTransition, ensureHideStyle } from 'assign-gingerly/transitionHelper.js';
|
|
3636
|
+
|
|
3637
|
+
// Inject the hide class style once per rootNode
|
|
3638
|
+
ensureHideStyle(myShadowRoot);
|
|
3639
|
+
|
|
3640
|
+
// Wrap any DOM mutation with view transition coordination
|
|
3641
|
+
withTransition(markerNode, 'show', true, () => {
|
|
3642
|
+
element.classList.remove('ag-hide');
|
|
3643
|
+
});
|
|
3644
|
+
|
|
3645
|
+
withTransition(markerNode, 'hide', true, () => {
|
|
3646
|
+
element.classList.add('ag-hide');
|
|
3647
|
+
});
|
|
3648
|
+
```
|
|
3649
|
+
|
|
3650
|
+
See the visual demo at `demos/view-transition-demo.html`.
|
|
3651
|
+
|
|
3652
|
+
### Multiple Handlers
|
|
3653
|
+
|
|
3654
|
+
When the RHS of a ` =>` key is an array, each element is treated as a separate handler config and they are executed sequentially (awaiting each before proceeding to the next). All handlers share the same LHS target.
|
|
3655
|
+
|
|
3656
|
+
```JavaScript
|
|
3657
|
+
await assignFrom(document.body, {
|
|
3658
|
+
'?.querySelector?..mainView =>': [
|
|
3659
|
+
{
|
|
3660
|
+
do: 'builtIns.lazyLoad',
|
|
3661
|
+
resolve: {
|
|
3662
|
+
if: '?.isVisible',
|
|
3663
|
+
instantiate: 'globalThis://viewTemplate',
|
|
3664
|
+
}
|
|
3665
|
+
},
|
|
3666
|
+
{
|
|
3667
|
+
do: 'applyTheme',
|
|
3668
|
+
resolve: {
|
|
3669
|
+
theme: '?.currentTheme'
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
]
|
|
3673
|
+
}, { withMethods: ['querySelector'], from: myVM, protocols: { globalThis: k => globalThis[k] } });
|
|
3674
|
+
```
|
|
3675
|
+
|
|
3676
|
+
**Behavior:**
|
|
3677
|
+
|
|
3678
|
+
- Empty array — silent no-op.
|
|
3679
|
+
- Single-element array — identical to passing the object directly.
|
|
3680
|
+
- Nested arrays — throws an error (not supported).
|
|
3681
|
+
- Mixed `do` values — fully supported, each handler is looked up independently.
|
|
3682
|
+
- Error handling — fail-fast. If a handler throws, remaining handlers are skipped.
|
|
3683
|
+
|
|
3684
|
+
## Looped Substitution (`where_x_in`, `where_y_in`, `where_z_in`)
|
|
3685
|
+
|
|
3686
|
+
`assignFrom` supports expanding template patterns into multiple concrete assignments via variable substitution. Placeholders `${x}`, `${y}`, and `${z}` in pattern keys and string values are replaced with each value from the corresponding option array.
|
|
3687
|
+
|
|
3688
|
+
```JavaScript
|
|
3689
|
+
const vm = {
|
|
3690
|
+
firstName: 'Monkey',
|
|
3691
|
+
lastName: 'Luffy'
|
|
3692
|
+
};
|
|
3693
|
+
|
|
3694
|
+
await assignFrom(myForm, {
|
|
3695
|
+
'?.[name="${x}"]': '?.${x}'
|
|
3696
|
+
}, {
|
|
3697
|
+
from: vm,
|
|
3698
|
+
withMethods: ['querySelector'],
|
|
3699
|
+
where_x_in: ['firstName', 'lastName']
|
|
3700
|
+
});
|
|
3701
|
+
|
|
3702
|
+
// Expands to the equivalent of:
|
|
3703
|
+
// '?.[name="firstName"]': '?.firstName' → querySelector('[name="firstName"]').value = 'Monkey'
|
|
3704
|
+
// '?.[name="lastName"]': '?.lastName' → querySelector('[name="lastName"]').value = 'Luffy'
|
|
3705
|
+
```
|
|
3706
|
+
|
|
3707
|
+
**How it works:**
|
|
3708
|
+
|
|
3709
|
+
1. Before any other processing, `assignFrom` checks for `where_x_in`, `where_y_in`, and `where_z_in` in options.
|
|
3710
|
+
2. For each pattern entry whose key or value contains the placeholder (e.g., `${x}`), the entry is expanded — one copy per value in the array.
|
|
3711
|
+
3. Substitution applies to both LHS keys and all RHS string values (including nested objects like handler `resolve` maps).
|
|
3712
|
+
4. Multiple variables produce a **cartesian product**: x is expanded first, then y, then z. Result count = x.length × y.length × z.length.
|
|
3713
|
+
5. Non-string RHS values pass through untouched.
|
|
3714
|
+
|
|
3715
|
+
**Cartesian expansion with multiple variables:**
|
|
3716
|
+
|
|
3717
|
+
```JavaScript
|
|
3718
|
+
await assignFrom(grid, {
|
|
3719
|
+
'?.querySelector?.[data-row="${x}"][data-col="${y}"]?.textContent': '${x}-${y}'
|
|
3720
|
+
}, {
|
|
3721
|
+
from: {},
|
|
3722
|
+
withMethods: ['querySelector'],
|
|
3723
|
+
where_x_in: ['1', '2'],
|
|
3724
|
+
where_y_in: ['A', 'B']
|
|
3725
|
+
});
|
|
3726
|
+
|
|
3727
|
+
// Produces 4 entries (2 × 2):
|
|
3728
|
+
// '?.[data-row="1"][data-col="A"]' → '1-A'
|
|
3729
|
+
// '?.[data-row="1"][data-col="B"]' → '1-B'
|
|
3730
|
+
// '?.[data-row="2"][data-col="A"]' → '2-A'
|
|
3731
|
+
// '?.[data-row="2"][data-col="B"]' → '2-B'
|
|
3732
|
+
```
|
|
3733
|
+
|
|
3734
|
+
**With handler ( =>) keys:**
|
|
3735
|
+
|
|
3736
|
+
Substitution applies inside handler `resolve` maps too:
|
|
3737
|
+
|
|
3738
|
+
```JavaScript
|
|
3739
|
+
await assignFrom(container, {
|
|
3740
|
+
'?.querySelector?..${x}View =>': {
|
|
3741
|
+
do: 'builtIns.lazyLoad',
|
|
3742
|
+
resolve: {
|
|
3743
|
+
if: '?.${x}Visible',
|
|
3744
|
+
instantiate: 'globalThis://${x}Template'
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
}, {
|
|
3748
|
+
from: vm,
|
|
3749
|
+
withMethods: ['querySelector'],
|
|
3750
|
+
protocols: { globalThis: k => globalThis[k] },
|
|
3751
|
+
where_x_in: ['home', 'settings', 'profile']
|
|
3752
|
+
});
|
|
3753
|
+
```
|
|
3754
|
+
|
|
3755
|
+
**Edge cases:**
|
|
3756
|
+
|
|
3757
|
+
- Empty array (`where_x_in: []`) — template entries produce nothing (silent no-op).
|
|
3758
|
+
- Missing option — if a pattern contains `${x}` but `where_x_in` is not provided, the literal `${x}` remains in the string.
|
|
3759
|
+
- Entries without placeholders — passed through unchanged.
|
|
3760
|
+
|
|
3761
|
+
### Built-in handler: `builtIns.join`
|
|
3762
|
+
|
|
3763
|
+
Joins a resolved array into a single string. Supports nested sub-arrays with "all-or-nothing" semantics for optional segments. Uses the **return-value protocol** — the handler returns the joined string, which `processHandlerCommands` assigns back to the LHS path.
|
|
3764
|
+
|
|
3765
|
+
```JavaScript
|
|
3766
|
+
const vm = {
|
|
3767
|
+
lastName: 'Targaryen',
|
|
3768
|
+
firstName: 'Helaena'
|
|
3769
|
+
};
|
|
3770
|
+
|
|
3771
|
+
await assignFrom(oElement, {
|
|
3772
|
+
'?.textContent =>': {
|
|
3773
|
+
do: 'builtIns.join',
|
|
3774
|
+
resolve: {
|
|
3775
|
+
value: ['?.lastName', ', ', '?.firstName']
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
}, { from: vm });
|
|
3779
|
+
|
|
3780
|
+
// oElement.textContent = 'Targaryen, Helaena'
|
|
3781
|
+
```
|
|
3782
|
+
|
|
3783
|
+
**How it works:**
|
|
3784
|
+
|
|
3785
|
+
1. The `resolve.value` array is resolved by `resolveValues` — `?.` path strings are replaced with actual values from `options.from`.
|
|
3786
|
+
2. Top-level `null`/`undefined` values are filtered out.
|
|
3787
|
+
3. Nested sub-arrays use **all-or-nothing** semantics: if any element in a sub-array resolves to `null`/`undefined`, the entire sub-array is dropped.
|
|
3788
|
+
4. Remaining elements are joined with the separator (default: `''`, empty string).
|
|
3789
|
+
5. The joined string is returned and assigned to the LHS path.
|
|
3790
|
+
|
|
3791
|
+
**Optional segments with nested arrays:**
|
|
3792
|
+
|
|
3793
|
+
```JavaScript
|
|
3794
|
+
const vm = {
|
|
3795
|
+
lastName: 'Targaryen',
|
|
3796
|
+
middleName: undefined, // not present
|
|
3797
|
+
firstName: 'Helaena'
|
|
3798
|
+
};
|
|
3799
|
+
|
|
3800
|
+
await assignFrom(oElement, {
|
|
3801
|
+
'?.textContent =>': {
|
|
3802
|
+
do: 'builtIns.join',
|
|
3803
|
+
resolve: {
|
|
3804
|
+
value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
}, { from: vm });
|
|
3808
|
+
|
|
3809
|
+
// middleName is undefined → sub-array [', ', undefined] is dropped entirely
|
|
3810
|
+
// oElement.textContent = 'Targaryen, Helaena'
|
|
3811
|
+
|
|
3812
|
+
// If middleName were 'D':
|
|
3813
|
+
// oElement.textContent = 'Targaryen, D, Helaena'
|
|
3814
|
+
```
|
|
3815
|
+
|
|
3816
|
+
**Custom separator:**
|
|
3817
|
+
|
|
3818
|
+
```JavaScript
|
|
3819
|
+
await assignFrom(oElement, {
|
|
3820
|
+
'?.textContent =>': {
|
|
3821
|
+
do: 'builtIns.join',
|
|
3822
|
+
separator: ' | ',
|
|
3823
|
+
resolve: {
|
|
3824
|
+
value: ['?.firstName', '?.lastName']
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}, { from: vm });
|
|
3828
|
+
|
|
3829
|
+
// oElement.textContent = 'Helaena | Targaryen'
|
|
3830
|
+
```
|
|
3831
|
+
|
|
3832
|
+
**Return-value protocol:**
|
|
3833
|
+
|
|
3834
|
+
When a handler's `assign()` method returns a non-`undefined` value, `processHandlerCommands` assigns it back to the LHS path. This is how `builtIns.join` sets `textContent` — the handler computes the string and returns it. Existing handlers like `builtIns.lazyLoad` return `undefined` (void) and operate by side effects, so they're unaffected.
|
|
3835
|
+
|
|
3836
|
+
### Built-in handler: `builtIns.microDataJoin`
|
|
3837
|
+
|
|
3838
|
+
Renders a template array as semantic microdata-annotated DOM. Each dynamic value gets an appropriate HTML element based on its JavaScript type, with `itemprop` set to the property name.
|
|
3839
|
+
|
|
3840
|
+
```JavaScript
|
|
3841
|
+
const vm = {
|
|
3842
|
+
lastName: 'Targaryen',
|
|
3843
|
+
firstName: 'Helaena',
|
|
3844
|
+
birthDT: new Date('July 1, 109'),
|
|
3845
|
+
age: 21,
|
|
3846
|
+
isHappy: false
|
|
3847
|
+
};
|
|
3848
|
+
|
|
3849
|
+
await assignFrom(oSection, {
|
|
3850
|
+
'?.querySelector?.div =>': {
|
|
3851
|
+
do: 'builtIns.microDataJoin',
|
|
3852
|
+
resolve: {
|
|
3853
|
+
template: [
|
|
3854
|
+
{ prop: 'firstName', val: '?.firstName' },
|
|
3855
|
+
' ',
|
|
3856
|
+
{ prop: 'lastName', val: '?.lastName' },
|
|
3857
|
+
' who was born on ',
|
|
3858
|
+
{ prop: 'birthDT', val: '?.birthDT' },
|
|
3859
|
+
' is ',
|
|
3860
|
+
{ prop: 'age', val: '?.age' },
|
|
3861
|
+
' years old'
|
|
3862
|
+
]
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}, { from: vm, withMethods: ['querySelector'] });
|
|
3866
|
+
```
|
|
3867
|
+
|
|
3868
|
+
Produces:
|
|
3869
|
+
|
|
3870
|
+
```html
|
|
3871
|
+
<div itemscope>
|
|
3872
|
+
<!--?start name="microDataJoin"-->
|
|
3873
|
+
<span itemprop="firstName">Helaena</span>
|
|
3874
|
+
|
|
3875
|
+
<span itemprop="lastName">Targaryen</span>
|
|
3876
|
+
who was born on
|
|
3877
|
+
<time itemprop="birthDT" datetime="0109-07-01T...">7/1/0109</time>
|
|
3878
|
+
is
|
|
3879
|
+
<data itemprop="age" value="21">21</data>
|
|
3880
|
+
years old
|
|
3881
|
+
<!--?end-->
|
|
3882
|
+
</div>
|
|
3883
|
+
```
|
|
3884
|
+
|
|
3885
|
+
**Type → element mapping:**
|
|
3886
|
+
|
|
3887
|
+
| JS Type | HTML Element | Attributes | textContent |
|
|
3888
|
+
|---------|-------------|------------|-------------|
|
|
3889
|
+
| `string` | `<span>` | `itemprop` | the value |
|
|
3890
|
+
| `number` | `<data>` | `itemprop`, `value` (raw) | locale-formatted |
|
|
3891
|
+
| `boolean` | `<data>` | `itemprop`, `value` (true/false) | empty |
|
|
3892
|
+
| `Date` | `<time>` | `itemprop`, `datetime` (ISO) | locale-formatted |
|
|
3893
|
+
|
|
3894
|
+
**Optional segments (nested arrays):**
|
|
3895
|
+
|
|
3896
|
+
Same all-or-nothing semantics as `builtIns.join` — if any `val` in a nested sub-array is null/undefined, the entire sub-array is dropped:
|
|
3897
|
+
|
|
3898
|
+
```JavaScript
|
|
3899
|
+
resolve: {
|
|
3900
|
+
template: [
|
|
3901
|
+
{ prop: 'firstName', val: '?.firstName' },
|
|
3902
|
+
[' ', { prop: 'middleName', val: '?.middleName' }], // dropped if middleName is undefined
|
|
3903
|
+
' ',
|
|
3904
|
+
{ prop: 'lastName', val: '?.lastName' }
|
|
3905
|
+
]
|
|
3906
|
+
}
|
|
3907
|
+
```
|
|
3908
|
+
|
|
3909
|
+
**Idempotent updates:**
|
|
3910
|
+
|
|
3911
|
+
Uses comment markers (`<!--?start name="microDataJoin"-->` / `<!--?end-->`) to track rendered content. On first call, creates all elements. On subsequent calls, updates existing elements in place (no re-creation) — `textContent`, `value`, and `datetime` attributes are updated without touching the DOM structure.
|
|
3912
|
+
|
|
3913
|
+
**Authoring with `md` tag:**
|
|
3914
|
+
|
|
3915
|
+
The `md` tagged template literal produces the `{prop, val}` structure from proxy objects — full autocomplete and type safety:
|
|
3916
|
+
|
|
3917
|
+
```TypeScript
|
|
3918
|
+
import { paths, md } from 'assign-gingerly/paths.js';
|
|
3919
|
+
|
|
3920
|
+
interface Person { firstName: string; lastName: string; birthDT: Date; age: number; }
|
|
3921
|
+
const $ = paths<Person>();
|
|
3922
|
+
|
|
3923
|
+
const template = md`${$.firstName} ${$.lastName} who was born on ${$.birthDT} is ${$.age} years old`;
|
|
3924
|
+
// Produces: [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'lastName', val: '?.lastName' }, ...]
|
|
3925
|
+
```
|
|
3926
|
+
|
|
3927
|
+
For custom property names or per-segment formatting, pass an explicit object:
|
|
3928
|
+
|
|
3929
|
+
```TypeScript
|
|
3930
|
+
const template = md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`;
|
|
3931
|
+
```
|
|
3932
|
+
|
|
3933
|
+
## Typed Path Authoring with `paths`, `sp`, and `md`
|
|
3934
|
+
|
|
3935
|
+
For JSON generated config files generated from TypeScript/`.mts`/`mjs` files during a build or server-side rendering, the `paths` utility provides compile-time autocomplete and type safety for `?.`-prefixed path strings. The `sp` tagged template literal ("split into parts") produces arrays suitable for `builtIns.join`. The `md` tagged template literal produces `{prop, val}` objects suitable for `builtIns.microDataJoin`.
|
|
3936
|
+
|
|
3937
|
+
```TypeScript
|
|
3938
|
+
import { paths, sp } from 'assign-gingerly/paths.js';
|
|
3939
|
+
|
|
3940
|
+
interface Person {
|
|
3941
|
+
firstName?: string;
|
|
3942
|
+
middleName?: string;
|
|
3943
|
+
lastName: string;
|
|
3944
|
+
address: { city: string; zip: string };
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
const $ = paths<Person>();
|
|
3948
|
+
|
|
3949
|
+
// sp produces: ['?.lastName', ', ', '?.firstName']
|
|
3950
|
+
// with full autocomplete on $.lastName, $.firstName, etc.
|
|
3951
|
+
export default {
|
|
3952
|
+
'?.textContent =>': {
|
|
3953
|
+
do: 'builtIns.join',
|
|
3954
|
+
resolve: {
|
|
3955
|
+
value: sp`${$.lastName}, ${$.firstName}`
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
};
|
|
3959
|
+
```
|
|
3960
|
+
|
|
3961
|
+
**How `paths` works:**
|
|
3962
|
+
|
|
3963
|
+
- `paths<T>()` creates a deeply-proxied object typed as `T`
|
|
3964
|
+
- Every property access returns a deeper proxy (e.g., `$.address.city`)
|
|
3965
|
+
- `.path` extracts the `?.`-prefixed string: `$.address.city.path` → `'?.address?.city'`
|
|
3966
|
+
- Inside `sp` template literals, `.path` is not needed — proxy objects are auto-detected
|
|
3967
|
+
|
|
3968
|
+
**How `sp` works:**
|
|
3969
|
+
|
|
3970
|
+
- `sp` (split into parts) is a tagged template literal
|
|
3971
|
+
- It interleaves static string segments with interpolated values
|
|
3972
|
+
- Path proxy objects are automatically converted to `?.`-prefixed strings
|
|
3973
|
+
- Arrays passed as interpolations are preserved as nested arrays (for optional segments)
|
|
3974
|
+
|
|
3975
|
+
**Optional segments in `sp`:**
|
|
3976
|
+
|
|
3977
|
+
```TypeScript
|
|
3978
|
+
const $ = paths<Person>();
|
|
3979
|
+
|
|
3980
|
+
// Nested array for all-or-nothing segment:
|
|
3981
|
+
const value = sp`${$.lastName}${[', ', $.middleName]}, ${$.firstName}`;
|
|
3982
|
+
// ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
3983
|
+
```
|
|
3984
|
+
|
|
3985
|
+
**Using `.path` outside of `sp`:**
|
|
3986
|
+
|
|
3987
|
+
When you need the path string in a non-`sp` context (object keys, plain arrays, other expressions):
|
|
3988
|
+
|
|
3989
|
+
```TypeScript
|
|
3990
|
+
const $ = paths<Person>();
|
|
3991
|
+
|
|
3992
|
+
$.lastName.path // '?.lastName'
|
|
3993
|
+
$.address.city.path // '?.address?.city'
|
|
3994
|
+
|
|
3995
|
+
// As an object key:
|
|
3996
|
+
const pattern = {
|
|
3997
|
+
[$.textContent.path]: '?.firstName' // '?.textContent': '?.firstName'
|
|
3998
|
+
};
|
|
3999
|
+
```
|
|
4000
|
+
|
|
4001
|
+
**Benefits:**
|
|
4002
|
+
|
|
4003
|
+
- Full IDE autocomplete on property names
|
|
4004
|
+
- Compile-time errors for typos (e.g., `$.lasName` → TS error)
|
|
4005
|
+
- Template literal syntax reads like a natural string template
|
|
4006
|
+
- Output is a plain JSON-serializable array
|
|
4007
|
+
- No runtime overhead beyond initial proxy creation
|
|
4008
|
+
|
|
4009
|
+
**`md` — microdata template tag:**
|
|
4010
|
+
|
|
4011
|
+
The `md` tag produces `{prop, val}` objects for `builtIns.microDataJoin`:
|
|
4012
|
+
|
|
4013
|
+
```TypeScript
|
|
4014
|
+
import { paths, md } from 'assign-gingerly/paths.js';
|
|
4015
|
+
|
|
4016
|
+
interface Person { firstName: string; lastName: string; birthDT: Date; age: number; }
|
|
4017
|
+
const $ = paths<Person>();
|
|
4018
|
+
|
|
4019
|
+
const template = md`${$.firstName} ${$.lastName} born ${$.birthDT}, age ${$.age}`;
|
|
4020
|
+
// [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'lastName', val: '?.lastName' }, ' born ', { prop: 'birthDT', val: '?.birthDT' }, ', age ', { prop: 'age', val: '?.age' }]
|
|
4021
|
+
```
|
|
4022
|
+
|
|
4023
|
+
For custom property names or per-segment config, pass an explicit object:
|
|
4024
|
+
|
|
4025
|
+
```TypeScript
|
|
4026
|
+
md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`
|
|
4027
|
+
// [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'birthDate', val: '?.birthDT', format: 'long' }]
|
|
4028
|
+
```
|
|
4029
|
+
|
|
4030
|
+
**Difference between `sp` and `md`:**
|
|
4031
|
+
|
|
4032
|
+
| Tag | Output for proxy interpolation | Use with |
|
|
4033
|
+
|-----|-------------------------------|----------|
|
|
4034
|
+
| `sp` | `'?.firstName'` (path string) | `builtIns.join` |
|
|
4035
|
+
| `md` | `{ prop: 'firstName', val: '?.firstName' }` | `builtIns.microDataJoin` |
|
|
4036
|
+
|
|
4037
|
+
Both auto-detect path proxies (no `.path` needed inside template literals) and preserve nested arrays for optional segments.
|
|
4038
|
+
|
|
4039
|
+
## Cached Element Resolution with `#[x]` and `withIds`
|
|
4040
|
+
|
|
4041
|
+
`assignFrom` supports cached element references via the `#[x]` syntax in LHS keys. This provides near-zero-cost repeated access to DOM elements (~10ns via WeakRef) instead of expensive `querySelector` calls (~3,000-17,000ns for class selectors at scale).
|
|
4042
|
+
|
|
4043
|
+
```TypeScript
|
|
4044
|
+
import { assignFrom } from 'assign-gingerly/assignFrom.js';
|
|
4045
|
+
|
|
4046
|
+
await assignFrom(document.body, {
|
|
4047
|
+
'#[main]?.textContent': '?.greeting',
|
|
4048
|
+
'#[main] =>': {
|
|
4049
|
+
do: 'builtIns.lazyLoad',
|
|
4050
|
+
resolve: { if: '?.showContent', instantiate: 'globalThis://myTemplate' }
|
|
4051
|
+
}
|
|
4052
|
+
}, {
|
|
4053
|
+
from: viewModel,
|
|
4054
|
+
withIds: {
|
|
4055
|
+
main: { qry: '.mainView' } // find by class, auto-assign ID, cache
|
|
4056
|
+
}
|
|
4057
|
+
});
|
|
4058
|
+
```
|
|
4059
|
+
|
|
4060
|
+
**How it works:**
|
|
4061
|
+
|
|
4062
|
+
1. On first encounter of `#[main]`, the element is found via `querySelector('.mainView')` against the target.
|
|
4063
|
+
2. If the element doesn't have an `id` attribute, one is auto-generated (`_ag0`, `_ag1`, etc.).
|
|
4064
|
+
3. A `WeakRef` to the element is cached in a module-level `WeakMap` keyed by rootNode.
|
|
4065
|
+
4. On subsequent calls, the cached `WeakRef.deref()` returns the element in ~10ns.
|
|
4066
|
+
5. If the WeakRef is collected (element was GC'd), falls back to `getElementById` (~10-100ns).
|
|
4067
|
+
|
|
4068
|
+
**Two forms of `withIds` configuration:**
|
|
4069
|
+
|
|
4070
|
+
```TypeScript
|
|
4071
|
+
withIds: {
|
|
4072
|
+
x: { qry: '.mainView' }, // Object form: querySelector on target, auto-assign ID
|
|
4073
|
+
y: 'existingId', // String form: element already has an ID, just cache it
|
|
4074
|
+
}
|
|
4075
|
+
```
|
|
4076
|
+
|
|
4077
|
+
**Chaining with `?.` paths:**
|
|
4078
|
+
|
|
4079
|
+
`#[x]` anchors the start of the path. Further `?.` segments chain from the resolved element:
|
|
4080
|
+
|
|
4081
|
+
```TypeScript
|
|
4082
|
+
await assignFrom(document.body, {
|
|
4083
|
+
'#[form]?.querySelector?..username?.value': '?.username',
|
|
4084
|
+
'#[form]?.querySelector?..email?.value': '?.email',
|
|
4085
|
+
'#[header]?.style?.color': '?.themeColor',
|
|
4086
|
+
}, {
|
|
4087
|
+
from: viewModel,
|
|
4088
|
+
withMethods: ['querySelector'],
|
|
4089
|
+
withIds: {
|
|
4090
|
+
form: { qry: '.registration-form' },
|
|
4091
|
+
header: 'page-header'
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
```
|
|
4095
|
+
|
|
4096
|
+
**With handlers (` =>`):**
|
|
4097
|
+
|
|
4098
|
+
```TypeScript
|
|
4099
|
+
await assignFrom(container, {
|
|
4100
|
+
'#[outlet] =>': {
|
|
4101
|
+
do: 'builtIns.lazyLoadSwitch',
|
|
4102
|
+
resolve: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView' }
|
|
4103
|
+
}
|
|
4104
|
+
}, {
|
|
4105
|
+
from: router,
|
|
4106
|
+
withIds: { outlet: { qry: '.router-outlet' } },
|
|
4107
|
+
protocols: { globalThis: k => globalThis[k] }
|
|
4108
|
+
});
|
|
4109
|
+
```
|
|
4110
|
+
|
|
4111
|
+
**Performance context (from benchmarks at 5000 elements):**
|
|
4112
|
+
|
|
4113
|
+
| Method | Chrome | Firefox | Safari |
|
|
4114
|
+
|--------|--------|---------|--------|
|
|
4115
|
+
| `querySelector('.class')` | 9,660ns | 6,810ns | 17,770ns |
|
|
4116
|
+
| `getElementById(id)` | 100ns | 20ns | 10ns |
|
|
4117
|
+
| `Map<id, WeakRef>.deref()` | 12ns | 40ns | 10ns |
|
|
4118
|
+
|
|
4119
|
+
The `#[x]` + `withIds` pattern gives you the Map+WeakRef speed tier automatically.
|
|
4120
|
+
|
|
4121
|
+
**Key behaviors:**
|
|
4122
|
+
|
|
4123
|
+
- **Lazy resolution** — elements are resolved on first encounter, not eagerly at the start.
|
|
4124
|
+
- **Auto-ID generation** — IDs are short and predictable (`_ag0`, `_ag1`, ...), unique within the rootNode.
|
|
4125
|
+
- **GC-safe** — WeakRef cache doesn't prevent element garbage collection.
|
|
4126
|
+
- **Scoped to rootNode** — works correctly inside Shadow DOM (IDs are scoped per shadow root).
|
|
4127
|
+
- **`assignFrom` only** — this feature is not available in `assignGingerly` or `assignTentatively`.
|
|
4128
|
+
|
|
4129
|
+
## Inferred Assignments
|
|
4130
|
+
|
|
4131
|
+
`assignFrom` can automatically distribute source values to matching DOM elements based on structural conventions — no manual path strings needed. The [inferencer](./inferencer/) submodule determines the correct property for each element type.
|
|
4132
|
+
|
|
4133
|
+
```TypeScript
|
|
4134
|
+
const vm = {
|
|
4135
|
+
name: 'Alice',
|
|
4136
|
+
email: 'alice@example.com',
|
|
4137
|
+
user: { role: 'admin', avatar: '/img/alice.png' }
|
|
4138
|
+
};
|
|
4139
|
+
|
|
4140
|
+
await assignFrom(outerDiv, {}, {
|
|
4141
|
+
from: vm,
|
|
4142
|
+
inferredAssignments: {
|
|
4143
|
+
byItemprop: ['name', 'email', 'user']
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4146
|
+
```
|
|
4147
|
+
|
|
4148
|
+
For each key in `byItemprop`, this finds `[itemprop="${key}"]` elements within the target (respecting itemscope boundaries) and sets the value using the inferred property:
|
|
4149
|
+
|
|
4150
|
+
| Element | Inferred Property |
|
|
4151
|
+
|---------|------------------|
|
|
4152
|
+
| `<span itemprop="name">` | `textContent` |
|
|
4153
|
+
| `<input itemprop="email">` | `value` |
|
|
4154
|
+
| `<time itemprop="date">` | `dateTime` |
|
|
4155
|
+
| `<data itemprop="count">` | `value` |
|
|
4156
|
+
| `<div itemprop="user" itemscope="user-card">` | `ish` (manager routing) |
|
|
4157
|
+
|
|
4158
|
+
**Scope perimeter:** Queries respect itemscope boundaries — `[itemprop="name"]` inside a nested `[itemscope]` won't be matched from the outer scope (donut-hole scoping).
|
|
4159
|
+
|
|
4160
|
+
**Pass `true` to infer all source keys:**
|
|
4161
|
+
|
|
4162
|
+
```TypeScript
|
|
4163
|
+
inferredAssignments: { byItemprop: true }
|
|
4164
|
+
```
|
|
4165
|
+
|
|
4166
|
+
For full details, see [docs/inferred-assignments.md](docs/inferred-assignments.md).
|
|
4167
|
+
|
|
4168
|
+
## Bulk Enhancement with `enhance` and `enhanceAll`
|
|
4169
|
+
|
|
4170
|
+
Apply enhancements in bulk to matching elements using EMC (Element Mount Configuration) JSON files that enhancement packages already publish. No manual registration step needed — enhancements are auto-loaded and registered on demand.
|
|
4171
|
+
|
|
4172
|
+
```TypeScript
|
|
4173
|
+
import { assignFrom } from 'assign-gingerly/assignFrom.js';
|
|
4174
|
+
|
|
4175
|
+
await assignFrom(shadowRoot, { /* normal assignments */ }, {
|
|
4176
|
+
from: vm,
|
|
4177
|
+
enhance: [
|
|
4178
|
+
{ emc: 'be-bound/emc.json', matching: '[name]' },
|
|
4179
|
+
{ emc: 'be-observant/emc.json', matching: '[itemprop]' },
|
|
4180
|
+
]
|
|
4181
|
+
});
|
|
4182
|
+
```
|
|
4183
|
+
|
|
4184
|
+
**How it works:**
|
|
4185
|
+
|
|
4186
|
+
1. Dynamically imports each EMC JSON file
|
|
4187
|
+
2. Extracts the `enhConfig` (spawn path, enhKey)
|
|
4188
|
+
3. Finds matching elements via `querySelectorAll` (using `matching` override, or the EMC's own selector)
|
|
4189
|
+
4. Registers the enhancement in the element's `enhancementRegistry` if not already present
|
|
4190
|
+
5. Spawns the enhancement on each matched element via `element.enh.get(registryItem)`
|
|
4191
|
+
|
|
4192
|
+
**Standalone usage (without `assignFrom`):**
|
|
4193
|
+
|
|
4194
|
+
```TypeScript
|
|
4195
|
+
import { enhanceAll } from 'assign-gingerly/enhanceAll.js';
|
|
4196
|
+
|
|
4197
|
+
await enhanceAll(shadowRoot, [
|
|
4198
|
+
{ emc: 'be-bound/emc.json', matching: '[name]' },
|
|
4199
|
+
]);
|
|
4200
|
+
```
|
|
4201
|
+
|
|
4202
|
+
**With `assignGingerly` (fire-and-forget):**
|
|
4203
|
+
|
|
4204
|
+
```TypeScript
|
|
4205
|
+
assignGingerly(shadowRoot, {
|
|
4206
|
+
'?.someProperty': 'value'
|
|
4207
|
+
}, {
|
|
4208
|
+
enhance: [{ emc: 'be-bound/emc.json', matching: '[name]' }]
|
|
4209
|
+
});
|
|
4210
|
+
// Enhancements apply asynchronously in the background
|
|
4211
|
+
```
|
|
4212
|
+
|
|
4213
|
+
**Configuration:**
|
|
4214
|
+
|
|
4215
|
+
| Field | Type | Description |
|
|
4216
|
+
|-------|------|-------------|
|
|
4217
|
+
| `emc` | `string` | Path to EMC JSON file (dynamically imported) |
|
|
4218
|
+
| `matching` | `string?` | CSS selector override (defaults to EMC's own selector) |
|
|
4219
|
+
| `parse` | `boolean?` | Parse attributes via withAttrs (default: `false`, Phase II) |
|
|
4220
|
+
|
|
4221
|
+
**Notes:**
|
|
4222
|
+
|
|
4223
|
+
- No scope perimeter is applied — the selector does all filtering. For reactive observation of new elements, use [mount-observer](https://github.com/bahrus/mount-observer) instead.
|
|
4224
|
+
- Requires JSON module import support (`import ... with { type: 'json' }`).
|
|
4225
|
+
- Enhancement modules are loaded lazily — zero cost until `enhance` is used.
|
|
4226
|
+
|
|
3428
4227
|
## Custom Assignment with `static assignTo` Protocol
|
|
3429
4228
|
|
|
3430
4229
|
Classes can opt into custom assignment behavior by defining a `static assignTo` method. When `assignGingerly` encounters a property whose current value is an instance of such a class, it delegates the assignment to `assignTo` instead of performing the default merge/replace logic.
|
|
@@ -4087,6 +4886,33 @@ This is useful for:
|
|
|
4087
4886
|
4. On first access, the getter spawns the feature instance, validates it (optionally), caches it, and returns it.
|
|
4088
4887
|
5. Because the property is getter-only (no setter), `assignGingerly` automatically merges into the spawned instance when assigning object values to that property.
|
|
4089
4888
|
|
|
4889
|
+
### Features at Registration Time
|
|
4890
|
+
|
|
4891
|
+
Features can also be bundled directly into enhancement and itemscope manager registration configs — no separate `assignFeatures` call needed:
|
|
4892
|
+
|
|
4893
|
+
```TypeScript
|
|
4894
|
+
// Enhancement with features
|
|
4895
|
+
enhancementRegistry.push({
|
|
4896
|
+
spawn: MyEnhancement,
|
|
4897
|
+
enhKey: 'myEnh',
|
|
4898
|
+
features: {
|
|
4899
|
+
photoTaker: { spawn: PhotoTakerImpl },
|
|
4900
|
+
badgeMaker: { spawn: BadgeMakerImpl },
|
|
4901
|
+
}
|
|
4902
|
+
});
|
|
4903
|
+
|
|
4904
|
+
// Itemscope manager with features
|
|
4905
|
+
customElements.itemscopeRegistry.define('my-manager', {
|
|
4906
|
+
manager: MyManagerClass,
|
|
4907
|
+
lifecycleKeys: { dispose: 'cleanup', resolved: 'isReady' },
|
|
4908
|
+
features: {
|
|
4909
|
+
roundabout: { customData: { /* ... */ } },
|
|
4910
|
+
}
|
|
4911
|
+
});
|
|
4912
|
+
```
|
|
4913
|
+
|
|
4914
|
+
This calls `assignFeatures` automatically under the hood using the shared `customElements.featuresRegistry`. The standalone `assignFeatures` call remains available for cases where you need to associate features with a class you don't own or after initial registration.
|
|
4915
|
+
|
|
4090
4916
|
### Basic example
|
|
4091
4917
|
|
|
4092
4918
|
```JavaScript
|
|
@@ -4959,3 +5785,19 @@ See [time-ticker/wireFeatures.js](https://github.com/bahrus/time-ticker/blob/bas
|
|
|
4959
5785
|
|
|
4960
5786
|
- **Nested features**: Support `?.path?.notation` keys directly in `assignFeatures` (without requiring `PropertyBag`).
|
|
4961
5787
|
- **`@each` + async interaction**: Combine async methods with iteration.
|
|
5788
|
+
|
|
5789
|
+
|
|
5790
|
+
## Viewing Locally
|
|
5791
|
+
|
|
5792
|
+
Any web server that serves static files with server-side includes will do but...
|
|
5793
|
+
|
|
5794
|
+
1. Install git
|
|
5795
|
+
2. Fork/clone this repo
|
|
5796
|
+
3. Install node.js
|
|
5797
|
+
4. Open command window to folder where you cloned this repo
|
|
5798
|
+
5. > git submodule add https://github.com/bahrus/types.git types
|
|
5799
|
+
6. > git submodule add https://github.com/bahrus/inferencer.git inferencer
|
|
5800
|
+
7. > git submodule update --init --recursive
|
|
5801
|
+
8. > npm install
|
|
5802
|
+
9. > npm run serve
|
|
5803
|
+
10. Open http://localhost:8000/demo/ in a modern browser
|