dalila 1.9.2 → 1.9.4
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 +35 -23
- package/dist/cli/check.d.ts +3 -0
- package/dist/cli/check.js +902 -0
- package/dist/cli/index.js +53 -11
- package/dist/cli/routes-generator.d.ts +25 -0
- package/dist/cli/routes-generator.js +28 -7
- package/dist/router/route-tables.d.ts +28 -7
- package/dist/router/route-tables.js +19 -0
- package/dist/router/router.js +87 -3
- package/dist/routes.generated.d.ts +2 -0
- package/dist/routes.generated.js +76 -0
- package/dist/routes.generated.manifest.d.ts +4 -0
- package/dist/routes.generated.manifest.js +32 -0
- package/dist/routes.generated.types.d.ts +11 -0
- package/dist/routes.generated.types.js +37 -0
- package/dist/runtime/bind.d.ts +47 -2
- package/dist/runtime/bind.js +702 -26
- package/dist/runtime/component.d.ts +74 -0
- package/dist/runtime/component.js +40 -0
- package/dist/runtime/fromHtml.d.ts +3 -2
- package/dist/runtime/fromHtml.js +0 -15
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +2 -1
- package/package.json +2 -2
- package/scripts/dev-server.cjs +47 -7
package/dist/runtime/bind.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { effect, createScope, withScope, isInDevMode, signal, computeVirtualRange } from '../core/index.js';
|
|
10
10
|
import { WRAPPED_HANDLER } from '../form/form.js';
|
|
11
11
|
import { linkScopeToDom, withDevtoolsDomTarget } from '../core/devtools.js';
|
|
12
|
+
import { isComponent, normalizePropDef, coercePropValue, kebabToCamel, camelToKebab } from './component.js';
|
|
12
13
|
// ============================================================================
|
|
13
14
|
// Utilities
|
|
14
15
|
// ============================================================================
|
|
@@ -18,6 +19,20 @@ import { linkScopeToDom, withDevtoolsDomTarget } from '../core/devtools.js';
|
|
|
18
19
|
function isSignal(value) {
|
|
19
20
|
return typeof value === 'function' && 'set' in value && 'update' in value;
|
|
20
21
|
}
|
|
22
|
+
function isWritableSignal(value) {
|
|
23
|
+
if (!isSignal(value))
|
|
24
|
+
return false;
|
|
25
|
+
// `computed()` exposes set/update that always throw. Probe with a no-op write
|
|
26
|
+
// (same value) to detect read-only signals without mutating state.
|
|
27
|
+
try {
|
|
28
|
+
const current = value.peek();
|
|
29
|
+
value.set(current);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
/**
|
|
22
37
|
* Resolve a value from ctx - handles signals, functions, and plain values.
|
|
23
38
|
* Only zero-arity functions are called (getters/computed). Functions with
|
|
@@ -749,6 +764,8 @@ function expressionDependsOnReactiveSource(node, ctx) {
|
|
|
749
764
|
// ============================================================================
|
|
750
765
|
const DEFAULT_EVENTS = ['click', 'input', 'change', 'submit', 'keydown', 'keyup'];
|
|
751
766
|
const DEFAULT_RAW_TEXT_SELECTORS = 'pre, code';
|
|
767
|
+
const COMPONENT_REGISTRY_KEY = '__dalila_component_registry__';
|
|
768
|
+
const COMPONENT_EMIT_KEY = '__dalila_component_emit__';
|
|
752
769
|
// ============================================================================
|
|
753
770
|
// Text Interpolation
|
|
754
771
|
// ============================================================================
|
|
@@ -1054,6 +1071,71 @@ function bindEvents(root, ctx, events, cleanups) {
|
|
|
1054
1071
|
}
|
|
1055
1072
|
}
|
|
1056
1073
|
// ============================================================================
|
|
1074
|
+
// d-emit-<event> Directive
|
|
1075
|
+
// ============================================================================
|
|
1076
|
+
/**
|
|
1077
|
+
* Bind all [d-emit-<event>] directives within root.
|
|
1078
|
+
* Only active inside component contexts (where COMPONENT_EMIT_KEY exists).
|
|
1079
|
+
* Works inside d-each because child contexts inherit via prototype chain.
|
|
1080
|
+
*/
|
|
1081
|
+
function bindEmit(root, ctx, cleanups) {
|
|
1082
|
+
const emitFn = ctx[COMPONENT_EMIT_KEY];
|
|
1083
|
+
if (typeof emitFn !== 'function')
|
|
1084
|
+
return;
|
|
1085
|
+
const elements = qsaIncludingRoot(root, '*');
|
|
1086
|
+
for (const el of elements) {
|
|
1087
|
+
for (const attrNode of Array.from(el.attributes)) {
|
|
1088
|
+
if (!attrNode.name.startsWith('d-emit-'))
|
|
1089
|
+
continue;
|
|
1090
|
+
if (attrNode.name === 'd-emit-value')
|
|
1091
|
+
continue;
|
|
1092
|
+
const eventName = attrNode.name.slice('d-emit-'.length).trim();
|
|
1093
|
+
const attr = attrNode.name;
|
|
1094
|
+
if (!eventName) {
|
|
1095
|
+
warn(`${attr}: missing DOM event name`);
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
const emitName = normalizeBinding(attrNode.value);
|
|
1099
|
+
if (!emitName) {
|
|
1100
|
+
warn(`${attr}: empty value ignored`);
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const payloadExpr = el.getAttribute('d-emit-value');
|
|
1104
|
+
const payloadRaw = payloadExpr?.trim();
|
|
1105
|
+
let payloadAst = null;
|
|
1106
|
+
if (payloadRaw) {
|
|
1107
|
+
try {
|
|
1108
|
+
payloadAst = parseExpression(payloadRaw);
|
|
1109
|
+
}
|
|
1110
|
+
catch (err) {
|
|
1111
|
+
warn(`${attr}: invalid d-emit-value="${payloadRaw}" (${err.message})`);
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
else if (payloadExpr !== null) {
|
|
1116
|
+
warn(`${attr}: d-emit-value is empty; emitting DOM Event instead`);
|
|
1117
|
+
}
|
|
1118
|
+
if (emitName.includes(':')) {
|
|
1119
|
+
warn(`${attr}: ":" syntax is no longer supported. Use d-emit-value instead.`);
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
const handler = (e) => {
|
|
1123
|
+
if (payloadAst) {
|
|
1124
|
+
const eventCtx = Object.create(ctx);
|
|
1125
|
+
eventCtx.$event = e;
|
|
1126
|
+
const result = evalExpressionAst(payloadAst, eventCtx);
|
|
1127
|
+
emitFn(emitName, result.ok ? result.value : undefined);
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
emitFn(emitName, e);
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
el.addEventListener(eventName, handler);
|
|
1134
|
+
cleanups.push(() => el.removeEventListener(eventName, handler));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// ============================================================================
|
|
1057
1139
|
// d-when Directive
|
|
1058
1140
|
// ============================================================================
|
|
1059
1141
|
/**
|
|
@@ -1532,7 +1614,17 @@ function bindEach(root, ctx, cleanups) {
|
|
|
1532
1614
|
const elements = qsaIncludingRoot(root, '[d-each]')
|
|
1533
1615
|
.filter(el => !el.parentElement?.closest('[d-each], [d-virtual-each]'));
|
|
1534
1616
|
for (const el of elements) {
|
|
1535
|
-
const
|
|
1617
|
+
const rawValue = el.getAttribute('d-each')?.trim() ?? '';
|
|
1618
|
+
let bindingName;
|
|
1619
|
+
let alias = 'item'; // default
|
|
1620
|
+
const asMatch = rawValue.match(/^(\S+)\s+as\s+(\S+)$/);
|
|
1621
|
+
if (asMatch) {
|
|
1622
|
+
bindingName = normalizeBinding(asMatch[1]);
|
|
1623
|
+
alias = asMatch[2];
|
|
1624
|
+
}
|
|
1625
|
+
else {
|
|
1626
|
+
bindingName = normalizeBinding(rawValue);
|
|
1627
|
+
}
|
|
1536
1628
|
if (!bindingName)
|
|
1537
1629
|
continue;
|
|
1538
1630
|
let binding = ctx[bindingName];
|
|
@@ -1588,7 +1680,7 @@ function bindEach(root, ctx, cleanups) {
|
|
|
1588
1680
|
if (keyBinding) {
|
|
1589
1681
|
if (keyBinding === '$index')
|
|
1590
1682
|
return index;
|
|
1591
|
-
if (keyBinding === 'item')
|
|
1683
|
+
if (keyBinding === alias || keyBinding === 'item')
|
|
1592
1684
|
return item;
|
|
1593
1685
|
if (typeof item === 'object' && item !== null && keyBinding in item) {
|
|
1594
1686
|
return item[keyBinding];
|
|
@@ -1627,8 +1719,11 @@ function bindEach(root, ctx, cleanups) {
|
|
|
1627
1719
|
};
|
|
1628
1720
|
metadataByKey.set(key, metadata);
|
|
1629
1721
|
itemsByKey.set(key, item);
|
|
1630
|
-
// Expose item + positional / collection helpers.
|
|
1631
|
-
itemCtx
|
|
1722
|
+
// Expose item under the alias name + positional / collection helpers.
|
|
1723
|
+
itemCtx[alias] = item;
|
|
1724
|
+
if (alias !== 'item') {
|
|
1725
|
+
itemCtx.item = item; // backward compat
|
|
1726
|
+
}
|
|
1632
1727
|
itemCtx.key = key;
|
|
1633
1728
|
itemCtx.$index = metadata.$index;
|
|
1634
1729
|
itemCtx.$count = metadata.$count;
|
|
@@ -1758,6 +1853,7 @@ function bindEach(root, ctx, cleanups) {
|
|
|
1758
1853
|
*/
|
|
1759
1854
|
function bindIf(root, ctx, cleanups) {
|
|
1760
1855
|
const elements = qsaIncludingRoot(root, '[d-if]');
|
|
1856
|
+
const processedElse = new Set();
|
|
1761
1857
|
for (const el of elements) {
|
|
1762
1858
|
const bindingName = normalizeBinding(el.getAttribute('d-if'));
|
|
1763
1859
|
if (!bindingName)
|
|
@@ -1767,29 +1863,69 @@ function bindIf(root, ctx, cleanups) {
|
|
|
1767
1863
|
warn(`d-if: "${bindingName}" not found in context`);
|
|
1768
1864
|
continue;
|
|
1769
1865
|
}
|
|
1866
|
+
// Detect d-else sibling BEFORE removing from DOM
|
|
1867
|
+
const elseEl = el.nextElementSibling?.hasAttribute('d-else') ? el.nextElementSibling : null;
|
|
1770
1868
|
const comment = document.createComment('d-if');
|
|
1771
1869
|
el.parentNode?.replaceChild(comment, el);
|
|
1772
1870
|
el.removeAttribute('d-if');
|
|
1773
1871
|
const htmlEl = el;
|
|
1872
|
+
// Handle d-else branch
|
|
1873
|
+
let elseHtmlEl = null;
|
|
1874
|
+
let elseComment = null;
|
|
1875
|
+
if (elseEl) {
|
|
1876
|
+
processedElse.add(elseEl);
|
|
1877
|
+
elseComment = document.createComment('d-else');
|
|
1878
|
+
elseEl.parentNode?.replaceChild(elseComment, elseEl);
|
|
1879
|
+
elseEl.removeAttribute('d-else');
|
|
1880
|
+
elseHtmlEl = elseEl;
|
|
1881
|
+
}
|
|
1774
1882
|
// Apply initial state synchronously to avoid FOUC
|
|
1775
1883
|
const initialValue = !!resolve(binding);
|
|
1776
1884
|
if (initialValue) {
|
|
1777
1885
|
comment.parentNode?.insertBefore(htmlEl, comment);
|
|
1778
1886
|
}
|
|
1887
|
+
else if (elseHtmlEl && elseComment) {
|
|
1888
|
+
elseComment.parentNode?.insertBefore(elseHtmlEl, elseComment);
|
|
1889
|
+
}
|
|
1779
1890
|
// Then create reactive effect to keep it updated
|
|
1780
|
-
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1891
|
+
if (elseHtmlEl && elseComment) {
|
|
1892
|
+
const capturedElseEl = elseHtmlEl;
|
|
1893
|
+
const capturedElseComment = elseComment;
|
|
1894
|
+
bindEffect(htmlEl, () => {
|
|
1895
|
+
const value = !!resolve(binding);
|
|
1896
|
+
if (value) {
|
|
1897
|
+
if (!htmlEl.parentNode) {
|
|
1898
|
+
comment.parentNode?.insertBefore(htmlEl, comment);
|
|
1899
|
+
}
|
|
1900
|
+
if (capturedElseEl.parentNode) {
|
|
1901
|
+
capturedElseEl.parentNode.removeChild(capturedElseEl);
|
|
1902
|
+
}
|
|
1785
1903
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1904
|
+
else {
|
|
1905
|
+
if (htmlEl.parentNode) {
|
|
1906
|
+
htmlEl.parentNode.removeChild(htmlEl);
|
|
1907
|
+
}
|
|
1908
|
+
if (!capturedElseEl.parentNode) {
|
|
1909
|
+
capturedElseComment.parentNode?.insertBefore(capturedElseEl, capturedElseComment);
|
|
1910
|
+
}
|
|
1790
1911
|
}
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
else {
|
|
1915
|
+
bindEffect(htmlEl, () => {
|
|
1916
|
+
const value = !!resolve(binding);
|
|
1917
|
+
if (value) {
|
|
1918
|
+
if (!htmlEl.parentNode) {
|
|
1919
|
+
comment.parentNode?.insertBefore(htmlEl, comment);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
if (htmlEl.parentNode) {
|
|
1924
|
+
htmlEl.parentNode.removeChild(htmlEl);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1793
1929
|
}
|
|
1794
1930
|
}
|
|
1795
1931
|
// ============================================================================
|
|
@@ -1833,6 +1969,38 @@ function bindHtml(root, ctx, cleanups) {
|
|
|
1833
1969
|
}
|
|
1834
1970
|
}
|
|
1835
1971
|
// ============================================================================
|
|
1972
|
+
// d-text Directive
|
|
1973
|
+
// ============================================================================
|
|
1974
|
+
/**
|
|
1975
|
+
* Bind all [d-text] directives within root.
|
|
1976
|
+
* Sets textContent — safe from XSS by design (no HTML parsing).
|
|
1977
|
+
* Counterpart to d-html which renders raw HTML.
|
|
1978
|
+
*/
|
|
1979
|
+
function bindText(root, ctx, cleanups) {
|
|
1980
|
+
const elements = qsaIncludingRoot(root, '[d-text]');
|
|
1981
|
+
for (const el of elements) {
|
|
1982
|
+
const bindingName = normalizeBinding(el.getAttribute('d-text'));
|
|
1983
|
+
if (!bindingName)
|
|
1984
|
+
continue;
|
|
1985
|
+
const binding = ctx[bindingName];
|
|
1986
|
+
if (binding === undefined) {
|
|
1987
|
+
warn(`d-text: "${bindingName}" not found in context`);
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
const htmlEl = el;
|
|
1991
|
+
// Sync initial render
|
|
1992
|
+
const initial = resolve(binding);
|
|
1993
|
+
htmlEl.textContent = initial == null ? '' : String(initial);
|
|
1994
|
+
// Reactive effect only if needed
|
|
1995
|
+
if (isSignal(binding) || (typeof binding === 'function' && binding.length === 0)) {
|
|
1996
|
+
bindEffect(htmlEl, () => {
|
|
1997
|
+
const v = resolve(binding);
|
|
1998
|
+
htmlEl.textContent = v == null ? '' : String(v);
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
// ============================================================================
|
|
1836
2004
|
// d-attr Directive
|
|
1837
2005
|
// ============================================================================
|
|
1838
2006
|
/**
|
|
@@ -1897,6 +2065,57 @@ function bindAttrs(root, ctx, cleanups) {
|
|
|
1897
2065
|
}
|
|
1898
2066
|
}
|
|
1899
2067
|
// ============================================================================
|
|
2068
|
+
// d-bind-* Directive (Two-way Binding)
|
|
2069
|
+
// ============================================================================
|
|
2070
|
+
/**
|
|
2071
|
+
* Bind all [d-bind-value] and [d-bind-checked] directives within root.
|
|
2072
|
+
* Two-way binding: signal → DOM (outbound) and DOM → signal (inbound).
|
|
2073
|
+
* Only works with signals — logs a warning otherwise.
|
|
2074
|
+
*/
|
|
2075
|
+
function bindTwoWay(root, ctx, cleanups) {
|
|
2076
|
+
const SUPPORTED = ['value', 'checked'];
|
|
2077
|
+
for (const prop of SUPPORTED) {
|
|
2078
|
+
const attr = `d-bind-${prop}`;
|
|
2079
|
+
const elements = qsaIncludingRoot(root, `[${attr}]`);
|
|
2080
|
+
for (const el of elements) {
|
|
2081
|
+
const bindingName = normalizeBinding(el.getAttribute(attr));
|
|
2082
|
+
if (!bindingName)
|
|
2083
|
+
continue;
|
|
2084
|
+
const binding = ctx[bindingName];
|
|
2085
|
+
if (!isSignal(binding)) {
|
|
2086
|
+
warn(`d-bind-${prop}: "${bindingName}" must be a signal`);
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
const writable = isWritableSignal(binding);
|
|
2090
|
+
if (!writable) {
|
|
2091
|
+
warn(`d-bind-${prop}: "${bindingName}" is read-only (inbound updates disabled)`);
|
|
2092
|
+
}
|
|
2093
|
+
el.removeAttribute(attr);
|
|
2094
|
+
// Outbound: signal → DOM
|
|
2095
|
+
const isBoolean = prop === 'checked';
|
|
2096
|
+
bindEffect(el, () => {
|
|
2097
|
+
const val = binding();
|
|
2098
|
+
if (isBoolean) {
|
|
2099
|
+
el[prop] = !!val;
|
|
2100
|
+
}
|
|
2101
|
+
else {
|
|
2102
|
+
el[prop] = val == null ? '' : String(val);
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
// Inbound: DOM → signal
|
|
2106
|
+
if (writable) {
|
|
2107
|
+
const eventName = el.tagName === 'SELECT' || isBoolean ? 'change' : 'input';
|
|
2108
|
+
const handler = () => {
|
|
2109
|
+
const val = isBoolean ? el.checked : el.value;
|
|
2110
|
+
binding.set(val);
|
|
2111
|
+
};
|
|
2112
|
+
el.addEventListener(eventName, handler);
|
|
2113
|
+
cleanups.push(() => el.removeEventListener(eventName, handler));
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
// ============================================================================
|
|
1900
2119
|
// Form Directives
|
|
1901
2120
|
// ============================================================================
|
|
1902
2121
|
/**
|
|
@@ -2445,6 +2664,344 @@ function bindArrayOperations(container, fieldArray, cleanups) {
|
|
|
2445
2664
|
}
|
|
2446
2665
|
}
|
|
2447
2666
|
// ============================================================================
|
|
2667
|
+
// d-ref — declarative element references
|
|
2668
|
+
// ============================================================================
|
|
2669
|
+
function bindRef(root, refs) {
|
|
2670
|
+
const elements = qsaIncludingRoot(root, '[d-ref]');
|
|
2671
|
+
for (const el of elements) {
|
|
2672
|
+
const name = el.getAttribute('d-ref');
|
|
2673
|
+
if (!name || !name.trim()) {
|
|
2674
|
+
warn('d-ref: empty ref name ignored');
|
|
2675
|
+
continue;
|
|
2676
|
+
}
|
|
2677
|
+
const trimmed = name.trim();
|
|
2678
|
+
if (refs.has(trimmed)) {
|
|
2679
|
+
warn(`d-ref: duplicate ref name "${trimmed}" in the same scope`);
|
|
2680
|
+
}
|
|
2681
|
+
refs.set(trimmed, el);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
// ============================================================================
|
|
2685
|
+
// Component System
|
|
2686
|
+
// ============================================================================
|
|
2687
|
+
function extractSlots(el) {
|
|
2688
|
+
const getSlotName = (node) => {
|
|
2689
|
+
const raw = node.getAttribute('d-slot') ?? node.getAttribute('slot');
|
|
2690
|
+
if (!raw)
|
|
2691
|
+
return null;
|
|
2692
|
+
const name = raw.trim();
|
|
2693
|
+
return name || null;
|
|
2694
|
+
};
|
|
2695
|
+
const namedSlots = new Map();
|
|
2696
|
+
const defaultSlot = document.createDocumentFragment();
|
|
2697
|
+
for (const child of Array.from(el.childNodes)) {
|
|
2698
|
+
if (child instanceof Element && child.tagName === 'TEMPLATE') {
|
|
2699
|
+
const name = getSlotName(child);
|
|
2700
|
+
if (name) {
|
|
2701
|
+
const frag = namedSlots.get(name) ?? document.createDocumentFragment();
|
|
2702
|
+
frag.append(...Array.from(child.content.childNodes));
|
|
2703
|
+
namedSlots.set(name, frag);
|
|
2704
|
+
}
|
|
2705
|
+
else {
|
|
2706
|
+
defaultSlot.appendChild(child);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
else if (child instanceof Element) {
|
|
2710
|
+
const name = getSlotName(child);
|
|
2711
|
+
if (name) {
|
|
2712
|
+
const frag = namedSlots.get(name) ?? document.createDocumentFragment();
|
|
2713
|
+
child.removeAttribute('d-slot');
|
|
2714
|
+
child.removeAttribute('slot');
|
|
2715
|
+
frag.appendChild(child);
|
|
2716
|
+
namedSlots.set(name, frag);
|
|
2717
|
+
}
|
|
2718
|
+
else {
|
|
2719
|
+
defaultSlot.appendChild(child);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
else {
|
|
2723
|
+
defaultSlot.appendChild(child);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
return { defaultSlot, namedSlots };
|
|
2727
|
+
}
|
|
2728
|
+
function fillSlots(root, defaultSlot, namedSlots) {
|
|
2729
|
+
for (const slotEl of Array.from(root.querySelectorAll('slot[name]'))) {
|
|
2730
|
+
const name = slotEl.getAttribute('name');
|
|
2731
|
+
const content = namedSlots.get(name);
|
|
2732
|
+
if (content && content.childNodes.length > 0)
|
|
2733
|
+
slotEl.replaceWith(content);
|
|
2734
|
+
}
|
|
2735
|
+
const defaultSlotEl = root.querySelector('slot:not([name])');
|
|
2736
|
+
if (defaultSlotEl && defaultSlot.childNodes.length > 0)
|
|
2737
|
+
defaultSlotEl.replaceWith(defaultSlot);
|
|
2738
|
+
}
|
|
2739
|
+
function bindSlotFragments(defaultSlot, namedSlots, parentCtx, events, cleanups) {
|
|
2740
|
+
const bindFrag = (frag) => {
|
|
2741
|
+
if (frag.childNodes.length === 0)
|
|
2742
|
+
return;
|
|
2743
|
+
const container = document.createElement('div');
|
|
2744
|
+
container.setAttribute('data-dalila-internal-bound', '');
|
|
2745
|
+
container.appendChild(frag);
|
|
2746
|
+
const handle = bind(container, parentCtx, { events, _skipLifecycle: true, _internal: true });
|
|
2747
|
+
cleanups.push(handle);
|
|
2748
|
+
while (container.firstChild)
|
|
2749
|
+
frag.appendChild(container.firstChild);
|
|
2750
|
+
};
|
|
2751
|
+
bindFrag(defaultSlot);
|
|
2752
|
+
for (const frag of namedSlots.values())
|
|
2753
|
+
bindFrag(frag);
|
|
2754
|
+
}
|
|
2755
|
+
function resolveComponentProps(el, parentCtx, def) {
|
|
2756
|
+
const props = {};
|
|
2757
|
+
const schema = def.props ?? {};
|
|
2758
|
+
const hasSchema = Object.keys(schema).length > 0;
|
|
2759
|
+
const PREFIX = 'd-props-';
|
|
2760
|
+
for (const attr of Array.from(el.attributes)) {
|
|
2761
|
+
if (!attr.name.startsWith(PREFIX))
|
|
2762
|
+
continue;
|
|
2763
|
+
const kebab = attr.name.slice(PREFIX.length);
|
|
2764
|
+
const propName = kebabToCamel(kebab);
|
|
2765
|
+
if (hasSchema && !(propName in schema)) {
|
|
2766
|
+
warn(`Component <${def.tag}>: d-props-${kebab} is not declared in props schema`);
|
|
2767
|
+
}
|
|
2768
|
+
const bindingName = normalizeBinding(attr.value);
|
|
2769
|
+
if (!bindingName)
|
|
2770
|
+
continue;
|
|
2771
|
+
const parentValue = parentCtx[bindingName];
|
|
2772
|
+
if (parentValue === undefined) {
|
|
2773
|
+
warn(`d-props-${kebab}: "${bindingName}" not found in parent context`);
|
|
2774
|
+
}
|
|
2775
|
+
// Read raw value — signals and zero-arity functions (getters/computed-like) are
|
|
2776
|
+
// unwrapped reactively; everything else passes through as-is.
|
|
2777
|
+
const isGetter = !isSignal(parentValue) && typeof parentValue === 'function' && parentValue.length === 0;
|
|
2778
|
+
const raw = isSignal(parentValue) ? parentValue() : isGetter ? parentValue() : parentValue;
|
|
2779
|
+
const propSignal = signal(raw);
|
|
2780
|
+
// Reactive sync: signals and zero-arity getters
|
|
2781
|
+
if (isSignal(parentValue) || isGetter) {
|
|
2782
|
+
effect(() => { propSignal.set(isSignal(parentValue) ? parentValue() : parentValue()); });
|
|
2783
|
+
}
|
|
2784
|
+
props[propName] = propSignal;
|
|
2785
|
+
}
|
|
2786
|
+
for (const [propName, propOption] of Object.entries(schema)) {
|
|
2787
|
+
if (props[propName])
|
|
2788
|
+
continue;
|
|
2789
|
+
const propDef = normalizePropDef(propOption);
|
|
2790
|
+
const kebabPropName = camelToKebab(propName);
|
|
2791
|
+
const attrName = el.hasAttribute(propName)
|
|
2792
|
+
? propName
|
|
2793
|
+
: (el.hasAttribute(kebabPropName) ? kebabPropName : null);
|
|
2794
|
+
if (attrName) {
|
|
2795
|
+
const raw = el.getAttribute(attrName);
|
|
2796
|
+
// Dev warning: Array/Object props should use d-props-*
|
|
2797
|
+
if (propDef.type === Array || propDef.type === Object) {
|
|
2798
|
+
warn(`Component <${def.tag}>: prop "${propName}" has type ${propDef.type === Array ? 'Array' : 'Object'} ` +
|
|
2799
|
+
`but received a static string attribute. Use d-props-${camelToKebab(propName)} to pass reactive data.`);
|
|
2800
|
+
}
|
|
2801
|
+
props[propName] = signal(coercePropValue(raw, propDef.type));
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
if (propDef.default !== undefined) {
|
|
2805
|
+
const defaultValue = typeof propDef.default === 'function'
|
|
2806
|
+
? propDef.default()
|
|
2807
|
+
: propDef.default;
|
|
2808
|
+
props[propName] = signal(defaultValue);
|
|
2809
|
+
}
|
|
2810
|
+
else {
|
|
2811
|
+
if (propDef.required) {
|
|
2812
|
+
warn(`Component <${def.tag}>: required prop "${propName}" was not provided`);
|
|
2813
|
+
}
|
|
2814
|
+
props[propName] = signal(undefined);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return props;
|
|
2819
|
+
}
|
|
2820
|
+
function getComponentRegistry(ctx) {
|
|
2821
|
+
const reg = ctx[COMPONENT_REGISTRY_KEY];
|
|
2822
|
+
return reg instanceof Map ? reg : null;
|
|
2823
|
+
}
|
|
2824
|
+
function bindComponents(root, ctx, events, cleanups, onMountError) {
|
|
2825
|
+
const registry = getComponentRegistry(ctx);
|
|
2826
|
+
if (!registry || registry.size === 0)
|
|
2827
|
+
return;
|
|
2828
|
+
const tagSelector = Array.from(registry.keys()).join(', ');
|
|
2829
|
+
const elements = qsaIncludingRoot(root, tagSelector);
|
|
2830
|
+
const boundary = root.closest('[data-dalila-internal-bound]');
|
|
2831
|
+
for (const el of elements) {
|
|
2832
|
+
// Skip stale entries from the initial snapshot.
|
|
2833
|
+
// Earlier iterations may replace/move nodes (e.g. slot projection),
|
|
2834
|
+
// so this element might no longer belong to the current bind boundary.
|
|
2835
|
+
if (!root.contains(el))
|
|
2836
|
+
continue;
|
|
2837
|
+
if (el.closest('[data-dalila-internal-bound]') !== boundary)
|
|
2838
|
+
continue;
|
|
2839
|
+
const tag = el.tagName.toLowerCase();
|
|
2840
|
+
const component = registry.get(tag);
|
|
2841
|
+
if (!component)
|
|
2842
|
+
continue;
|
|
2843
|
+
const def = component.definition;
|
|
2844
|
+
// 1. Extract slots
|
|
2845
|
+
const { defaultSlot, namedSlots } = extractSlots(el);
|
|
2846
|
+
// 2. Create component DOM
|
|
2847
|
+
const templateEl = document.createElement('template');
|
|
2848
|
+
templateEl.innerHTML = def.template.trim();
|
|
2849
|
+
const content = templateEl.content;
|
|
2850
|
+
// Dev-mode template validation
|
|
2851
|
+
if (isInDevMode()) {
|
|
2852
|
+
if (!def.template.trim()) {
|
|
2853
|
+
warn(`Component <${def.tag}>: template is empty`);
|
|
2854
|
+
}
|
|
2855
|
+
else if (content.childNodes.length === 0) {
|
|
2856
|
+
warn(`Component <${def.tag}>: template produced no DOM nodes`);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
// Single-root optimization: no wrapper needed
|
|
2860
|
+
// A d-each on the sole element will clone siblings at runtime, so it needs a container.
|
|
2861
|
+
const elementChildren = Array.from(content.children);
|
|
2862
|
+
const hasOnlyOneElement = elementChildren.length === 1
|
|
2863
|
+
&& !elementChildren[0].hasAttribute('d-each')
|
|
2864
|
+
&& Array.from(content.childNodes).every(n => n === elementChildren[0] || (n.nodeType === 3 && !n.textContent.trim()));
|
|
2865
|
+
let componentRoot;
|
|
2866
|
+
if (hasOnlyOneElement) {
|
|
2867
|
+
componentRoot = elementChildren[0];
|
|
2868
|
+
content.removeChild(componentRoot);
|
|
2869
|
+
}
|
|
2870
|
+
else {
|
|
2871
|
+
componentRoot = document.createElement('dalila-c');
|
|
2872
|
+
componentRoot.style.display = 'contents';
|
|
2873
|
+
componentRoot.appendChild(content);
|
|
2874
|
+
}
|
|
2875
|
+
// 3. Create component scope (child of current template scope)
|
|
2876
|
+
const componentScope = createScope();
|
|
2877
|
+
const pendingMountCallbacks = [];
|
|
2878
|
+
// 4. Within component scope: resolve props, run setup, bind
|
|
2879
|
+
let componentHandle = null;
|
|
2880
|
+
// Collect d-on-* event handlers from the component tag for ctx.emit()
|
|
2881
|
+
const componentEventHandlers = {};
|
|
2882
|
+
for (const attr of Array.from(el.attributes)) {
|
|
2883
|
+
if (!attr.name.startsWith('d-on-'))
|
|
2884
|
+
continue;
|
|
2885
|
+
const eventName = attr.name.slice(5); // "d-on-select" → "select"
|
|
2886
|
+
const handlerName = normalizeBinding(attr.value);
|
|
2887
|
+
if (!handlerName)
|
|
2888
|
+
continue;
|
|
2889
|
+
const handler = ctx[handlerName];
|
|
2890
|
+
if (typeof handler === 'function') {
|
|
2891
|
+
componentEventHandlers[eventName] = handler;
|
|
2892
|
+
}
|
|
2893
|
+
else if (handler !== undefined) {
|
|
2894
|
+
warn(`Component <${def.tag}>: d-on-${eventName}="${handlerName}" is not a function`);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
withScope(componentScope, () => {
|
|
2898
|
+
// 4a. Resolve props
|
|
2899
|
+
const propSignals = resolveComponentProps(el, ctx, def);
|
|
2900
|
+
// 4b. Create ref accessor + emit
|
|
2901
|
+
const setupCtx = {
|
|
2902
|
+
ref: (name) => componentHandle?.getRef(name) ?? null,
|
|
2903
|
+
refs: () => componentHandle?.getRefs() ?? Object.freeze({}),
|
|
2904
|
+
emit: (event, ...args) => {
|
|
2905
|
+
const handler = componentEventHandlers[event];
|
|
2906
|
+
if (typeof handler === 'function')
|
|
2907
|
+
handler(...args);
|
|
2908
|
+
},
|
|
2909
|
+
onMount: (fn) => {
|
|
2910
|
+
pendingMountCallbacks.push(fn);
|
|
2911
|
+
},
|
|
2912
|
+
onCleanup: (fn) => {
|
|
2913
|
+
componentScope.onCleanup(fn);
|
|
2914
|
+
},
|
|
2915
|
+
};
|
|
2916
|
+
// 4c. Run setup
|
|
2917
|
+
let setupReturn = {};
|
|
2918
|
+
if (def.setup) {
|
|
2919
|
+
setupReturn = def.setup(propSignals, setupCtx);
|
|
2920
|
+
for (const key of Object.keys(setupReturn)) {
|
|
2921
|
+
if (key in propSignals) {
|
|
2922
|
+
warn(`Component <${def.tag}>: setup() returned "${key}" which overrides a prop binding`);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
// 4d. Build component bind context (propagate registry for nested components)
|
|
2927
|
+
const componentCtx = { ...propSignals, ...setupReturn };
|
|
2928
|
+
const parentRegistry = getComponentRegistry(ctx);
|
|
2929
|
+
if (parentRegistry) {
|
|
2930
|
+
componentCtx[COMPONENT_REGISTRY_KEY] = parentRegistry;
|
|
2931
|
+
}
|
|
2932
|
+
// 4d'. Store emit function for d-emit-* directives
|
|
2933
|
+
componentCtx[COMPONENT_EMIT_KEY] = (event, ...args) => {
|
|
2934
|
+
const handler = componentEventHandlers[event];
|
|
2935
|
+
if (typeof handler === 'function')
|
|
2936
|
+
handler(...args);
|
|
2937
|
+
};
|
|
2938
|
+
// 4e. Bind slot content with PARENT context/scope
|
|
2939
|
+
const parentScope = componentScope.parent;
|
|
2940
|
+
if (parentScope) {
|
|
2941
|
+
withScope(parentScope, () => {
|
|
2942
|
+
bindSlotFragments(defaultSlot, namedSlots, ctx, events, cleanups);
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
// 4f. Fill slots
|
|
2946
|
+
fillSlots(componentRoot, defaultSlot, namedSlots);
|
|
2947
|
+
// 4g. Mark as bound boundary
|
|
2948
|
+
componentRoot.setAttribute('data-dalila-internal-bound', '');
|
|
2949
|
+
// 4h. Bind component template
|
|
2950
|
+
componentHandle = bind(componentRoot, componentCtx, { events, _skipLifecycle: true, _internal: true });
|
|
2951
|
+
cleanups.push(componentHandle);
|
|
2952
|
+
});
|
|
2953
|
+
// 5. Replace original tag with component DOM
|
|
2954
|
+
el.replaceWith(componentRoot);
|
|
2955
|
+
// 6. Run component onMount callbacks after the DOM swap.
|
|
2956
|
+
if (pendingMountCallbacks.length > 0) {
|
|
2957
|
+
withScope(componentScope, () => {
|
|
2958
|
+
for (const cb of pendingMountCallbacks) {
|
|
2959
|
+
if (onMountError === 'throw') {
|
|
2960
|
+
cb();
|
|
2961
|
+
}
|
|
2962
|
+
else {
|
|
2963
|
+
try {
|
|
2964
|
+
cb();
|
|
2965
|
+
}
|
|
2966
|
+
catch (err) {
|
|
2967
|
+
console.error(`[Dalila] Component <${def.tag}> onMount() threw:`, err);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
// 7. Register scope cleanup
|
|
2974
|
+
cleanups.push(() => componentScope.dispose());
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
// ============================================================================
|
|
2978
|
+
// Global Configuration
|
|
2979
|
+
// ============================================================================
|
|
2980
|
+
let globalConfig = {};
|
|
2981
|
+
/**
|
|
2982
|
+
* Set global defaults for all `bind()` / `mount()` calls.
|
|
2983
|
+
*
|
|
2984
|
+
* Options set here are merged with per-call options (per-call wins).
|
|
2985
|
+
* Call with an empty object to reset.
|
|
2986
|
+
*
|
|
2987
|
+
* @example
|
|
2988
|
+
* ```ts
|
|
2989
|
+
* import { configure } from 'dalila/runtime';
|
|
2990
|
+
*
|
|
2991
|
+
* configure({
|
|
2992
|
+
* components: [FruitPicker],
|
|
2993
|
+
* onMountError: 'log',
|
|
2994
|
+
* });
|
|
2995
|
+
* ```
|
|
2996
|
+
*/
|
|
2997
|
+
export function configure(config) {
|
|
2998
|
+
if (Object.keys(config).length === 0) {
|
|
2999
|
+
globalConfig = {};
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
globalConfig = { ...globalConfig, ...config };
|
|
3003
|
+
}
|
|
3004
|
+
// ============================================================================
|
|
2448
3005
|
// Main bind() Function
|
|
2449
3006
|
// ============================================================================
|
|
2450
3007
|
/**
|
|
@@ -2471,7 +3028,76 @@ function bindArrayOperations(container, fieldArray, cleanups) {
|
|
|
2471
3028
|
* ```
|
|
2472
3029
|
*/
|
|
2473
3030
|
export function bind(root, ctx, options = {}) {
|
|
3031
|
+
// ── Merge global config with per-call options ──
|
|
3032
|
+
if (Object.keys(globalConfig).length > 0) {
|
|
3033
|
+
const { components: globalComponents, ...globalRest } = globalConfig;
|
|
3034
|
+
const { components: localComponents, ...localRest } = options;
|
|
3035
|
+
const mergedOpts = { ...globalRest, ...localRest };
|
|
3036
|
+
// Combine component registries: local takes precedence over global
|
|
3037
|
+
if (globalComponents || localComponents) {
|
|
3038
|
+
const combined = {};
|
|
3039
|
+
const mergeComponents = (src) => {
|
|
3040
|
+
if (!src)
|
|
3041
|
+
return;
|
|
3042
|
+
if (Array.isArray(src)) {
|
|
3043
|
+
for (const comp of src) {
|
|
3044
|
+
if (isComponent(comp))
|
|
3045
|
+
combined[comp.definition.tag] = comp;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
else {
|
|
3049
|
+
for (const [key, comp] of Object.entries(src)) {
|
|
3050
|
+
if (isComponent(comp))
|
|
3051
|
+
combined[comp.definition.tag] = comp;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
mergeComponents(globalComponents);
|
|
3056
|
+
mergeComponents(localComponents); // local wins
|
|
3057
|
+
mergedOpts.components = combined;
|
|
3058
|
+
}
|
|
3059
|
+
options = mergedOpts;
|
|
3060
|
+
}
|
|
3061
|
+
// ── Resolve string selector ──
|
|
3062
|
+
if (typeof root === 'string') {
|
|
3063
|
+
const found = document.querySelector(root);
|
|
3064
|
+
if (!found)
|
|
3065
|
+
throw new Error(`[Dalila] bind: element not found: ${root}`);
|
|
3066
|
+
root = found;
|
|
3067
|
+
}
|
|
3068
|
+
// ── Component registry propagation via context ──
|
|
3069
|
+
if (options.components) {
|
|
3070
|
+
const existing = ctx[COMPONENT_REGISTRY_KEY];
|
|
3071
|
+
const merged = new Map(existing instanceof Map ? existing : []);
|
|
3072
|
+
if (Array.isArray(options.components)) {
|
|
3073
|
+
for (const comp of options.components) {
|
|
3074
|
+
if (!isComponent(comp)) {
|
|
3075
|
+
warn('bind: components[] contains an invalid component entry');
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
merged.set(comp.definition.tag, comp);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
else {
|
|
3082
|
+
for (const [key, comp] of Object.entries(options.components)) {
|
|
3083
|
+
if (!isComponent(comp)) {
|
|
3084
|
+
warn(`bind: components["${key}"] is not a valid component`);
|
|
3085
|
+
continue;
|
|
3086
|
+
}
|
|
3087
|
+
const tag = comp.definition.tag;
|
|
3088
|
+
if (key !== tag) {
|
|
3089
|
+
warn(`bind: components key "${key}" differs from component tag "${tag}" (using "${tag}")`);
|
|
3090
|
+
}
|
|
3091
|
+
merged.set(tag, comp);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
// Preserve prototype/inherited lookups from the original context.
|
|
3095
|
+
const ctxWithRegistry = Object.create(ctx);
|
|
3096
|
+
ctxWithRegistry[COMPONENT_REGISTRY_KEY] = merged;
|
|
3097
|
+
ctx = ctxWithRegistry;
|
|
3098
|
+
}
|
|
2474
3099
|
const events = options.events ?? DEFAULT_EVENTS;
|
|
3100
|
+
const onMountError = options.onMountError ?? 'log';
|
|
2475
3101
|
const rawTextSelectors = options.rawTextSelectors ?? DEFAULT_RAW_TEXT_SELECTORS;
|
|
2476
3102
|
const templatePlanCacheConfig = resolveTemplatePlanCacheConfig(options);
|
|
2477
3103
|
const benchSession = createBindBenchSession();
|
|
@@ -2484,6 +3110,7 @@ export function bind(root, ctx, options = {}) {
|
|
|
2484
3110
|
// Create a scope for this template binding
|
|
2485
3111
|
const templateScope = createScope();
|
|
2486
3112
|
const cleanups = [];
|
|
3113
|
+
const refs = new Map();
|
|
2487
3114
|
linkScopeToDom(templateScope, root, describeBindRoot(root));
|
|
2488
3115
|
// Run all bindings within the template scope
|
|
2489
3116
|
withScope(templateScope, () => {
|
|
@@ -2495,24 +3122,34 @@ export function bind(root, ctx, options = {}) {
|
|
|
2495
3122
|
bindVirtualEach(root, ctx, cleanups);
|
|
2496
3123
|
// 4. d-each — must run early: removes templates before TreeWalker visits them
|
|
2497
3124
|
bindEach(root, ctx, cleanups);
|
|
2498
|
-
// 5.
|
|
3125
|
+
// 5. Components — must run after d-each but before d-ref / text interpolation
|
|
3126
|
+
bindComponents(root, ctx, events, cleanups, onMountError);
|
|
3127
|
+
// 6. d-ref — collect element references (after d-each removes templates)
|
|
3128
|
+
bindRef(root, refs);
|
|
3129
|
+
// 6.5. d-text — safe textContent binding (before text interpolation)
|
|
3130
|
+
bindText(root, ctx, cleanups);
|
|
3131
|
+
// 7. Text interpolation (template plan cache + lazy parser fallback)
|
|
2499
3132
|
bindTextInterpolation(root, ctx, rawTextSelectors, templatePlanCacheConfig, benchSession);
|
|
2500
|
-
//
|
|
3133
|
+
// 8. d-attr bindings
|
|
2501
3134
|
bindAttrs(root, ctx, cleanups);
|
|
2502
|
-
//
|
|
3135
|
+
// 9. d-bind-* two-way bindings
|
|
3136
|
+
bindTwoWay(root, ctx, cleanups);
|
|
3137
|
+
// 10. d-html bindings
|
|
2503
3138
|
bindHtml(root, ctx, cleanups);
|
|
2504
|
-
//
|
|
3139
|
+
// 11. Form fields — register fields with form instances
|
|
2505
3140
|
bindField(root, ctx, cleanups);
|
|
2506
|
-
//
|
|
3141
|
+
// 12. Event bindings
|
|
2507
3142
|
bindEvents(root, ctx, events, cleanups);
|
|
2508
|
-
//
|
|
3143
|
+
// 13. d-emit-* bindings (component template → parent)
|
|
3144
|
+
bindEmit(root, ctx, cleanups);
|
|
3145
|
+
// 14. d-when directive
|
|
2509
3146
|
bindWhen(root, ctx, cleanups);
|
|
2510
|
-
//
|
|
3147
|
+
// 15. d-match directive
|
|
2511
3148
|
bindMatch(root, ctx, cleanups);
|
|
2512
|
-
//
|
|
3149
|
+
// 16. Form error displays — BEFORE d-if to bind errors in conditionally rendered sections
|
|
2513
3150
|
bindError(root, ctx, cleanups);
|
|
2514
3151
|
bindFormError(root, ctx, cleanups);
|
|
2515
|
-
//
|
|
3152
|
+
// 17. d-if — must run last: elements are fully bound before conditional removal
|
|
2516
3153
|
bindIf(root, ctx, cleanups);
|
|
2517
3154
|
});
|
|
2518
3155
|
// Bindings complete: remove loading state and mark as ready.
|
|
@@ -2524,8 +3161,8 @@ export function bind(root, ctx, options = {}) {
|
|
|
2524
3161
|
});
|
|
2525
3162
|
}
|
|
2526
3163
|
flushBindBenchSession(benchSession);
|
|
2527
|
-
// Return dispose
|
|
2528
|
-
|
|
3164
|
+
// Return BindHandle (callable dispose + ref accessors)
|
|
3165
|
+
const dispose = () => {
|
|
2529
3166
|
// Run manual cleanups (event listeners)
|
|
2530
3167
|
for (const cleanup of cleanups) {
|
|
2531
3168
|
if (typeof cleanup === 'function') {
|
|
@@ -2540,6 +3177,7 @@ export function bind(root, ctx, options = {}) {
|
|
|
2540
3177
|
}
|
|
2541
3178
|
}
|
|
2542
3179
|
cleanups.length = 0;
|
|
3180
|
+
refs.clear();
|
|
2543
3181
|
// Dispose template scope (stops all effects)
|
|
2544
3182
|
try {
|
|
2545
3183
|
templateScope.dispose();
|
|
@@ -2550,6 +3188,15 @@ export function bind(root, ctx, options = {}) {
|
|
|
2550
3188
|
}
|
|
2551
3189
|
}
|
|
2552
3190
|
};
|
|
3191
|
+
const handle = Object.assign(dispose, {
|
|
3192
|
+
getRef(name) {
|
|
3193
|
+
return refs.get(name) ?? null;
|
|
3194
|
+
},
|
|
3195
|
+
getRefs() {
|
|
3196
|
+
return Object.freeze(Object.fromEntries(refs));
|
|
3197
|
+
},
|
|
3198
|
+
});
|
|
3199
|
+
return handle;
|
|
2553
3200
|
}
|
|
2554
3201
|
// ============================================================================
|
|
2555
3202
|
// Convenience: Auto-bind on DOMContentLoaded
|
|
@@ -2584,3 +3231,32 @@ export function autoBind(selector, ctx, options) {
|
|
|
2584
3231
|
}
|
|
2585
3232
|
});
|
|
2586
3233
|
}
|
|
3234
|
+
export function mount(first, second, third) {
|
|
3235
|
+
// Overload 1: mount(selector, vm, options?)
|
|
3236
|
+
if (typeof first === 'string' && !isComponent(first)) {
|
|
3237
|
+
return bind(first, second, (third ?? {}));
|
|
3238
|
+
}
|
|
3239
|
+
// Overload 2: mount(component, target, props?)
|
|
3240
|
+
const component = first;
|
|
3241
|
+
const target = second;
|
|
3242
|
+
const props = third;
|
|
3243
|
+
const def = component.definition;
|
|
3244
|
+
const el = document.createElement(def.tag);
|
|
3245
|
+
if (props) {
|
|
3246
|
+
for (const key of Object.keys(props)) {
|
|
3247
|
+
el.setAttribute(`d-props-${camelToKebab(key)}`, key);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
target.appendChild(el);
|
|
3251
|
+
const parentCtx = {};
|
|
3252
|
+
if (props) {
|
|
3253
|
+
for (const [key, value] of Object.entries(props)) {
|
|
3254
|
+
parentCtx[key] = isSignal(value) ? value : signal(value);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
return bind(el, parentCtx, {
|
|
3258
|
+
components: { [def.tag]: component },
|
|
3259
|
+
_skipLifecycle: true,
|
|
3260
|
+
_internal: true,
|
|
3261
|
+
});
|
|
3262
|
+
}
|