assign-gingerly 0.0.64 → 0.0.66

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.
Files changed (42) hide show
  1. package/README.md +85 -26
  2. package/assignFrom.js +21 -19
  3. package/assignFrom.ts +28 -25
  4. package/assignFromAsync.js +10 -7
  5. package/assignFromAsync.ts +15 -98
  6. package/assignGingerly.js +54 -26
  7. package/assignGingerly.ts +62 -27
  8. package/assignTentatively.js +8 -0
  9. package/assignTentatively.ts +6 -0
  10. package/{builtInEmoji.ts → emojis.js} +46 -34
  11. package/emojis.ts +52 -0
  12. package/getValues.js +80 -25
  13. package/getValues.ts +85 -26
  14. package/handlers/addEventListener.js +153 -0
  15. package/handlers/addEventListener.ts +178 -0
  16. package/handlers/arr.js +13 -0
  17. package/handlers/arr.ts +13 -0
  18. package/handlers/lazyLoad.js +3 -3
  19. package/handlers/lazyLoad.ts +3 -3
  20. package/handlers/manageTemplateList.js +48 -21
  21. package/handlers/manageTemplateList.ts +51 -21
  22. package/handlers/nudge.js +23 -0
  23. package/handlers/nudge.ts +23 -0
  24. package/index.js +2 -0
  25. package/index.ts +2 -0
  26. package/inferencer/types/assign-gingerly/types.d.ts +327 -1
  27. package/inferencer/types/mount-observer/types.d.ts +10 -2
  28. package/inferencer/types/roundabout/types.d.ts +1 -1
  29. package/inferencer/types/three-peat/types.d.ts +18 -0
  30. package/object-extension.js +1 -1
  31. package/package.json +12 -4
  32. package/paths.ts +1 -1
  33. package/{withIdsCorrector.js → pinCorrector.js} +5 -5
  34. package/{withIdsCorrector.ts → pinCorrector.ts} +5 -5
  35. package/processHandlerCommands.js +4 -2
  36. package/processHandlerCommands.ts +4 -2
  37. package/resolveIdRef.js +8 -8
  38. package/resolveIdRef.ts +9 -9
  39. package/resolveValues.js +20 -88
  40. package/resolveValues.ts +17 -91
  41. package/types/assign-gingerly/types.d.ts +159 -3
  42. package/builtInEmoji.js +0 -26
package/README.md CHANGED
@@ -94,10 +94,24 @@ For most use cases — including `manageTemplateList`, reactive merge cycles, an
94
94
  assignFrom adds support for:
95
95
 
96
96
  1. Resolving RHS path strings against a source object (`from`).
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).
99
- 4. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
100
- 5. Spread merging via the `"..."` key.
97
+ 2. Target-relative root references via `$0` so paths can resolve from the first argument passed to `assignFrom`/`assignFromAsync` (the target object) instead of the `from` object.
98
+ 3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols).
99
+ 4. Handler plugins via the ` =>` operator for custom logic (fire-and-forget in sync mode, awaitable in async mode).
100
+ 5. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
101
+ 6. Spread merging via the `"..."` key.
102
+
103
+ Example:
104
+
105
+ ```TypeScript
106
+ const target = { value: 'target-value' };
107
+ const source = { value: 'from-value' };
108
+
109
+ assignFrom(target, {
110
+ resolved: '$0?.value'
111
+ }, { from: source });
112
+
113
+ console.log(target.resolved); // 'target-value'
114
+ ```
101
115
 
102
116
  All of assignGingerly's features (nested paths, `withMethods`, `aka`, `@each`, `@eachTime`, registry, etc.) are inherited.
103
117
 
@@ -476,11 +490,11 @@ assignGingerly(div, {
476
490
 
477
491
  // With aliases (concise)
478
492
  assignGingerly(div, {
479
- '?.$?.my-element?.c?.+': 'highlighted',
480
- '?.$?.your-element?.c?.+': 'active'
493
+ '?.🔍?.my-element?.🎨?.+': 'highlighted',
494
+ '?.🔍?.your-element?.🎨?.+': 'active'
481
495
  }, {
482
496
  withMethods: ['querySelector', 'add'],
483
- aka: { '$': 'querySelector', 'c': 'classList', '+': 'add' }
497
+ aka: { '🔍': 'querySelector', '🎨': 'classList', '+': 'add' }
484
498
  });
485
499
  ```
486
500
 
@@ -536,6 +550,33 @@ assignGingerly(element, {
536
550
  - Improves readability when you have many similar operations
537
551
  - Works seamlessly with `withMethods`
538
552
 
553
+ **Shorthand for method aliases:**
554
+
555
+ If you frequently pair aliases with method calls, you can use `akaMethods` as a compact alternative to passing both `withMethods` and `aka` separately. Each entry in `akaMethods` maps an alias to the method name that should be treated as a callable method:
556
+
557
+ ```TypeScript
558
+ const div = document.createElement('div');
559
+
560
+ div.innerHTML = `
561
+ <my-element></my-element>
562
+ `;
563
+
564
+ assignGingerly(div, {
565
+ '?.🔍?.my-element?.🎨?.+': 'highlighted'
566
+ }, {
567
+ akaMethods: {
568
+ '🔍': 'querySelector',
569
+ '🎨': 'classList',
570
+ '+': 'add'
571
+ }
572
+ });
573
+
574
+ const myElement = div.querySelector('my-element');
575
+ console.log(myElement?.classList.contains('highlighted')); // true
576
+ ```
577
+
578
+ `akaMethods` is additive: it complements the existing `withMethods` and `aka` options rather than replacing them.
579
+
539
580
  ## Example 3e - ForEach with @each
540
581
 
541
582
  The `@each` symbol allows you to iterate over collections and apply operations to each item. This works with any iterable including Arrays, NodeList, HTMLCollection, and more.
@@ -775,7 +816,7 @@ While we are in the business of passing values of object A into object B, we mig
775
816
 
776
817
  | Operator | Name | Description | Example |
777
818
  |----------|------|-------------|---------|
778
- | ` +=` | Increment | Add to numeric value, concatenate strings, append to arrays | `'count +=': 5` |
819
+ | ` +=` | Increment | Add to numeric, concat strings, append to arrays, or [bind events](docs/event-binding.md) | `'count +=': 5` |
779
820
  | ` =!` | Toggle | Negate a boolean (or any value via `!`) | `'visible =!': '.'` |
780
821
  | ` -=` | Delete | Remove properties from an object | `'?.data -=': 'key'` |
781
822
  | ` Y=` | Merge | Recursively `assignGingerly` into a sub-object | `'style Y=': { width: '100px' }` |
@@ -816,6 +857,8 @@ The `+=` command syntax is `<path> +=` where the path uses the `?.` nested notat
816
857
  | LHS type | RHS type | Result |
817
858
  |----------|----------|--------|
818
859
  | number | number | addition (`2 += 3` → `5`) |
860
+ | number | string (numeric) | parse + addition (`5 += '3'` → `8`) |
861
+ | number | string (non-numeric) | string concatenation (`5 += 'px'` → `'5px'`) |
819
862
  | string | any | string concatenation (`"hello" += 3` → `"hello3"`) |
820
863
  | array | array | array concatenation (`[1,2] += [3,4]` → `[1,2,3,4]`) |
821
864
  | array | non-array | push single item (`[1,2] += 3` → `[1,2,3]`) |
@@ -835,6 +878,22 @@ assignGingerly(obj, {
835
878
  assignGingerly(obj, { '?.tags +=': 'e' }); // ['a', 'b', 'c', 'd', 'e']
836
879
  ```
837
880
 
881
+ **Event binding with `+=`:**
882
+
883
+ When the LHS resolves to a DOM Element and the RHS is an object with an `on` property, `+=` attaches a declarative event listener:
884
+
885
+ ```JavaScript
886
+ assignFrom(this.shadowRoot, {
887
+ '?.querySelector?.button +=': {
888
+ on: 'click',
889
+ '?.isHappy =!': '.', // toggle host property
890
+ fromLHS: { '?.age +=': '?.dataset.diff' } // read from button, assign to host
891
+ }
892
+ }, { from: this, withMethods: ['querySelector'] });
893
+ ```
894
+
895
+ The handler is lazy-loaded on demand. For full details including assignment vectors, dedup, nudge, and custom event dispatch, see [docs/event-binding.md](docs/event-binding.md).
896
+
838
897
  ## Example 5 - Toggling boolean values and negating
839
898
 
840
899
  The `=!` command allows us to toggle boolean values:
@@ -3859,7 +3918,7 @@ await assignFromAsync(container, {
3859
3918
  if: '?.showPanel',
3860
3919
  instantiate: 'globalThis://panelTemplate',
3861
3920
  assign: {
3862
- assignToFragment: {
3921
+ toClone: {
3863
3922
  '#[title]?.textContent': '?.panelTitle',
3864
3923
  '#[body]?.textContent': '?.panelContent'
3865
3924
  },
@@ -3872,7 +3931,7 @@ await assignFromAsync(container, {
3872
3931
  }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
3873
3932
  ```
3874
3933
 
3875
- 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`).
3934
+ The `assign.toClone` 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`).
3876
3935
 
3877
3936
  ### View Transitions
3878
3937
 
@@ -4189,7 +4248,7 @@ assignFrom(document.body, {
4189
4248
  instantiate: 'globalThis://country-ranking',
4190
4249
  },
4191
4250
  fromEachItem: {
4192
- assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
4251
+ toClone: { '?.querySelector?.tr?.ish': '?.' },
4193
4252
  withOptions: { withMethods: ['querySelector'], infer: true },
4194
4253
  get: { key: '?.rank' }
4195
4254
  }
@@ -4205,7 +4264,7 @@ assignFrom(document.body, {
4205
4264
 
4206
4265
  1. Resolves `forEach` (iterable) and `instantiate` (template) from the `resolve` block
4207
4266
  2. Clones the template once per item, buffering all clones into a `DocumentFragment`
4208
- 3. For each clone, calls `assignFrom(clone, assignToFragment, { from: item, ...withOptions })` — distributing the item's data
4267
+ 3. For each clone, calls `assignFrom(clone, toClone, { from: item, ...withOptions })` — distributing the item's data
4209
4268
  4. Inserts the fragment between comment markers in one DOM operation
4210
4269
  5. On subsequent calls, reconciles by `key` — adds new items, removes missing ones, updates existing clones in place
4211
4270
 
@@ -4218,13 +4277,13 @@ The `key` field (in `fromEachItem.get`) identifies each item for stable identity
4218
4277
 
4219
4278
  Without `key`, positional matching is used (item[i] → clone[i]).
4220
4279
 
4221
- **Shared parent data (`fromSource`):**
4280
+ **Shared parent data (`fromHost`):**
4222
4281
 
4223
4282
  Pass data from the outer VM into each clone (e.g., aggregate totals):
4224
4283
 
4225
4284
  ```JavaScript
4226
- fromSource: {
4227
- assignToFragment: {
4285
+ fromHost: {
4286
+ toClone: {
4228
4287
  '?.querySelector?.[part~="totalMedalCount"]?.textContent': '?.totalMedalCount'
4229
4288
  },
4230
4289
  withOptions: { withMethods: ['querySelector'] }
@@ -4387,7 +4446,7 @@ md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`
4387
4446
 
4388
4447
  Both auto-detect path proxies (no `.path` needed inside template literals) and preserve nested arrays for optional segments.
4389
4448
 
4390
- ## Cached Element Resolution with `#[x]` and `withIds`
4449
+ ## Cached Element Resolution with `#[x]` and `pin`
4391
4450
 
4392
4451
  `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).
4393
4452
 
@@ -4402,7 +4461,7 @@ await assignFromAsync(document.body, {
4402
4461
  }
4403
4462
  }, {
4404
4463
  from: viewModel,
4405
- withIds: {
4464
+ pin: {
4406
4465
  main: { qry: '.mainView' } // find by class, auto-assign ID, cache
4407
4466
  }
4408
4467
  });
@@ -4416,12 +4475,12 @@ await assignFromAsync(document.body, {
4416
4475
  4. On subsequent calls, the cached `WeakRef.deref()` returns the element in ~10ns.
4417
4476
  5. If the WeakRef is collected (element was GC'd), falls back to `getElementById` (~10-100ns).
4418
4477
 
4419
- **`withIds` — stable references with auto-assigned IDs:**
4478
+ **`pin` — stable references with auto-assigned IDs:**
4420
4479
 
4421
4480
  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.
4422
4481
 
4423
4482
  ```TypeScript
4424
- withIds: {
4483
+ pin: {
4425
4484
  x: { qry: '.mainView' }, // querySelector on target, auto-assign ID
4426
4485
  y: 'existingId', // element already has an ID, cache via WeakRef
4427
4486
  z: { path: [0, 1], expect: 'input', fallback: true }, // child index path + auto-ID + validation
@@ -4451,14 +4510,14 @@ at: {
4451
4510
  - You want a clean DOM (no auto-generated `id` attributes)
4452
4511
  - You're rendering many items (1,000 rows × 2 refs = 2,000 fewer DOM attributes)
4453
4512
 
4454
- Both `withIds` and `at` use the same `#[x]` syntax on LHS and RHS keys.
4513
+ Both `pin` and `at` use the same `#[x]` syntax on LHS and RHS keys.
4455
4514
 
4456
4515
  **Validated paths with `expect` and `fallback`:**
4457
4516
 
4458
- Both `withIds` and `at` support the `{ path, expect, fallback }` form for dev-time validation:
4517
+ Both `pin` and `at` support the `{ path, expect, fallback }` form for dev-time validation:
4459
4518
 
4460
4519
  ```TypeScript
4461
- withIds: {
4520
+ pin: {
4462
4521
  nameInput: { path: [1], expect: 'input' }, // warn if [1] isn't an <input>
4463
4522
  label: { path: [0], expect: 'label', fallback: true } // warn + recover via querySelector
4464
4523
  }
@@ -4481,7 +4540,7 @@ assignFrom(document.body, {
4481
4540
  }, {
4482
4541
  from: viewModel,
4483
4542
  withMethods: ['querySelector'],
4484
- withIds: {
4543
+ pin: {
4485
4544
  form: { qry: '.registration-form' },
4486
4545
  header: 'page-header'
4487
4546
  }
@@ -4498,7 +4557,7 @@ await assignFromAsync(container, {
4498
4557
  }
4499
4558
  }, {
4500
4559
  from: router,
4501
- withIds: { outlet: { qry: '.router-outlet' } },
4560
+ pin: { outlet: { qry: '.router-outlet' } },
4502
4561
  protocols: { globalThis: k => globalThis[k] }
4503
4562
  });
4504
4563
  ```
@@ -4511,7 +4570,7 @@ await assignFromAsync(container, {
4511
4570
  | `getElementById(id)` | 100ns | 20ns | 10ns |
4512
4571
  | `Map<id, WeakRef>.deref()` | 12ns | 40ns | 10ns |
4513
4572
 
4514
- The `#[x]` + `withIds` pattern gives you the Map+WeakRef speed tier automatically.
4573
+ The `#[x]` + `pin` pattern gives you the Map+WeakRef speed tier automatically.
4515
4574
 
4516
4575
  **RHS references (`#[x]` on the value side):**
4517
4576
 
@@ -4524,7 +4583,7 @@ assignFrom(form, {
4524
4583
  '?.headerText': '#[info]?.dataset?.user', // → nested property access
4525
4584
  }, {
4526
4585
  from: {},
4527
- withIds: { nameInput: { qry: 'input' }, info: { qry: '.info' } },
4586
+ pin: { nameInput: { qry: 'input' }, info: { qry: '.info' } },
4528
4587
  withMethods: ['querySelector']
4529
4588
  });
4530
4589
  ```
package/assignFrom.js CHANGED
@@ -49,7 +49,8 @@ function resolveTernaryValue(value, source, options) {
49
49
  return getValue(value, source, {
50
50
  withMethods: options.withMethods,
51
51
  aka: options.aka,
52
- protocols: options.protocols
52
+ protocols: options.protocols,
53
+ root: options.root
53
54
  });
54
55
  }
55
56
  if (typeof value === 'string' && value.includes('://') && options.protocols) {
@@ -60,7 +61,8 @@ function resolveTernaryValue(value, source, options) {
60
61
  return getValue(value, source, {
61
62
  withMethods: options.withMethods,
62
63
  aka: options.aka,
63
- protocols: options.protocols
64
+ protocols: options.protocols,
65
+ root: options.root
64
66
  });
65
67
  }
66
68
  }
@@ -287,16 +289,16 @@ export function categorizeKeys(expandedPattern) {
287
289
  return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
288
290
  }
289
291
  /**
290
- * Merge withIds and at into a single lookup map for resolveIdVariable.
292
+ * Merge pin and at into a single lookup map for resolveIdVariable.
291
293
  */
292
294
  function getEffectiveIds(options) {
293
- if (!options.withIds && !options.at)
295
+ if (!options.pin && !options.at)
294
296
  return undefined;
295
- if (options.withIds && !options.at)
296
- return options.withIds;
297
- if (!options.withIds && options.at)
297
+ if (options.pin && !options.at)
298
+ return options.pin;
299
+ if (!options.pin && options.at)
298
300
  return options.at;
299
- return { ...options.withIds, ...options.at };
301
+ return { ...options.pin, ...options.at };
300
302
  }
301
303
  /**
302
304
  * Process #[x] normal keys synchronously.
@@ -305,6 +307,7 @@ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, option
305
307
  const ids = getEffectiveIds(options);
306
308
  if (!ids)
307
309
  return;
310
+ const { withMethods, aka, akaMethods, protocols, from } = options;
308
311
  for (const key of idRefNormalKeys) {
309
312
  const parsed = parseIdRef(key);
310
313
  if (!parsed)
@@ -314,11 +317,11 @@ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, option
314
317
  continue;
315
318
  const value = expandedPattern[key];
316
319
  if (parsed.remainingPath) {
317
- const resolvedValue = getValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
320
+ const resolvedValue = getValues({ __v: value }, from, { withMethods, aka, akaMethods, protocols, root: target });
318
321
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
319
322
  }
320
323
  else {
321
- const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
324
+ const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, protocols, root: target });
322
325
  if (!('__v' in resolvedValue)) {
323
326
  assignGingerly(el, resolvedValue, options);
324
327
  }
@@ -342,6 +345,7 @@ export function assignFrom(target, pattern, options, permissions) {
342
345
  const expandedPattern = expandSubstitutions(pattern, options);
343
346
  // Categorize keys
344
347
  const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
348
+ const resolveOptions = { ...options, root: target };
345
349
  // Process ?= ternary keys (sync)
346
350
  if (ternaryKeys.length > 0) {
347
351
  const ternaryResolved = {};
@@ -352,7 +356,7 @@ export function assignFrom(target, pattern, options, permissions) {
352
356
  const arr = expandedPattern[key];
353
357
  if (!Array.isArray(arr) || arr.length < 2)
354
358
  continue;
355
- const result = evaluateTernary(arr, options.from, options);
359
+ const result = evaluateTernary(arr, options.from, resolveOptions);
356
360
  if (result !== TERNARY_SKIP) {
357
361
  ternaryResolved[lhsPath] = result;
358
362
  }
@@ -364,7 +368,7 @@ export function assignFrom(target, pattern, options, permissions) {
364
368
  // Process normal keys via getValues (sync) + assignGingerly
365
369
  if (Object.keys(normalPattern).length > 0) {
366
370
  // Resolve #[x] references on RHS values before getValues
367
- if (options.withIds || options.at) {
371
+ if (options.pin || options.at) {
368
372
  const ids = getEffectiveIds(options);
369
373
  for (const key of Object.keys(normalPattern)) {
370
374
  const value = normalPattern[key];
@@ -379,7 +383,9 @@ export function assignFrom(target, pattern, options, permissions) {
379
383
  normalPattern[key] = getValue(remainingPath, el, {
380
384
  withMethods: options.withMethods,
381
385
  aka: options.aka,
382
- protocols: options.protocols
386
+ akaMethods: options.akaMethods,
387
+ protocols: options.protocols,
388
+ root: target
383
389
  });
384
390
  }
385
391
  else {
@@ -390,11 +396,7 @@ export function assignFrom(target, pattern, options, permissions) {
390
396
  }
391
397
  }
392
398
  }
393
- const resolved = getValues(normalPattern, options.from, {
394
- withMethods: options.withMethods,
395
- aka: options.aka,
396
- protocols: options.protocols
397
- });
399
+ const resolved = getValues(normalPattern, options.from, resolveOptions);
398
400
  handleSpreads(resolved);
399
401
  assignGingerly(target, resolved, options);
400
402
  }
@@ -409,7 +411,7 @@ export function assignFrom(target, pattern, options, permissions) {
409
411
  });
410
412
  }
411
413
  // Process #[x] handler keys — fire-and-forget (async)
412
- if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
414
+ if (idRefHandlerKeys.length > 0 && (options.pin || options.at)) {
413
415
  const ids = getEffectiveIds(options);
414
416
  import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
415
417
  for (const key of idRefHandlerKeys) {
package/assignFrom.ts CHANGED
@@ -10,14 +10,14 @@
10
10
  */
11
11
 
12
12
  import { getValues, getValue } from './getValues.js';
13
- import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
13
+ import assignGingerly from './assignGingerly.js';
14
14
  import { resolveIdVariable, parseIdRef } from './resolveIdRef.js';
15
15
  import { processInferredAssignments } from './inferredAssignments.js';
16
16
  import type { AssignPermissions } from './isAllowedImportPath.js';
17
+ import type { AssignFromOptions, AssignFromHandler, AssignFromHandlerConstructor } from './types/assign-gingerly/types.js';
17
18
 
18
- // Re-export types and interfaces for consumers
19
- export type { AssignFromOptions, AssignFromHandler, AssignFromHandlerConstructor } from './assignFromAsync.js';
20
- import type { AssignFromOptions } from './assignFromAsync.js';
19
+ // Re-export types for consumers
20
+ export type { AssignFromOptions, AssignFromHandler, AssignFromHandlerConstructor };
21
21
 
22
22
  /**
23
23
  * Supported substitution variables and their option keys.
@@ -54,12 +54,13 @@ export function parseTernaryCommand(key: string): string | null {
54
54
  * Resolve a single value — if it's a `?.` path string, resolve against source.
55
55
  * If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
56
56
  */
57
- function resolveTernaryValue(value: any, source: any, options: AssignFromOptions): any {
57
+ function resolveTernaryValue(value: any, source: any, options: AssignFromOptions | any): any {
58
58
  if (typeof value === 'string' && value.startsWith('?.')) {
59
59
  return getValue(value, source, {
60
60
  withMethods: options.withMethods,
61
61
  aka: options.aka,
62
- protocols: options.protocols
62
+ protocols: options.protocols,
63
+ root: options.root
63
64
  });
64
65
  }
65
66
  if (typeof value === 'string' && value.includes('://') && options.protocols) {
@@ -70,7 +71,8 @@ function resolveTernaryValue(value: any, source: any, options: AssignFromOptions
70
71
  return getValue(value, source, {
71
72
  withMethods: options.withMethods,
72
73
  aka: options.aka,
73
- protocols: options.protocols
74
+ protocols: options.protocols,
75
+ root: options.root
74
76
  });
75
77
  }
76
78
  }
@@ -299,13 +301,13 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
299
301
  }
300
302
 
301
303
  /**
302
- * Merge withIds and at into a single lookup map for resolveIdVariable.
304
+ * Merge pin and at into a single lookup map for resolveIdVariable.
303
305
  */
304
306
  function getEffectiveIds(options: AssignFromOptions): Record<string, any> | undefined {
305
- if (!options.withIds && !options.at) return undefined;
306
- if (options.withIds && !options.at) return options.withIds;
307
- if (!options.withIds && options.at) return options.at;
308
- return { ...options.withIds, ...options.at };
307
+ if (!options.pin && !options.at) return undefined;
308
+ if (options.pin && !options.at) return options.pin;
309
+ if (!options.pin && options.at) return options.at;
310
+ return { ...options.pin, ...options.at };
309
311
  }
310
312
 
311
313
  /**
@@ -320,6 +322,7 @@ function processIdRefNormalKeys(
320
322
  const ids = getEffectiveIds(options);
321
323
  if (!ids) return;
322
324
 
325
+ const { withMethods, aka, akaMethods, protocols, from } = options;
323
326
  for (const key of idRefNormalKeys) {
324
327
  const parsed = parseIdRef(key);
325
328
  if (!parsed) continue;
@@ -330,15 +333,15 @@ function processIdRefNormalKeys(
330
333
  const value = expandedPattern[key];
331
334
  if (parsed.remainingPath) {
332
335
  const resolvedValue = getValues(
333
- { __v: value }, options.from,
334
- { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
336
+ { __v: value }, from,
337
+ { withMethods, aka, akaMethods, protocols, root: target }
335
338
  );
336
339
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
337
340
  } else {
338
341
  const resolvedValue = getValues(
339
342
  typeof value === 'object' && value !== null ? value : { __v: value },
340
- options.from,
341
- { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
343
+ from,
344
+ { withMethods, aka, akaMethods, protocols, root: target }
342
345
  );
343
346
  if (!('__v' in resolvedValue)) {
344
347
  assignGingerly(el, resolvedValue, options);
@@ -371,6 +374,8 @@ export function assignFrom(
371
374
  // Categorize keys
372
375
  const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
373
376
 
377
+ const resolveOptions = { ...options, root: target } as any;
378
+
374
379
  // Process ?= ternary keys (sync)
375
380
  if (ternaryKeys.length > 0) {
376
381
  const ternaryResolved: Record<string, any> = {};
@@ -379,7 +384,7 @@ export function assignFrom(
379
384
  if (!lhsPath) continue;
380
385
  const arr = expandedPattern[key];
381
386
  if (!Array.isArray(arr) || arr.length < 2) continue;
382
- const result = evaluateTernary(arr, options.from, options);
387
+ const result = evaluateTernary(arr, options.from, resolveOptions);
383
388
  if (result !== TERNARY_SKIP) {
384
389
  ternaryResolved[lhsPath] = result;
385
390
  }
@@ -392,7 +397,7 @@ export function assignFrom(
392
397
  // Process normal keys via getValues (sync) + assignGingerly
393
398
  if (Object.keys(normalPattern).length > 0) {
394
399
  // Resolve #[x] references on RHS values before getValues
395
- if (options.withIds || options.at) {
400
+ if (options.pin || options.at) {
396
401
  const ids = getEffectiveIds(options)!;
397
402
  for (const key of Object.keys(normalPattern)) {
398
403
  const value = normalPattern[key];
@@ -407,7 +412,9 @@ export function assignFrom(
407
412
  normalPattern[key] = getValue(remainingPath, el, {
408
413
  withMethods: options.withMethods,
409
414
  aka: options.aka,
410
- protocols: options.protocols
415
+ akaMethods: options.akaMethods,
416
+ protocols: options.protocols,
417
+ root: target
411
418
  });
412
419
  } else {
413
420
  normalPattern[key] = el.id; // bare #[x] → ID string
@@ -418,11 +425,7 @@ export function assignFrom(
418
425
  }
419
426
  }
420
427
 
421
- const resolved = getValues(normalPattern, options.from, {
422
- withMethods: options.withMethods,
423
- aka: options.aka,
424
- protocols: options.protocols
425
- });
428
+ const resolved = getValues(normalPattern, options.from, resolveOptions);
426
429
 
427
430
  handleSpreads(resolved);
428
431
  assignGingerly(target, resolved, options);
@@ -441,7 +444,7 @@ export function assignFrom(
441
444
  }
442
445
 
443
446
  // Process #[x] handler keys — fire-and-forget (async)
444
- if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
447
+ if (idRefHandlerKeys.length > 0 && (options.pin || options.at)) {
445
448
  const ids = getEffectiveIds(options)!;
446
449
  import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
447
450
  for (const key of idRefHandlerKeys) {
@@ -34,16 +34,19 @@ export async function assignFromAsync(target, pattern, options, permissions) {
34
34
  const resolved = await resolveValues(normalPattern, options.from, {
35
35
  withMethods: options.withMethods,
36
36
  aka: options.aka,
37
- protocols: options.protocols
37
+ akaMethods: options.akaMethods,
38
+ protocols: options.protocols,
39
+ root: target
38
40
  });
39
41
  // Recursively handle "..." spread keys at all nesting levels
40
42
  handleSpreads(resolved);
41
43
  assignGingerly(target, resolved, options);
42
44
  }
43
45
  // Process #[x] normal keys — resolve element, then apply remaining path + value
44
- if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
45
- const ids = { ...options.withIds, ...options.at };
46
+ if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
47
+ const ids = { ...options.pin, ...options.at };
46
48
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
49
+ const { withMethods, aka, akaMethods, protocols, from } = options;
47
50
  for (const key of idRefNormalKeys) {
48
51
  const parsed = parseIdRef(key);
49
52
  if (!parsed)
@@ -54,13 +57,13 @@ export async function assignFromAsync(target, pattern, options, permissions) {
54
57
  const value = expandedPattern[key];
55
58
  if (parsed.remainingPath) {
56
59
  // Resolve the RHS value
57
- const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
60
+ const resolvedValue = await resolveValues({ __v: value }, from, { withMethods, aka, akaMethods, protocols, root: el });
58
61
  // Apply remaining path on the resolved element
59
62
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
60
63
  }
61
64
  else {
62
65
  // No remaining path — resolve and assign directly to the element
63
- const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
66
+ const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, protocols, root: el });
64
67
  if ('__v' in resolvedValue) {
65
68
  // Single value — can't assign to element root without a path
66
69
  }
@@ -76,8 +79,8 @@ export async function assignFromAsync(target, pattern, options, permissions) {
76
79
  await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
77
80
  }
78
81
  // Process #[x] handler keys — resolve element, then pass to handler processing
79
- if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
80
- const ids = { ...options.withIds, ...options.at };
82
+ if (idRefHandlerKeys.length > 0 && (options.pin || options.at)) {
83
+ const ids = { ...options.pin, ...options.at };
81
84
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
82
85
  _processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
83
86
  for (const key of idRefHandlerKeys) {