mount-observer 0.1.11 → 0.1.13

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/MountObserver.ts CHANGED
@@ -21,9 +21,10 @@ import {
21
21
  type MutationCallback
22
22
  } from './SharedMutationObserver.js';
23
23
  import { withScopePerimeter } from './withScopePerimeter.js';
24
+ import { getRegistryRoot } from './getRegistryRoot.js';
24
25
  import type { assignTentatively as AssignTentativelyType } from 'assign-gingerly/assignTentatively.js';
25
26
 
26
- export class MountObserver extends EventTarget implements IMountObserver {
27
+ export class MountObserver<TKeys extends string = string> extends EventTarget implements IMountObserver {
27
28
  // Static registry for registered handlers
28
29
  static #handlerRegistry = new Map<string, Constructor>();
29
30
 
@@ -38,6 +39,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
38
39
  #options: MountObserverOptions;
39
40
  #abortController: AbortController;
40
41
  #modules: any[] = [];
42
+ #configFromPromise: Promise<void> | undefined;
41
43
  #mountedElements: WeakDual<Element> = {
42
44
  weakSet: new WeakSet(),
43
45
  setWeak: new Set()
@@ -48,7 +50,13 @@ export class MountObserver extends EventTarget implements IMountObserver {
48
50
  #rootNode: WeakRef<Node> | undefined;
49
51
  #importsLoaded = false;
50
52
  #mediaQueryCleanup?: () => void;
53
+ #rootSizeCleanup?: () => void;
54
+ #intersectionCleanup?: () => void;
55
+ #connectionCleanup?: () => void;
56
+ #intersectionObserver?: IntersectionObserver;
51
57
  #mediaMatches: boolean = true;
58
+ #rootSizeMatches: boolean = true;
59
+ #connectionMatches: boolean = true;
52
60
  #asgMtSource: Record<string, any> | undefined;
53
61
  #asgDisMtSource: Record<string, any> | undefined;
54
62
  #stageMtSource: Record<string, any> | undefined;
@@ -56,18 +64,53 @@ export class MountObserver extends EventTarget implements IMountObserver {
56
64
  #assignTentatively: typeof AssignTentativelyType | undefined;
57
65
  #elementNotifiers = new WeakMap<Element, EventTarget>();
58
66
  #notifierMountedElements = new WeakSet<Element>();
67
+ #subObservers: Map<string, MountObserver> | undefined;
59
68
 
60
- constructor(config: MountConfig, options: MountObserverOptions = {}) {
69
+ #mergeHandlerDefaults(config: MountConfig): MountConfig {
70
+ const doValue = config.do;
71
+
72
+ // Only process if do is a string (single handler reference)
73
+ if (typeof doValue !== 'string') {
74
+ return config;
75
+ }
76
+
77
+ // Look up the handler class
78
+ const HandlerClass = MountObserver.#handlerRegistry.get(doValue);
79
+ if (!HandlerClass) {
80
+ // Validation will catch this later
81
+ return config;
82
+ }
83
+
84
+ // Extract static properties from the handler class
85
+ const handlerDefaults: Partial<MountConfig> = {};
86
+ const proto = HandlerClass as any;
87
+
88
+ // Get all static properties
89
+ for (const key of Object.getOwnPropertyNames(proto)) {
90
+ if (key !== 'prototype' && key !== 'length' && key !== 'name') {
91
+ handlerDefaults[key as keyof MountConfig] = proto[key];
92
+ }
93
+ }
94
+
95
+ // Merge: handler defaults first, then inline config (inline trumps)
96
+ // Using object spread - inline config overwrites handler defaults
97
+ return { ...handlerDefaults, ...config };
98
+ }
99
+
100
+ constructor(config: MountConfig<TKeys>, options: MountObserverOptions = {}) {
61
101
  super();
62
102
 
63
- this.#init = config;
103
+ // Merge handler defaults if do is a string reference
104
+ const mergedConfig = this.#mergeHandlerDefaults(config);
105
+
106
+ this.#init = mergedConfig;
64
107
  this.#options = options;
65
108
  this.#abortController = new AbortController();
66
109
 
67
110
  const {
68
- assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness,
69
- import: imp
70
- } = config;
111
+ assignOnMount, assignOnDismount, stageOnMount, do: doValue, loadingEagerness,
112
+ import: imp, configFrom
113
+ } = mergedConfig;
71
114
  // Make a copy of assignOnMount config using structuredClone
72
115
  if (assignOnMount !== undefined) {
73
116
  this.#asgMtSource = structuredClone(assignOnMount);
@@ -90,9 +133,9 @@ export class MountObserver extends EventTarget implements IMountObserver {
90
133
  this.#validateDoHandlers();
91
134
  }
92
135
 
93
- // Validate reference property if present
94
- if (reference !== undefined) {
95
- this.#validateReference();
136
+ // Load configFrom modules if specified
137
+ if (configFrom !== undefined) {
138
+ this.#configFromPromise = this.#loadConfigFrom();
96
139
  }
97
140
 
98
141
  // Start loading imports if eager
@@ -115,35 +158,87 @@ export class MountObserver extends EventTarget implements IMountObserver {
115
158
  }
116
159
  }
117
160
  }
118
-
119
- #validateReference(): void {
120
- if (!this.#init.import) {
121
- throw new Error('reference property requires import to be defined');
161
+
162
+ /**
163
+ * Loads configuration from external modules specified in configFrom property.
164
+ * Merges multiple configs left-to-right, with inline config taking final precedence.
165
+ */
166
+ async #loadConfigFrom(): Promise<void> {
167
+ const { configFrom } = this.#init;
168
+ if (!configFrom) return;
169
+
170
+ // Normalize to array
171
+ const configPaths = Array.isArray(configFrom) ? configFrom : [configFrom];
172
+
173
+ // Check for duplicates
174
+ const pathSet = new Set<string>();
175
+ for (const path of configPaths) {
176
+ if (pathSet.has(path)) {
177
+ throw new Error(`Duplicate configFrom module: '${path}'`);
178
+ }
179
+ pathSet.add(path);
122
180
  }
123
181
 
124
- // Normalize import to array
125
- const imports = Array.isArray(this.#init.import)
126
- ? this.#init.import
127
- : [this.#init.import];
182
+ // Load all modules
183
+ const loadedConfigs: MountConfig[] = [];
184
+ for (const path of configPaths) {
185
+ try {
186
+ const module = await import(path);
128
187
 
129
- // Normalize reference to array
130
- const references = arr(this.#init.reference);
188
+ if (!module.mountConfig) {
189
+ throw new Error(`Module '${path}' does not export 'mountConfig'`);
190
+ }
191
+
192
+ if (typeof module.mountConfig !== 'object' || module.mountConfig === null) {
193
+ throw new Error(`Module '${path}' exports invalid mountConfig: must be an object`);
194
+ }
131
195
 
132
- // Validate each reference index
133
- for (const index of references) {
134
- // Check if index is within bounds
135
- if (index < 0 || index >= imports.length) {
136
- throw new Error(`reference index ${index} is out of bounds (import array length: ${imports.length})`);
196
+ loadedConfigs.push(module.mountConfig);
197
+ } catch (error) {
198
+ // Re-throw with better context if it's not already our error
199
+ if (error instanceof Error && !error.message.includes(path)) {
200
+ throw new Error(`Failed to load config from '${path}': ${error.message}`);
201
+ }
202
+ throw error;
137
203
  }
204
+ }
138
205
 
139
- const importItem = imports[index];
206
+ // Merge configs: loaded configs first (left-to-right), then inline config
207
+ // Save the original inline config
208
+ const inlineConfig = { ...this.#init };
140
209
 
141
- // Check if it's a JS module (not a 2D array with type option)
142
- if (Array.isArray(importItem)) {
143
- throw new Error(`reference index ${index} points to a non-JS module import (array with type option)`);
144
- }
210
+ // Start with empty object, merge all loaded configs, then merge inline
211
+ let mergedConfig: MountConfig = {};
212
+ for (const loadedConfig of loadedConfigs) {
213
+ mergedConfig = Object.assign(mergedConfig, loadedConfig);
214
+ }
215
+
216
+ // Inline config takes final precedence
217
+ mergedConfig = Object.assign(mergedConfig, inlineConfig);
218
+
219
+ // Update the init config with merged result
220
+ this.#init = mergedConfig;
221
+ }
222
+
223
+ /**
224
+ * Creates and initializes sub-observers from the `with` property.
225
+ * Each sub-observer observes the same root node as the parent.
226
+ * Sub-observers are stored in #subObservers Map for lifecycle management.
227
+ */
228
+ async #createSubObservers(rootNode: Node): Promise<void> {
229
+ const withConfig = this.#init.with;
230
+ if (!withConfig) return;
231
+
232
+ this.#subObservers = new Map();
233
+
234
+ for (const [key, subConfig] of Object.entries(withConfig)) {
235
+ const subObserver = new MountObserver(subConfig as MountConfig);
236
+ this.#subObservers.set(key, subObserver);
237
+ await subObserver.observe(rootNode);
145
238
  }
146
239
  }
240
+
241
+
147
242
 
148
243
 
149
244
  async #setupMediaQuery(): Promise<void> {
@@ -165,10 +260,79 @@ export class MountObserver extends EventTarget implements IMountObserver {
165
260
  this.#mediaQueryCleanup = result.cleanup;
166
261
  }
167
262
 
263
+ async #setupRootSizeObserver(): Promise<void> {
264
+ if (!this.#rootNode) {
265
+ throw new Error('Cannot setup root size observer before observe() is called');
266
+ }
267
+
268
+ const { setupRootSizeObserver } = await import('./rootSizeObserver.js');
269
+ const result = setupRootSizeObserver(
270
+ this.#init,
271
+ this.#rootNode,
272
+ this.#mountedElements,
273
+ this.#modules,
274
+ this,
275
+ (node) => this.#processNode(node)
276
+ );
277
+
278
+ this.#rootSizeMatches = result.conditionMatches;
279
+ this.#rootSizeCleanup = result.cleanup;
280
+ }
281
+
282
+ async #setupElementIntersection(): Promise<void> {
283
+ if (!this.#rootNode) {
284
+ throw new Error('Cannot setup element intersection before observe() is called');
285
+ }
286
+
287
+ const { setupElementIntersection } = await import('./elementIntersection.js');
288
+ const result = setupElementIntersection(
289
+ this.#init,
290
+ this.#rootNode,
291
+ this.#mountedElements,
292
+ this.#modules,
293
+ this,
294
+ (element) => this.#matchesSelector(element),
295
+ (element) => this.#handleMatch(element)
296
+ );
297
+
298
+ this.#intersectionObserver = result.intersectionObserver;
299
+ this.#intersectionCleanup = result.cleanup;
300
+ }
301
+
302
+ async #setupConnectionMonitor(): Promise<void> {
303
+ if (!this.#rootNode) {
304
+ throw new Error('Cannot setup connection monitor before observe() is called');
305
+ }
306
+
307
+ const { setupConnectionMonitor } = await import('./connectionMonitor.js');
308
+ const result = setupConnectionMonitor(
309
+ this.#init,
310
+ this.#rootNode,
311
+ this.#mountedElements,
312
+ this.#modules,
313
+ this,
314
+ (node) => this.#processNode(node)
315
+ );
316
+
317
+ this.#connectionMatches = result.conditionMatches;
318
+ this.#connectionCleanup = result.cleanup;
319
+ }
320
+
168
321
  get disconnectedSignal(): AbortSignal {
169
322
  return this.#abortController.signal;
170
323
  }
171
324
 
325
+ get mountedElements(): Element[] {
326
+ const elements: Element[] = [];
327
+ for (const ref of this.#mountedElements.setWeak) {
328
+ const element = ref.deref();
329
+ if (element !== undefined) {
330
+ elements.push(element);
331
+ }
332
+ }
333
+ return elements;
334
+ }
335
+
172
336
  getNotifier(element: Element): EventTarget {
173
337
  // Return cached notifier if it exists
174
338
  let notifier = this.#elementNotifiers.get(element);
@@ -182,10 +346,29 @@ export class MountObserver extends EventTarget implements IMountObserver {
182
346
  return notifier;
183
347
  }
184
348
 
185
- async observe(rootNode: Node): Promise<void> {
349
+ /**
350
+ * Begins observing elements within the provided node.
351
+ *
352
+ * @param observedNode - The node to observe for matching elements. This is the root
353
+ * of the observation scope where the mutation observer will be
354
+ * registered. All matching elements within this node (and its
355
+ * descendants) will trigger mount callbacks.
356
+ *
357
+ * Common values:
358
+ * - `document` - Observe the entire document
359
+ * - `element` - Observe a specific subtree
360
+ * - `shadowRoot` - Observe within a shadow DOM
361
+ */
362
+ async observe(observedNode: Node): Promise<void> {
186
363
  if (this.#rootNode) {
187
364
  throw new Error('Already observing');
188
365
  }
366
+
367
+ // Wait for configFrom loading to complete if it was started
368
+ if (this.#configFromPromise) {
369
+ await this.#configFromPromise;
370
+ }
371
+
189
372
  if(this.#asgMtSource || this.#asgDisMtSource){
190
373
  await import('assign-gingerly/object-extension.js');
191
374
  }
@@ -194,27 +377,45 @@ export class MountObserver extends EventTarget implements IMountObserver {
194
377
  this.#assignTentatively = assignTentatively;
195
378
  }
196
379
 
197
- this.#rootNode = new WeakRef(rootNode);
380
+ this.#rootNode = new WeakRef(observedNode);
381
+
382
+ // Create sub-observers from `with` property
383
+ await this.#createSubObservers(observedNode);
198
384
 
199
385
  // Set up media query if specified (needs rootNode to be set first)
200
386
  if (this.#init.withMediaMatching) {
201
387
  await this.#setupMediaQuery();
202
388
  }
203
389
 
390
+ // Set up root size observer if specified (needs rootNode to be set first)
391
+ if (this.#init.whereObservedRootSizeMatches) {
392
+ await this.#setupRootSizeObserver();
393
+ }
394
+
395
+ // Set up element intersection observer if specified (needs rootNode to be set first)
396
+ if (this.#init.whereElementIntersectsWith) {
397
+ await this.#setupElementIntersection();
398
+ }
399
+
400
+ // Set up connection monitor if specified (needs rootNode to be set first)
401
+ if (this.#init.whereConnectionHas) {
402
+ await this.#setupConnectionMonitor();
403
+ }
404
+
204
405
  // Wait for eager imports to complete if they were started in constructor
205
406
  if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
206
407
  await this.#loadImports();
207
408
  }
208
409
 
209
- // Process existing elements only if media matches
210
- if (this.#mediaMatches) {
211
- this.#processNode(rootNode);
410
+ // Process existing elements only if all conditions match
411
+ if (this.#mediaMatches && this.#rootSizeMatches && this.#connectionMatches) {
412
+ this.#processNode(observedNode);
212
413
  }
213
414
 
214
415
  // Create mutation callback
215
416
  this.#mutationCallback = (mutations) => {
216
- // Skip processing if media doesn't match
217
- if (!this.#mediaMatches) {
417
+ // Skip processing if any condition doesn't match
418
+ if (!this.#mediaMatches || !this.#rootSizeMatches || !this.#connectionMatches) {
218
419
  return;
219
420
  }
220
421
 
@@ -240,12 +441,21 @@ export class MountObserver extends EventTarget implements IMountObserver {
240
441
  };
241
442
 
242
443
  // Register with shared mutation observer
243
- registerSharedObserver(rootNode, this.#mutationCallback, observerConfig);
444
+ registerSharedObserver(observedNode, this.#mutationCallback, observerConfig);
244
445
  }
245
446
 
246
447
  disconnect(): void {
247
448
  const rootNode = this.#rootNode?.deref();
248
449
 
450
+ // Disconnect all sub-observers first (recursive)
451
+ if (this.#subObservers) {
452
+ for (const subObserver of this.#subObservers.values()) {
453
+ subObserver.disconnect();
454
+ }
455
+ this.#subObservers.clear();
456
+ this.#subObservers = undefined;
457
+ }
458
+
249
459
  // Unregister from shared mutation observer
250
460
  if (rootNode && this.#mutationCallback) {
251
461
  unregisterSharedObserver(rootNode, this.#mutationCallback);
@@ -258,6 +468,24 @@ export class MountObserver extends EventTarget implements IMountObserver {
258
468
  this.#mediaQueryCleanup = undefined;
259
469
  }
260
470
 
471
+ // Remove root size observer
472
+ if (this.#rootSizeCleanup) {
473
+ this.#rootSizeCleanup();
474
+ this.#rootSizeCleanup = undefined;
475
+ }
476
+
477
+ // Remove intersection observer
478
+ if (this.#intersectionCleanup) {
479
+ this.#intersectionCleanup();
480
+ this.#intersectionCleanup = undefined;
481
+ }
482
+
483
+ // Remove connection monitor
484
+ if (this.#connectionCleanup) {
485
+ this.#connectionCleanup();
486
+ this.#connectionCleanup = undefined;
487
+ }
488
+
261
489
  this.#abortController.abort();
262
490
  this.#rootNode = undefined;
263
491
  }
@@ -272,26 +500,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
272
500
  this.#modules = await loadImports(this.#init.import);
273
501
  this.#importsLoaded = true;
274
502
 
275
- // Validate referenced withInstance if reference is specified
276
- if (this.#init.reference !== undefined) {
277
- const references = arr(this.#init.reference);
278
-
279
- for (const index of references) {
280
- const module = this.#modules[index];
281
- if (module && module.withInstance !== undefined) {
282
- // Validate that it's a Constructor or array of Constructors
283
- const withInstance = module.withInstance;
284
- const constructors = arr(withInstance);
285
-
286
- for (const constructor of constructors) {
287
- if (typeof constructor !== 'function') {
288
- throw new Error(`Referenced module at index ${index} exports invalid withInstance: must be a Constructor or array of Constructors`);
289
- }
290
- }
291
- }
292
- }
293
- }
294
-
295
503
  this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
296
504
  }
297
505
 
@@ -300,7 +508,11 @@ export class MountObserver extends EventTarget implements IMountObserver {
300
508
  if (node.nodeType === Node.ELEMENT_NODE) {
301
509
  const element = node as Element;
302
510
 
303
- if (this.#matchesSelector(element)) {
511
+ // If intersection observer is active, start observing the element
512
+ // The intersection callback will handle mounting when it intersects
513
+ if (this.#intersectionObserver) {
514
+ this.#intersectionObserver.observe(element);
515
+ } else if (this.#matchesSelector(element)) {
304
516
  this.#handleMatch(element);
305
517
  }
306
518
  }
@@ -310,8 +522,12 @@ export class MountObserver extends EventTarget implements IMountObserver {
310
522
  const root = node as DocumentFragment;
311
523
 
312
524
  // Get all elements matching the CSS selector first
313
- root.querySelectorAll(this.#init.matching).forEach(child => {
314
- if (this.#matchesSelector(child)) {
525
+ const matches = root.querySelectorAll(this.#init.matching);
526
+ matches.forEach(child => {
527
+ // If intersection observer is active, start observing the element
528
+ if (this.#intersectionObserver) {
529
+ this.#intersectionObserver.observe(child);
530
+ } else if (this.#matchesSelector(child)) {
315
531
  this.#handleMatch(child);
316
532
  }
317
533
  });
@@ -319,59 +535,70 @@ export class MountObserver extends EventTarget implements IMountObserver {
319
535
  }
320
536
 
321
537
  #matchesSelector(element: Element): boolean {
322
- //TODO: reduce redundncy with this.#init?
323
- // Check matching condition
324
- if (!this.#init.matching) {
325
- return false;
326
- }
327
-
328
- const matchesElement = element.matches(this.#init.matching);
329
- if (!matchesElement) {
330
- return false;
331
- }
332
-
333
- // Check withScopePerimeter condition if specified (donut hole scoping)
334
- if (this.#init.withScopePerimeter) {
335
- const rootNode = this.#rootNode?.deref();
336
- if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
538
+ //TODO: reduce redundncy with this.#init?
539
+ // Check matching condition
540
+ if (!this.#init.matching) {
337
541
  return false;
338
542
  }
339
- }
340
-
341
- // Check withInstance condition if specified
342
- if (this.#init.withInstance) {
343
- const constructors = arr(this.#init.withInstance);
344
-
345
- // Element must be an instance of at least one constructor (OR logic for array)
346
- const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
347
-
348
- if (!matchesInstanceOf) {
543
+
544
+ const matchesElement = element.matches(this.#init.matching);
545
+ if (!matchesElement) {
349
546
  return false;
350
547
  }
351
- }
352
-
353
- // Check referenced withInstance if imports are loaded and reference is specified
354
- if (this.#importsLoaded && this.#init.reference !== undefined) {
355
- const references = arr(this.#init.reference);
356
-
357
- for (const index of references) {
358
- const module = this.#modules[index];
359
- if (module && module.withInstance !== undefined) {
360
- const constructors = arr(module.withInstance);
361
-
362
- // Element must be an instance of at least one constructor (OR logic within this module)
363
- const matchesInstanceOf = constructors.some((constructor: Constructor) => element instanceof constructor);
364
-
365
- if (!matchesInstanceOf) {
366
- return false;
367
- }
548
+
549
+ // Check that element's customElementRegistry matches root node's registry
550
+ const rootNode = this.#rootNode?.deref();
551
+ if (rootNode) {
552
+ const registriesMatch = (rootNode as any).customElementRegistry === (element as any).customElementRegistry;
553
+
554
+ // If whereDifferentCustomElementRegistry is true, exclude matching registries
555
+ if (this.#init.whereDifferentCustomElementRegistry) {
556
+ if (registriesMatch) return false;
557
+ } else {
558
+ // Default behavior: exclude non-matching registries
559
+ if (!registriesMatch) return false;
368
560
  }
369
561
  }
562
+
563
+ // Check withScopePerimeter condition if specified (donut hole scoping)
564
+ if (this.#init.withScopePerimeter) {
565
+ if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
566
+ return false;
567
+ }
568
+ }
569
+
570
+ // Check whereObservedRootSizeMatches condition if specified
571
+ if (this.#init.whereObservedRootSizeMatches && !this.#rootSizeMatches) {
572
+ return false;
573
+ }
574
+
575
+ // Check whereInstanceOf condition if specified
576
+ if (this.#init.whereInstanceOf) {
577
+ const constructors = arr(this.#init.whereInstanceOf);
578
+
579
+ // Element must be an instance of at least one constructor (OR logic for array)
580
+ const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
581
+
582
+ if (!matchesInstanceOf) {
583
+ return false;
584
+ }
585
+ }
586
+
587
+ // Check whereLocalNameMatches condition if specified
588
+ if (this.#init.whereLocalNameMatches) {
589
+ const pattern = typeof this.#init.whereLocalNameMatches === 'string'
590
+ ? new RegExp(this.#init.whereLocalNameMatches)
591
+ : this.#init.whereLocalNameMatches;
592
+
593
+ if (!pattern.test(element.localName)) {
594
+ return false;
595
+ }
596
+ }
597
+
598
+ // All conditions passed
599
+ return true;
370
600
  }
371
601
 
372
- // All conditions passed
373
- return true;
374
- }
375
602
 
376
603
  async #handleMatch(element: Element): Promise<void> {
377
604
  if (this.#processedDoForElement.has(element)) {
@@ -397,12 +624,55 @@ export class MountObserver extends EventTarget implements IMountObserver {
397
624
  return;
398
625
  }
399
626
 
400
- const context: MountContext = {
627
+ const context: MountContext<TKeys> = {
401
628
  modules: this.#modules,
402
629
  observer: this,
403
630
  rootNode,
404
- MountConfig: this.#init,
631
+ mountConfig: this.#init,
405
632
  };
633
+
634
+ // Add withObservers if sub-observers exist
635
+ if (this.#subObservers && this.#subObservers.size > 0) {
636
+ context.withObservers = {} as {[K in TKeys]: IMountObserver};
637
+ for (const [key, subObserver] of this.#subObservers.entries()) {
638
+ (context.withObservers as any)[key] = subObserver;
639
+ }
640
+ }
641
+
642
+ // Check shouldMount condition if specified (final gate before mounting)
643
+ if (this.#init.shouldMount) {
644
+ try {
645
+ const shouldMount = this.#init.shouldMount(element, context);
646
+ if (!shouldMount) {
647
+ // shouldMount returned false - don't mount this element
648
+ // Remove from processed set so it can be re-evaluated later
649
+ this.#processedDoForElement.delete(element);
650
+ // Remove from mounted elements tracking
651
+ this.#mountedElements.weakSet.delete(element);
652
+ for (const ref of this.#mountedElements.setWeak) {
653
+ if (ref.deref() === element) {
654
+ this.#mountedElements.setWeak.delete(ref);
655
+ break;
656
+ }
657
+ }
658
+ return;
659
+ }
660
+ } catch (error) {
661
+ // shouldMount threw an error - treat as false and log
662
+ console.error('shouldMount check failed:', error);
663
+ // Remove from processed set so it can be re-evaluated later
664
+ this.#processedDoForElement.delete(element);
665
+ // Remove from mounted elements tracking
666
+ this.#mountedElements.weakSet.delete(element);
667
+ for (const ref of this.#mountedElements.setWeak) {
668
+ if (ref.deref() === element) {
669
+ this.#mountedElements.setWeak.delete(ref);
670
+ break;
671
+ }
672
+ }
673
+ return;
674
+ }
675
+ }
406
676
 
407
677
  // Apply assignGingerly if specified
408
678
  if (this.#asgMtSource) {
@@ -437,18 +707,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
437
707
  }
438
708
  }
439
709
 
440
- // Call referenced do functions from imported modules
441
- if (this.#init.reference !== undefined) {
442
- const references = arr(this.#init.reference);
443
-
444
- for (const index of references) {
445
- const module = this.#modules[index];
446
- if (module && typeof module.do === 'function') {
447
- module.do(element, context);
448
- }
449
- }
450
- }
451
-
452
710
  // Dispatch mount event
453
711
  const mountEvent = new MountEvent(element, this.#modules, this.#init, context);
454
712
  this.dispatchEvent(mountEvent);
@@ -539,12 +797,20 @@ export class MountObserver extends EventTarget implements IMountObserver {
539
797
  return;
540
798
  }
541
799
 
542
- const context: MountContext = {
800
+ const context: MountContext<TKeys> = {
543
801
  modules: this.#modules,
544
802
  observer: this,
545
803
  rootNode,
546
- MountConfig: this.#init,
804
+ mountConfig: this.#init,
547
805
  };
806
+
807
+ // Add withObservers if sub-observers exist
808
+ if (this.#subObservers && this.#subObservers.size > 0) {
809
+ context.withObservers = {} as {[K in TKeys]: IMountObserver};
810
+ for (const [key, subObserver] of this.#subObservers.entries()) {
811
+ (context.withObservers as any)[key] = subObserver;
812
+ }
813
+ }
548
814
 
549
815
 
550
816
  // Dispatch dismount event