assign-gingerly 0.0.57 → 0.0.59
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 +243 -131
- package/assignFrom.js +124 -129
- package/assignFrom.ts +146 -233
- package/assignFromAsync.js +118 -0
- package/assignFromAsync.ts +240 -0
- package/getValues.js +223 -0
- package/getValues.ts +255 -0
- package/handlers/join.ts +1 -1
- package/handlers/lazyLoad.js +2 -2
- package/handlers/lazyLoad.ts +3 -3
- package/handlers/lazyLoadSwitch.ts +1 -1
- package/handlers/manageTemplateList.js +203 -0
- package/handlers/manageTemplateList.ts +240 -0
- package/handlers/microDataJoin.ts +1 -1
- package/index.js +1 -0
- package/index.ts +2 -0
- package/inferredAssignments.js +1 -1
- package/inferredAssignments.ts +2 -2
- package/markerUtils.js +136 -127
- package/package.json +18 -1
- package/processHandlerCommands.js +30 -3
- package/processHandlerCommands.ts +31 -7
- package/resolveValues.js +41 -125
- package/resolveValues.ts +131 -255
- package/transitionHelper.js +11 -5
- package/transitionHelper.ts +11 -5
- package/types/assign-gingerly/types.d.ts +51 -0
- package/waitForSettled.js +57 -0
- package/waitForSettled.ts +65 -0
package/README.md
CHANGED
|
@@ -58,15 +58,29 @@ assignTentatively provides a far more limited subset of functionality compared t
|
|
|
58
58
|
|
|
59
59
|
The third fundamental utility function is:
|
|
60
60
|
|
|
61
|
-
## assignFrom
|
|
61
|
+
## assignFrom (and assignFromAsync)
|
|
62
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.
|
|
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` is synchronous** — it resolves paths, expands substitutions, handles spreads, processes `#[x]` refs, and runs inferred assignments all without yielding to the event loop. Handler commands (` =>`), `beVigilant`, and `enhance` are fire-and-forget (kicked off asynchronously in the background).
|
|
66
|
+
|
|
67
|
+
**`assignFromAsync`** is the awaitable variant for when you need to:
|
|
68
|
+
- Use async protocol handlers (e.g., `fetch`-based resolution)
|
|
69
|
+
- `await` handler completion before proceeding
|
|
70
|
+
- Wait for `enhance` (EMC JSON imports) to finish
|
|
71
|
+
|
|
72
|
+
```TypeScript
|
|
73
|
+
import { assignFrom } from 'assign-gingerly/assignFrom.js'; // sync (default)
|
|
74
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js'; // async when needed
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For most use cases — including `manageTemplateList`, reactive merge cycles, and DOM binding — the sync `assignFrom` is the right choice. It delivers near-vanilla-JS performance by avoiding microtask overhead.
|
|
64
78
|
|
|
65
79
|
assignFrom adds support for:
|
|
66
80
|
|
|
67
81
|
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 (
|
|
82
|
+
2. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols).
|
|
83
|
+
3. Handler plugins via the ` =>` operator for custom logic (fire-and-forget in sync mode, awaitable in async mode).
|
|
70
84
|
4. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
|
|
71
85
|
5. Spread merging via the `"..."` key.
|
|
72
86
|
|
|
@@ -3382,14 +3396,14 @@ assignFrom(target, {
|
|
|
3382
3396
|
|
|
3383
3397
|
For full documentation, see [docs/assignFrom.md](docs/assignFrom.md).
|
|
3384
3398
|
|
|
3385
|
-
## Protocol Resolution in `
|
|
3399
|
+
## Protocol Resolution in `getValues` and `assignFrom`
|
|
3386
3400
|
|
|
3387
|
-
`
|
|
3401
|
+
`getValues` (and by extension `assignFrom`) supports resolving values from external sources via protocol-prefixed strings. This enables declarative references to `globalThis`, `localStorage`, `sessionStorage`, or custom stores.
|
|
3388
3402
|
|
|
3389
3403
|
```JavaScript
|
|
3390
|
-
import {
|
|
3404
|
+
import { getValues } from 'assign-gingerly/getValues.js';
|
|
3391
3405
|
|
|
3392
|
-
const result =
|
|
3406
|
+
const result = getValues({
|
|
3393
3407
|
baseURL: 'globalThis://myAppConfig?.apiBaseUrl',
|
|
3394
3408
|
authToken: 'localStorage://auth?.token',
|
|
3395
3409
|
label: '?.title' // normal path resolution still works
|
|
@@ -3405,7 +3419,7 @@ const result = await resolveValues({
|
|
|
3405
3419
|
|
|
3406
3420
|
1. If a value contains `://` and the part before it matches a key in `protocols`, it's treated as a protocol reference.
|
|
3407
3421
|
2. The protocol handler is called with the key portion (between `://` and the first `?.`, or end of string).
|
|
3408
|
-
3. If a `?.` path follows the key, it's resolved against the handler's result using `
|
|
3422
|
+
3. If a `?.` path follows the key, it's resolved against the handler's result using `getValue`.
|
|
3409
3423
|
4. If the protocol isn't found in the map, the value passes through unchanged (no error).
|
|
3410
3424
|
|
|
3411
3425
|
**Path after protocol key:**
|
|
@@ -3425,7 +3439,7 @@ const result = await resolveValues({
|
|
|
3425
3439
|
```JavaScript
|
|
3426
3440
|
import { assignFrom } from 'assign-gingerly/assignFrom.js';
|
|
3427
3441
|
|
|
3428
|
-
|
|
3442
|
+
assignFrom(myForm, {
|
|
3429
3443
|
"...": "globalThis://qmywdO1vr0SwyuIe4fvzxQ",
|
|
3430
3444
|
path: "api/v2/:operation/:expression",
|
|
3431
3445
|
headers: {
|
|
@@ -3439,7 +3453,96 @@ await assignFrom(myForm, {
|
|
|
3439
3453
|
|
|
3440
3454
|
The `"..."` key causes the resolved object to be merged (spread) into the result before passing to `assignGingerly`, rather than being assigned to a property named `"..."`.
|
|
3441
3455
|
|
|
3442
|
-
**
|
|
3456
|
+
**Async protocol handlers:**
|
|
3457
|
+
|
|
3458
|
+
For protocol handlers that return Promises (e.g., `fetch`, IndexedDB), use `resolveValues` (async) or `assignFromAsync`:
|
|
3459
|
+
|
|
3460
|
+
```JavaScript
|
|
3461
|
+
import { resolveValues } from 'assign-gingerly/resolveValues.js';
|
|
3462
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
|
|
3463
|
+
|
|
3464
|
+
// resolveValues is the async variant of getValues
|
|
3465
|
+
const result = await resolveValues(pattern, source, {
|
|
3466
|
+
protocols: { api: async (key) => (await fetch(`/api/${key}`)).json() }
|
|
3467
|
+
});
|
|
3468
|
+
```
|
|
3469
|
+
|
|
3470
|
+
## Looped Substitution (`where_x_in`, `where_y_in`, `where_z_in`)
|
|
3471
|
+
|
|
3472
|
+
`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.
|
|
3473
|
+
|
|
3474
|
+
```JavaScript
|
|
3475
|
+
const vm = {
|
|
3476
|
+
firstName: 'Monkey',
|
|
3477
|
+
lastName: 'Luffy'
|
|
3478
|
+
};
|
|
3479
|
+
|
|
3480
|
+
assignFrom(myForm, {
|
|
3481
|
+
'?.[name="${x}"]': '?.${x}'
|
|
3482
|
+
}, {
|
|
3483
|
+
from: vm,
|
|
3484
|
+
withMethods: ['querySelector'],
|
|
3485
|
+
where_x_in: ['firstName', 'lastName']
|
|
3486
|
+
});
|
|
3487
|
+
|
|
3488
|
+
// Expands to the equivalent of:
|
|
3489
|
+
// '?.[name="firstName"]': '?.firstName' → querySelector('[name="firstName"]').value = 'Monkey'
|
|
3490
|
+
// '?.[name="lastName"]': '?.lastName' → querySelector('[name="lastName"]').value = 'Luffy'
|
|
3491
|
+
```
|
|
3492
|
+
|
|
3493
|
+
**How it works:**
|
|
3494
|
+
|
|
3495
|
+
1. Before any other processing, `assignFrom` checks for `where_x_in`, `where_y_in`, and `where_z_in` in options.
|
|
3496
|
+
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.
|
|
3497
|
+
3. Substitution applies to both LHS keys and all RHS string values (including nested objects like handler `resolve` maps).
|
|
3498
|
+
4. Multiple variables produce a **cartesian product**: x is expanded first, then y, then z. Result count = x.length × y.length × z.length.
|
|
3499
|
+
5. Non-string RHS values pass through untouched.
|
|
3500
|
+
|
|
3501
|
+
**Cartesian expansion with multiple variables:**
|
|
3502
|
+
|
|
3503
|
+
```JavaScript
|
|
3504
|
+
assignFrom(grid, {
|
|
3505
|
+
'?.querySelector?.[data-row="${x}"][data-col="${y}"]?.textContent': '${x}-${y}'
|
|
3506
|
+
}, {
|
|
3507
|
+
from: {},
|
|
3508
|
+
withMethods: ['querySelector'],
|
|
3509
|
+
where_x_in: ['1', '2'],
|
|
3510
|
+
where_y_in: ['A', 'B']
|
|
3511
|
+
});
|
|
3512
|
+
|
|
3513
|
+
// Produces 4 entries (2 × 2):
|
|
3514
|
+
// '?.[data-row="1"][data-col="A"]' → '1-A'
|
|
3515
|
+
// '?.[data-row="1"][data-col="B"]' → '1-B'
|
|
3516
|
+
// '?.[data-row="2"][data-col="A"]' → '2-A'
|
|
3517
|
+
// '?.[data-row="2"][data-col="B"]' → '2-B'
|
|
3518
|
+
```
|
|
3519
|
+
|
|
3520
|
+
**With handler ( =>) keys:**
|
|
3521
|
+
|
|
3522
|
+
Substitution applies inside handler `resolve` maps too:
|
|
3523
|
+
|
|
3524
|
+
```JavaScript
|
|
3525
|
+
await assignFromAsync(container, {
|
|
3526
|
+
'?.querySelector?..${x}View =>': {
|
|
3527
|
+
do: 'builtIns.lazyLoad',
|
|
3528
|
+
get: {
|
|
3529
|
+
if: '?.${x}Visible',
|
|
3530
|
+
instantiate: 'globalThis://${x}Template'
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}, {
|
|
3534
|
+
from: vm,
|
|
3535
|
+
withMethods: ['querySelector'],
|
|
3536
|
+
protocols: { globalThis: k => globalThis[k] },
|
|
3537
|
+
where_x_in: ['home', 'settings', 'profile']
|
|
3538
|
+
});
|
|
3539
|
+
```
|
|
3540
|
+
|
|
3541
|
+
**Edge cases:**
|
|
3542
|
+
|
|
3543
|
+
- Empty array (`where_x_in: []`) — template entries produce nothing (silent no-op).
|
|
3544
|
+
- Missing option — if a pattern contains `${x}` but `where_x_in` is not provided, the literal `${x}` remains in the string.
|
|
3545
|
+
- Entries without placeholders — passed through unchanged.
|
|
3443
3546
|
|
|
3444
3547
|
## assignFrom Handlers (` =>` operator)
|
|
3445
3548
|
|
|
@@ -3450,7 +3553,7 @@ The `"..."` key causes the resolved object to be merged (spread) into the result
|
|
|
3450
3553
|
Handlers are provided via the `handlers` option — scoped to each `assignFrom` call:
|
|
3451
3554
|
|
|
3452
3555
|
```JavaScript
|
|
3453
|
-
import {
|
|
3556
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
|
|
3454
3557
|
|
|
3455
3558
|
class MyListHandler {
|
|
3456
3559
|
constructor(config) {
|
|
@@ -3463,10 +3566,10 @@ class MyListHandler {
|
|
|
3463
3566
|
}
|
|
3464
3567
|
}
|
|
3465
3568
|
|
|
3466
|
-
await
|
|
3569
|
+
await assignFromAsync(myElement, {
|
|
3467
3570
|
'?.querySelector?.tbody =>': {
|
|
3468
3571
|
do: 'my-list',
|
|
3469
|
-
|
|
3572
|
+
get: {
|
|
3470
3573
|
list: '?.rankings',
|
|
3471
3574
|
template: 'globalThis://myTemplate'
|
|
3472
3575
|
}
|
|
@@ -3484,10 +3587,10 @@ await assignFrom(myElement, {
|
|
|
3484
3587
|
Handlers can also be specified as import paths (dynamically loaded on demand):
|
|
3485
3588
|
|
|
3486
3589
|
```JavaScript
|
|
3487
|
-
await
|
|
3590
|
+
await assignFromAsync(myElement, {
|
|
3488
3591
|
'?.querySelector?.tbody =>': {
|
|
3489
3592
|
do: 'my-list',
|
|
3490
|
-
|
|
3593
|
+
get: { list: '?.rankings', template: 'globalThis://myTemplate' }
|
|
3491
3594
|
}
|
|
3492
3595
|
}, {
|
|
3493
3596
|
from: viewModel,
|
|
@@ -3507,25 +3610,42 @@ Built-in handlers (`builtIns.lazyLoad`, `builtIns.join`, etc.) auto-load without
|
|
|
3507
3610
|
**How it works:**
|
|
3508
3611
|
|
|
3509
3612
|
1. Keys ending with ` =>` are separated from normal keys.
|
|
3510
|
-
2. Normal keys are processed via `
|
|
3613
|
+
2. Normal keys are processed via `getValues` + `assignGingerly` as usual.
|
|
3511
3614
|
3. For handler keys: the LHS path is evaluated (with `withMethods` support) to get the target.
|
|
3512
|
-
4. The `
|
|
3513
|
-
5. The
|
|
3615
|
+
4. The `get` map (if present) is processed synchronously via `getValues` — no thread yield.
|
|
3616
|
+
5. The `resolve` map (if present) is processed asynchronously via `resolveValues` — yields to the microtask queue. Results are merged with `get` results.
|
|
3617
|
+
6. 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
3618
|
|
|
3515
|
-
**The `resolve`
|
|
3619
|
+
**The `get` and `resolve` maps support:**
|
|
3516
3620
|
- `?.` paths — resolved against `options.from`
|
|
3517
3621
|
- Protocol strings — resolved via `options.protocols`
|
|
3518
3622
|
- Plain literals — passed through unchanged
|
|
3519
3623
|
|
|
3624
|
+
Use `get` for performance-sensitive handlers (synchronous, no yield). Use `resolve` when you need async protocol handlers (e.g., `fetch`). Both can coexist — `get` runs first, `resolve` merges on top:
|
|
3625
|
+
|
|
3626
|
+
```JavaScript
|
|
3627
|
+
'?.querySelector?.tbody =>': {
|
|
3628
|
+
do: 'builtIns.manageTemplateList',
|
|
3629
|
+
get: {
|
|
3630
|
+
forEach: '?.rankings',
|
|
3631
|
+
instantiate: 'globalThis://country-ranking',
|
|
3632
|
+
},
|
|
3633
|
+
resolve: {
|
|
3634
|
+
// Only for genuinely async protocols
|
|
3635
|
+
remoteConfig: 'api://settings'
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
```
|
|
3639
|
+
|
|
3520
3640
|
### Built-in handler: `builtIns.lazyLoad`
|
|
3521
3641
|
|
|
3522
3642
|
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
3643
|
|
|
3524
3644
|
```JavaScript
|
|
3525
|
-
await
|
|
3645
|
+
await assignFromAsync(document.body, {
|
|
3526
3646
|
'?.querySelector?..mainView =>': {
|
|
3527
3647
|
do: 'builtIns.lazyLoad',
|
|
3528
|
-
|
|
3648
|
+
get: {
|
|
3529
3649
|
if: '?.isVisible',
|
|
3530
3650
|
instantiate: 'globalThis://myTemplate',
|
|
3531
3651
|
}
|
|
@@ -3533,7 +3653,7 @@ await assignFrom(document.body, {
|
|
|
3533
3653
|
}, { withMethods: ['querySelector'], from: myVM, protocols: { globalThis: k => globalThis[k] } });
|
|
3534
3654
|
```
|
|
3535
3655
|
|
|
3536
|
-
**
|
|
3656
|
+
**Parameters:**
|
|
3537
3657
|
|
|
3538
3658
|
| Parameter | Type | Description |
|
|
3539
3659
|
|-----------|------|-------------|
|
|
@@ -3556,10 +3676,10 @@ This is useful for conditional rendering, routing, and lazy-loading views.
|
|
|
3556
3676
|
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
3677
|
|
|
3558
3678
|
```JavaScript
|
|
3559
|
-
await
|
|
3679
|
+
await assignFromAsync(container, {
|
|
3560
3680
|
'?.querySelector?..outlet =>': {
|
|
3561
3681
|
do: 'builtIns.lazyLoad',
|
|
3562
|
-
|
|
3682
|
+
get: {
|
|
3563
3683
|
if: '?.isVisible',
|
|
3564
3684
|
instantiate: 'globalThis://myTemplate',
|
|
3565
3685
|
transitional: true,
|
|
@@ -3598,10 +3718,10 @@ The handler injects a minimal default style (`.ag-hide { display: none }`) once
|
|
|
3598
3718
|
|
|
3599
3719
|
**Custom hide class:**
|
|
3600
3720
|
|
|
3601
|
-
Use the `hideClass`
|
|
3721
|
+
Use the `hideClass` parameter to use a different CSS class name:
|
|
3602
3722
|
|
|
3603
3723
|
```JavaScript
|
|
3604
|
-
|
|
3724
|
+
get: {
|
|
3605
3725
|
if: '?.isVisible',
|
|
3606
3726
|
instantiate: 'globalThis://myTemplate',
|
|
3607
3727
|
transitional: true,
|
|
@@ -3618,11 +3738,11 @@ resolve: {
|
|
|
3618
3738
|
**Routing example with transitions:**
|
|
3619
3739
|
|
|
3620
3740
|
```JavaScript
|
|
3621
|
-
await
|
|
3741
|
+
await assignFromAsync(container, {
|
|
3622
3742
|
'?.querySelector?..routerOutlet =>': [
|
|
3623
|
-
{ do: 'builtIns.lazyLoadSwitch',
|
|
3624
|
-
{ do: 'builtIns.lazyLoadSwitch',
|
|
3625
|
-
{ do: 'builtIns.lazyLoadSwitch',
|
|
3743
|
+
{ do: 'builtIns.lazyLoadSwitch', get: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView', transitional: true } },
|
|
3744
|
+
{ do: 'builtIns.lazyLoadSwitch', get: { lhs: '?.route', rhs: 'settings', instantiate: 'globalThis://settingsView', transitional: true } },
|
|
3745
|
+
{ do: 'builtIns.lazyLoadSwitch', get: { lhs: '?.route', rhs: 'profile', instantiate: 'globalThis://profileView', transitional: true } },
|
|
3626
3746
|
]
|
|
3627
3747
|
}, { withMethods: ['querySelector'], from: router, protocols: { globalThis: k => globalThis[k] } });
|
|
3628
3748
|
```
|
|
@@ -3654,18 +3774,18 @@ See the visual demo at `demos/view-transition-demo.html`.
|
|
|
3654
3774
|
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
3775
|
|
|
3656
3776
|
```JavaScript
|
|
3657
|
-
await
|
|
3777
|
+
await assignFromAsync(document.body, {
|
|
3658
3778
|
'?.querySelector?..mainView =>': [
|
|
3659
3779
|
{
|
|
3660
3780
|
do: 'builtIns.lazyLoad',
|
|
3661
|
-
|
|
3781
|
+
get: {
|
|
3662
3782
|
if: '?.isVisible',
|
|
3663
3783
|
instantiate: 'globalThis://viewTemplate',
|
|
3664
3784
|
}
|
|
3665
3785
|
},
|
|
3666
3786
|
{
|
|
3667
3787
|
do: 'applyTheme',
|
|
3668
|
-
|
|
3788
|
+
get: {
|
|
3669
3789
|
theme: '?.currentTheme'
|
|
3670
3790
|
}
|
|
3671
3791
|
}
|
|
@@ -3681,83 +3801,6 @@ await assignFrom(document.body, {
|
|
|
3681
3801
|
- Mixed `do` values — fully supported, each handler is looked up independently.
|
|
3682
3802
|
- Error handling — fail-fast. If a handler throws, remaining handlers are skipped.
|
|
3683
3803
|
|
|
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
3804
|
### Built-in handler: `builtIns.join`
|
|
3762
3805
|
|
|
3763
3806
|
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.
|
|
@@ -3768,10 +3811,10 @@ const vm = {
|
|
|
3768
3811
|
firstName: 'Helaena'
|
|
3769
3812
|
};
|
|
3770
3813
|
|
|
3771
|
-
|
|
3814
|
+
assignFrom(oElement, {
|
|
3772
3815
|
'?.textContent =>': {
|
|
3773
3816
|
do: 'builtIns.join',
|
|
3774
|
-
|
|
3817
|
+
get: {
|
|
3775
3818
|
value: ['?.lastName', ', ', '?.firstName']
|
|
3776
3819
|
}
|
|
3777
3820
|
}
|
|
@@ -3782,7 +3825,7 @@ await assignFrom(oElement, {
|
|
|
3782
3825
|
|
|
3783
3826
|
**How it works:**
|
|
3784
3827
|
|
|
3785
|
-
1. The `
|
|
3828
|
+
1. The `get.value` array is resolved by `getValues` — `?.` path strings are replaced with actual values from `options.from`.
|
|
3786
3829
|
2. Top-level `null`/`undefined` values are filtered out.
|
|
3787
3830
|
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
3831
|
4. Remaining elements are joined with the separator (default: `''`, empty string).
|
|
@@ -3797,10 +3840,10 @@ const vm = {
|
|
|
3797
3840
|
firstName: 'Helaena'
|
|
3798
3841
|
};
|
|
3799
3842
|
|
|
3800
|
-
|
|
3843
|
+
assignFrom(oElement, {
|
|
3801
3844
|
'?.textContent =>': {
|
|
3802
3845
|
do: 'builtIns.join',
|
|
3803
|
-
|
|
3846
|
+
get: {
|
|
3804
3847
|
value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
3805
3848
|
}
|
|
3806
3849
|
}
|
|
@@ -3816,11 +3859,11 @@ await assignFrom(oElement, {
|
|
|
3816
3859
|
**Custom separator:**
|
|
3817
3860
|
|
|
3818
3861
|
```JavaScript
|
|
3819
|
-
|
|
3862
|
+
assignFrom(oElement, {
|
|
3820
3863
|
'?.textContent =>': {
|
|
3821
3864
|
do: 'builtIns.join',
|
|
3822
3865
|
separator: ' | ',
|
|
3823
|
-
|
|
3866
|
+
get: {
|
|
3824
3867
|
value: ['?.firstName', '?.lastName']
|
|
3825
3868
|
}
|
|
3826
3869
|
}
|
|
@@ -3846,10 +3889,10 @@ const vm = {
|
|
|
3846
3889
|
isHappy: false
|
|
3847
3890
|
};
|
|
3848
3891
|
|
|
3849
|
-
|
|
3892
|
+
assignFrom(oSection, {
|
|
3850
3893
|
'?.querySelector?.div =>': {
|
|
3851
3894
|
do: 'builtIns.microDataJoin',
|
|
3852
|
-
|
|
3895
|
+
get: {
|
|
3853
3896
|
template: [
|
|
3854
3897
|
{ prop: 'firstName', val: '?.firstName' },
|
|
3855
3898
|
' ',
|
|
@@ -3896,7 +3939,7 @@ Produces:
|
|
|
3896
3939
|
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
3940
|
|
|
3898
3941
|
```JavaScript
|
|
3899
|
-
|
|
3942
|
+
get: {
|
|
3900
3943
|
template: [
|
|
3901
3944
|
{ prop: 'firstName', val: '?.firstName' },
|
|
3902
3945
|
[' ', { prop: 'middleName', val: '?.middleName' }], // dropped if middleName is undefined
|
|
@@ -3930,6 +3973,75 @@ For custom property names or per-segment formatting, pass an explicit object:
|
|
|
3930
3973
|
const template = md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`;
|
|
3931
3974
|
```
|
|
3932
3975
|
|
|
3976
|
+
### Built-in handler: `builtIns.manageTemplateList`
|
|
3977
|
+
|
|
3978
|
+
Clones a template once per item in an iterable, with keyed reconciliation for efficient updates. Each clone receives its item's data via `assignFrom`, and optionally shared data from the parent source.
|
|
3979
|
+
|
|
3980
|
+
```JavaScript
|
|
3981
|
+
assignFrom(document.body, {
|
|
3982
|
+
'?.querySelector?.tbody =>': {
|
|
3983
|
+
do: 'builtIns.manageTemplateList',
|
|
3984
|
+
get: {
|
|
3985
|
+
forEach: '?.rankings',
|
|
3986
|
+
instantiate: 'globalThis://country-ranking',
|
|
3987
|
+
},
|
|
3988
|
+
fromEachItem: {
|
|
3989
|
+
assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
3990
|
+
withOptions: { withMethods: ['querySelector'], inferredAssignments: true },
|
|
3991
|
+
get: { key: '?.rank' }
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
}, {
|
|
3995
|
+
from: olympics2024Summary,
|
|
3996
|
+
withMethods: ['querySelector'],
|
|
3997
|
+
protocols: { globalThis: k => globalThis[k] }
|
|
3998
|
+
});
|
|
3999
|
+
```
|
|
4000
|
+
|
|
4001
|
+
**How it works:**
|
|
4002
|
+
|
|
4003
|
+
1. Resolves `forEach` (iterable) and `instantiate` (template) from the `resolve` block
|
|
4004
|
+
2. Clones the template once per item, buffering all clones into a `DocumentFragment`
|
|
4005
|
+
3. For each clone, calls `assignFrom(clone, assignToFragment, { from: item, ...withOptions })` — distributing the item's data
|
|
4006
|
+
4. Inserts the fragment between comment markers in one DOM operation
|
|
4007
|
+
5. On subsequent calls, reconciles by `key` — adds new items, removes missing ones, updates existing clones in place
|
|
4008
|
+
|
|
4009
|
+
**Keyed reconciliation:**
|
|
4010
|
+
|
|
4011
|
+
The `key` field (in `fromEachItem.get`) identifies each item for stable identity across updates:
|
|
4012
|
+
- New key → clone + append
|
|
4013
|
+
- Key removed → hide (or remove if `forget: true`)
|
|
4014
|
+
- Key still present → update clone in place (no re-cloning)
|
|
4015
|
+
|
|
4016
|
+
Without `key`, positional matching is used (item[i] → clone[i]).
|
|
4017
|
+
|
|
4018
|
+
**Shared parent data (`fromSource`):**
|
|
4019
|
+
|
|
4020
|
+
Pass data from the outer VM into each clone (e.g., aggregate totals):
|
|
4021
|
+
|
|
4022
|
+
```JavaScript
|
|
4023
|
+
fromSource: {
|
|
4024
|
+
assignToFragment: {
|
|
4025
|
+
'?.querySelector?.[part~="totalMedalCount"]?.textContent': '?.totalMedalCount'
|
|
4026
|
+
},
|
|
4027
|
+
withOptions: { withMethods: ['querySelector'] }
|
|
4028
|
+
}
|
|
4029
|
+
```
|
|
4030
|
+
|
|
4031
|
+
**Wait for async rendering (`waitForSettled`):**
|
|
4032
|
+
|
|
4033
|
+
Optionally wait for async operations inside the fragment to complete before committing to the live DOM:
|
|
4034
|
+
|
|
4035
|
+
```JavaScript
|
|
4036
|
+
get: {
|
|
4037
|
+
forEach: '?.rankings',
|
|
4038
|
+
instantiate: 'globalThis://country-ranking',
|
|
4039
|
+
waitForSettled: true, // or { idleMs: 50, timeout: 2000 }
|
|
4040
|
+
}
|
|
4041
|
+
```
|
|
4042
|
+
|
|
4043
|
+
For full details, see [docs/manage-template-list.md](docs/manage-template-list.md).
|
|
4044
|
+
|
|
3933
4045
|
## Typed Path Authoring with `paths`, `sp`, and `md`
|
|
3934
4046
|
|
|
3935
4047
|
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`.
|
|
@@ -3951,7 +4063,7 @@ const $ = paths<Person>();
|
|
|
3951
4063
|
export default {
|
|
3952
4064
|
'?.textContent =>': {
|
|
3953
4065
|
do: 'builtIns.join',
|
|
3954
|
-
|
|
4066
|
+
get: {
|
|
3955
4067
|
value: sp`${$.lastName}, ${$.firstName}`
|
|
3956
4068
|
}
|
|
3957
4069
|
}
|
|
@@ -4041,13 +4153,13 @@ Both auto-detect path proxies (no `.path` needed inside template literals) and p
|
|
|
4041
4153
|
`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
4154
|
|
|
4043
4155
|
```TypeScript
|
|
4044
|
-
import {
|
|
4156
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
|
|
4045
4157
|
|
|
4046
|
-
await
|
|
4158
|
+
await assignFromAsync(document.body, {
|
|
4047
4159
|
'#[main]?.textContent': '?.greeting',
|
|
4048
4160
|
'#[main] =>': {
|
|
4049
4161
|
do: 'builtIns.lazyLoad',
|
|
4050
|
-
|
|
4162
|
+
get: { if: '?.showContent', instantiate: 'globalThis://myTemplate' }
|
|
4051
4163
|
}
|
|
4052
4164
|
}, {
|
|
4053
4165
|
from: viewModel,
|
|
@@ -4079,7 +4191,7 @@ withIds: {
|
|
|
4079
4191
|
`#[x]` anchors the start of the path. Further `?.` segments chain from the resolved element:
|
|
4080
4192
|
|
|
4081
4193
|
```TypeScript
|
|
4082
|
-
|
|
4194
|
+
assignFrom(document.body, {
|
|
4083
4195
|
'#[form]?.querySelector?..username?.value': '?.username',
|
|
4084
4196
|
'#[form]?.querySelector?..email?.value': '?.email',
|
|
4085
4197
|
'#[header]?.style?.color': '?.themeColor',
|
|
@@ -4096,10 +4208,10 @@ await assignFrom(document.body, {
|
|
|
4096
4208
|
**With handlers (` =>`):**
|
|
4097
4209
|
|
|
4098
4210
|
```TypeScript
|
|
4099
|
-
await
|
|
4211
|
+
await assignFromAsync(container, {
|
|
4100
4212
|
'#[outlet] =>': {
|
|
4101
4213
|
do: 'builtIns.lazyLoadSwitch',
|
|
4102
|
-
|
|
4214
|
+
get: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView' }
|
|
4103
4215
|
}
|
|
4104
4216
|
}, {
|
|
4105
4217
|
from: router,
|
|
@@ -4137,7 +4249,7 @@ const vm = {
|
|
|
4137
4249
|
user: { role: 'admin', avatar: '/img/alice.png' }
|
|
4138
4250
|
};
|
|
4139
4251
|
|
|
4140
|
-
|
|
4252
|
+
assignFrom(outerDiv, {}, {
|
|
4141
4253
|
from: vm,
|
|
4142
4254
|
inferredAssignments: {
|
|
4143
4255
|
byItemprop: ['name', 'email', 'user']
|
|
@@ -4170,9 +4282,9 @@ For full details, see [docs/inferred-assignments.md](docs/inferred-assignments.m
|
|
|
4170
4282
|
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
4283
|
|
|
4172
4284
|
```TypeScript
|
|
4173
|
-
import {
|
|
4285
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
|
|
4174
4286
|
|
|
4175
|
-
await
|
|
4287
|
+
await assignFromAsync(shadowRoot, { /* normal assignments */ }, {
|
|
4176
4288
|
from: vm,
|
|
4177
4289
|
enhance: [
|
|
4178
4290
|
{ emc: 'be-bound/emc.json', matching: '[name]' },
|