assign-gingerly 0.0.59 → 0.0.61
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 +307 -14
- package/assignFrom-extension.js +25 -0
- package/assignFrom-extension.ts +53 -0
- package/assignFrom.js +190 -12
- package/assignFrom.ts +184 -11
- package/assignFromAsync-extension.js +28 -0
- package/assignFromAsync-extension.ts +58 -0
- package/assignFromAsync.js +11 -9
- package/assignFromAsync.ts +27 -11
- package/assignGingerly.js +50 -0
- package/assignGingerly.ts +51 -0
- package/builtInEmoji.js +26 -0
- package/builtInEmoji.ts +34 -0
- package/handlers/lazyLoad.js +33 -2
- package/handlers/lazyLoad.ts +46 -1
- package/handlers/manageTemplateList.js +54 -31
- package/handlers/manageTemplateList.ts +51 -28
- package/handlers/rangeSelector.js +52 -0
- package/handlers/rangeSelector.ts +90 -0
- package/inferencer/inferencer.js +9 -21
- package/inferencer/inferencer.ts +10 -21
- package/inferredAssignments.js +34 -4
- package/inferredAssignments.ts +56 -6
- package/package.json +13 -1
- package/playwright.config.ts +3 -2
- package/processHandlerCommands.js +7 -1
- package/processHandlerCommands.ts +8 -1
- package/resolveIdRef.js +70 -19
- package/resolveIdRef.ts +73 -21
- package/types/assign-gingerly/types.d.ts +10 -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.
|
|
@@ -285,7 +300,7 @@ In real-world use cases, you often need to replace one object with another of a
|
|
|
285
300
|
|
|
286
301
|
**Exception: classes with `static assignTo`**
|
|
287
302
|
|
|
288
|
-
If the current value is an instance of a class that defines
|
|
303
|
+
If the current value is an instance of a class that defines `static assignTo`, that method is called instead of replacing. This allows classes to opt into custom assignment behavior (e.g., reactive models, validated records, iterable collections with private lists). See the full [Custom Assignment with `static assignTo` Protocol](#custom-assignment-with-static-assignto-protocol) section below for details, examples, and the method signature.
|
|
289
304
|
|
|
290
305
|
```TypeScript
|
|
291
306
|
class TodoList {
|
|
@@ -754,7 +769,20 @@ assignGingerly(div, {
|
|
|
754
769
|
- Testing is done in mount-observer package (no tests in assign-gingerly)
|
|
755
770
|
- Single @eachTime per path (nested @eachTime not currently supported)
|
|
756
771
|
|
|
757
|
-
|
|
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`.
|
|
758
786
|
|
|
759
787
|
## Example 4 - Incrementing values with += command
|
|
760
788
|
|
|
@@ -900,7 +928,101 @@ console.log(obj);
|
|
|
900
928
|
|
|
901
929
|
|
|
902
930
|
|
|
903
|
-
## 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
|
|
904
1026
|
|
|
905
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:
|
|
906
1028
|
|
|
@@ -1190,7 +1312,7 @@ obj
|
|
|
1190
1312
|
console.log(obj); // { a: 1, b: { c: 2 }, d: 3 }
|
|
1191
1313
|
```
|
|
1192
1314
|
|
|
1193
|
-
**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`.
|
|
1194
1316
|
|
|
1195
1317
|
The prototype extensions are non-enumerable and won't appear in `Object.keys()` or `for...in` loops.
|
|
1196
1318
|
|
|
@@ -1424,7 +1546,7 @@ interface EnhancementConfig<T, TObj = Element> {
|
|
|
1424
1546
|
|
|
1425
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.
|
|
1426
1548
|
|
|
1427
|
-
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.
|
|
1428
1550
|
|
|
1429
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.
|
|
1430
1552
|
|
|
@@ -3607,6 +3729,23 @@ Import paths must be local (relative, absolute, or bare specifiers — no cross-
|
|
|
3607
3729
|
|
|
3608
3730
|
Built-in handlers (`builtIns.lazyLoad`, `builtIns.join`, etc.) auto-load without needing to be listed in `handlers`.
|
|
3609
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
|
+
|
|
3610
3749
|
**How it works:**
|
|
3611
3750
|
|
|
3612
3751
|
1. Keys ending with ` =>` are separated from normal keys.
|
|
@@ -3661,16 +3800,72 @@ await assignFromAsync(document.body, {
|
|
|
3661
3800
|
| `instantiate` | HTMLTemplateElement | The template to clone (typically resolved via globalThis protocol) |
|
|
3662
3801
|
| `method` | string | `'appendChild'` (default) or `'prepend'` — where to place markers |
|
|
3663
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) |
|
|
3664
3805
|
|
|
3665
3806
|
**Behavior:**
|
|
3666
3807
|
|
|
3667
|
-
- **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.
|
|
3668
3809
|
- **Show (content exists but hidden):** Removes `hidden` attribute from elements between markers.
|
|
3669
3810
|
- **Hide (`if` = false, `forget` = false):** Adds `hidden` attribute to elements between markers.
|
|
3670
3811
|
- **Remove (`if` = false, `forget` = true):** Removes nodes between markers entirely. Markers persist for re-insertion if `if` becomes true again.
|
|
3671
3812
|
|
|
3672
3813
|
This is useful for conditional rendering, routing, and lazy-loading views.
|
|
3673
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
|
+
|
|
3674
3869
|
### View Transitions
|
|
3675
3870
|
|
|
3676
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:
|
|
@@ -3987,7 +4182,7 @@ assignFrom(document.body, {
|
|
|
3987
4182
|
},
|
|
3988
4183
|
fromEachItem: {
|
|
3989
4184
|
assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
3990
|
-
withOptions: { withMethods: ['querySelector'],
|
|
4185
|
+
withOptions: { withMethods: ['querySelector'], infer: true },
|
|
3991
4186
|
get: { key: '?.rank' }
|
|
3992
4187
|
}
|
|
3993
4188
|
}
|
|
@@ -4042,6 +4237,42 @@ get: {
|
|
|
4042
4237
|
|
|
4043
4238
|
For full details, see [docs/manage-template-list.md](docs/manage-template-list.md).
|
|
4044
4239
|
|
|
4240
|
+
### Built-in handler: `builtIns.rangeSelector`
|
|
4241
|
+
|
|
4242
|
+
Evaluates a value against a series of range conditions and merges the matched case's properties into the target. Converts imperative if/else-if chains into declarative JSON configs.
|
|
4243
|
+
|
|
4244
|
+
```JavaScript
|
|
4245
|
+
assignFrom(element, {
|
|
4246
|
+
'?. =>': {
|
|
4247
|
+
do: 'builtIns.rangeSelector',
|
|
4248
|
+
get: {
|
|
4249
|
+
value: '?.count',
|
|
4250
|
+
when: [
|
|
4251
|
+
{ '<=': 10, merge: { status: 'low', statusMessage: 'Low count' } },
|
|
4252
|
+
{ '<': 20, merge: { status: 'medium', statusMessage: 'Medium count' } },
|
|
4253
|
+
{ merge: { status: 'high', statusMessage: 'High count!' } }
|
|
4254
|
+
]
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
}, { from: vm });
|
|
4258
|
+
```
|
|
4259
|
+
|
|
4260
|
+
**How it works:**
|
|
4261
|
+
|
|
4262
|
+
1. Resolves `value` from the source (e.g., `vm.count`)
|
|
4263
|
+
2. Iterates `when` cases in order — first match wins
|
|
4264
|
+
3. Each case can have operator keys (`<=`, `<`, `>=`, `>`, `===`, `!==`) as conditions
|
|
4265
|
+
4. Multiple operators per case = AND logic (e.g., `{ '>=': 10, '<': 20, merge: {...} }`)
|
|
4266
|
+
5. No operator keys = default/catch-all
|
|
4267
|
+
6. Merges the matched case's `merge` object into the target via `assignGingerly`
|
|
4268
|
+
|
|
4269
|
+
**Supported operators:** `<=`, `<`, `>=`, `>`, `===`, `!==`
|
|
4270
|
+
|
|
4271
|
+
**Notes:**
|
|
4272
|
+
- Comparison uses JavaScript semantics (`false < true`, strings compare lexicographically)
|
|
4273
|
+
- Fully JSON-serializable — no functions, no special types
|
|
4274
|
+
- Lazy-loaded on demand like all built-in handlers
|
|
4275
|
+
|
|
4045
4276
|
## Typed Path Authoring with `paths`, `sp`, and `md`
|
|
4046
4277
|
|
|
4047
4278
|
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`.
|
|
@@ -4177,15 +4408,59 @@ await assignFromAsync(document.body, {
|
|
|
4177
4408
|
4. On subsequent calls, the cached `WeakRef.deref()` returns the element in ~10ns.
|
|
4178
4409
|
5. If the WeakRef is collected (element was GC'd), falls back to `getElementById` (~10-100ns).
|
|
4179
4410
|
|
|
4180
|
-
|
|
4411
|
+
**`withIds` — stable references with auto-assigned IDs:**
|
|
4412
|
+
|
|
4413
|
+
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.
|
|
4414
|
+
|
|
4415
|
+
```TypeScript
|
|
4416
|
+
withIds: {
|
|
4417
|
+
x: { qry: '.mainView' }, // querySelector on target, auto-assign ID
|
|
4418
|
+
y: 'existingId', // element already has an ID, cache via WeakRef
|
|
4419
|
+
z: { path: [0, 1], expect: 'input', fallback: true }, // child index path + auto-ID + validation
|
|
4420
|
+
}
|
|
4421
|
+
```
|
|
4422
|
+
|
|
4423
|
+
| Form | First access | Subsequent | Use case |
|
|
4424
|
+
|------|-------------|------------|----------|
|
|
4425
|
+
| `'existingId'` | getElementById (~10-100ns) | WeakRef cache (~10ns) | Singleton elements with known IDs |
|
|
4426
|
+
| `{ qry: '.x' }` | querySelector (~3,000ns) | — (re-queries each call) | Target-relative elements, stable against mutations |
|
|
4427
|
+
| `{ path: [0, 1] }` | children[i] (~2-4ns) | — (re-traverses each call) | Fast + stable (ID protects against future DOM changes) |
|
|
4428
|
+
|
|
4429
|
+
**`at` — lightweight positional references (no IDs, no DOM pollution):**
|
|
4430
|
+
|
|
4431
|
+
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:
|
|
4432
|
+
|
|
4433
|
+
```TypeScript
|
|
4434
|
+
at: {
|
|
4435
|
+
a: [0], // target.children[0]
|
|
4436
|
+
b: [1], // target.children[1]
|
|
4437
|
+
c: { path: [0, 2], expect: '.info', fallback: true } // with validation
|
|
4438
|
+
}
|
|
4439
|
+
```
|
|
4440
|
+
|
|
4441
|
+
`at` resolves by child index every call — no IDs assigned, no WeakRef cache, no DOM attributes added. ~2-4ns per index step. Use when:
|
|
4442
|
+
- The template structure is stable (you own it)
|
|
4443
|
+
- You want a clean DOM (no auto-generated `id` attributes)
|
|
4444
|
+
- You're rendering many items (1,000 rows × 2 refs = 2,000 fewer DOM attributes)
|
|
4445
|
+
|
|
4446
|
+
Both `withIds` and `at` use the same `#[x]` syntax on LHS and RHS keys.
|
|
4447
|
+
|
|
4448
|
+
**Validated paths with `expect` and `fallback`:**
|
|
4449
|
+
|
|
4450
|
+
Both `withIds` and `at` support the `{ path, expect, fallback }` form for dev-time validation:
|
|
4181
4451
|
|
|
4182
4452
|
```TypeScript
|
|
4183
4453
|
withIds: {
|
|
4184
|
-
|
|
4185
|
-
|
|
4454
|
+
nameInput: { path: [1], expect: 'input' }, // warn if [1] isn't an <input>
|
|
4455
|
+
label: { path: [0], expect: 'label', fallback: true } // warn + recover via querySelector
|
|
4186
4456
|
}
|
|
4187
4457
|
```
|
|
4188
4458
|
|
|
4459
|
+
- `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.
|
|
4460
|
+
- `fallback: true` — on mismatch, automatically recovers by falling back to `querySelector(expect)`. The corrected element is cached for subsequent access.
|
|
4461
|
+
- The correction diagnostic is loaded lazily (fire-and-forget dynamic import) — zero payload in the happy path.
|
|
4462
|
+
- Strip `expect`/`fallback` in production builds for maximum performance (or leave them — the overhead is a single `matches()` call).
|
|
4463
|
+
|
|
4189
4464
|
**Chaining with `?.` paths:**
|
|
4190
4465
|
|
|
4191
4466
|
`#[x]` anchors the start of the path. Further `?.` segments chain from the resolved element:
|
|
@@ -4230,6 +4505,24 @@ await assignFromAsync(container, {
|
|
|
4230
4505
|
|
|
4231
4506
|
The `#[x]` + `withIds` pattern gives you the Map+WeakRef speed tier automatically.
|
|
4232
4507
|
|
|
4508
|
+
**RHS references (`#[x]` on the value side):**
|
|
4509
|
+
|
|
4510
|
+
`#[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:
|
|
4511
|
+
|
|
4512
|
+
```TypeScript
|
|
4513
|
+
assignFrom(form, {
|
|
4514
|
+
'?.querySelector?.label?.htmlFor': '#[nameInput]', // → the input's ID string
|
|
4515
|
+
'?.querySelector?.label?.title': '#[nameInput]?.type', // → 'text', 'email', etc.
|
|
4516
|
+
'?.headerText': '#[info]?.dataset?.user', // → nested property access
|
|
4517
|
+
}, {
|
|
4518
|
+
from: {},
|
|
4519
|
+
withIds: { nameInput: { qry: 'input' }, info: { qry: '.info' } },
|
|
4520
|
+
withMethods: ['querySelector']
|
|
4521
|
+
});
|
|
4522
|
+
```
|
|
4523
|
+
|
|
4524
|
+
This is useful for accessibility patterns (setting `label[for]` to an auto-generated input ID) and cross-referencing elements declaratively.
|
|
4525
|
+
|
|
4233
4526
|
**Key behaviors:**
|
|
4234
4527
|
|
|
4235
4528
|
- **Lazy resolution** — elements are resolved on first encounter, not eagerly at the start.
|
|
@@ -4251,7 +4544,7 @@ const vm = {
|
|
|
4251
4544
|
|
|
4252
4545
|
assignFrom(outerDiv, {}, {
|
|
4253
4546
|
from: vm,
|
|
4254
|
-
|
|
4547
|
+
infer: {
|
|
4255
4548
|
byItemprop: ['name', 'email', 'user']
|
|
4256
4549
|
}
|
|
4257
4550
|
});
|
|
@@ -4272,7 +4565,7 @@ For each key in `byItemprop`, this finds `[itemprop="${key}"]` elements within t
|
|
|
4272
4565
|
**Pass `true` to infer all source keys:**
|
|
4273
4566
|
|
|
4274
4567
|
```TypeScript
|
|
4275
|
-
|
|
4568
|
+
infer: { byItemprop: true }
|
|
4276
4569
|
```
|
|
4277
4570
|
|
|
4278
4571
|
For full details, see [docs/inferred-assignments.md](docs/inferred-assignments.md).
|
|
@@ -4338,7 +4631,7 @@ assignGingerly(shadowRoot, {
|
|
|
4338
4631
|
|
|
4339
4632
|
## Custom Assignment with `static assignTo` Protocol
|
|
4340
4633
|
|
|
4341
|
-
|
|
4634
|
+
As [introduced earlier](#example-3b---class-instances-are-normally-replaced), classes can opt into custom assignment behavior by defining a `static assignTo` method. When `assignGingerly` encounters a property whose current value is an instance of such a class, it delegates the assignment to `assignTo` instead of performing the default merge/replace logic. This section covers the full API, method signature, and advanced use cases.
|
|
4342
4635
|
|
|
4343
4636
|
```JavaScript
|
|
4344
4637
|
class ReactiveModel {
|
|
@@ -5912,4 +6205,4 @@ Any web server that serves static files with server-side includes will do but...
|
|
|
5912
6205
|
7. > git submodule update --init --recursive
|
|
5913
6206
|
8. > npm install
|
|
5914
6207
|
9. > npm run serve
|
|
5915
|
-
10. Open http://localhost:8000/demo/ in a modern browser
|
|
6208
|
+
10. Open http://localhost:8000/demo/ in a modern browser
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assignFrom-extension.js — Adds assignFrom to Object.prototype.
|
|
3
|
+
*
|
|
4
|
+
* Import this module for the side effect of extending all objects with
|
|
5
|
+
* the assignFrom method, enabling fluent method chaining:
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import 'assign-gingerly/assignFrom-extension.js';
|
|
9
|
+
*
|
|
10
|
+
* oElement
|
|
11
|
+
* .assignFrom({ '?.textContent': '?.greeting' }, { from: vm1 })
|
|
12
|
+
* .assignFrom({ '?.style Y=': { color: '?.themeColor' } }, { from: vm2 });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { assignFrom } from './assignFrom.js';
|
|
16
|
+
|
|
17
|
+
Object.defineProperty(Object.prototype, 'assignFrom', {
|
|
18
|
+
value: function (pattern, options) {
|
|
19
|
+
assignFrom(this, pattern, options);
|
|
20
|
+
return this;
|
|
21
|
+
},
|
|
22
|
+
writable: true,
|
|
23
|
+
enumerable: false,
|
|
24
|
+
configurable: true,
|
|
25
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assignFrom-extension.ts — Adds assignFrom to Object.prototype.
|
|
3
|
+
*
|
|
4
|
+
* Import this module for the side effect of extending all objects with
|
|
5
|
+
* the assignFrom method, enabling fluent method chaining:
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import 'assign-gingerly/assignFrom-extension.js';
|
|
9
|
+
*
|
|
10
|
+
* oElement
|
|
11
|
+
* .assignFrom({ '?.textContent': '?.greeting' }, { from: vm1 })
|
|
12
|
+
* .assignFrom({ '?.style Y=': { color: '?.themeColor' } }, { from: vm2 });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { assignFrom } from './assignFrom.js';
|
|
16
|
+
import type { AssignFromOptions } from './assignFromAsync.js';
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
interface Object {
|
|
20
|
+
/**
|
|
21
|
+
* Resolve RHS path strings from a source object and assign into this object.
|
|
22
|
+
* Synchronous — handlers are fire-and-forget.
|
|
23
|
+
*
|
|
24
|
+
* @param pattern - Object with LHS paths as keys and RHS path strings (or literals) as values
|
|
25
|
+
* @param options - Configuration including `from` (source object), protocols, withMethods, etc.
|
|
26
|
+
* @returns This object after assignment
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* oElement.assignFrom({
|
|
30
|
+
* '?.textContent': '?.greeting',
|
|
31
|
+
* '?.style Y=': { width: '?.width' }
|
|
32
|
+
* }, { from: viewModel });
|
|
33
|
+
*/
|
|
34
|
+
assignFrom(
|
|
35
|
+
pattern: Record<string, any>,
|
|
36
|
+
options: AssignFromOptions
|
|
37
|
+
): this;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Object.defineProperty(Object.prototype, 'assignFrom', {
|
|
42
|
+
value: function <T extends object>(
|
|
43
|
+
this: T,
|
|
44
|
+
pattern: Record<string, any>,
|
|
45
|
+
options: AssignFromOptions
|
|
46
|
+
): T {
|
|
47
|
+
assignFrom(this, pattern, options);
|
|
48
|
+
return this;
|
|
49
|
+
},
|
|
50
|
+
writable: true,
|
|
51
|
+
enumerable: false,
|
|
52
|
+
configurable: true,
|
|
53
|
+
});
|