dalila 1.9.8 → 1.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/runtime/bind.js +165 -3
- package/dist/runtime/boundary.d.ts +104 -0
- package/dist/runtime/boundary.js +304 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/lazy.d.ts +109 -0
- package/dist/runtime/lazy.js +254 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ bind(document.getElementById('app')!, ctx);
|
|
|
60
60
|
|
|
61
61
|
- [Template Binding](./docs/runtime/bind.md) — `bind()`, `mount()`, `configure()`, transitions, portal, text interpolation, events
|
|
62
62
|
- [Components](./docs/runtime/component.md) — `defineComponent`, typed props/emits/refs, slots
|
|
63
|
+
- [Lazy Loading](./docs/runtime/lazy.md) — `createLazyComponent`, `d-lazy`, `createSuspense` wrapper, code splitting
|
|
64
|
+
- [Error Boundary](./docs/runtime/boundary.md) — `createErrorBoundary`, `createErrorBoundaryState`, `withErrorBoundary`, `d-boundary`
|
|
63
65
|
- [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
|
|
64
66
|
|
|
65
67
|
### Routing
|
package/dist/runtime/bind.js
CHANGED
|
@@ -10,6 +10,8 @@ import { effect, createScope, withScope, isInDevMode, signal, computeVirtualRang
|
|
|
10
10
|
import { WRAPPED_HANDLER } from '../form/form.js';
|
|
11
11
|
import { linkScopeToDom, withDevtoolsDomTarget } from '../core/devtools.js';
|
|
12
12
|
import { isComponent, normalizePropDef, coercePropValue, kebabToCamel, camelToKebab } from './component.js';
|
|
13
|
+
import { observeLazyElement, getLazyComponent } from './lazy.js';
|
|
14
|
+
import { bindBoundary } from './boundary.js';
|
|
13
15
|
// ============================================================================
|
|
14
16
|
// Utilities
|
|
15
17
|
// ============================================================================
|
|
@@ -1355,6 +1357,161 @@ function bindPortal(root, ctx, cleanups) {
|
|
|
1355
1357
|
}
|
|
1356
1358
|
}
|
|
1357
1359
|
// ============================================================================
|
|
1360
|
+
// d-lazy Directive
|
|
1361
|
+
// ============================================================================
|
|
1362
|
+
/**
|
|
1363
|
+
* Bind all [d-lazy] directives within root.
|
|
1364
|
+
* Loads a lazy component when it enters the viewport.
|
|
1365
|
+
*/
|
|
1366
|
+
function bindLazy(root, ctx, cleanups, refs, events) {
|
|
1367
|
+
const elements = qsaIncludingRoot(root, '[d-lazy]');
|
|
1368
|
+
for (const el of elements) {
|
|
1369
|
+
const lazyComponentName = normalizeBinding(el.getAttribute('d-lazy'));
|
|
1370
|
+
if (!lazyComponentName)
|
|
1371
|
+
continue;
|
|
1372
|
+
const lazyResult = getLazyComponent(lazyComponentName);
|
|
1373
|
+
if (!lazyResult) {
|
|
1374
|
+
warn(`d-lazy: component "${lazyComponentName}" not found. Use createLazyComponent() to create it.`);
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
const { state } = lazyResult;
|
|
1378
|
+
const htmlEl = el;
|
|
1379
|
+
// Get loading and error templates from attributes
|
|
1380
|
+
const loadingTemplate = el.getAttribute('d-lazy-loading') ?? state.loadingTemplate ?? '';
|
|
1381
|
+
const errorTemplate = el.getAttribute('d-lazy-error') ?? state.errorTemplate ?? '';
|
|
1382
|
+
// Remove the d-lazy attribute to prevent reprocessing
|
|
1383
|
+
el.removeAttribute('d-lazy');
|
|
1384
|
+
el.removeAttribute('d-lazy-loading');
|
|
1385
|
+
el.removeAttribute('d-lazy-error');
|
|
1386
|
+
// Track the current rendered node (starts as the original placeholder element)
|
|
1387
|
+
let currentNode = htmlEl;
|
|
1388
|
+
let componentMounted = false;
|
|
1389
|
+
let componentDispose = null;
|
|
1390
|
+
let componentEl = null;
|
|
1391
|
+
let hasIntersected = false;
|
|
1392
|
+
const refName = normalizeBinding(htmlEl.getAttribute('d-ref'));
|
|
1393
|
+
const syncRef = (node) => {
|
|
1394
|
+
if (!refName)
|
|
1395
|
+
return;
|
|
1396
|
+
if (node instanceof Element) {
|
|
1397
|
+
refs.set(refName, node);
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
const replaceCurrentNode = (nextNode) => {
|
|
1401
|
+
const parent = currentNode.parentNode;
|
|
1402
|
+
if (!parent)
|
|
1403
|
+
return;
|
|
1404
|
+
parent.replaceChild(nextNode, currentNode);
|
|
1405
|
+
currentNode = nextNode;
|
|
1406
|
+
syncRef(nextNode);
|
|
1407
|
+
};
|
|
1408
|
+
const unmountComponent = () => {
|
|
1409
|
+
if (componentDispose) {
|
|
1410
|
+
componentDispose();
|
|
1411
|
+
componentDispose = null;
|
|
1412
|
+
}
|
|
1413
|
+
componentMounted = false;
|
|
1414
|
+
componentEl = null;
|
|
1415
|
+
};
|
|
1416
|
+
// Function to render the loaded component
|
|
1417
|
+
const renderComponent = () => {
|
|
1418
|
+
const comp = state.component();
|
|
1419
|
+
if (!comp)
|
|
1420
|
+
return;
|
|
1421
|
+
// Create component element
|
|
1422
|
+
const compDef = comp.definition;
|
|
1423
|
+
const compEl = document.createElement(compDef.tag);
|
|
1424
|
+
// Copy attributes from placeholder to component
|
|
1425
|
+
for (const attr of Array.from(htmlEl.attributes)) {
|
|
1426
|
+
if (!attr.name.startsWith('d-')) {
|
|
1427
|
+
compEl.setAttribute(attr.name, attr.value);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (componentMounted && componentEl === compEl)
|
|
1431
|
+
return;
|
|
1432
|
+
replaceCurrentNode(compEl);
|
|
1433
|
+
componentEl = compEl;
|
|
1434
|
+
// Bind the component
|
|
1435
|
+
const parentCtx = Object.create(ctx);
|
|
1436
|
+
const parent = compEl.parentNode;
|
|
1437
|
+
const nextSibling = compEl.nextSibling;
|
|
1438
|
+
componentDispose = bind(compEl, parentCtx, {
|
|
1439
|
+
components: { [compDef.tag]: comp },
|
|
1440
|
+
events,
|
|
1441
|
+
_skipLifecycle: true,
|
|
1442
|
+
});
|
|
1443
|
+
// bind() may replace the component host node; keep currentNode/ref pointing to the connected node.
|
|
1444
|
+
if (!compEl.isConnected && parent) {
|
|
1445
|
+
const renderedNode = nextSibling ? nextSibling.previousSibling : parent.lastChild;
|
|
1446
|
+
if (renderedNode instanceof Node) {
|
|
1447
|
+
currentNode = renderedNode;
|
|
1448
|
+
syncRef(renderedNode);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
componentMounted = true;
|
|
1452
|
+
};
|
|
1453
|
+
// Function to show loading state
|
|
1454
|
+
const showLoading = () => {
|
|
1455
|
+
if (loadingTemplate) {
|
|
1456
|
+
if (componentMounted) {
|
|
1457
|
+
unmountComponent();
|
|
1458
|
+
}
|
|
1459
|
+
const loadingEl = document.createElement('div');
|
|
1460
|
+
loadingEl.innerHTML = loadingTemplate;
|
|
1461
|
+
replaceCurrentNode(loadingEl);
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
// Function to show error state
|
|
1465
|
+
const showError = (err) => {
|
|
1466
|
+
if (componentMounted) {
|
|
1467
|
+
unmountComponent();
|
|
1468
|
+
}
|
|
1469
|
+
if (errorTemplate) {
|
|
1470
|
+
const errorEl = document.createElement('div');
|
|
1471
|
+
errorEl.innerHTML = errorTemplate;
|
|
1472
|
+
replaceCurrentNode(errorEl);
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
warn(`d-lazy: failed to load "${lazyComponentName}": ${err.message}`);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
const syncFromState = () => {
|
|
1479
|
+
// Always read reactive state so this effect stays subscribed even before visibility.
|
|
1480
|
+
const loading = state.loading();
|
|
1481
|
+
const error = state.error();
|
|
1482
|
+
const comp = state.component();
|
|
1483
|
+
if (!hasIntersected)
|
|
1484
|
+
return;
|
|
1485
|
+
if (error) {
|
|
1486
|
+
showError(error);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (loading && !comp) {
|
|
1490
|
+
showLoading();
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (comp && !componentMounted) {
|
|
1494
|
+
renderComponent();
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
// React to loading state changes
|
|
1498
|
+
bindEffect(htmlEl, () => {
|
|
1499
|
+
syncFromState();
|
|
1500
|
+
});
|
|
1501
|
+
// Observe element for viewport visibility
|
|
1502
|
+
const cleanupObserver = observeLazyElement(htmlEl, () => {
|
|
1503
|
+
hasIntersected = true;
|
|
1504
|
+
syncFromState();
|
|
1505
|
+
state.load();
|
|
1506
|
+
}, 0 // Trigger when element enters viewport
|
|
1507
|
+
);
|
|
1508
|
+
cleanups.push(() => {
|
|
1509
|
+
cleanupObserver();
|
|
1510
|
+
unmountComponent();
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
// ============================================================================
|
|
1358
1515
|
// d-when Directive
|
|
1359
1516
|
// ============================================================================
|
|
1360
1517
|
/**
|
|
@@ -3927,11 +4084,14 @@ export function bind(root, ctx, options = {}) {
|
|
|
3927
4084
|
bindVirtualEach(root, ctx, cleanups);
|
|
3928
4085
|
// 4. d-each — must run early: removes templates before TreeWalker visits them
|
|
3929
4086
|
bindEach(root, ctx, cleanups);
|
|
3930
|
-
// 5.
|
|
4087
|
+
// 5. d-boundary — must run before child directive/component passes
|
|
4088
|
+
// to avoid binding boundary children twice (original + cloned subtree).
|
|
4089
|
+
bindBoundary(root, ctx, cleanups);
|
|
4090
|
+
// 6. Components — must run after d-each but before d-ref / text interpolation
|
|
3931
4091
|
bindComponents(root, ctx, events, cleanups, onMountError);
|
|
3932
|
-
//
|
|
4092
|
+
// 7. d-ref — collect element references (after d-each removes templates)
|
|
3933
4093
|
bindRef(root, refs);
|
|
3934
|
-
//
|
|
4094
|
+
// 7.5. d-text — safe textContent binding (before text interpolation)
|
|
3935
4095
|
bindText(root, ctx, cleanups);
|
|
3936
4096
|
// 7. Text interpolation (template plan cache + lazy parser fallback)
|
|
3937
4097
|
bindTextInterpolation(root, ctx, rawTextSelectors, templatePlanCacheConfig, benchSession);
|
|
@@ -3955,6 +4115,8 @@ export function bind(root, ctx, options = {}) {
|
|
|
3955
4115
|
bindError(root, ctx, cleanups);
|
|
3956
4116
|
bindFormError(root, ctx, cleanups);
|
|
3957
4117
|
// 17. d-portal — move already-bound elements to external targets
|
|
4118
|
+
// d-lazy directive - loads component when it enters viewport
|
|
4119
|
+
bindLazy(root, ctx, cleanups, refs, events);
|
|
3958
4120
|
bindPortal(root, ctx, cleanups);
|
|
3959
4121
|
// 18. d-if — must run last: elements are fully bound before conditional removal
|
|
3960
4122
|
bindIf(root, ctx, cleanups, transitionRegistry);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dalila Error Boundary
|
|
3
|
+
*
|
|
4
|
+
* Provides error boundary component that captures errors in children
|
|
5
|
+
* and displays a fallback template.
|
|
6
|
+
*
|
|
7
|
+
* @module dalila/runtime/boundary
|
|
8
|
+
*/
|
|
9
|
+
import { signal, Signal } from '../core/index.js';
|
|
10
|
+
import type { Component } from './component.js';
|
|
11
|
+
import type { BindContext, DisposeFunction } from './bind.js';
|
|
12
|
+
export interface ErrorBoundaryOptions {
|
|
13
|
+
/** Template to show when an error occurs */
|
|
14
|
+
fallback: string;
|
|
15
|
+
/** Callback when error is caught */
|
|
16
|
+
onError?: (error: Error) => void;
|
|
17
|
+
/** Callback when error is reset */
|
|
18
|
+
onReset?: () => void;
|
|
19
|
+
}
|
|
20
|
+
export interface ErrorBoundaryState {
|
|
21
|
+
/** The error that was caught */
|
|
22
|
+
error: ReturnType<typeof signal<Error | null>>;
|
|
23
|
+
/** Function to reset the error and retry */
|
|
24
|
+
reset: () => void;
|
|
25
|
+
/** Whether there's an error */
|
|
26
|
+
hasError: () => boolean;
|
|
27
|
+
}
|
|
28
|
+
export type ErrorBoundaryResult = {
|
|
29
|
+
/** The error boundary component */
|
|
30
|
+
component: Component;
|
|
31
|
+
/** Access to error boundary state */
|
|
32
|
+
state: ErrorBoundaryState;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Creates an error boundary component that catches errors in its children.
|
|
36
|
+
*
|
|
37
|
+
* @param options - Configuration for the error boundary
|
|
38
|
+
* @returns Component with error boundary functionality
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const ErrorBoundary = createErrorBoundary({
|
|
43
|
+
* fallback: '<div class="error">Something went wrong</div>',
|
|
44
|
+
* onError: (err) => console.error(err),
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // Use as component
|
|
48
|
+
* <error-boundary>
|
|
49
|
+
* <MyComponent />
|
|
50
|
+
* </error-boundary>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function createErrorBoundary(options: ErrorBoundaryOptions): Component;
|
|
54
|
+
/**
|
|
55
|
+
* Bind d-boundary directive - wraps children with error handling
|
|
56
|
+
* This is typically used inside a component that provides error state
|
|
57
|
+
*/
|
|
58
|
+
export declare function bindBoundary(root: Element, ctx: BindContext, cleanups: DisposeFunction[]): void;
|
|
59
|
+
/**
|
|
60
|
+
* Wraps a function with error boundary logic
|
|
61
|
+
*
|
|
62
|
+
* @param fn - Function that might throw
|
|
63
|
+
* @param errorSignal - Signal to store the error
|
|
64
|
+
* @returns Result of the function or undefined if error
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const result = withErrorBoundary(
|
|
69
|
+
* () => riskyOperation(),
|
|
70
|
+
* errorSignal
|
|
71
|
+
* );
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function withErrorBoundary<T>(fn: () => T, errorSignal: Signal<Error | null>): T | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Creates error boundary state for use in component setup
|
|
77
|
+
*
|
|
78
|
+
* @param options - Configuration options
|
|
79
|
+
* @returns Error boundary state and methods
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const MyComponent = defineComponent({
|
|
84
|
+
* tag: 'my-component',
|
|
85
|
+
* setup(props, ctx) {
|
|
86
|
+
* const { error, reset, hasError } = createErrorBoundaryState({
|
|
87
|
+
* onError: (err) => logError(err),
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* const handleClick = () => {
|
|
91
|
+
* withErrorBoundary(() => {
|
|
92
|
+
* // risky operation
|
|
93
|
+
* }, error);
|
|
94
|
+
* };
|
|
95
|
+
*
|
|
96
|
+
* return { error, handleClick };
|
|
97
|
+
* }
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function createErrorBoundaryState(options?: {
|
|
102
|
+
onError?: (error: Error) => void;
|
|
103
|
+
onReset?: () => void;
|
|
104
|
+
}): ErrorBoundaryState;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dalila Error Boundary
|
|
3
|
+
*
|
|
4
|
+
* Provides error boundary component that captures errors in children
|
|
5
|
+
* and displays a fallback template.
|
|
6
|
+
*
|
|
7
|
+
* @module dalila/runtime/boundary
|
|
8
|
+
*/
|
|
9
|
+
import { effect, signal } from '../core/index.js';
|
|
10
|
+
import { bind } from './bind.js';
|
|
11
|
+
import { defineComponent } from './component.js';
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Error Boundary Component
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Creates an error boundary component that catches errors in its children.
|
|
17
|
+
*
|
|
18
|
+
* @param options - Configuration for the error boundary
|
|
19
|
+
* @returns Component with error boundary functionality
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const ErrorBoundary = createErrorBoundary({
|
|
24
|
+
* fallback: '<div class="error">Something went wrong</div>',
|
|
25
|
+
* onError: (err) => console.error(err),
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Use as component
|
|
29
|
+
* <error-boundary>
|
|
30
|
+
* <MyComponent />
|
|
31
|
+
* </error-boundary>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function createErrorBoundary(options) {
|
|
35
|
+
const { fallback: fallbackTemplate = '<div>Error occurred</div>', onError, onReset, } = options;
|
|
36
|
+
// Create signal for tracking error state
|
|
37
|
+
const errorSignal = signal(null);
|
|
38
|
+
const hasError = () => {
|
|
39
|
+
return errorSignal() !== null;
|
|
40
|
+
};
|
|
41
|
+
const reset = () => {
|
|
42
|
+
errorSignal.set(null);
|
|
43
|
+
onReset?.();
|
|
44
|
+
};
|
|
45
|
+
const escapedFallback = escapeAttribute(fallbackTemplate);
|
|
46
|
+
// Create the error boundary component
|
|
47
|
+
const tag = `error-boundary-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
48
|
+
const boundaryComponent = defineComponent({
|
|
49
|
+
tag,
|
|
50
|
+
template: `<div d-boundary="${escapedFallback}" d-boundary-error="$$boundaryError" d-boundary-reset="$$boundaryReset"><slot></slot></div>`,
|
|
51
|
+
props: {},
|
|
52
|
+
setup: () => {
|
|
53
|
+
if (onError) {
|
|
54
|
+
effect(() => {
|
|
55
|
+
const error = errorSignal();
|
|
56
|
+
if (error)
|
|
57
|
+
onError(error);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Return error state and reset function
|
|
61
|
+
return {
|
|
62
|
+
$$boundaryError: errorSignal,
|
|
63
|
+
$$boundaryReset: reset,
|
|
64
|
+
$$boundaryHasError: hasError,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// Return component with state
|
|
69
|
+
const result = {
|
|
70
|
+
component: boundaryComponent,
|
|
71
|
+
state: {
|
|
72
|
+
error: errorSignal,
|
|
73
|
+
reset,
|
|
74
|
+
hasError,
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return boundaryComponent;
|
|
78
|
+
}
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// d-boundary Directive (for use within existing bind context)
|
|
81
|
+
// ============================================================================
|
|
82
|
+
/**
|
|
83
|
+
* Bind d-boundary directive - wraps children with error handling
|
|
84
|
+
* This is typically used inside a component that provides error state
|
|
85
|
+
*/
|
|
86
|
+
export function bindBoundary(root, ctx, cleanups) {
|
|
87
|
+
const elements = qsaIncludingRoot(root, '[d-boundary]');
|
|
88
|
+
const boundary = root.closest('[data-dalila-internal-bound]');
|
|
89
|
+
const consumedNested = new WeakSet();
|
|
90
|
+
for (const el of elements) {
|
|
91
|
+
if (consumedNested.has(el))
|
|
92
|
+
continue;
|
|
93
|
+
// Skip stale nodes from the initial snapshot.
|
|
94
|
+
if (!root.contains(el))
|
|
95
|
+
continue;
|
|
96
|
+
if (el.closest('[data-dalila-internal-bound]') !== boundary)
|
|
97
|
+
continue;
|
|
98
|
+
for (const nested of Array.from(el.querySelectorAll('[d-boundary]'))) {
|
|
99
|
+
consumedNested.add(nested);
|
|
100
|
+
}
|
|
101
|
+
const fallbackTemplate = el.getAttribute('d-boundary')?.trim() || '<div>Error occurred</div>';
|
|
102
|
+
const errorBindingName = normalizeBinding(el.getAttribute('d-boundary-error'));
|
|
103
|
+
const resetBindingName = normalizeBinding(el.getAttribute('d-boundary-reset'));
|
|
104
|
+
// Get error and reset from context
|
|
105
|
+
const errorSignal = errorBindingName ? ctx[errorBindingName] : null;
|
|
106
|
+
const resetFn = resetBindingName ? ctx[resetBindingName] : null;
|
|
107
|
+
// Remove d-boundary attributes to prevent reprocessing
|
|
108
|
+
el.removeAttribute('d-boundary');
|
|
109
|
+
el.removeAttribute('d-boundary-error');
|
|
110
|
+
el.removeAttribute('d-boundary-reset');
|
|
111
|
+
const templateChildren = Array.from(el.childNodes).map((child) => child.cloneNode(true));
|
|
112
|
+
const host = el;
|
|
113
|
+
// Preserve host node and render boundary content inside it.
|
|
114
|
+
while (host.firstChild) {
|
|
115
|
+
host.removeChild(host.firstChild);
|
|
116
|
+
}
|
|
117
|
+
let mountedNode = null;
|
|
118
|
+
let mountedDispose = null;
|
|
119
|
+
const unmountCurrent = () => {
|
|
120
|
+
if (mountedDispose) {
|
|
121
|
+
mountedDispose();
|
|
122
|
+
mountedDispose = null;
|
|
123
|
+
}
|
|
124
|
+
if (mountedNode && mountedNode.parentNode) {
|
|
125
|
+
mountedNode.parentNode.removeChild(mountedNode);
|
|
126
|
+
}
|
|
127
|
+
mountedNode = null;
|
|
128
|
+
};
|
|
129
|
+
const mountChildren = () => {
|
|
130
|
+
unmountCurrent();
|
|
131
|
+
const childCtx = Object.create(ctx);
|
|
132
|
+
if (errorSignal)
|
|
133
|
+
childCtx.error = errorSignal;
|
|
134
|
+
if (resetFn)
|
|
135
|
+
childCtx.reset = resetFn;
|
|
136
|
+
const container = document.createElement('div');
|
|
137
|
+
container.setAttribute('data-boundary-children', '');
|
|
138
|
+
container.setAttribute('data-dalila-internal-bound', '');
|
|
139
|
+
for (const child of templateChildren) {
|
|
140
|
+
container.appendChild(child.cloneNode(true));
|
|
141
|
+
}
|
|
142
|
+
host.appendChild(container);
|
|
143
|
+
mountedDispose = bind(container, childCtx, {
|
|
144
|
+
_skipLifecycle: true,
|
|
145
|
+
});
|
|
146
|
+
mountedNode = container;
|
|
147
|
+
};
|
|
148
|
+
const mountError = (error) => {
|
|
149
|
+
unmountCurrent();
|
|
150
|
+
const errorCtx = Object.create(ctx);
|
|
151
|
+
if (errorSignal)
|
|
152
|
+
errorCtx.error = errorSignal;
|
|
153
|
+
if (resetFn)
|
|
154
|
+
errorCtx.reset = resetFn;
|
|
155
|
+
const errorDisplay = document.createElement('div');
|
|
156
|
+
errorDisplay.setAttribute('data-boundary-error', '');
|
|
157
|
+
errorDisplay.setAttribute('data-dalila-internal-bound', '');
|
|
158
|
+
errorDisplay.innerHTML = fallbackTemplate;
|
|
159
|
+
const errorMsg = errorDisplay.querySelector('[data-error-message]');
|
|
160
|
+
if (errorMsg) {
|
|
161
|
+
errorMsg.textContent = error.message;
|
|
162
|
+
}
|
|
163
|
+
host.appendChild(errorDisplay);
|
|
164
|
+
mountedDispose = bind(errorDisplay, errorCtx, {
|
|
165
|
+
_skipLifecycle: true,
|
|
166
|
+
});
|
|
167
|
+
mountedNode = errorDisplay;
|
|
168
|
+
};
|
|
169
|
+
if (errorSignal) {
|
|
170
|
+
const getCurrentError = () => (typeof errorSignal === 'function' ? errorSignal() : null);
|
|
171
|
+
let lastError = getCurrentError();
|
|
172
|
+
if (lastError) {
|
|
173
|
+
mountError(lastError);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
mountChildren();
|
|
177
|
+
}
|
|
178
|
+
const disposeEffect = effect(() => {
|
|
179
|
+
const error = getCurrentError();
|
|
180
|
+
if (error === lastError)
|
|
181
|
+
return;
|
|
182
|
+
lastError = error;
|
|
183
|
+
if (error) {
|
|
184
|
+
mountError(error);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
mountChildren();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
cleanups.push(disposeEffect);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
mountChildren();
|
|
194
|
+
}
|
|
195
|
+
cleanups.push(() => {
|
|
196
|
+
unmountCurrent();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Helper to find elements including root
|
|
201
|
+
function qsaIncludingRoot(root, selector) {
|
|
202
|
+
const out = [];
|
|
203
|
+
if (root.matches(selector))
|
|
204
|
+
out.push(root);
|
|
205
|
+
out.push(...Array.from(root.querySelectorAll(selector)));
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
// Helper to normalize binding
|
|
209
|
+
function normalizeBinding(raw) {
|
|
210
|
+
if (!raw)
|
|
211
|
+
return null;
|
|
212
|
+
const trimmed = raw.trim();
|
|
213
|
+
if (!trimmed)
|
|
214
|
+
return null;
|
|
215
|
+
return trimmed;
|
|
216
|
+
}
|
|
217
|
+
function escapeAttribute(value) {
|
|
218
|
+
return value
|
|
219
|
+
.replace(/&/g, '&')
|
|
220
|
+
.replace(/"/g, '"')
|
|
221
|
+
.replace(/</g, '<')
|
|
222
|
+
.replace(/>/g, '>');
|
|
223
|
+
}
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// withErrorBoundary Helper
|
|
226
|
+
// ============================================================================
|
|
227
|
+
/**
|
|
228
|
+
* Wraps a function with error boundary logic
|
|
229
|
+
*
|
|
230
|
+
* @param fn - Function that might throw
|
|
231
|
+
* @param errorSignal - Signal to store the error
|
|
232
|
+
* @returns Result of the function or undefined if error
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* const result = withErrorBoundary(
|
|
237
|
+
* () => riskyOperation(),
|
|
238
|
+
* errorSignal
|
|
239
|
+
* );
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
export function withErrorBoundary(fn, errorSignal) {
|
|
243
|
+
try {
|
|
244
|
+
return fn();
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
errorSignal.set(error instanceof Error ? error : new Error(String(error)));
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// createErrorBoundaryState (for use in setup)
|
|
253
|
+
// ============================================================================
|
|
254
|
+
/**
|
|
255
|
+
* Creates error boundary state for use in component setup
|
|
256
|
+
*
|
|
257
|
+
* @param options - Configuration options
|
|
258
|
+
* @returns Error boundary state and methods
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* const MyComponent = defineComponent({
|
|
263
|
+
* tag: 'my-component',
|
|
264
|
+
* setup(props, ctx) {
|
|
265
|
+
* const { error, reset, hasError } = createErrorBoundaryState({
|
|
266
|
+
* onError: (err) => logError(err),
|
|
267
|
+
* });
|
|
268
|
+
*
|
|
269
|
+
* const handleClick = () => {
|
|
270
|
+
* withErrorBoundary(() => {
|
|
271
|
+
* // risky operation
|
|
272
|
+
* }, error);
|
|
273
|
+
* };
|
|
274
|
+
*
|
|
275
|
+
* return { error, handleClick };
|
|
276
|
+
* }
|
|
277
|
+
* });
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
export function createErrorBoundaryState(options) {
|
|
281
|
+
const errorSignal = signal(null);
|
|
282
|
+
const hasError = () => {
|
|
283
|
+
return errorSignal() !== null;
|
|
284
|
+
};
|
|
285
|
+
const reset = () => {
|
|
286
|
+
errorSignal.set(null);
|
|
287
|
+
options?.onReset?.();
|
|
288
|
+
};
|
|
289
|
+
// Set up error handler
|
|
290
|
+
const handleError = options?.onError;
|
|
291
|
+
if (handleError) {
|
|
292
|
+
effect(() => {
|
|
293
|
+
const error = errorSignal();
|
|
294
|
+
if (error) {
|
|
295
|
+
handleError(error);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
error: errorSignal,
|
|
301
|
+
reset,
|
|
302
|
+
hasError,
|
|
303
|
+
};
|
|
304
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -12,3 +12,7 @@ export { fromHtml } from './fromHtml.js';
|
|
|
12
12
|
export type { FromHtmlOptions } from './fromHtml.js';
|
|
13
13
|
export { defineComponent } from './component.js';
|
|
14
14
|
export type { Component, ComponentDefinition, PropsSchema, PropSignals, SetupContext, TypedPropSignals, TypedSetupContext, EmitsSchema, RefsSchema, TypedEmit, TypedRef, } from './component.js';
|
|
15
|
+
export { createLazyComponent, createSuspense, getLazyComponent, preloadLazyComponent, isLazyComponentLoaded, getLazyComponentState, observeLazyElement, } from './lazy.js';
|
|
16
|
+
export type { LazyComponentOptions, LazyComponentState, LazyComponentLoader, LazyComponentResult, } from './lazy.js';
|
|
17
|
+
export { createErrorBoundary, bindBoundary, withErrorBoundary, createErrorBoundaryState, } from './boundary.js';
|
|
18
|
+
export type { ErrorBoundaryOptions, ErrorBoundaryState, ErrorBoundaryResult, } from './boundary.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -9,3 +9,5 @@
|
|
|
9
9
|
export { bind, autoBind, mount, configure, createPortalTarget, getVirtualListController, scrollToVirtualIndex } from './bind.js';
|
|
10
10
|
export { fromHtml } from './fromHtml.js';
|
|
11
11
|
export { defineComponent } from './component.js';
|
|
12
|
+
export { createLazyComponent, createSuspense, getLazyComponent, preloadLazyComponent, isLazyComponentLoaded, getLazyComponentState, observeLazyElement, } from './lazy.js';
|
|
13
|
+
export { createErrorBoundary, bindBoundary, withErrorBoundary, createErrorBoundaryState, } from './boundary.js';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dalila Lazy Components
|
|
3
|
+
*
|
|
4
|
+
* Provides lazy loading for components with:
|
|
5
|
+
* - createLazyComponent: Create a component that loads on demand
|
|
6
|
+
* - d-lazy directive: Load component when it enters viewport
|
|
7
|
+
* - d-suspense: Show fallback while component is loading
|
|
8
|
+
*
|
|
9
|
+
* @module dalila/runtime/lazy
|
|
10
|
+
*/
|
|
11
|
+
import { signal } from '../core/index.js';
|
|
12
|
+
import type { Component } from './component.js';
|
|
13
|
+
export interface LazyComponentOptions {
|
|
14
|
+
/** Loading fallback template */
|
|
15
|
+
loading?: string;
|
|
16
|
+
/** Error template to show on failure */
|
|
17
|
+
error?: string;
|
|
18
|
+
/** Delay before showing loading state (ms) */
|
|
19
|
+
loadingDelay?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface LazyComponentState {
|
|
22
|
+
/** Whether the component is currently loading */
|
|
23
|
+
loading: ReturnType<typeof signal<boolean>>;
|
|
24
|
+
/** Whether the component has failed to load */
|
|
25
|
+
error: ReturnType<typeof signal<Error | null>>;
|
|
26
|
+
/** The loaded component (if successful) */
|
|
27
|
+
component: ReturnType<typeof signal<Component | null>>;
|
|
28
|
+
/** Function to trigger loading */
|
|
29
|
+
load: () => void;
|
|
30
|
+
/** Function to retry after error */
|
|
31
|
+
retry: () => void;
|
|
32
|
+
/** Whether the component has been loaded */
|
|
33
|
+
loaded: ReturnType<typeof signal<boolean>>;
|
|
34
|
+
/** Default loading template (used by d-lazy when attribute is omitted) */
|
|
35
|
+
loadingTemplate: string;
|
|
36
|
+
/** Default error template (used by d-lazy when attribute is omitted) */
|
|
37
|
+
errorTemplate: string;
|
|
38
|
+
}
|
|
39
|
+
export type LazyComponentLoader = () => Promise<{
|
|
40
|
+
default: Component;
|
|
41
|
+
} | Component>;
|
|
42
|
+
export interface LazyComponentResult {
|
|
43
|
+
/** The lazy component definition */
|
|
44
|
+
component: Component;
|
|
45
|
+
/** Access to lazy loading state */
|
|
46
|
+
state: LazyComponentState;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get a lazy component by its tag name
|
|
50
|
+
*/
|
|
51
|
+
export declare function getLazyComponent(tag: string): LazyComponentResult | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Creates a lazy-loaded component that defers loading until needed.
|
|
54
|
+
*
|
|
55
|
+
* @param loader - Function that returns a promise resolving to the component
|
|
56
|
+
* @param options - Configuration options for the lazy component
|
|
57
|
+
* @returns Component definition with loading state
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const LazyModal = createLazyComponent(() => import('./Modal.svelte'));
|
|
62
|
+
*
|
|
63
|
+
* // With custom loading template
|
|
64
|
+
* const LazyModal = createLazyComponent(
|
|
65
|
+
* () => import('./Modal.svelte'),
|
|
66
|
+
* { loading: '<div>Loading...</div>' }
|
|
67
|
+
* );
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function createLazyComponent(loader: LazyComponentLoader, options?: LazyComponentOptions): Component;
|
|
71
|
+
/**
|
|
72
|
+
* Creates a suspense component that shows loading/ error states
|
|
73
|
+
* while child components are loading.
|
|
74
|
+
*
|
|
75
|
+
* @param options - Configuration for suspense behavior
|
|
76
|
+
* @returns Component that wraps children with suspense behavior
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```html
|
|
80
|
+
* <d-suspense>
|
|
81
|
+
* <d-placeholder>Loading...</d-placeholder>
|
|
82
|
+
* <my-heavy-component></my-heavy-component>
|
|
83
|
+
* </d-suspense>
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function createSuspense(options?: {
|
|
87
|
+
/** Template to show while loading */
|
|
88
|
+
loading?: string;
|
|
89
|
+
/** Template to show on error */
|
|
90
|
+
error?: string;
|
|
91
|
+
/** Delay before showing loading state */
|
|
92
|
+
loadingDelay?: number;
|
|
93
|
+
}): Component;
|
|
94
|
+
/**
|
|
95
|
+
* Preload a lazy component
|
|
96
|
+
*/
|
|
97
|
+
export declare function preloadLazyComponent(tag: string): void;
|
|
98
|
+
/**
|
|
99
|
+
* Check if a lazy component is loaded
|
|
100
|
+
*/
|
|
101
|
+
export declare function isLazyComponentLoaded(tag: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Get lazy component loading state
|
|
104
|
+
*/
|
|
105
|
+
export declare function getLazyComponentState(tag: string): LazyComponentState | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Observe an element for lazy loading when it enters viewport
|
|
108
|
+
*/
|
|
109
|
+
export declare function observeLazyElement(el: Element, loadCallback: () => void, threshold?: number): () => void;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dalila Lazy Components
|
|
3
|
+
*
|
|
4
|
+
* Provides lazy loading for components with:
|
|
5
|
+
* - createLazyComponent: Create a component that loads on demand
|
|
6
|
+
* - d-lazy directive: Load component when it enters viewport
|
|
7
|
+
* - d-suspense: Show fallback while component is loading
|
|
8
|
+
*
|
|
9
|
+
* @module dalila/runtime/lazy
|
|
10
|
+
*/
|
|
11
|
+
import { signal } from '../core/index.js';
|
|
12
|
+
import { defineComponent, isComponent } from './component.js';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Lazy Component Registry
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Global registry for lazy-loaded components
|
|
18
|
+
*/
|
|
19
|
+
const lazyComponentRegistry = new Map();
|
|
20
|
+
/**
|
|
21
|
+
* Get a lazy component by its tag name
|
|
22
|
+
*/
|
|
23
|
+
export function getLazyComponent(tag) {
|
|
24
|
+
return lazyComponentRegistry.get(tag);
|
|
25
|
+
}
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// createLazyComponent
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Creates a lazy-loaded component that defers loading until needed.
|
|
31
|
+
*
|
|
32
|
+
* @param loader - Function that returns a promise resolving to the component
|
|
33
|
+
* @param options - Configuration options for the lazy component
|
|
34
|
+
* @returns Component definition with loading state
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const LazyModal = createLazyComponent(() => import('./Modal.svelte'));
|
|
39
|
+
*
|
|
40
|
+
* // With custom loading template
|
|
41
|
+
* const LazyModal = createLazyComponent(
|
|
42
|
+
* () => import('./Modal.svelte'),
|
|
43
|
+
* { loading: '<div>Loading...</div>' }
|
|
44
|
+
* );
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function createLazyComponent(loader, options = {}) {
|
|
48
|
+
const { loadingDelay = 0, loading = '', error = '' } = options;
|
|
49
|
+
// Create signals for tracking load state
|
|
50
|
+
const loadingSignal = signal(false);
|
|
51
|
+
const errorSignal = signal(null);
|
|
52
|
+
const componentSignal = signal(null);
|
|
53
|
+
const loadedSignal = signal(false);
|
|
54
|
+
let loadingTimeout = null;
|
|
55
|
+
let loadInFlight = false;
|
|
56
|
+
const load = () => {
|
|
57
|
+
if (loadedSignal() || loadInFlight)
|
|
58
|
+
return;
|
|
59
|
+
loadInFlight = true;
|
|
60
|
+
// Handle loading delay
|
|
61
|
+
if (loadingDelay > 0) {
|
|
62
|
+
loadingTimeout = setTimeout(() => {
|
|
63
|
+
loadingSignal.set(true);
|
|
64
|
+
}, loadingDelay);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
loadingSignal.set(true);
|
|
68
|
+
}
|
|
69
|
+
errorSignal.set(null);
|
|
70
|
+
Promise.resolve()
|
|
71
|
+
.then(() => loader())
|
|
72
|
+
.then((module) => {
|
|
73
|
+
// Clear timeout if still pending
|
|
74
|
+
if (loadingTimeout) {
|
|
75
|
+
clearTimeout(loadingTimeout);
|
|
76
|
+
loadingTimeout = null;
|
|
77
|
+
}
|
|
78
|
+
const loadedComp = isComponent(module)
|
|
79
|
+
? module
|
|
80
|
+
: ('default' in module ? module.default : null);
|
|
81
|
+
if (!loadedComp) {
|
|
82
|
+
throw new Error('Lazy component: failed to load component from module');
|
|
83
|
+
}
|
|
84
|
+
componentSignal.set(loadedComp);
|
|
85
|
+
loadingSignal.set(false);
|
|
86
|
+
loadedSignal.set(true);
|
|
87
|
+
loadInFlight = false;
|
|
88
|
+
})
|
|
89
|
+
.catch((err) => {
|
|
90
|
+
if (loadingTimeout) {
|
|
91
|
+
clearTimeout(loadingTimeout);
|
|
92
|
+
loadingTimeout = null;
|
|
93
|
+
}
|
|
94
|
+
errorSignal.set(err instanceof Error ? err : new Error(String(err)));
|
|
95
|
+
loadingSignal.set(false);
|
|
96
|
+
loadInFlight = false;
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const retry = () => {
|
|
100
|
+
errorSignal.set(null);
|
|
101
|
+
load();
|
|
102
|
+
};
|
|
103
|
+
// Create the wrapper component
|
|
104
|
+
const tag = `lazy-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
105
|
+
const wrapperComponent = defineComponent({
|
|
106
|
+
tag,
|
|
107
|
+
template: '', // Required: empty template for lazy wrapper
|
|
108
|
+
props: {},
|
|
109
|
+
setup: () => {
|
|
110
|
+
// Auto-load when component is mounted
|
|
111
|
+
// The actual loading is triggered by the d-lazy directive or manually
|
|
112
|
+
return {
|
|
113
|
+
$$lazyLoading: loadingSignal,
|
|
114
|
+
$$lazyError: errorSignal,
|
|
115
|
+
$$lazyComponent: componentSignal,
|
|
116
|
+
$$lazyLoad: load,
|
|
117
|
+
$$lazyRetry: retry,
|
|
118
|
+
$$lazyLoaded: loadedSignal,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// Register in global registry for lookup by d-lazy directive
|
|
123
|
+
const result = {
|
|
124
|
+
component: wrapperComponent,
|
|
125
|
+
state: {
|
|
126
|
+
loading: loadingSignal,
|
|
127
|
+
error: errorSignal,
|
|
128
|
+
component: componentSignal,
|
|
129
|
+
load,
|
|
130
|
+
retry,
|
|
131
|
+
loaded: loadedSignal,
|
|
132
|
+
loadingTemplate: loading,
|
|
133
|
+
errorTemplate: error,
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
lazyComponentRegistry.set(tag, result);
|
|
137
|
+
return wrapperComponent;
|
|
138
|
+
}
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// d-suspense Component
|
|
141
|
+
// ============================================================================
|
|
142
|
+
/**
|
|
143
|
+
* Creates a suspense component that shows loading/ error states
|
|
144
|
+
* while child components are loading.
|
|
145
|
+
*
|
|
146
|
+
* @param options - Configuration for suspense behavior
|
|
147
|
+
* @returns Component that wraps children with suspense behavior
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```html
|
|
151
|
+
* <d-suspense>
|
|
152
|
+
* <d-placeholder>Loading...</d-placeholder>
|
|
153
|
+
* <my-heavy-component></my-heavy-component>
|
|
154
|
+
* </d-suspense>
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function createSuspense(options = {}) {
|
|
158
|
+
const suspenseTag = `d-suspense-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
159
|
+
return defineComponent({
|
|
160
|
+
tag: suspenseTag,
|
|
161
|
+
props: {},
|
|
162
|
+
// Keep children rendered; loading/error orchestration can be layered on top.
|
|
163
|
+
template: `<div data-suspense=""><slot></slot></div>`,
|
|
164
|
+
setup: () => {
|
|
165
|
+
// Reserved for future suspense orchestration (loading/error/loadingDelay).
|
|
166
|
+
void options;
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Lazy Loading Utilities
|
|
173
|
+
// ============================================================================
|
|
174
|
+
/**
|
|
175
|
+
* Preload a lazy component
|
|
176
|
+
*/
|
|
177
|
+
export function preloadLazyComponent(tag) {
|
|
178
|
+
const lazyResult = lazyComponentRegistry.get(tag);
|
|
179
|
+
if (lazyResult) {
|
|
180
|
+
lazyResult.state.load();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if a lazy component is loaded
|
|
185
|
+
*/
|
|
186
|
+
export function isLazyComponentLoaded(tag) {
|
|
187
|
+
const lazyResult = lazyComponentRegistry.get(tag);
|
|
188
|
+
return lazyResult?.state.loaded() ?? false;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get lazy component loading state
|
|
192
|
+
*/
|
|
193
|
+
export function getLazyComponentState(tag) {
|
|
194
|
+
return lazyComponentRegistry.get(tag)?.state;
|
|
195
|
+
}
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Intersection Observer for d-lazy
|
|
198
|
+
// ============================================================================
|
|
199
|
+
const lazyObserverElements = new WeakMap();
|
|
200
|
+
const lazyObservers = new Map();
|
|
201
|
+
function getLazyIntersectionObserver(threshold = 0) {
|
|
202
|
+
const currentCtor = globalThis.IntersectionObserver;
|
|
203
|
+
const cached = lazyObservers.get(threshold);
|
|
204
|
+
if (cached && cached.ctor === currentCtor) {
|
|
205
|
+
return cached.observer;
|
|
206
|
+
}
|
|
207
|
+
const observer = new IntersectionObserver((entries) => {
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (entry.isIntersecting) {
|
|
210
|
+
const data = lazyObserverElements.get(entry.target);
|
|
211
|
+
if (data?.callback) {
|
|
212
|
+
data.callback();
|
|
213
|
+
}
|
|
214
|
+
// Stop observing after triggering
|
|
215
|
+
observer?.unobserve(entry.target);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}, {
|
|
219
|
+
root: null, // viewport
|
|
220
|
+
rootMargin: '0px',
|
|
221
|
+
threshold,
|
|
222
|
+
});
|
|
223
|
+
lazyObservers.set(threshold, { observer, ctor: currentCtor });
|
|
224
|
+
return observer;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Observe an element for lazy loading when it enters viewport
|
|
228
|
+
*/
|
|
229
|
+
export function observeLazyElement(el, loadCallback, threshold = 0) {
|
|
230
|
+
if (typeof globalThis.IntersectionObserver !== 'function') {
|
|
231
|
+
// Graceful fallback for environments without IntersectionObserver.
|
|
232
|
+
// Load only if still connected after the current bind pass.
|
|
233
|
+
// This avoids eager loading elements removed by directives like d-if.
|
|
234
|
+
let canceled = false;
|
|
235
|
+
queueMicrotask(() => {
|
|
236
|
+
if (canceled)
|
|
237
|
+
return;
|
|
238
|
+
if (el.isConnected) {
|
|
239
|
+
loadCallback();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return () => {
|
|
243
|
+
canceled = true;
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const observer = getLazyIntersectionObserver(threshold);
|
|
247
|
+
lazyObserverElements.set(el, { callback: loadCallback, threshold });
|
|
248
|
+
observer.observe(el);
|
|
249
|
+
// Return cleanup function
|
|
250
|
+
return () => {
|
|
251
|
+
lazyObserverElements.delete(el);
|
|
252
|
+
observer.unobserve(el);
|
|
253
|
+
};
|
|
254
|
+
}
|