assign-gingerly 0.0.58 → 0.0.60
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 +511 -142
- package/assignFrom-extension.js +25 -0
- package/assignFrom-extension.ts +53 -0
- package/assignFrom.js +306 -133
- package/assignFrom.ts +318 -232
- package/assignFromAsync-extension.js +28 -0
- package/assignFromAsync-extension.ts +58 -0
- package/assignFromAsync.js +120 -0
- package/assignFromAsync.ts +256 -0
- package/assignGingerly.js +50 -0
- package/assignGingerly.ts +51 -0
- package/builtInEmoji.js +25 -0
- package/builtInEmoji.ts +33 -0
- package/getValues.js +223 -0
- package/getValues.ts +255 -0
- package/handlers/join.ts +1 -1
- package/handlers/lazyLoad.js +33 -2
- package/handlers/lazyLoad.ts +47 -2
- package/handlers/lazyLoadSwitch.ts +1 -1
- package/handlers/manageTemplateList.js +226 -0
- package/handlers/manageTemplateList.ts +263 -0
- package/handlers/microDataJoin.ts +1 -1
- package/index.js +1 -0
- package/index.ts +2 -0
- package/inferencer/inferencer.js +9 -21
- package/inferencer/inferencer.ts +10 -21
- package/inferredAssignments.js +35 -5
- package/inferredAssignments.ts +58 -8
- package/markerUtils.js +136 -127
- package/package.json +30 -1
- package/playwright.config.ts +3 -2
- package/processHandlerCommands.js +36 -4
- package/processHandlerCommands.ts +38 -8
- package/resolveIdRef.js +70 -19
- package/resolveIdRef.ts +73 -21
- package/resolveValues.js +41 -125
- package/resolveValues.ts +131 -255
- package/types/assign-gingerly/types.d.ts +61 -0
- package/waitForSettled.js +57 -0
- package/waitForSettled.ts +65 -0
- package/withIdsCorrector.js +47 -0
- package/withIdsCorrector.ts +59 -0
package/README.md
CHANGED
|
@@ -19,6 +19,21 @@ has the "side effect" of enhancing the platform API in a way that this proposal
|
|
|
19
19
|
|
|
20
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
|
+
For `assignFrom` method chaining on objects, separate extension modules are available — import only what you need:
|
|
23
|
+
|
|
24
|
+
```JavaScript
|
|
25
|
+
import 'assign-gingerly/assignFrom-extension.js'; // adds obj.assignFrom() — sync
|
|
26
|
+
import 'assign-gingerly/assignFromAsync-extension.js'; // adds obj.assignFromAsync() — async
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This enables fluent patterns like:
|
|
30
|
+
|
|
31
|
+
```JavaScript
|
|
32
|
+
oElement
|
|
33
|
+
.assignFrom({ '?.textContent': '?.greeting' }, { from: vm1 })
|
|
34
|
+
.assignFrom({ '?.style Y=': { width: '?.w' } }, { from: vm2 });
|
|
35
|
+
```
|
|
36
|
+
|
|
22
37
|
## Object Extension Pattern
|
|
23
38
|
|
|
24
39
|
Not only does this polyfill package allow merging data properties onto objects that are expecting them, this polyfill also provides the ability to merge *augmented behavior* onto run-time objects without sub classing all such objects of the same type. This includes the ability to spawn an instance of a class and "merge" it into the API of the original object in an elegant way that is easy to wrap one's brain around, without ever blocking access to the original object or breaking it.
|
|
@@ -58,15 +73,29 @@ assignTentatively provides a far more limited subset of functionality compared t
|
|
|
58
73
|
|
|
59
74
|
The third fundamental utility function is:
|
|
60
75
|
|
|
61
|
-
## assignFrom
|
|
76
|
+
## assignFrom (and assignFromAsync)
|
|
77
|
+
|
|
78
|
+
`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.
|
|
62
79
|
|
|
63
|
-
assignFrom
|
|
80
|
+
**`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).
|
|
81
|
+
|
|
82
|
+
**`assignFromAsync`** is the awaitable variant for when you need to:
|
|
83
|
+
- Use async protocol handlers (e.g., `fetch`-based resolution)
|
|
84
|
+
- `await` handler completion before proceeding
|
|
85
|
+
- Wait for `enhance` (EMC JSON imports) to finish
|
|
86
|
+
|
|
87
|
+
```TypeScript
|
|
88
|
+
import { assignFrom } from 'assign-gingerly/assignFrom.js'; // sync (default)
|
|
89
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js'; // async when needed
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
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
93
|
|
|
65
94
|
assignFrom adds support for:
|
|
66
95
|
|
|
67
96
|
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 (
|
|
97
|
+
2. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols).
|
|
98
|
+
3. Handler plugins via the ` =>` operator for custom logic (fire-and-forget in sync mode, awaitable in async mode).
|
|
70
99
|
4. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
|
|
71
100
|
5. Spread merging via the `"..."` key.
|
|
72
101
|
|
|
@@ -740,7 +769,20 @@ assignGingerly(div, {
|
|
|
740
769
|
- Testing is done in mount-observer package (no tests in assign-gingerly)
|
|
741
770
|
- Single @eachTime per path (nested @eachTime not currently supported)
|
|
742
771
|
|
|
743
|
-
|
|
772
|
+
## Common Operation Support
|
|
773
|
+
|
|
774
|
+
While we are in the business of passing values of object A into object B, we might as well add some extremely common behavior that allows updating properties of object B based on the current values of object B -- things like incrementing, toggling, deleting, and merging into sub-objects. Deleting is critical for assignTentatively, but is included with both functions.
|
|
775
|
+
|
|
776
|
+
| Operator | Name | Description | Example |
|
|
777
|
+
|----------|------|-------------|---------|
|
|
778
|
+
| ` +=` | Increment | Add to numeric value, concatenate strings, append to arrays | `'count +=': 5` |
|
|
779
|
+
| ` =!` | Toggle | Negate a boolean (or any value via `!`) | `'visible =!': '.'` |
|
|
780
|
+
| ` -=` | Delete | Remove properties from an object | `'?.data -=': 'key'` |
|
|
781
|
+
| ` Y=` | Merge | Recursively `assignGingerly` into a sub-object | `'style Y=': { width: '100px' }` |
|
|
782
|
+
| ` ?=` | Ternary | Conditional assignment (assignFrom only) — [details](docs/ternary-assignment.md) | `'?.text ?=': ['?.cond', 'yes', 'no']` |
|
|
783
|
+
| ` =>` | Handler | Invoke a handler plugin (assignFrom only) | `'?.el =>': { do: 'builtIns.join', ... }` |
|
|
784
|
+
|
|
785
|
+
All operators use a space before the suffix to distinguish them from property names. They compose with `?.` nested paths and `withMethods`.
|
|
744
786
|
|
|
745
787
|
## Example 4 - Incrementing values with += command
|
|
746
788
|
|
|
@@ -886,7 +928,101 @@ console.log(obj);
|
|
|
886
928
|
|
|
887
929
|
|
|
888
930
|
|
|
889
|
-
## Example 7 -
|
|
931
|
+
## Example 7 - Merging into sub-objects with Y= command
|
|
932
|
+
|
|
933
|
+
The `Y=` command recursively merges an object into an existing sub-object — like a nested `assignGingerly` call, declared inline:
|
|
934
|
+
|
|
935
|
+
```TypeScript
|
|
936
|
+
const element = {
|
|
937
|
+
style: { color: 'blue', display: 'block' },
|
|
938
|
+
dataset: { userId: '1' }
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
assignGingerly(element, {
|
|
942
|
+
'style Y=': {
|
|
943
|
+
width: '100px',
|
|
944
|
+
height: '50px'
|
|
945
|
+
},
|
|
946
|
+
'dataset Y=': {
|
|
947
|
+
role: 'admin'
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
console.log(element.style);
|
|
952
|
+
// { color: 'blue', display: 'block', width: '100px', height: '50px' }
|
|
953
|
+
|
|
954
|
+
console.log(element.dataset);
|
|
955
|
+
// { userId: '1', role: 'admin' }
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
This is equivalent to the more verbose path-per-property approach:
|
|
959
|
+
|
|
960
|
+
```TypeScript
|
|
961
|
+
assignGingerly(element, {
|
|
962
|
+
'?.style?.width': '100px',
|
|
963
|
+
'?.style?.height': '50px',
|
|
964
|
+
'?.dataset?.role': 'admin'
|
|
965
|
+
});
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
**With `?.` nested paths:**
|
|
969
|
+
|
|
970
|
+
```TypeScript
|
|
971
|
+
const app = { config: { database: { host: 'localhost', port: 5432 } } };
|
|
972
|
+
|
|
973
|
+
assignGingerly(app, {
|
|
974
|
+
'?.config?.database Y=': {
|
|
975
|
+
port: 3306,
|
|
976
|
+
ssl: true
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
// app.config.database = { host: 'localhost', port: 3306, ssl: true }
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
**Nested `Y=` (recursive composition):**
|
|
983
|
+
|
|
984
|
+
`Y=` composes — the merged object can itself contain `Y=` keys:
|
|
985
|
+
|
|
986
|
+
```TypeScript
|
|
987
|
+
const app = {
|
|
988
|
+
config: {
|
|
989
|
+
database: { host: 'localhost', port: 5432 },
|
|
990
|
+
appName: 'OldApp'
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
assignGingerly(app, {
|
|
995
|
+
'config Y=': {
|
|
996
|
+
'database Y=': {
|
|
997
|
+
port: 3306,
|
|
998
|
+
ssl: true
|
|
999
|
+
},
|
|
1000
|
+
appName: 'NewApp'
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
// config.database = { host: 'localhost', port: 3306, ssl: true }
|
|
1004
|
+
// config.appName = 'NewApp'
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
**Mixing with other operators:**
|
|
1008
|
+
|
|
1009
|
+
```TypeScript
|
|
1010
|
+
assignGingerly(obj, {
|
|
1011
|
+
'style Y=': { width: '100px' }, // merge into sub-object
|
|
1012
|
+
'?.style?.opacity': '1', // direct path assignment
|
|
1013
|
+
'counter +=': 1, // increment
|
|
1014
|
+
textContent: 'Hello' // plain assignment
|
|
1015
|
+
});
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
**Behavior notes:**
|
|
1019
|
+
- If the target property doesn't exist or isn't an object, the command is a silent no-op
|
|
1020
|
+
- Arrays in the RHS replace the target property (not concatenated) — consistent with normal `assignGingerly` behavior
|
|
1021
|
+
- Works with `withMethods` for DOM path evaluation (e.g., `'?.querySelector?..panel?.style Y='`)
|
|
1022
|
+
- The name `Y=` evokes a merge sign (Y) — two paths converging into one — combined with `=` to signal an assignment operator
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
## Example 8 - Reversible assignments with assignTentatively
|
|
890
1026
|
|
|
891
1027
|
The `assignTentatively` function works like `assignGingerly` but with a powerful addition: **reversibility**. It tracks changes and generates a reversal object that can undo all modifications:
|
|
892
1028
|
|
|
@@ -1176,7 +1312,7 @@ obj
|
|
|
1176
1312
|
console.log(obj); // { a: 1, b: { c: 2 }, d: 3 }
|
|
1177
1313
|
```
|
|
1178
1314
|
|
|
1179
|
-
**Note**: The `assignTentatively` method on Object.prototype is simply an alias for `assignGingerly` and does **not** provide the reversibility features of the standalone `assignTentatively` function described in Example
|
|
1315
|
+
**Note**: The `assignTentatively` method on Object.prototype is simply an alias for `assignGingerly` and does **not** provide the reversibility features of the standalone `assignTentatively` function described in Example 8. For reversible assignments, use the standalone function from `assign-gingerly/assignTentatively`.
|
|
1180
1316
|
|
|
1181
1317
|
The prototype extensions are non-enumerable and won't appear in `Object.keys()` or `for...in` loops.
|
|
1182
1318
|
|
|
@@ -1410,7 +1546,7 @@ interface EnhancementConfig<T, TObj = Element> {
|
|
|
1410
1546
|
|
|
1411
1547
|
The `withAttrs` property enables automatic attribute parsing when the enhancement is spawned. See the [Parsing Attributes with parseWithAttrs](#parsing-attributes-with-parsewithattrs) section for details.
|
|
1412
1548
|
|
|
1413
|
-
It also tips off extending polyfills / libraries, in particular mount-observer, to be on
|
|
1549
|
+
It also tips off extending polyfills / libraries, in particular mount-observer, to be on the lookout for the attributes specified by withAttrs. But *assign-gingerly, by itself, performs **no** DOM observing to automatically spawn the class instance*. It expects consumers of the polyfill to programmatically attach such behavior/enhancements, and/or rely on alternative, higher level packages to be vigilant for enhancement opportunities.
|
|
1414
1550
|
|
|
1415
1551
|
The `canSpawn` static method allows enhancement classes to conditionally block spawning based on the target object. See the [Conditional Spawning with canSpawn](#conditional-spawning-with-canspawn) section for details.
|
|
1416
1552
|
|
|
@@ -3382,14 +3518,14 @@ assignFrom(target, {
|
|
|
3382
3518
|
|
|
3383
3519
|
For full documentation, see [docs/assignFrom.md](docs/assignFrom.md).
|
|
3384
3520
|
|
|
3385
|
-
## Protocol Resolution in `
|
|
3521
|
+
## Protocol Resolution in `getValues` and `assignFrom`
|
|
3386
3522
|
|
|
3387
|
-
`
|
|
3523
|
+
`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
3524
|
|
|
3389
3525
|
```JavaScript
|
|
3390
|
-
import {
|
|
3526
|
+
import { getValues } from 'assign-gingerly/getValues.js';
|
|
3391
3527
|
|
|
3392
|
-
const result =
|
|
3528
|
+
const result = getValues({
|
|
3393
3529
|
baseURL: 'globalThis://myAppConfig?.apiBaseUrl',
|
|
3394
3530
|
authToken: 'localStorage://auth?.token',
|
|
3395
3531
|
label: '?.title' // normal path resolution still works
|
|
@@ -3405,7 +3541,7 @@ const result = await resolveValues({
|
|
|
3405
3541
|
|
|
3406
3542
|
1. If a value contains `://` and the part before it matches a key in `protocols`, it's treated as a protocol reference.
|
|
3407
3543
|
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 `
|
|
3544
|
+
3. If a `?.` path follows the key, it's resolved against the handler's result using `getValue`.
|
|
3409
3545
|
4. If the protocol isn't found in the map, the value passes through unchanged (no error).
|
|
3410
3546
|
|
|
3411
3547
|
**Path after protocol key:**
|
|
@@ -3425,7 +3561,7 @@ const result = await resolveValues({
|
|
|
3425
3561
|
```JavaScript
|
|
3426
3562
|
import { assignFrom } from 'assign-gingerly/assignFrom.js';
|
|
3427
3563
|
|
|
3428
|
-
|
|
3564
|
+
assignFrom(myForm, {
|
|
3429
3565
|
"...": "globalThis://qmywdO1vr0SwyuIe4fvzxQ",
|
|
3430
3566
|
path: "api/v2/:operation/:expression",
|
|
3431
3567
|
headers: {
|
|
@@ -3439,7 +3575,96 @@ await assignFrom(myForm, {
|
|
|
3439
3575
|
|
|
3440
3576
|
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
3577
|
|
|
3442
|
-
**
|
|
3578
|
+
**Async protocol handlers:**
|
|
3579
|
+
|
|
3580
|
+
For protocol handlers that return Promises (e.g., `fetch`, IndexedDB), use `resolveValues` (async) or `assignFromAsync`:
|
|
3581
|
+
|
|
3582
|
+
```JavaScript
|
|
3583
|
+
import { resolveValues } from 'assign-gingerly/resolveValues.js';
|
|
3584
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
|
|
3585
|
+
|
|
3586
|
+
// resolveValues is the async variant of getValues
|
|
3587
|
+
const result = await resolveValues(pattern, source, {
|
|
3588
|
+
protocols: { api: async (key) => (await fetch(`/api/${key}`)).json() }
|
|
3589
|
+
});
|
|
3590
|
+
```
|
|
3591
|
+
|
|
3592
|
+
## Looped Substitution (`where_x_in`, `where_y_in`, `where_z_in`)
|
|
3593
|
+
|
|
3594
|
+
`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.
|
|
3595
|
+
|
|
3596
|
+
```JavaScript
|
|
3597
|
+
const vm = {
|
|
3598
|
+
firstName: 'Monkey',
|
|
3599
|
+
lastName: 'Luffy'
|
|
3600
|
+
};
|
|
3601
|
+
|
|
3602
|
+
assignFrom(myForm, {
|
|
3603
|
+
'?.[name="${x}"]': '?.${x}'
|
|
3604
|
+
}, {
|
|
3605
|
+
from: vm,
|
|
3606
|
+
withMethods: ['querySelector'],
|
|
3607
|
+
where_x_in: ['firstName', 'lastName']
|
|
3608
|
+
});
|
|
3609
|
+
|
|
3610
|
+
// Expands to the equivalent of:
|
|
3611
|
+
// '?.[name="firstName"]': '?.firstName' → querySelector('[name="firstName"]').value = 'Monkey'
|
|
3612
|
+
// '?.[name="lastName"]': '?.lastName' → querySelector('[name="lastName"]').value = 'Luffy'
|
|
3613
|
+
```
|
|
3614
|
+
|
|
3615
|
+
**How it works:**
|
|
3616
|
+
|
|
3617
|
+
1. Before any other processing, `assignFrom` checks for `where_x_in`, `where_y_in`, and `where_z_in` in options.
|
|
3618
|
+
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.
|
|
3619
|
+
3. Substitution applies to both LHS keys and all RHS string values (including nested objects like handler `resolve` maps).
|
|
3620
|
+
4. Multiple variables produce a **cartesian product**: x is expanded first, then y, then z. Result count = x.length × y.length × z.length.
|
|
3621
|
+
5. Non-string RHS values pass through untouched.
|
|
3622
|
+
|
|
3623
|
+
**Cartesian expansion with multiple variables:**
|
|
3624
|
+
|
|
3625
|
+
```JavaScript
|
|
3626
|
+
assignFrom(grid, {
|
|
3627
|
+
'?.querySelector?.[data-row="${x}"][data-col="${y}"]?.textContent': '${x}-${y}'
|
|
3628
|
+
}, {
|
|
3629
|
+
from: {},
|
|
3630
|
+
withMethods: ['querySelector'],
|
|
3631
|
+
where_x_in: ['1', '2'],
|
|
3632
|
+
where_y_in: ['A', 'B']
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
// Produces 4 entries (2 × 2):
|
|
3636
|
+
// '?.[data-row="1"][data-col="A"]' → '1-A'
|
|
3637
|
+
// '?.[data-row="1"][data-col="B"]' → '1-B'
|
|
3638
|
+
// '?.[data-row="2"][data-col="A"]' → '2-A'
|
|
3639
|
+
// '?.[data-row="2"][data-col="B"]' → '2-B'
|
|
3640
|
+
```
|
|
3641
|
+
|
|
3642
|
+
**With handler ( =>) keys:**
|
|
3643
|
+
|
|
3644
|
+
Substitution applies inside handler `resolve` maps too:
|
|
3645
|
+
|
|
3646
|
+
```JavaScript
|
|
3647
|
+
await assignFromAsync(container, {
|
|
3648
|
+
'?.querySelector?..${x}View =>': {
|
|
3649
|
+
do: 'builtIns.lazyLoad',
|
|
3650
|
+
get: {
|
|
3651
|
+
if: '?.${x}Visible',
|
|
3652
|
+
instantiate: 'globalThis://${x}Template'
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
}, {
|
|
3656
|
+
from: vm,
|
|
3657
|
+
withMethods: ['querySelector'],
|
|
3658
|
+
protocols: { globalThis: k => globalThis[k] },
|
|
3659
|
+
where_x_in: ['home', 'settings', 'profile']
|
|
3660
|
+
});
|
|
3661
|
+
```
|
|
3662
|
+
|
|
3663
|
+
**Edge cases:**
|
|
3664
|
+
|
|
3665
|
+
- Empty array (`where_x_in: []`) — template entries produce nothing (silent no-op).
|
|
3666
|
+
- Missing option — if a pattern contains `${x}` but `where_x_in` is not provided, the literal `${x}` remains in the string.
|
|
3667
|
+
- Entries without placeholders — passed through unchanged.
|
|
3443
3668
|
|
|
3444
3669
|
## assignFrom Handlers (` =>` operator)
|
|
3445
3670
|
|
|
@@ -3450,7 +3675,7 @@ The `"..."` key causes the resolved object to be merged (spread) into the result
|
|
|
3450
3675
|
Handlers are provided via the `handlers` option — scoped to each `assignFrom` call:
|
|
3451
3676
|
|
|
3452
3677
|
```JavaScript
|
|
3453
|
-
import {
|
|
3678
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
|
|
3454
3679
|
|
|
3455
3680
|
class MyListHandler {
|
|
3456
3681
|
constructor(config) {
|
|
@@ -3463,10 +3688,10 @@ class MyListHandler {
|
|
|
3463
3688
|
}
|
|
3464
3689
|
}
|
|
3465
3690
|
|
|
3466
|
-
await
|
|
3691
|
+
await assignFromAsync(myElement, {
|
|
3467
3692
|
'?.querySelector?.tbody =>': {
|
|
3468
3693
|
do: 'my-list',
|
|
3469
|
-
|
|
3694
|
+
get: {
|
|
3470
3695
|
list: '?.rankings',
|
|
3471
3696
|
template: 'globalThis://myTemplate'
|
|
3472
3697
|
}
|
|
@@ -3484,10 +3709,10 @@ await assignFrom(myElement, {
|
|
|
3484
3709
|
Handlers can also be specified as import paths (dynamically loaded on demand):
|
|
3485
3710
|
|
|
3486
3711
|
```JavaScript
|
|
3487
|
-
await
|
|
3712
|
+
await assignFromAsync(myElement, {
|
|
3488
3713
|
'?.querySelector?.tbody =>': {
|
|
3489
3714
|
do: 'my-list',
|
|
3490
|
-
|
|
3715
|
+
get: { list: '?.rankings', template: 'globalThis://myTemplate' }
|
|
3491
3716
|
}
|
|
3492
3717
|
}, {
|
|
3493
3718
|
from: viewModel,
|
|
@@ -3504,28 +3729,62 @@ Import paths must be local (relative, absolute, or bare specifiers — no cross-
|
|
|
3504
3729
|
|
|
3505
3730
|
Built-in handlers (`builtIns.lazyLoad`, `builtIns.join`, etc.) auto-load without needing to be listed in `handlers`.
|
|
3506
3731
|
|
|
3732
|
+
**Handler aliases:** The `handlers` option also accepts built-in names as values, allowing you to define short aliases (including emoji) for concise configs:
|
|
3733
|
+
|
|
3734
|
+
```JavaScript
|
|
3735
|
+
import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
3736
|
+
// { '📦': 'builtIns.lazyLoad', '🎚️': 'builtIns.lazyLoadSwitch', '🔗': 'builtIns.join', '🏷️': 'builtIns.microDataJoin', '📋': 'builtIns.manageTemplateList' }
|
|
3737
|
+
|
|
3738
|
+
assignFrom(target, {
|
|
3739
|
+
'?.textContent =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
|
|
3740
|
+
}, { from: vm, handlers: builtInEmoji });
|
|
3741
|
+
|
|
3742
|
+
// Or define your own:
|
|
3743
|
+
assignFrom(target, pattern, {
|
|
3744
|
+
from: vm,
|
|
3745
|
+
handlers: { 'sw': 'builtIns.lazyLoadSwitch', 'list': 'builtIns.manageTemplateList' }
|
|
3746
|
+
});
|
|
3747
|
+
```
|
|
3748
|
+
|
|
3507
3749
|
**How it works:**
|
|
3508
3750
|
|
|
3509
3751
|
1. Keys ending with ` =>` are separated from normal keys.
|
|
3510
|
-
2. Normal keys are processed via `
|
|
3752
|
+
2. Normal keys are processed via `getValues` + `assignGingerly` as usual.
|
|
3511
3753
|
3. For handler keys: the LHS path is evaluated (with `withMethods` support) to get the target.
|
|
3512
|
-
4. The `
|
|
3513
|
-
5. The
|
|
3754
|
+
4. The `get` map (if present) is processed synchronously via `getValues` — no thread yield.
|
|
3755
|
+
5. The `resolve` map (if present) is processed asynchronously via `resolveValues` — yields to the microtask queue. Results are merged with `get` results.
|
|
3756
|
+
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
3757
|
|
|
3515
|
-
**The `resolve`
|
|
3758
|
+
**The `get` and `resolve` maps support:**
|
|
3516
3759
|
- `?.` paths — resolved against `options.from`
|
|
3517
3760
|
- Protocol strings — resolved via `options.protocols`
|
|
3518
3761
|
- Plain literals — passed through unchanged
|
|
3519
3762
|
|
|
3763
|
+
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:
|
|
3764
|
+
|
|
3765
|
+
```JavaScript
|
|
3766
|
+
'?.querySelector?.tbody =>': {
|
|
3767
|
+
do: 'builtIns.manageTemplateList',
|
|
3768
|
+
get: {
|
|
3769
|
+
forEach: '?.rankings',
|
|
3770
|
+
instantiate: 'globalThis://country-ranking',
|
|
3771
|
+
},
|
|
3772
|
+
resolve: {
|
|
3773
|
+
// Only for genuinely async protocols
|
|
3774
|
+
remoteConfig: 'api://settings'
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
```
|
|
3778
|
+
|
|
3520
3779
|
### Built-in handler: `builtIns.lazyLoad`
|
|
3521
3780
|
|
|
3522
3781
|
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
3782
|
|
|
3524
3783
|
```JavaScript
|
|
3525
|
-
await
|
|
3784
|
+
await assignFromAsync(document.body, {
|
|
3526
3785
|
'?.querySelector?..mainView =>': {
|
|
3527
3786
|
do: 'builtIns.lazyLoad',
|
|
3528
|
-
|
|
3787
|
+
get: {
|
|
3529
3788
|
if: '?.isVisible',
|
|
3530
3789
|
instantiate: 'globalThis://myTemplate',
|
|
3531
3790
|
}
|
|
@@ -3533,7 +3792,7 @@ await assignFrom(document.body, {
|
|
|
3533
3792
|
}, { withMethods: ['querySelector'], from: myVM, protocols: { globalThis: k => globalThis[k] } });
|
|
3534
3793
|
```
|
|
3535
3794
|
|
|
3536
|
-
**
|
|
3795
|
+
**Parameters:**
|
|
3537
3796
|
|
|
3538
3797
|
| Parameter | Type | Description |
|
|
3539
3798
|
|-----------|------|-------------|
|
|
@@ -3541,25 +3800,81 @@ await assignFrom(document.body, {
|
|
|
3541
3800
|
| `instantiate` | HTMLTemplateElement | The template to clone (typically resolved via globalThis protocol) |
|
|
3542
3801
|
| `method` | string | `'appendChild'` (default) or `'prepend'` — where to place markers |
|
|
3543
3802
|
| `forget` | boolean | If true, removes nodes entirely when `if` is false (default: hides with `hidden` attribute) |
|
|
3803
|
+
| `placeholder` | string | Name of a pre-existing marker pair whose content is removed on first activation |
|
|
3804
|
+
| `assign` | object | Assignment config applied to cloned content before insertion (see below) |
|
|
3544
3805
|
|
|
3545
3806
|
**Behavior:**
|
|
3546
3807
|
|
|
3547
|
-
- **First load (`if` = true, no existing content):** Clones the template, inserts content between `<!--?start name="X"-->` / `<!--?end-->` comment markers.
|
|
3808
|
+
- **First load (`if` = true, no existing content):** Clones the template, inserts content between `<!--?start name="X"-->` / `<!--?end-->` comment markers. If `placeholder` is specified, its content is removed first.
|
|
3548
3809
|
- **Show (content exists but hidden):** Removes `hidden` attribute from elements between markers.
|
|
3549
3810
|
- **Hide (`if` = false, `forget` = false):** Adds `hidden` attribute to elements between markers.
|
|
3550
3811
|
- **Remove (`if` = false, `forget` = true):** Removes nodes between markers entirely. Markers persist for re-insertion if `if` becomes true again.
|
|
3551
3812
|
|
|
3552
3813
|
This is useful for conditional rendering, routing, and lazy-loading views.
|
|
3553
3814
|
|
|
3815
|
+
**Placeholder content (SSR/streaming):**
|
|
3816
|
+
|
|
3817
|
+
Use `placeholder` to remove pre-rendered "loading" content when real content first activates. The server streams placeholder content inside named markers; once JS runs and the condition is met, the placeholder is cleared and the template is cloned in its place:
|
|
3818
|
+
|
|
3819
|
+
```html
|
|
3820
|
+
<div class="mainView">
|
|
3821
|
+
<!--?start name="loading"-->
|
|
3822
|
+
<div class="skeleton">Loading...</div>
|
|
3823
|
+
<!--?end-->
|
|
3824
|
+
</div>
|
|
3825
|
+
```
|
|
3826
|
+
|
|
3827
|
+
```JavaScript
|
|
3828
|
+
await assignFromAsync(container, {
|
|
3829
|
+
'?.querySelector?..mainView =>': {
|
|
3830
|
+
do: 'builtIns.lazyLoad',
|
|
3831
|
+
get: {
|
|
3832
|
+
if: '?.isReady',
|
|
3833
|
+
instantiate: 'globalThis://mainTemplate',
|
|
3834
|
+
placeholder: 'loading',
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
}, { withMethods: ['querySelector'], from: vm, protocols: { globalThis: k => globalThis[k] } });
|
|
3838
|
+
```
|
|
3839
|
+
|
|
3840
|
+
The placeholder is removed only on first activation (one-shot). If `if` later becomes false and then true again, the template is re-shown from its own markers — the placeholder does not reappear.
|
|
3841
|
+
|
|
3842
|
+
**Assigning values to cloned content (`assign`):**
|
|
3843
|
+
|
|
3844
|
+
Use `assign` to populate the cloned template's elements before they are inserted into the DOM. This uses the same pattern as `manageTemplateList`'s `fromEachItem`:
|
|
3845
|
+
|
|
3846
|
+
```JavaScript
|
|
3847
|
+
await assignFromAsync(container, {
|
|
3848
|
+
'?.querySelector?..panel =>': {
|
|
3849
|
+
do: 'builtIns.lazyLoad',
|
|
3850
|
+
get: {
|
|
3851
|
+
if: '?.showPanel',
|
|
3852
|
+
instantiate: 'globalThis://panelTemplate',
|
|
3853
|
+
assign: {
|
|
3854
|
+
assignToFragment: {
|
|
3855
|
+
'#[title]?.textContent': '?.panelTitle',
|
|
3856
|
+
'#[body]?.textContent': '?.panelContent'
|
|
3857
|
+
},
|
|
3858
|
+
withOptions: {
|
|
3859
|
+
at: { title: [0], body: [1] }
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
}, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
3865
|
+
```
|
|
3866
|
+
|
|
3867
|
+
The `assign.assignToFragment` paths resolve against `options.from` (the same source that drives the `if` condition). For multi-element templates, use `assign.configs` (same zip semantics as `manageTemplateList`).
|
|
3868
|
+
|
|
3554
3869
|
### View Transitions
|
|
3555
3870
|
|
|
3556
3871
|
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
3872
|
|
|
3558
3873
|
```JavaScript
|
|
3559
|
-
await
|
|
3874
|
+
await assignFromAsync(container, {
|
|
3560
3875
|
'?.querySelector?..outlet =>': {
|
|
3561
3876
|
do: 'builtIns.lazyLoad',
|
|
3562
|
-
|
|
3877
|
+
get: {
|
|
3563
3878
|
if: '?.isVisible',
|
|
3564
3879
|
instantiate: 'globalThis://myTemplate',
|
|
3565
3880
|
transitional: true,
|
|
@@ -3598,10 +3913,10 @@ The handler injects a minimal default style (`.ag-hide { display: none }`) once
|
|
|
3598
3913
|
|
|
3599
3914
|
**Custom hide class:**
|
|
3600
3915
|
|
|
3601
|
-
Use the `hideClass`
|
|
3916
|
+
Use the `hideClass` parameter to use a different CSS class name:
|
|
3602
3917
|
|
|
3603
3918
|
```JavaScript
|
|
3604
|
-
|
|
3919
|
+
get: {
|
|
3605
3920
|
if: '?.isVisible',
|
|
3606
3921
|
instantiate: 'globalThis://myTemplate',
|
|
3607
3922
|
transitional: true,
|
|
@@ -3618,11 +3933,11 @@ resolve: {
|
|
|
3618
3933
|
**Routing example with transitions:**
|
|
3619
3934
|
|
|
3620
3935
|
```JavaScript
|
|
3621
|
-
await
|
|
3936
|
+
await assignFromAsync(container, {
|
|
3622
3937
|
'?.querySelector?..routerOutlet =>': [
|
|
3623
|
-
{ do: 'builtIns.lazyLoadSwitch',
|
|
3624
|
-
{ do: 'builtIns.lazyLoadSwitch',
|
|
3625
|
-
{ do: 'builtIns.lazyLoadSwitch',
|
|
3938
|
+
{ do: 'builtIns.lazyLoadSwitch', get: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView', transitional: true } },
|
|
3939
|
+
{ do: 'builtIns.lazyLoadSwitch', get: { lhs: '?.route', rhs: 'settings', instantiate: 'globalThis://settingsView', transitional: true } },
|
|
3940
|
+
{ do: 'builtIns.lazyLoadSwitch', get: { lhs: '?.route', rhs: 'profile', instantiate: 'globalThis://profileView', transitional: true } },
|
|
3626
3941
|
]
|
|
3627
3942
|
}, { withMethods: ['querySelector'], from: router, protocols: { globalThis: k => globalThis[k] } });
|
|
3628
3943
|
```
|
|
@@ -3654,18 +3969,18 @@ See the visual demo at `demos/view-transition-demo.html`.
|
|
|
3654
3969
|
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
3970
|
|
|
3656
3971
|
```JavaScript
|
|
3657
|
-
await
|
|
3972
|
+
await assignFromAsync(document.body, {
|
|
3658
3973
|
'?.querySelector?..mainView =>': [
|
|
3659
3974
|
{
|
|
3660
3975
|
do: 'builtIns.lazyLoad',
|
|
3661
|
-
|
|
3976
|
+
get: {
|
|
3662
3977
|
if: '?.isVisible',
|
|
3663
3978
|
instantiate: 'globalThis://viewTemplate',
|
|
3664
3979
|
}
|
|
3665
3980
|
},
|
|
3666
3981
|
{
|
|
3667
3982
|
do: 'applyTheme',
|
|
3668
|
-
|
|
3983
|
+
get: {
|
|
3669
3984
|
theme: '?.currentTheme'
|
|
3670
3985
|
}
|
|
3671
3986
|
}
|
|
@@ -3681,83 +3996,6 @@ await assignFrom(document.body, {
|
|
|
3681
3996
|
- Mixed `do` values — fully supported, each handler is looked up independently.
|
|
3682
3997
|
- Error handling — fail-fast. If a handler throws, remaining handlers are skipped.
|
|
3683
3998
|
|
|
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
3999
|
### Built-in handler: `builtIns.join`
|
|
3762
4000
|
|
|
3763
4001
|
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 +4006,10 @@ const vm = {
|
|
|
3768
4006
|
firstName: 'Helaena'
|
|
3769
4007
|
};
|
|
3770
4008
|
|
|
3771
|
-
|
|
4009
|
+
assignFrom(oElement, {
|
|
3772
4010
|
'?.textContent =>': {
|
|
3773
4011
|
do: 'builtIns.join',
|
|
3774
|
-
|
|
4012
|
+
get: {
|
|
3775
4013
|
value: ['?.lastName', ', ', '?.firstName']
|
|
3776
4014
|
}
|
|
3777
4015
|
}
|
|
@@ -3782,7 +4020,7 @@ await assignFrom(oElement, {
|
|
|
3782
4020
|
|
|
3783
4021
|
**How it works:**
|
|
3784
4022
|
|
|
3785
|
-
1. The `
|
|
4023
|
+
1. The `get.value` array is resolved by `getValues` — `?.` path strings are replaced with actual values from `options.from`.
|
|
3786
4024
|
2. Top-level `null`/`undefined` values are filtered out.
|
|
3787
4025
|
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
4026
|
4. Remaining elements are joined with the separator (default: `''`, empty string).
|
|
@@ -3797,10 +4035,10 @@ const vm = {
|
|
|
3797
4035
|
firstName: 'Helaena'
|
|
3798
4036
|
};
|
|
3799
4037
|
|
|
3800
|
-
|
|
4038
|
+
assignFrom(oElement, {
|
|
3801
4039
|
'?.textContent =>': {
|
|
3802
4040
|
do: 'builtIns.join',
|
|
3803
|
-
|
|
4041
|
+
get: {
|
|
3804
4042
|
value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
3805
4043
|
}
|
|
3806
4044
|
}
|
|
@@ -3816,11 +4054,11 @@ await assignFrom(oElement, {
|
|
|
3816
4054
|
**Custom separator:**
|
|
3817
4055
|
|
|
3818
4056
|
```JavaScript
|
|
3819
|
-
|
|
4057
|
+
assignFrom(oElement, {
|
|
3820
4058
|
'?.textContent =>': {
|
|
3821
4059
|
do: 'builtIns.join',
|
|
3822
4060
|
separator: ' | ',
|
|
3823
|
-
|
|
4061
|
+
get: {
|
|
3824
4062
|
value: ['?.firstName', '?.lastName']
|
|
3825
4063
|
}
|
|
3826
4064
|
}
|
|
@@ -3846,10 +4084,10 @@ const vm = {
|
|
|
3846
4084
|
isHappy: false
|
|
3847
4085
|
};
|
|
3848
4086
|
|
|
3849
|
-
|
|
4087
|
+
assignFrom(oSection, {
|
|
3850
4088
|
'?.querySelector?.div =>': {
|
|
3851
4089
|
do: 'builtIns.microDataJoin',
|
|
3852
|
-
|
|
4090
|
+
get: {
|
|
3853
4091
|
template: [
|
|
3854
4092
|
{ prop: 'firstName', val: '?.firstName' },
|
|
3855
4093
|
' ',
|
|
@@ -3896,7 +4134,7 @@ Produces:
|
|
|
3896
4134
|
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
4135
|
|
|
3898
4136
|
```JavaScript
|
|
3899
|
-
|
|
4137
|
+
get: {
|
|
3900
4138
|
template: [
|
|
3901
4139
|
{ prop: 'firstName', val: '?.firstName' },
|
|
3902
4140
|
[' ', { prop: 'middleName', val: '?.middleName' }], // dropped if middleName is undefined
|
|
@@ -3930,6 +4168,75 @@ For custom property names or per-segment formatting, pass an explicit object:
|
|
|
3930
4168
|
const template = md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`;
|
|
3931
4169
|
```
|
|
3932
4170
|
|
|
4171
|
+
### Built-in handler: `builtIns.manageTemplateList`
|
|
4172
|
+
|
|
4173
|
+
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.
|
|
4174
|
+
|
|
4175
|
+
```JavaScript
|
|
4176
|
+
assignFrom(document.body, {
|
|
4177
|
+
'?.querySelector?.tbody =>': {
|
|
4178
|
+
do: 'builtIns.manageTemplateList',
|
|
4179
|
+
get: {
|
|
4180
|
+
forEach: '?.rankings',
|
|
4181
|
+
instantiate: 'globalThis://country-ranking',
|
|
4182
|
+
},
|
|
4183
|
+
fromEachItem: {
|
|
4184
|
+
assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
4185
|
+
withOptions: { withMethods: ['querySelector'], infer: true },
|
|
4186
|
+
get: { key: '?.rank' }
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
}, {
|
|
4190
|
+
from: olympics2024Summary,
|
|
4191
|
+
withMethods: ['querySelector'],
|
|
4192
|
+
protocols: { globalThis: k => globalThis[k] }
|
|
4193
|
+
});
|
|
4194
|
+
```
|
|
4195
|
+
|
|
4196
|
+
**How it works:**
|
|
4197
|
+
|
|
4198
|
+
1. Resolves `forEach` (iterable) and `instantiate` (template) from the `resolve` block
|
|
4199
|
+
2. Clones the template once per item, buffering all clones into a `DocumentFragment`
|
|
4200
|
+
3. For each clone, calls `assignFrom(clone, assignToFragment, { from: item, ...withOptions })` — distributing the item's data
|
|
4201
|
+
4. Inserts the fragment between comment markers in one DOM operation
|
|
4202
|
+
5. On subsequent calls, reconciles by `key` — adds new items, removes missing ones, updates existing clones in place
|
|
4203
|
+
|
|
4204
|
+
**Keyed reconciliation:**
|
|
4205
|
+
|
|
4206
|
+
The `key` field (in `fromEachItem.get`) identifies each item for stable identity across updates:
|
|
4207
|
+
- New key → clone + append
|
|
4208
|
+
- Key removed → hide (or remove if `forget: true`)
|
|
4209
|
+
- Key still present → update clone in place (no re-cloning)
|
|
4210
|
+
|
|
4211
|
+
Without `key`, positional matching is used (item[i] → clone[i]).
|
|
4212
|
+
|
|
4213
|
+
**Shared parent data (`fromSource`):**
|
|
4214
|
+
|
|
4215
|
+
Pass data from the outer VM into each clone (e.g., aggregate totals):
|
|
4216
|
+
|
|
4217
|
+
```JavaScript
|
|
4218
|
+
fromSource: {
|
|
4219
|
+
assignToFragment: {
|
|
4220
|
+
'?.querySelector?.[part~="totalMedalCount"]?.textContent': '?.totalMedalCount'
|
|
4221
|
+
},
|
|
4222
|
+
withOptions: { withMethods: ['querySelector'] }
|
|
4223
|
+
}
|
|
4224
|
+
```
|
|
4225
|
+
|
|
4226
|
+
**Wait for async rendering (`waitForSettled`):**
|
|
4227
|
+
|
|
4228
|
+
Optionally wait for async operations inside the fragment to complete before committing to the live DOM:
|
|
4229
|
+
|
|
4230
|
+
```JavaScript
|
|
4231
|
+
get: {
|
|
4232
|
+
forEach: '?.rankings',
|
|
4233
|
+
instantiate: 'globalThis://country-ranking',
|
|
4234
|
+
waitForSettled: true, // or { idleMs: 50, timeout: 2000 }
|
|
4235
|
+
}
|
|
4236
|
+
```
|
|
4237
|
+
|
|
4238
|
+
For full details, see [docs/manage-template-list.md](docs/manage-template-list.md).
|
|
4239
|
+
|
|
3933
4240
|
## Typed Path Authoring with `paths`, `sp`, and `md`
|
|
3934
4241
|
|
|
3935
4242
|
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 +4258,7 @@ const $ = paths<Person>();
|
|
|
3951
4258
|
export default {
|
|
3952
4259
|
'?.textContent =>': {
|
|
3953
4260
|
do: 'builtIns.join',
|
|
3954
|
-
|
|
4261
|
+
get: {
|
|
3955
4262
|
value: sp`${$.lastName}, ${$.firstName}`
|
|
3956
4263
|
}
|
|
3957
4264
|
}
|
|
@@ -4041,13 +4348,13 @@ Both auto-detect path proxies (no `.path` needed inside template literals) and p
|
|
|
4041
4348
|
`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
4349
|
|
|
4043
4350
|
```TypeScript
|
|
4044
|
-
import {
|
|
4351
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
|
|
4045
4352
|
|
|
4046
|
-
await
|
|
4353
|
+
await assignFromAsync(document.body, {
|
|
4047
4354
|
'#[main]?.textContent': '?.greeting',
|
|
4048
4355
|
'#[main] =>': {
|
|
4049
4356
|
do: 'builtIns.lazyLoad',
|
|
4050
|
-
|
|
4357
|
+
get: { if: '?.showContent', instantiate: 'globalThis://myTemplate' }
|
|
4051
4358
|
}
|
|
4052
4359
|
}, {
|
|
4053
4360
|
from: viewModel,
|
|
@@ -4065,21 +4372,65 @@ await assignFrom(document.body, {
|
|
|
4065
4372
|
4. On subsequent calls, the cached `WeakRef.deref()` returns the element in ~10ns.
|
|
4066
4373
|
5. If the WeakRef is collected (element was GC'd), falls back to `getElementById` (~10-100ns).
|
|
4067
4374
|
|
|
4068
|
-
|
|
4375
|
+
**`withIds` — stable references with auto-assigned IDs:**
|
|
4376
|
+
|
|
4377
|
+
All forms assign a unique ID to the resolved element (if it doesn't have one). This makes the reference resilient to future DOM mutations — once an element has an ID, it can be found regardless of structural changes.
|
|
4378
|
+
|
|
4379
|
+
```TypeScript
|
|
4380
|
+
withIds: {
|
|
4381
|
+
x: { qry: '.mainView' }, // querySelector on target, auto-assign ID
|
|
4382
|
+
y: 'existingId', // element already has an ID, cache via WeakRef
|
|
4383
|
+
z: { path: [0, 1], expect: 'input', fallback: true }, // child index path + auto-ID + validation
|
|
4384
|
+
}
|
|
4385
|
+
```
|
|
4386
|
+
|
|
4387
|
+
| Form | First access | Subsequent | Use case |
|
|
4388
|
+
|------|-------------|------------|----------|
|
|
4389
|
+
| `'existingId'` | getElementById (~10-100ns) | WeakRef cache (~10ns) | Singleton elements with known IDs |
|
|
4390
|
+
| `{ qry: '.x' }` | querySelector (~3,000ns) | — (re-queries each call) | Target-relative elements, stable against mutations |
|
|
4391
|
+
| `{ path: [0, 1] }` | children[i] (~2-4ns) | — (re-traverses each call) | Fast + stable (ID protects against future DOM changes) |
|
|
4392
|
+
|
|
4393
|
+
**`at` — lightweight positional references (no IDs, no DOM pollution):**
|
|
4394
|
+
|
|
4395
|
+
For repeated elements (e.g., per-row in `manageTemplateList`) where you control the template structure and don't want to assign 2,000 IDs to the DOM:
|
|
4396
|
+
|
|
4397
|
+
```TypeScript
|
|
4398
|
+
at: {
|
|
4399
|
+
a: [0], // target.children[0]
|
|
4400
|
+
b: [1], // target.children[1]
|
|
4401
|
+
c: { path: [0, 2], expect: '.info', fallback: true } // with validation
|
|
4402
|
+
}
|
|
4403
|
+
```
|
|
4404
|
+
|
|
4405
|
+
`at` resolves by child index every call — no IDs assigned, no WeakRef cache, no DOM attributes added. ~2-4ns per index step. Use when:
|
|
4406
|
+
- The template structure is stable (you own it)
|
|
4407
|
+
- You want a clean DOM (no auto-generated `id` attributes)
|
|
4408
|
+
- You're rendering many items (1,000 rows × 2 refs = 2,000 fewer DOM attributes)
|
|
4409
|
+
|
|
4410
|
+
Both `withIds` and `at` use the same `#[x]` syntax on LHS and RHS keys.
|
|
4411
|
+
|
|
4412
|
+
**Validated paths with `expect` and `fallback`:**
|
|
4413
|
+
|
|
4414
|
+
Both `withIds` and `at` support the `{ path, expect, fallback }` form for dev-time validation:
|
|
4069
4415
|
|
|
4070
4416
|
```TypeScript
|
|
4071
4417
|
withIds: {
|
|
4072
|
-
|
|
4073
|
-
|
|
4418
|
+
nameInput: { path: [1], expect: 'input' }, // warn if [1] isn't an <input>
|
|
4419
|
+
label: { path: [0], expect: 'label', fallback: true } // warn + recover via querySelector
|
|
4074
4420
|
}
|
|
4075
4421
|
```
|
|
4076
4422
|
|
|
4423
|
+
- `expect` — a CSS selector checked via `element.matches()` (~50-200ns). If the element at `path` doesn't match, a console warning is logged with the correct coordinates.
|
|
4424
|
+
- `fallback: true` — on mismatch, automatically recovers by falling back to `querySelector(expect)`. The corrected element is cached for subsequent access.
|
|
4425
|
+
- The correction diagnostic is loaded lazily (fire-and-forget dynamic import) — zero payload in the happy path.
|
|
4426
|
+
- Strip `expect`/`fallback` in production builds for maximum performance (or leave them — the overhead is a single `matches()` call).
|
|
4427
|
+
|
|
4077
4428
|
**Chaining with `?.` paths:**
|
|
4078
4429
|
|
|
4079
4430
|
`#[x]` anchors the start of the path. Further `?.` segments chain from the resolved element:
|
|
4080
4431
|
|
|
4081
4432
|
```TypeScript
|
|
4082
|
-
|
|
4433
|
+
assignFrom(document.body, {
|
|
4083
4434
|
'#[form]?.querySelector?..username?.value': '?.username',
|
|
4084
4435
|
'#[form]?.querySelector?..email?.value': '?.email',
|
|
4085
4436
|
'#[header]?.style?.color': '?.themeColor',
|
|
@@ -4096,10 +4447,10 @@ await assignFrom(document.body, {
|
|
|
4096
4447
|
**With handlers (` =>`):**
|
|
4097
4448
|
|
|
4098
4449
|
```TypeScript
|
|
4099
|
-
await
|
|
4450
|
+
await assignFromAsync(container, {
|
|
4100
4451
|
'#[outlet] =>': {
|
|
4101
4452
|
do: 'builtIns.lazyLoadSwitch',
|
|
4102
|
-
|
|
4453
|
+
get: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView' }
|
|
4103
4454
|
}
|
|
4104
4455
|
}, {
|
|
4105
4456
|
from: router,
|
|
@@ -4118,6 +4469,24 @@ await assignFrom(container, {
|
|
|
4118
4469
|
|
|
4119
4470
|
The `#[x]` + `withIds` pattern gives you the Map+WeakRef speed tier automatically.
|
|
4120
4471
|
|
|
4472
|
+
**RHS references (`#[x]` on the value side):**
|
|
4473
|
+
|
|
4474
|
+
`#[x]` can also appear on the RHS (value side) to read properties from a cached element. Bare `#[x]` resolves to the element's ID string; append `?.` paths to access other properties:
|
|
4475
|
+
|
|
4476
|
+
```TypeScript
|
|
4477
|
+
assignFrom(form, {
|
|
4478
|
+
'?.querySelector?.label?.htmlFor': '#[nameInput]', // → the input's ID string
|
|
4479
|
+
'?.querySelector?.label?.title': '#[nameInput]?.type', // → 'text', 'email', etc.
|
|
4480
|
+
'?.headerText': '#[info]?.dataset?.user', // → nested property access
|
|
4481
|
+
}, {
|
|
4482
|
+
from: {},
|
|
4483
|
+
withIds: { nameInput: { qry: 'input' }, info: { qry: '.info' } },
|
|
4484
|
+
withMethods: ['querySelector']
|
|
4485
|
+
});
|
|
4486
|
+
```
|
|
4487
|
+
|
|
4488
|
+
This is useful for accessibility patterns (setting `label[for]` to an auto-generated input ID) and cross-referencing elements declaratively.
|
|
4489
|
+
|
|
4121
4490
|
**Key behaviors:**
|
|
4122
4491
|
|
|
4123
4492
|
- **Lazy resolution** — elements are resolved on first encounter, not eagerly at the start.
|
|
@@ -4137,9 +4506,9 @@ const vm = {
|
|
|
4137
4506
|
user: { role: 'admin', avatar: '/img/alice.png' }
|
|
4138
4507
|
};
|
|
4139
4508
|
|
|
4140
|
-
|
|
4509
|
+
assignFrom(outerDiv, {}, {
|
|
4141
4510
|
from: vm,
|
|
4142
|
-
|
|
4511
|
+
infer: {
|
|
4143
4512
|
byItemprop: ['name', 'email', 'user']
|
|
4144
4513
|
}
|
|
4145
4514
|
});
|
|
@@ -4160,7 +4529,7 @@ For each key in `byItemprop`, this finds `[itemprop="${key}"]` elements within t
|
|
|
4160
4529
|
**Pass `true` to infer all source keys:**
|
|
4161
4530
|
|
|
4162
4531
|
```TypeScript
|
|
4163
|
-
|
|
4532
|
+
infer: { byItemprop: true }
|
|
4164
4533
|
```
|
|
4165
4534
|
|
|
4166
4535
|
For full details, see [docs/inferred-assignments.md](docs/inferred-assignments.md).
|
|
@@ -4170,9 +4539,9 @@ For full details, see [docs/inferred-assignments.md](docs/inferred-assignments.m
|
|
|
4170
4539
|
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
4540
|
|
|
4172
4541
|
```TypeScript
|
|
4173
|
-
import {
|
|
4542
|
+
import { assignFromAsync } from 'assign-gingerly/assignFromAsync.js';
|
|
4174
4543
|
|
|
4175
|
-
await
|
|
4544
|
+
await assignFromAsync(shadowRoot, { /* normal assignments */ }, {
|
|
4176
4545
|
from: vm,
|
|
4177
4546
|
enhance: [
|
|
4178
4547
|
{ emc: 'be-bound/emc.json', matching: '[name]' },
|
|
@@ -5800,4 +6169,4 @@ Any web server that serves static files with server-side includes will do but...
|
|
|
5800
6169
|
7. > git submodule update --init --recursive
|
|
5801
6170
|
8. > npm install
|
|
5802
6171
|
9. > npm run serve
|
|
5803
|
-
10. Open http://localhost:8000/demo/ in a modern browser
|
|
6172
|
+
10. Open http://localhost:8000/demo/ in a modern browser
|