assign-gingerly 0.0.65 → 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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * handlers/addEventListener.ts — Event binding handler for += operator.
3
+ *
4
+ * Dynamically imported when assignGingerly/assignFrom detects an += with
5
+ * an object RHS containing an `on` property on a DOM Element LHS.
6
+ *
7
+ * Attaches an event listener that executes assign vectors on event fire.
8
+ */
9
+ import assignGingerly from '../assignGingerly.js';
10
+ /**
11
+ * WeakMap for dedup: Element → Map<key, AbortController>
12
+ */
13
+ const keyMap = new WeakMap();
14
+ /**
15
+ * Reserved keys that are not shorthand assign patterns.
16
+ */
17
+ const RESERVED_KEYS = new Set([
18
+ 'on', 'get', 'fromLHS', 'fromHost', 'fromEvent', 'fromTarget',
19
+ 'toTarget', 'toHost', 'toLHS', 'withOptions', 'toTargetOptions',
20
+ 'toHostOptions', 'toLHSOptions', 'dispatch', 'nudge'
21
+ ]);
22
+ /**
23
+ * Extract shorthand keys (implicit toHost) from a vector config.
24
+ * Any key not in RESERVED_KEYS that starts with '?.' or contains an operator is shorthand.
25
+ */
26
+ function extractShorthand(config) {
27
+ const shorthand = {};
28
+ let hasAny = false;
29
+ for (const key of Object.keys(config)) {
30
+ if (!RESERVED_KEYS.has(key)) {
31
+ shorthand[key] = config[key];
32
+ hasAny = true;
33
+ }
34
+ }
35
+ return hasAny ? shorthand : null;
36
+ }
37
+ /**
38
+ * Process a single AssignDispatchVector — execute all toTarget/toHost/toLHS assignments.
39
+ */
40
+ function processVector(vector, source, target, host, lhs, inheritedOptions, useAssignFrom) {
41
+ const destinations = [
42
+ { key: 'toTarget', dest: target },
43
+ { key: 'toHost', dest: host },
44
+ { key: 'toLHS', dest: lhs },
45
+ ];
46
+ for (const { key, dest } of destinations) {
47
+ const pattern = vector[key];
48
+ if (!pattern || Object.keys(pattern).length === 0)
49
+ continue;
50
+ if (useAssignFrom && source != null) {
51
+ // Dynamic import assignFrom on demand (fire-and-forget context, already async)
52
+ import('../assignFrom.js').then(({ assignFrom }) => {
53
+ assignFrom(dest, pattern, { from: source, ...inheritedOptions, ...vector.withOptions });
54
+ });
55
+ }
56
+ else {
57
+ assignGingerly(dest, pattern, inheritedOptions);
58
+ }
59
+ }
60
+ // Handle shorthand (implicit toHost)
61
+ const shorthand = extractShorthand(vector);
62
+ if (shorthand) {
63
+ if (useAssignFrom && source != null) {
64
+ import('../assignFrom.js').then(({ assignFrom }) => {
65
+ assignFrom(host, shorthand, { from: source, ...inheritedOptions, ...vector.withOptions });
66
+ });
67
+ }
68
+ else {
69
+ assignGingerly(host, shorthand, inheritedOptions);
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Attach an event listener based on AddEventListenerConfig.
75
+ *
76
+ * @param lhs - The DOM element to attach the listener to
77
+ * @param config - The event handler configuration
78
+ * @param target - The assignFrom target (first arg)
79
+ * @param host - The options.from (source/view model)
80
+ * @param inheritedOptions - Parent assignFrom options (withMethods, aka, etc.)
81
+ */
82
+ export function attachEventListener(lhs, config, target, host, inheritedOptions) {
83
+ const { on: eventName, get: getConfig, fromLHS, fromHost, fromTarget, fromEvent, dispatch } = config;
84
+ // Resolve get config values
85
+ const { abortController, key, nudge, options: listenerOptions, stopPropagation, preventDefault, dispatch: getDispatch } = getConfig ?? {};
86
+ // Handle dedup via key
87
+ let controller;
88
+ if (key) {
89
+ let elMap = keyMap.get(lhs);
90
+ if (!elMap) {
91
+ elMap = new Map();
92
+ keyMap.set(lhs, elMap);
93
+ }
94
+ // Abort previous listener with same key
95
+ const prev = elMap.get(key);
96
+ if (prev)
97
+ prev.abort();
98
+ // Create new controller for this key
99
+ controller = new AbortController();
100
+ elMap.set(key, controller);
101
+ }
102
+ else if (abortController instanceof AbortController) {
103
+ controller = abortController;
104
+ }
105
+ else {
106
+ controller = new AbortController();
107
+ }
108
+ // Attach the listener
109
+ lhs.addEventListener(eventName, (event) => {
110
+ if (stopPropagation)
111
+ event.stopPropagation();
112
+ if (preventDefault)
113
+ event.preventDefault();
114
+ // Static assignments (no from) — top-level toTarget/toHost/toLHS + shorthand
115
+ processVector(config, null, target, host, lhs, inheritedOptions, false);
116
+ // fromLHS assignments
117
+ if (fromLHS) {
118
+ processVector(fromLHS, lhs, target, host, lhs, inheritedOptions, true);
119
+ }
120
+ // fromHost assignments
121
+ if (fromHost) {
122
+ processVector(fromHost, host, target, host, lhs, inheritedOptions, true);
123
+ }
124
+ // fromTarget assignments
125
+ if (fromTarget) {
126
+ processVector(fromTarget, target, target, host, lhs, inheritedOptions, true);
127
+ }
128
+ // fromEvent assignments
129
+ if (fromEvent) {
130
+ processVector(fromEvent, event, target, host, lhs, inheritedOptions, true);
131
+ }
132
+ // Dispatch custom event if configured
133
+ const dispatchConfig = dispatch || getDispatch;
134
+ if (dispatchConfig && dispatchConfig.type) {
135
+ const EventCtr = (typeof dispatchConfig.eventCtr === 'function'
136
+ ? dispatchConfig.eventCtr
137
+ : CustomEvent);
138
+ const evt = new EventCtr(dispatchConfig.type, {
139
+ detail: dispatchConfig.detail,
140
+ bubbles: dispatchConfig.bubbles ?? true,
141
+ cancelable: dispatchConfig.cancelable ?? false,
142
+ composed: dispatchConfig.composed ?? true,
143
+ });
144
+ lhs.dispatchEvent(evt);
145
+ }
146
+ }, { signal: controller.signal, ...listenerOptions });
147
+ // Nudge: remove disabled, add interaction hints
148
+ if (nudge) {
149
+ import('./nudge.js').then(({ nudge: nudgeFn }) => {
150
+ nudgeFn(lhs);
151
+ });
152
+ }
153
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * handlers/addEventListener.ts — Event binding handler for += operator.
3
+ *
4
+ * Dynamically imported when assignGingerly/assignFrom detects an += with
5
+ * an object RHS containing an `on` property on a DOM Element LHS.
6
+ *
7
+ * Attaches an event listener that executes assign vectors on event fire.
8
+ */
9
+
10
+ import assignGingerly from '../assignGingerly.js';
11
+ import type { AddEventListenerConfig, AssignDispatchVector } from '../types/assign-gingerly/types.js';
12
+
13
+ /**
14
+ * WeakMap for dedup: Element → Map<key, AbortController>
15
+ */
16
+ const keyMap = new WeakMap<Element, Map<string, AbortController>>();
17
+
18
+ /**
19
+ * Reserved keys that are not shorthand assign patterns.
20
+ */
21
+ const RESERVED_KEYS = new Set([
22
+ 'on', 'get', 'fromLHS', 'fromHost', 'fromEvent', 'fromTarget',
23
+ 'toTarget', 'toHost', 'toLHS', 'withOptions', 'toTargetOptions',
24
+ 'toHostOptions', 'toLHSOptions', 'dispatch', 'nudge'
25
+ ]);
26
+
27
+ /**
28
+ * Extract shorthand keys (implicit toHost) from a vector config.
29
+ * Any key not in RESERVED_KEYS that starts with '?.' or contains an operator is shorthand.
30
+ */
31
+ function extractShorthand(config: Record<string, any>): Record<string, any> | null {
32
+ const shorthand: Record<string, any> = {};
33
+ let hasAny = false;
34
+ for (const key of Object.keys(config)) {
35
+ if (!RESERVED_KEYS.has(key)) {
36
+ shorthand[key] = config[key];
37
+ hasAny = true;
38
+ }
39
+ }
40
+ return hasAny ? shorthand : null;
41
+ }
42
+
43
+ /**
44
+ * Process a single AssignDispatchVector — execute all toTarget/toHost/toLHS assignments.
45
+ */
46
+ function processVector(
47
+ vector: AssignDispatchVector,
48
+ source: any,
49
+ target: any,
50
+ host: any,
51
+ lhs: Element,
52
+ inheritedOptions: any,
53
+ useAssignFrom: boolean
54
+ ): void {
55
+ const destinations = [
56
+ { key: 'toTarget' as const, dest: target },
57
+ { key: 'toHost' as const, dest: host },
58
+ { key: 'toLHS' as const, dest: lhs },
59
+ ];
60
+
61
+ for (const { key, dest } of destinations) {
62
+ const pattern = vector[key];
63
+ if (!pattern || Object.keys(pattern).length === 0) continue;
64
+ if (useAssignFrom && source != null) {
65
+ // Dynamic import assignFrom on demand (fire-and-forget context, already async)
66
+ import('../assignFrom.js').then(({ assignFrom }) => {
67
+ assignFrom(dest, pattern, { from: source, ...inheritedOptions, ...vector.withOptions });
68
+ });
69
+ } else {
70
+ assignGingerly(dest, pattern, inheritedOptions);
71
+ }
72
+ }
73
+
74
+ // Handle shorthand (implicit toHost)
75
+ const shorthand = extractShorthand(vector as Record<string, any>);
76
+ if (shorthand) {
77
+ if (useAssignFrom && source != null) {
78
+ import('../assignFrom.js').then(({ assignFrom }) => {
79
+ assignFrom(host, shorthand, { from: source, ...inheritedOptions, ...vector.withOptions });
80
+ });
81
+ } else {
82
+ assignGingerly(host, shorthand, inheritedOptions);
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Attach an event listener based on AddEventListenerConfig.
89
+ *
90
+ * @param lhs - The DOM element to attach the listener to
91
+ * @param config - The event handler configuration
92
+ * @param target - The assignFrom target (first arg)
93
+ * @param host - The options.from (source/view model)
94
+ * @param inheritedOptions - Parent assignFrom options (withMethods, aka, etc.)
95
+ */
96
+ export function attachEventListener(
97
+ lhs: Element,
98
+ config: AddEventListenerConfig,
99
+ target: any,
100
+ host: any,
101
+ inheritedOptions: any
102
+ ): void {
103
+ const { on: eventName, get: getConfig, fromLHS, fromHost, fromTarget, fromEvent, dispatch } = config;
104
+
105
+ // Resolve get config values
106
+ const { abortController, key, nudge, options: listenerOptions, stopPropagation, preventDefault, dispatch: getDispatch } = getConfig ?? {} as any;
107
+
108
+ // Handle dedup via key
109
+ let controller: AbortController;
110
+ if (key) {
111
+ let elMap = keyMap.get(lhs);
112
+ if (!elMap) {
113
+ elMap = new Map();
114
+ keyMap.set(lhs, elMap);
115
+ }
116
+ // Abort previous listener with same key
117
+ const prev = elMap.get(key);
118
+ if (prev) prev.abort();
119
+ // Create new controller for this key
120
+ controller = new AbortController();
121
+ elMap.set(key, controller);
122
+ } else if (abortController instanceof AbortController) {
123
+ controller = abortController;
124
+ } else {
125
+ controller = new AbortController();
126
+ }
127
+
128
+ // Attach the listener
129
+ lhs.addEventListener(eventName, (event: Event) => {
130
+ if (stopPropagation) event.stopPropagation();
131
+ if (preventDefault) event.preventDefault();
132
+
133
+ // Static assignments (no from) — top-level toTarget/toHost/toLHS + shorthand
134
+ processVector(config, null, target, host, lhs, inheritedOptions, false);
135
+
136
+ // fromLHS assignments
137
+ if (fromLHS) {
138
+ processVector(fromLHS, lhs, target, host, lhs, inheritedOptions, true);
139
+ }
140
+
141
+ // fromHost assignments
142
+ if (fromHost) {
143
+ processVector(fromHost, host, target, host, lhs, inheritedOptions, true);
144
+ }
145
+
146
+ // fromTarget assignments
147
+ if (fromTarget) {
148
+ processVector(fromTarget, target, target, host, lhs, inheritedOptions, true);
149
+ }
150
+
151
+ // fromEvent assignments
152
+ if (fromEvent) {
153
+ processVector(fromEvent, event, target, host, lhs, inheritedOptions, true);
154
+ }
155
+
156
+ // Dispatch custom event if configured
157
+ const dispatchConfig = dispatch || getDispatch;
158
+ if (dispatchConfig && dispatchConfig.type) {
159
+ const EventCtr = (typeof dispatchConfig.eventCtr === 'function'
160
+ ? dispatchConfig.eventCtr
161
+ : CustomEvent) as typeof CustomEvent;
162
+ const evt = new EventCtr(dispatchConfig.type, {
163
+ detail: dispatchConfig.detail,
164
+ bubbles: dispatchConfig.bubbles ?? true,
165
+ cancelable: dispatchConfig.cancelable ?? false,
166
+ composed: dispatchConfig.composed ?? true,
167
+ });
168
+ lhs.dispatchEvent(evt);
169
+ }
170
+ }, { signal: controller.signal, ...listenerOptions });
171
+
172
+ // Nudge: remove disabled, add interaction hints
173
+ if (nudge) {
174
+ import('./nudge.js').then(({ nudge: nudgeFn }) => {
175
+ nudgeFn(lhs);
176
+ });
177
+ }
178
+ }
@@ -217,10 +217,10 @@ export class LazyLoadHandler {
217
217
  const len = Math.min(elements.length, assign.configs.length);
218
218
  for (let j = 0; j < len; j++) {
219
219
  const cfg = assign.configs[j];
220
- assignFrom(elements[j], cfg.assignToFragment ?? {}, { from, ...cfg.withOptions });
220
+ assignFrom(elements[j], cfg.toClone ?? {}, { from, ...cfg.withOptions });
221
221
  }
222
- } else if (assign.assignToFragment) {
223
- assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
222
+ } else if (assign.toClone) {
223
+ assignFrom(elements[0], assign.toClone, { from, ...assign.withOptions });
224
224
  }
225
225
  }
226
226
  async onCloneInserted(nodes, lhsTarget, resolvedParams) {
@@ -317,10 +317,10 @@ export class LazyLoadHandler implements AssignFromHandler {
317
317
  const len = Math.min(elements.length, assign.configs.length);
318
318
  for (let j = 0; j < len; j++) {
319
319
  const cfg = assign.configs[j];
320
- assignFrom(elements[j], cfg.assignToFragment ?? {}, { from, ...cfg.withOptions });
320
+ assignFrom(elements[j], cfg.toClone ?? {}, { from, ...cfg.withOptions });
321
321
  }
322
- } else if (assign.assignToFragment) {
323
- assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
322
+ } else if (assign.toClone) {
323
+ assignFrom(elements[0], assign.toClone, { from, ...assign.withOptions });
324
324
  }
325
325
  }
326
326
 
@@ -17,7 +17,7 @@
17
17
  * instantiate: 'globalThis://country-ranking',
18
18
  * },
19
19
  * fromEachItem: {
20
- * assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
20
+ * '?.querySelector?.tr?.ish': '?.',
21
21
  * withOptions: { withMethods: ['querySelector'], infer: true },
22
22
  * resolve: { key: '?.rank' }
23
23
  * }
@@ -28,6 +28,24 @@ import { findMarkers, createMarkers } from '../markerUtils.js';
28
28
  import { resolveValue } from '../resolveValues.js';
29
29
  import { assignFrom } from '../assignFrom.js';
30
30
  import { processInferredAssignments } from '../inferredAssignments.js';
31
+ /**
32
+ * Reserved keys in fromEachItem config — not treated as shorthand patterns.
33
+ */
34
+ const RESERVED_KEYS = new Set(['toClone', 'withOptions', 'resolve', 'get', 'configs']);
35
+ /**
36
+ * Extract shorthand patterns from fromEachItem config (non-reserved keys → toClone).
37
+ */
38
+ function extractToClone(config) {
39
+ if (!config)
40
+ return {};
41
+ const result = {};
42
+ for (const key of Object.keys(config)) {
43
+ if (!RESERVED_KEYS.has(key)) {
44
+ result[key] = config[key];
45
+ }
46
+ }
47
+ return result;
48
+ }
31
49
  const listStateMap = new WeakMap();
32
50
  /**
33
51
  * ManageTemplateListHandler — clones a template per iterable item with keyed reconciliation.
@@ -46,20 +64,23 @@ export class ManageTemplateListHandler {
46
64
  if (!items || typeof items[Symbol.iterator] !== 'function') {
47
65
  return; // Nothing to iterate
48
66
  }
49
- const fromEachItem = this.config.fromEachItem;
67
+ const { fromEachItem, fromHost: fromHostConfig, fromTarget: fromTargetConfig } = this.config;
50
68
  const configs = fromEachItem?.configs; // Array form for multi-element templates
51
- const assignToFragment = fromEachItem?.assignToFragment ?? {};
52
69
  const withOptions = fromEachItem?.withOptions ?? {};
53
- const perItemResolve = fromEachItem?.resolve ?? {};
54
- const keyPath = perItemResolve.key; // e.g., '?.rank'
55
- // fromSource config assigns from the outer `from` (parent VM) to each clone
56
- const fromSource = this.config.fromSource;
57
- const sourceAssignToFragment = fromSource?.assignToFragment;
58
- const sourceWithOptions = fromSource?.withOptions ?? {};
59
- // Detect fast path: no assignToFragment patterns, just infer (only for non-configs mode)
60
- const hasAssignPatterns = !configs && Object.keys(assignToFragment).length > 0;
70
+ const perItemResolve = fromEachItem?.resolve ?? fromEachItem?.get ?? {};
71
+ const { key: keyPath } = perItemResolve; // e.g., '?.rank'
72
+ // Resolve toClone patterns: explicit `toClone` key, or shorthand (non-reserved keys)
73
+ const toClone = fromEachItem?.toClone ?? extractToClone(fromEachItem);
74
+ // fromHost config — assigns from options.from (host/VM) to each clone
75
+ const hostToClone = fromHostConfig ? (fromHostConfig.toClone ?? extractToClone(fromHostConfig)) : undefined;
76
+ const hostWithOptions = fromHostConfig?.withOptions ?? {};
77
+ // fromTarget config assigns from the target element to each clone
78
+ const targetToClone = fromTargetConfig ? (fromTargetConfig.toClone ?? extractToClone(fromTargetConfig)) : undefined;
79
+ const targetWithOptions = fromTargetConfig?.withOptions ?? {};
80
+ // Detect fast path: no toClone patterns, just infer (only for non-configs mode)
81
+ const hasAssignPatterns = !configs && Object.keys(toClone).length > 0;
61
82
  const inferredConfig = !configs && withOptions.infer;
62
- const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
83
+ const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !hostToClone && !targetToClone;
63
84
  const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
64
85
  // Find or create markers
65
86
  let [startMarker, endMarker] = findMarkers(lhsTarget, name);
@@ -122,7 +143,7 @@ export class ManageTemplateListHandler {
122
143
  const len = Math.min(elements.length, configs.length);
123
144
  for (let j = 0; j < len; j++) {
124
145
  const cfg = configs[j];
125
- assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
146
+ assignFrom(elements[j], cfg.toClone ?? {}, { from: item, ...cfg.withOptions });
126
147
  }
127
148
  }
128
149
  else {
@@ -132,10 +153,13 @@ export class ManageTemplateListHandler {
132
153
  processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
133
154
  }
134
155
  else {
135
- assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
156
+ assignFrom(rootEl, toClone, { from: item, ...withOptions });
136
157
  }
137
- if (sourceAssignToFragment && options?.from) {
138
- assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
158
+ if (hostToClone && options?.from) {
159
+ assignFrom(rootEl, hostToClone, { from: options.from, ...hostWithOptions });
160
+ }
161
+ if (targetToClone) {
162
+ assignFrom(rootEl, targetToClone, { from: lhsTarget, ...targetWithOptions });
139
163
  }
140
164
  }
141
165
  }
@@ -168,7 +192,7 @@ export class ManageTemplateListHandler {
168
192
  const len = Math.min(elements.length, configs.length);
169
193
  for (let j = 0; j < len; j++) {
170
194
  const cfg = configs[j];
171
- assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
195
+ assignFrom(elements[j], cfg.toClone ?? {}, { from: item, ...cfg.withOptions });
172
196
  }
173
197
  }
174
198
  else {
@@ -180,10 +204,13 @@ export class ManageTemplateListHandler {
180
204
  processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
181
205
  }
182
206
  else {
183
- assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
207
+ assignFrom(rootEl, toClone, { from: item, ...withOptions });
208
+ }
209
+ if (hostToClone && options?.from) {
210
+ assignFrom(rootEl, hostToClone, { from: options.from, ...hostWithOptions });
184
211
  }
185
- if (sourceAssignToFragment && options?.from) {
186
- assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
212
+ if (targetToClone) {
213
+ assignFrom(rootEl, targetToClone, { from: lhsTarget, ...targetWithOptions });
187
214
  }
188
215
  }
189
216
  }
@@ -17,7 +17,7 @@
17
17
  * instantiate: 'globalThis://country-ranking',
18
18
  * },
19
19
  * fromEachItem: {
20
- * assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
20
+ * '?.querySelector?.tr?.ish': '?.',
21
21
  * withOptions: { withMethods: ['querySelector'], infer: true },
22
22
  * resolve: { key: '?.rank' }
23
23
  * }
@@ -32,6 +32,25 @@ import { resolveValue } from '../resolveValues.js';
32
32
  import { assignFrom } from '../assignFrom.js';
33
33
  import { processInferredAssignments } from '../inferredAssignments.js';
34
34
 
35
+ /**
36
+ * Reserved keys in fromEachItem config — not treated as shorthand patterns.
37
+ */
38
+ const RESERVED_KEYS = new Set(['toClone', 'withOptions', 'resolve', 'get', 'configs']);
39
+
40
+ /**
41
+ * Extract shorthand patterns from fromEachItem config (non-reserved keys → toClone).
42
+ */
43
+ function extractToClone(config: any): Record<string, any> {
44
+ if (!config) return {};
45
+ const result: Record<string, any> = {};
46
+ for (const key of Object.keys(config)) {
47
+ if (!RESERVED_KEYS.has(key)) {
48
+ result[key] = config[key];
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+
35
54
  /**
36
55
  * State stored per list instance (keyed by start marker).
37
56
  * Tracks the mapping of keys to their cloned DOM nodes.
@@ -74,22 +93,27 @@ export class ManageTemplateListHandler implements AssignFromHandler {
74
93
  return; // Nothing to iterate
75
94
  }
76
95
 
77
- const fromEachItem = this.config.fromEachItem;
96
+ const { fromEachItem, fromHost: fromHostConfig, fromTarget: fromTargetConfig } = this.config;
78
97
  const configs = fromEachItem?.configs; // Array form for multi-element templates
79
- const assignToFragment = fromEachItem?.assignToFragment ?? {};
80
98
  const withOptions = fromEachItem?.withOptions ?? {};
81
- const perItemResolve = fromEachItem?.resolve ?? {};
82
- const keyPath = perItemResolve.key; // e.g., '?.rank'
99
+ const perItemResolve = fromEachItem?.resolve ?? fromEachItem?.get ?? {};
100
+ const { key: keyPath } = perItemResolve; // e.g., '?.rank'
101
+
102
+ // Resolve toClone patterns: explicit `toClone` key, or shorthand (non-reserved keys)
103
+ const toClone = fromEachItem?.toClone ?? extractToClone(fromEachItem);
83
104
 
84
- // fromSource config — assigns from the outer `from` (parent VM) to each clone
85
- const fromSource = this.config.fromSource;
86
- const sourceAssignToFragment = fromSource?.assignToFragment;
87
- const sourceWithOptions = fromSource?.withOptions ?? {};
105
+ // fromHost config — assigns from options.from (host/VM) to each clone
106
+ const hostToClone = fromHostConfig ? (fromHostConfig.toClone ?? extractToClone(fromHostConfig)) : undefined;
107
+ const hostWithOptions = fromHostConfig?.withOptions ?? {};
88
108
 
89
- // Detect fast path: no assignToFragment patterns, just infer (only for non-configs mode)
90
- const hasAssignPatterns = !configs && Object.keys(assignToFragment).length > 0;
109
+ // fromTarget config assigns from the target element to each clone
110
+ const targetToClone = fromTargetConfig ? (fromTargetConfig.toClone ?? extractToClone(fromTargetConfig)) : undefined;
111
+ const targetWithOptions = fromTargetConfig?.withOptions ?? {};
112
+
113
+ // Detect fast path: no toClone patterns, just infer (only for non-configs mode)
114
+ const hasAssignPatterns = !configs && Object.keys(toClone).length > 0;
91
115
  const inferredConfig = !configs && withOptions.infer;
92
- const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
116
+ const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !hostToClone && !targetToClone;
93
117
 
94
118
  const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
95
119
 
@@ -159,7 +183,7 @@ export class ManageTemplateListHandler implements AssignFromHandler {
159
183
  const len = Math.min(elements.length, configs.length);
160
184
  for (let j = 0; j < len; j++) {
161
185
  const cfg = configs[j];
162
- assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
186
+ assignFrom(elements[j], cfg.toClone ?? {}, { from: item, ...cfg.withOptions });
163
187
  }
164
188
  } else {
165
189
  const rootEl = existingNodes.find(n => n instanceof Element) as Element | undefined;
@@ -167,10 +191,13 @@ export class ManageTemplateListHandler implements AssignFromHandler {
167
191
  if (processInferred) {
168
192
  processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
169
193
  } else {
170
- assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
194
+ assignFrom(rootEl, toClone, { from: item, ...withOptions });
195
+ }
196
+ if (hostToClone && options?.from) {
197
+ assignFrom(rootEl, hostToClone, { from: options.from, ...hostWithOptions });
171
198
  }
172
- if (sourceAssignToFragment && options?.from) {
173
- assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
199
+ if (targetToClone) {
200
+ assignFrom(rootEl, targetToClone, { from: lhsTarget, ...targetWithOptions });
174
201
  }
175
202
  }
176
203
  }
@@ -201,7 +228,7 @@ export class ManageTemplateListHandler implements AssignFromHandler {
201
228
  const len = Math.min(elements.length, configs.length);
202
229
  for (let j = 0; j < len; j++) {
203
230
  const cfg = configs[j];
204
- assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
231
+ assignFrom(elements[j], cfg.toClone ?? {}, { from: item, ...cfg.withOptions });
205
232
  }
206
233
  } else {
207
234
  const rootEl = clonedNodes.find(n => n instanceof Element) as Element | undefined;
@@ -212,11 +239,14 @@ export class ManageTemplateListHandler implements AssignFromHandler {
212
239
  if (processInferred) {
213
240
  processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
214
241
  } else {
215
- assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
242
+ assignFrom(rootEl, toClone, { from: item, ...withOptions });
216
243
  }
217
244
 
218
- if (sourceAssignToFragment && options?.from) {
219
- assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
245
+ if (hostToClone && options?.from) {
246
+ assignFrom(rootEl, hostToClone, { from: options.from, ...hostWithOptions });
247
+ }
248
+ if (targetToClone) {
249
+ assignFrom(rootEl, targetToClone, { from: lhsTarget, ...targetWithOptions });
220
250
  }
221
251
  }
222
252
  }