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 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. 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.
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 (template instantiation, DOM manipulation, etc.).
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 `resolveValues` and `assignFrom`
3399
+ ## Protocol Resolution in `getValues` and `assignFrom`
3386
3400
 
3387
- `resolveValues` (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.
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 { resolveValues } from 'assign-gingerly/resolveValues.js';
3404
+ import { getValues } from 'assign-gingerly/getValues.js';
3391
3405
 
3392
- const result = await resolveValues({
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 `resolveValue`.
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
- await assignFrom(myForm, {
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
- **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.
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 { assignFrom } from 'assign-gingerly/assignFrom.js';
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 assignFrom(myElement, {
3569
+ await assignFromAsync(myElement, {
3467
3570
  '?.querySelector?.tbody =>': {
3468
3571
  do: 'my-list',
3469
- resolve: {
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 assignFrom(myElement, {
3590
+ await assignFromAsync(myElement, {
3488
3591
  '?.querySelector?.tbody =>': {
3489
3592
  do: 'my-list',
3490
- resolve: { list: '?.rankings', template: 'globalThis://myTemplate' }
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 `resolveValues` + `assignGingerly` as usual.
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 `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.
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` map supports:**
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 assignFrom(document.body, {
3645
+ await assignFromAsync(document.body, {
3526
3646
  '?.querySelector?..mainView =>': {
3527
3647
  do: 'builtIns.lazyLoad',
3528
- resolve: {
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
- **Resolve parameters:**
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 assignFrom(container, {
3679
+ await assignFromAsync(container, {
3560
3680
  '?.querySelector?..outlet =>': {
3561
3681
  do: 'builtIns.lazyLoad',
3562
- resolve: {
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` resolve parameter to use a different CSS class name:
3721
+ Use the `hideClass` parameter to use a different CSS class name:
3602
3722
 
3603
3723
  ```JavaScript
3604
- resolve: {
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 assignFrom(container, {
3741
+ await assignFromAsync(container, {
3622
3742
  '?.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 } },
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 assignFrom(document.body, {
3777
+ await assignFromAsync(document.body, {
3658
3778
  '?.querySelector?..mainView =>': [
3659
3779
  {
3660
3780
  do: 'builtIns.lazyLoad',
3661
- resolve: {
3781
+ get: {
3662
3782
  if: '?.isVisible',
3663
3783
  instantiate: 'globalThis://viewTemplate',
3664
3784
  }
3665
3785
  },
3666
3786
  {
3667
3787
  do: 'applyTheme',
3668
- resolve: {
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
- await assignFrom(oElement, {
3814
+ assignFrom(oElement, {
3772
3815
  '?.textContent =>': {
3773
3816
  do: 'builtIns.join',
3774
- resolve: {
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 `resolve.value` array is resolved by `resolveValues` — `?.` path strings are replaced with actual values from `options.from`.
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
- await assignFrom(oElement, {
3843
+ assignFrom(oElement, {
3801
3844
  '?.textContent =>': {
3802
3845
  do: 'builtIns.join',
3803
- resolve: {
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
- await assignFrom(oElement, {
3862
+ assignFrom(oElement, {
3820
3863
  '?.textContent =>': {
3821
3864
  do: 'builtIns.join',
3822
3865
  separator: ' | ',
3823
- resolve: {
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
- await assignFrom(oSection, {
3892
+ assignFrom(oSection, {
3850
3893
  '?.querySelector?.div =>': {
3851
3894
  do: 'builtIns.microDataJoin',
3852
- resolve: {
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
- resolve: {
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
- resolve: {
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 { assignFrom } from 'assign-gingerly/assignFrom.js';
4156
+ import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
4045
4157
 
4046
- await assignFrom(document.body, {
4158
+ await assignFromAsync(document.body, {
4047
4159
  '#[main]?.textContent': '?.greeting',
4048
4160
  '#[main] =>': {
4049
4161
  do: 'builtIns.lazyLoad',
4050
- resolve: { if: '?.showContent', instantiate: 'globalThis://myTemplate' }
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
- await assignFrom(document.body, {
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 assignFrom(container, {
4211
+ await assignFromAsync(container, {
4100
4212
  '#[outlet] =>': {
4101
4213
  do: 'builtIns.lazyLoadSwitch',
4102
- resolve: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView' }
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
- await assignFrom(outerDiv, {}, {
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 { assignFrom } from 'assign-gingerly/assignFrom.js';
4285
+ import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
4174
4286
 
4175
- await assignFrom(shadowRoot, { /* normal assignments */ }, {
4287
+ await assignFromAsync(shadowRoot, { /* normal assignments */ }, {
4176
4288
  from: vm,
4177
4289
  enhance: [
4178
4290
  { emc: 'be-bound/emc.json', matching: '[name]' },