assign-gingerly 0.0.59 → 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.
@@ -51,6 +51,17 @@ export interface AssignFromOptions extends IAssignGingerlyOptions {
51
51
  */
52
52
  withIds?: Record<string, string | { qry: string }>;
53
53
 
54
+ /**
55
+ * Positional element references for use with `#[varName]` syntax.
56
+ * Resolves elements by child index path — no IDs assigned, no caching.
57
+ *
58
+ * - Array value: child index path (e.g., [0, 1] = target.children[0].children[1])
59
+ * - Object value: { path: [...], expect?: 'selector', fallback?: true }
60
+ * expect: validates via element.matches(), logs correction if wrong
61
+ * fallback: on mismatch, recovers via querySelector(expect)
62
+ */
63
+ at?: Record<string, number[] | { path: number[]; expect?: string; fallback?: boolean }>;
64
+
54
65
  /**
55
66
  * Handler implementations scoped to this call.
56
67
  * Key: the `do` name referenced in handler configs.
@@ -79,13 +90,16 @@ export interface AssignFromOptions extends IAssignGingerlyOptions {
79
90
  * matched element (textContent, value, checked, dateTime, ish, etc.).
80
91
  *
81
92
  * @example
82
- * inferredAssignments: {
93
+ * infer: {
83
94
  * byItemprop: ['user', 'name', 'email'], // or true for all source keys
84
95
  * beVigilant: true, // watch for new matching elements (requires signal)
85
96
  * }
86
97
  */
87
- inferredAssignments?: {
98
+ infer?: {
88
99
  byItemprop?: string[] | true;
100
+ '|'?: string[] | true;
101
+ byName?: string[] | true | { props: string[] | true; outside: string };
102
+ '@'?: string[] | true | { props: string[] | true; outside: string };
89
103
  /** Watch for new matching elements via MutationObserver. Requires options.signal for cleanup. */
90
104
  beVigilant?: boolean;
91
105
  };
@@ -150,13 +164,14 @@ export async function assignFromAsync(
150
164
  }
151
165
 
152
166
  // Process #[x] normal keys — resolve element, then apply remaining path + value
153
- if (idRefNormalKeys.length > 0 && options.withIds) {
167
+ if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
168
+ const ids = { ...options.withIds, ...options.at };
154
169
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
155
170
  for (const key of idRefNormalKeys) {
156
171
  const parsed = parseIdRef(key);
157
172
  if (!parsed) continue;
158
173
 
159
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
174
+ const el = resolveIdVariable(parsed.varName, target, ids);
160
175
  if (!el) continue;
161
176
 
162
177
  const value = expandedPattern[key];
@@ -190,7 +205,8 @@ export async function assignFromAsync(
190
205
  await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
191
206
  }
192
207
  // Process #[x] handler keys — resolve element, then pass to handler processing
193
- if (idRefHandlerKeys.length > 0 && options.withIds) {
208
+ if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
209
+ const ids = { ...options.withIds, ...options.at };
194
210
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
195
211
  _processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
196
212
 
@@ -198,7 +214,7 @@ export async function assignFromAsync(
198
214
  const parsed = parseIdRef(key);
199
215
  if (!parsed) continue;
200
216
 
201
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
217
+ const el = resolveIdVariable(parsed.varName, target, ids);
202
218
  if (!el) continue;
203
219
 
204
220
  // Build a synthetic key for processHandlerCommands:
@@ -216,17 +232,17 @@ export async function assignFromAsync(
216
232
  }
217
233
 
218
234
  // Process inferred assignments — dynamically imported only when option is present
219
- if (options.inferredAssignments) {
235
+ if (options.infer) {
220
236
  const { processInferredAssignments } = await import('./inferredAssignments.js');
221
- await processInferredAssignments(target, options.from, options.inferredAssignments);
237
+ await processInferredAssignments(target, options.from, options.infer);
222
238
 
223
239
  // Set up MutationObserver for new matching elements if beVigilant
224
- if (options.inferredAssignments.beVigilant) {
240
+ if (options.infer.beVigilant) {
225
241
  if (!options.signal) {
226
- throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
242
+ throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
227
243
  }
228
244
  const { setupVigilantObserver } = await import('./beVigilant.js');
229
- setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
245
+ setupVigilantObserver(target, options.from, options.infer, options.signal);
230
246
  }
231
247
  }
232
248
 
package/assignGingerly.js CHANGED
@@ -226,6 +226,21 @@ function parseDeleteCommand(key) {
226
226
  }
227
227
  return key.substring(0, key.length - 3); // Remove ' -=' suffix
228
228
  }
229
+ /**
230
+ * Helper function to check if a key represents a Y= merge command
231
+ */
232
+ function isMergeCommand(key) {
233
+ return key.endsWith(' Y=');
234
+ }
235
+ /**
236
+ * Helper function to parse a Y= merge command and extract the path
237
+ */
238
+ function parseMergeCommand(key) {
239
+ if (!isMergeCommand(key)) {
240
+ return null;
241
+ }
242
+ return key.substring(0, key.length - 3); // Remove ' Y=' suffix
243
+ }
229
244
  /**
230
245
  * Helper function to parse a path string with ?. notation
231
246
  * Always splits on '?.' delimiter, preserving dots that are part of values
@@ -769,6 +784,41 @@ export function assignGingerly(target, source, options, permissions) {
769
784
  }
770
785
  continue;
771
786
  }
787
+ // Handle Y= merge commands (recursive assignGingerly into sub-object)
788
+ if (isMergeCommand(key)) {
789
+ const path = parseMergeCommand(key);
790
+ if (path) {
791
+ // Navigate to the target sub-object
792
+ let mergeTarget;
793
+ if (isNestedPath(path)) {
794
+ if (withMethodsSet) {
795
+ const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
796
+ mergeTarget = result.target[result.lastKey];
797
+ }
798
+ else {
799
+ const pathParts = parsePath(path);
800
+ mergeTarget = target;
801
+ for (const part of pathParts) {
802
+ if (mergeTarget && typeof mergeTarget === 'object' && part in mergeTarget) {
803
+ mergeTarget = mergeTarget[part];
804
+ }
805
+ else {
806
+ mergeTarget = undefined;
807
+ break;
808
+ }
809
+ }
810
+ }
811
+ }
812
+ else {
813
+ mergeTarget = target[path];
814
+ }
815
+ // Recursively merge if target is a valid object
816
+ if (mergeTarget && typeof mergeTarget === 'object') {
817
+ assignGingerly(mergeTarget, value, options, permissions);
818
+ }
819
+ }
820
+ continue;
821
+ }
772
822
  if (isNestedPath(key)) {
773
823
  const pathParts = parsePath(key);
774
824
  // Check if path contains @each or @eachTime (forEach)
package/assignGingerly.ts CHANGED
@@ -388,6 +388,23 @@ function parseDeleteCommand(key: string): string | null {
388
388
  return key.substring(0, key.length - 3); // Remove ' -=' suffix
389
389
  }
390
390
 
391
+ /**
392
+ * Helper function to check if a key represents a Y= merge command
393
+ */
394
+ function isMergeCommand(key: string): boolean {
395
+ return key.endsWith(' Y=');
396
+ }
397
+
398
+ /**
399
+ * Helper function to parse a Y= merge command and extract the path
400
+ */
401
+ function parseMergeCommand(key: string): string | null {
402
+ if (!isMergeCommand(key)) {
403
+ return null;
404
+ }
405
+ return key.substring(0, key.length - 3); // Remove ' Y=' suffix
406
+ }
407
+
391
408
  /**
392
409
  * Helper function to parse a path string with ?. notation
393
410
  * Always splits on '?.' delimiter, preserving dots that are part of values
@@ -968,6 +985,40 @@ export function assignGingerly(
968
985
  continue;
969
986
  }
970
987
 
988
+ // Handle Y= merge commands (recursive assignGingerly into sub-object)
989
+ if (isMergeCommand(key)) {
990
+ const path = parseMergeCommand(key);
991
+ if (path) {
992
+ // Navigate to the target sub-object
993
+ let mergeTarget: any;
994
+ if (isNestedPath(path)) {
995
+ if (withMethodsSet) {
996
+ const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
997
+ mergeTarget = result.target[result.lastKey];
998
+ } else {
999
+ const pathParts = parsePath(path);
1000
+ mergeTarget = target;
1001
+ for (const part of pathParts) {
1002
+ if (mergeTarget && typeof mergeTarget === 'object' && part in mergeTarget) {
1003
+ mergeTarget = mergeTarget[part];
1004
+ } else {
1005
+ mergeTarget = undefined;
1006
+ break;
1007
+ }
1008
+ }
1009
+ }
1010
+ } else {
1011
+ mergeTarget = target[path];
1012
+ }
1013
+
1014
+ // Recursively merge if target is a valid object
1015
+ if (mergeTarget && typeof mergeTarget === 'object') {
1016
+ assignGingerly(mergeTarget, value, options, permissions);
1017
+ }
1018
+ }
1019
+ continue;
1020
+ }
1021
+
971
1022
  if (isNestedPath(key)) {
972
1023
  const pathParts = parsePath(key);
973
1024
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * builtInEmoji.js — Predefined emoji aliases for built-in handlers.
3
+ *
4
+ * Import and spread into the `handlers` option for concise handler configs:
5
+ *
6
+ * @example
7
+ * import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
8
+ *
9
+ * assignFrom(target, {
10
+ * '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
11
+ * }, { from: vm, handlers: builtInEmoji });
12
+ */
13
+
14
+ /**
15
+ * Emoji → built-in handler name mapping.
16
+ */
17
+ export const builtInEmoji = {
18
+ '📦': 'builtIns.lazyLoad',
19
+ '🎚️': 'builtIns.lazyLoadSwitch',
20
+ '🔗': 'builtIns.join',
21
+ '🏷️': 'builtIns.microDataJoin',
22
+ '📋': 'builtIns.manageTemplateList',
23
+ };
24
+
25
+ export default builtInEmoji;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * builtInEmoji.ts — Predefined emoji aliases for built-in handlers.
3
+ *
4
+ * Import and spread into the `handlers` option for concise handler configs:
5
+ *
6
+ * @example
7
+ * import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
8
+ *
9
+ * assignFrom(target, {
10
+ * '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
11
+ * }, { from: vm, handlers: builtInEmoji });
12
+ */
13
+
14
+ /**
15
+ * Emoji → built-in handler name mapping.
16
+ *
17
+ * | Emoji | Handler |
18
+ * |-------|---------|
19
+ * | 📦 | builtIns.lazyLoad |
20
+ * | 🎚️ | builtIns.lazyLoadSwitch |
21
+ * | 🔗 | builtIns.join |
22
+ * | 🏷️ | builtIns.microDataJoin |
23
+ * | 📋 | builtIns.manageTemplateList |
24
+ */
25
+ export const builtInEmoji: Record<string, string> = {
26
+ '📦': 'builtIns.lazyLoad',
27
+ '🎚️': 'builtIns.lazyLoadSwitch',
28
+ '🔗': 'builtIns.join',
29
+ '🏷️': 'builtIns.microDataJoin',
30
+ '📋': 'builtIns.manageTemplateList',
31
+ };
32
+
33
+ export default builtInEmoji;
@@ -37,14 +37,17 @@ function getMarkerName(templateEl) {
37
37
  *
38
38
  * Exported so it can be subclassed for custom behavior.
39
39
  */
40
+ import { assignFrom } from '../assignFrom.js';
40
41
  export class LazyLoadHandler {
41
42
  config;
43
+ _options;
42
44
  static #markerCounter = 0;
43
45
  constructor(config) {
44
46
  this.config = config;
45
47
  }
46
- async assign(lhsTarget, resolvedParams) {
47
- const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS, hideCss, markerName, toggleInert = false, toggleDisabled = false } = resolvedParams;
48
+ async assign(lhsTarget, resolvedParams, options) {
49
+ this._options = options;
50
+ const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS, hideCss, markerName, toggleInert = false, toggleDisabled = false, placeholder } = resolvedParams;
48
51
  if (!(lhsTarget instanceof Element)) {
49
52
  throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
50
53
  }
@@ -83,6 +86,16 @@ export class LazyLoadHandler {
83
86
  }
84
87
  }
85
88
  else {
89
+ // Remove placeholder content if specified
90
+ if (placeholder) {
91
+ const [phStart, phEnd] = findMarkers(lhsTarget, placeholder);
92
+ if (phStart && phEnd) {
93
+ const phNodes = getNodesBetweenMarkers(phStart, phEnd);
94
+ for (const node of phNodes) {
95
+ node.parentNode?.removeChild(node);
96
+ }
97
+ }
98
+ }
86
99
  if (method === 'after') {
87
100
  [startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
88
101
  } else {
@@ -164,6 +177,7 @@ export class LazyLoadHandler {
164
177
  throw new Error(`builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`);
165
178
  }
166
179
  const nodes = Array.from(content.childNodes);
180
+ this.applyAssign(nodes, resolvedParams);
167
181
  endMarker.parentNode.insertBefore(content, endMarker);
168
182
  return nodes;
169
183
  }
@@ -179,6 +193,7 @@ export class LazyLoadHandler {
179
193
  throw new Error(`builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`);
180
194
  }
181
195
  const nodes = Array.from(content.childNodes);
196
+ this.applyAssign(nodes, resolvedParams);
182
197
  endMarker.parentNode.insertBefore(content, endMarker);
183
198
  await this.onCloneInserted(nodes, lhsTarget, resolvedParams);
184
199
  if (resolvedParams.onInstantiated && typeof resolvedParams.onInstantiated === 'function') {
@@ -192,6 +207,22 @@ export class LazyLoadHandler {
192
207
  }
193
208
  return nodes;
194
209
  }
210
+ applyAssign(nodes, resolvedParams) {
211
+ const assign = resolvedParams.assign;
212
+ if (!assign) return;
213
+ const elements = nodes.filter(n => n instanceof Element);
214
+ if (elements.length === 0) return;
215
+ const from = this._options?.from ?? {};
216
+ if (assign.configs && Array.isArray(assign.configs)) {
217
+ const len = Math.min(elements.length, assign.configs.length);
218
+ for (let j = 0; j < len; j++) {
219
+ const cfg = assign.configs[j];
220
+ assignFrom(elements[j], cfg.assignToFragment ?? {}, { from, ...cfg.withOptions });
221
+ }
222
+ } else if (assign.assignToFragment) {
223
+ assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
224
+ }
225
+ }
195
226
  async onCloneInserted(nodes, lhsTarget, resolvedParams) {
196
227
  // No-op by default. Subclasses override.
197
228
  }
@@ -23,6 +23,7 @@ import type { AssignFromHandler } from '../assignFromAsync.js';
23
23
  import type { LazyLoadResolvedParams, LazyLoadInstantiatedContext } from '../types/assign-gingerly/types.js';
24
24
  import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
25
25
  import { findMarkers, createMarkers, getNodesBetweenMarkers, findMarkersSibling, createMarkersSibling, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
26
+ import { assignFrom } from '../assignFrom.js';
26
27
 
27
28
  export type { LazyLoadResolvedParams, LazyLoadInstantiatedContext };
28
29
 
@@ -46,13 +47,15 @@ function getMarkerName(templateEl: any): string {
46
47
  */
47
48
  export class LazyLoadHandler implements AssignFromHandler {
48
49
  config: any;
50
+ _options: any;
49
51
  static #markerCounter = 0;
50
52
 
51
53
  constructor(config: any) {
52
54
  this.config = config;
53
55
  }
54
56
 
55
- async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams): Promise<void> {
57
+ async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams, options?: any): Promise<void> {
58
+ this._options = options; // Store for applyAssign access
56
59
  const {
57
60
  if: condition,
58
61
  instantiate,
@@ -64,6 +67,7 @@ export class LazyLoadHandler implements AssignFromHandler {
64
67
  markerName,
65
68
  toggleInert = false,
66
69
  toggleDisabled = false,
70
+ placeholder,
67
71
  } = resolvedParams;
68
72
 
69
73
  if (!(lhsTarget instanceof Element)) {
@@ -112,6 +116,16 @@ export class LazyLoadHandler implements AssignFromHandler {
112
116
  }
113
117
  } else {
114
118
  // No markers — first time. Create markers and clone template.
119
+ // Remove placeholder content if specified
120
+ if (placeholder) {
121
+ const [phStart, phEnd] = findMarkers(lhsTarget, placeholder);
122
+ if (phStart && phEnd) {
123
+ const phNodes = getNodesBetweenMarkers(phStart, phEnd);
124
+ for (const node of phNodes) {
125
+ node.parentNode?.removeChild(node);
126
+ }
127
+ }
128
+ }
115
129
  if (method === 'after') {
116
130
  [startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
117
131
  } else {
@@ -229,6 +243,10 @@ export class LazyLoadHandler implements AssignFromHandler {
229
243
  }
230
244
 
231
245
  const nodes = Array.from(content.childNodes);
246
+
247
+ // Apply assignments to cloned content before insertion
248
+ this.applyAssign(nodes, resolvedParams);
249
+
232
250
  endMarker.parentNode!.insertBefore(content, endMarker);
233
251
  return nodes;
234
252
  }
@@ -259,6 +277,9 @@ export class LazyLoadHandler implements AssignFromHandler {
259
277
  // Capture nodes before insertion (childNodes empties after insertBefore)
260
278
  const nodes = Array.from(content.childNodes);
261
279
 
280
+ // Apply assignments to cloned content before insertion
281
+ this.applyAssign(nodes, resolvedParams);
282
+
262
283
  // Insert before endMarker
263
284
  endMarker.parentNode!.insertBefore(content, endMarker);
264
285
 
@@ -279,6 +300,30 @@ export class LazyLoadHandler implements AssignFromHandler {
279
300
  return nodes;
280
301
  }
281
302
 
303
+ /**
304
+ * Apply assignFrom to cloned nodes based on the `assign` config.
305
+ * Supports single-element and multi-element (configs array) templates.
306
+ */
307
+ protected applyAssign(nodes: Node[], resolvedParams: LazyLoadResolvedParams): void {
308
+ const assign = (resolvedParams as any).assign;
309
+ if (!assign) return;
310
+
311
+ const elements = nodes.filter(n => n instanceof Element) as Element[];
312
+ if (elements.length === 0) return;
313
+
314
+ const from = this._options?.from ?? {};
315
+
316
+ if (assign.configs && Array.isArray(assign.configs)) {
317
+ const len = Math.min(elements.length, assign.configs.length);
318
+ for (let j = 0; j < len; j++) {
319
+ const cfg = assign.configs[j];
320
+ assignFrom(elements[j], cfg.assignToFragment ?? {}, { from, ...cfg.withOptions });
321
+ }
322
+ } else if (assign.assignToFragment) {
323
+ assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
324
+ }
325
+ }
326
+
282
327
  /**
283
328
  * Hook called after template content is cloned and inserted.
284
329
  * Override in subclasses for custom post-clone logic.
@@ -18,7 +18,7 @@
18
18
  * },
19
19
  * fromEachItem: {
20
20
  * assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
21
- * withOptions: { withMethods: ['querySelector'], inferredAssignments: true },
21
+ * withOptions: { withMethods: ['querySelector'], infer: true },
22
22
  * resolve: { key: '?.rank' }
23
23
  * }
24
24
  * }
@@ -47,6 +47,7 @@ export class ManageTemplateListHandler {
47
47
  return; // Nothing to iterate
48
48
  }
49
49
  const fromEachItem = this.config.fromEachItem;
50
+ const configs = fromEachItem?.configs; // Array form for multi-element templates
50
51
  const assignToFragment = fromEachItem?.assignToFragment ?? {};
51
52
  const withOptions = fromEachItem?.withOptions ?? {};
52
53
  const perItemResolve = fromEachItem?.resolve ?? {};
@@ -55,10 +56,10 @@ export class ManageTemplateListHandler {
55
56
  const fromSource = this.config.fromSource;
56
57
  const sourceAssignToFragment = fromSource?.assignToFragment;
57
58
  const sourceWithOptions = fromSource?.withOptions ?? {};
58
- // Detect fast path: no assignToFragment patterns, just inferredAssignments
59
- const hasAssignPatterns = Object.keys(assignToFragment).length > 0;
60
- const inferredConfig = withOptions.inferredAssignments;
61
- const useFastPath = !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
59
+ // Detect fast path: no assignToFragment patterns, just infer (only for non-configs mode)
60
+ const hasAssignPatterns = !configs && Object.keys(assignToFragment).length > 0;
61
+ const inferredConfig = !configs && withOptions.infer;
62
+ const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
62
63
  const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
63
64
  // Find or create markers
64
65
  let [startMarker, endMarker] = findMarkers(lhsTarget, name);
@@ -112,19 +113,30 @@ export class ManageTemplateListHandler {
112
113
  if (state.keyToNodes.has(key) && oldKeys.has(key)) {
113
114
  // Existing item — update in place
114
115
  const existingNodes = state.keyToNodes.get(key);
115
- const rootEl = existingNodes.find(n => n instanceof Element);
116
- if (rootEl) {
117
- const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
118
- if (shouldYield)
119
- await new Promise(r => setTimeout(r, 0));
120
- if (processInferred) {
121
- processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
116
+ const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
117
+ if (shouldYield)
118
+ await new Promise(r => setTimeout(r, 0));
119
+ if (configs) {
120
+ // Multi-element: zip configs with element nodes
121
+ const elements = existingNodes.filter(n => n instanceof Element);
122
+ const len = Math.min(elements.length, configs.length);
123
+ for (let j = 0; j < len; j++) {
124
+ const cfg = configs[j];
125
+ assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
122
126
  }
123
- else {
124
- assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
125
- }
126
- if (sourceAssignToFragment && options?.from) {
127
- assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
127
+ }
128
+ else {
129
+ const rootEl = existingNodes.find(n => n instanceof Element);
130
+ if (rootEl) {
131
+ if (processInferred) {
132
+ processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
133
+ }
134
+ else {
135
+ assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
136
+ }
137
+ if (sourceAssignToFragment && options?.from) {
138
+ assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
139
+ }
128
140
  }
129
141
  }
130
142
  newKeyToNodes.set(key, existingNodes);
@@ -147,21 +159,32 @@ export class ManageTemplateListHandler {
147
159
  }
148
160
  const clonedNodes = Array.from(content.childNodes);
149
161
  // Apply per-item assignments to the cloned fragment
150
- const rootEl = clonedNodes.find(n => n instanceof Element);
151
- if (rootEl) {
152
- const tempContainer = document.createDocumentFragment();
153
- tempContainer.appendChild(content);
154
- const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
155
- if (shouldYield)
156
- await new Promise(r => setTimeout(r, 0));
157
- if (processInferred) {
158
- processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
162
+ const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
163
+ if (shouldYield)
164
+ await new Promise(r => setTimeout(r, 0));
165
+ if (configs) {
166
+ // Multi-element: zip configs with element nodes
167
+ const elements = clonedNodes.filter(n => n instanceof Element);
168
+ const len = Math.min(elements.length, configs.length);
169
+ for (let j = 0; j < len; j++) {
170
+ const cfg = configs[j];
171
+ assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
159
172
  }
160
- else {
161
- assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
162
- }
163
- if (sourceAssignToFragment && options?.from) {
164
- assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
173
+ }
174
+ else {
175
+ const rootEl = clonedNodes.find(n => n instanceof Element);
176
+ if (rootEl) {
177
+ const tempContainer = document.createDocumentFragment();
178
+ tempContainer.appendChild(content);
179
+ if (processInferred) {
180
+ processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
181
+ }
182
+ else {
183
+ assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
184
+ }
185
+ if (sourceAssignToFragment && options?.from) {
186
+ assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
187
+ }
165
188
  }
166
189
  }
167
190
  for (const node of clonedNodes) {