dalila 1.4.2 → 1.4.3
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/dist/context/auto-scope.d.ts +167 -0
- package/dist/context/auto-scope.js +381 -0
- package/dist/context/context.d.ts +111 -0
- package/dist/context/context.js +283 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/raw.d.ts +2 -0
- package/dist/context/raw.js +2 -0
- package/dist/core/dev.d.ts +7 -0
- package/dist/core/dev.js +14 -0
- package/dist/core/for.d.ts +42 -0
- package/dist/core/for.js +311 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +14 -0
- package/dist/core/key.d.ts +33 -0
- package/dist/core/key.js +83 -0
- package/dist/core/match.d.ts +22 -0
- package/dist/core/match.js +175 -0
- package/dist/core/mutation.d.ts +55 -0
- package/dist/core/mutation.js +128 -0
- package/dist/core/persist.d.ts +63 -0
- package/dist/core/persist.js +371 -0
- package/dist/core/query.d.ts +72 -0
- package/dist/core/query.js +184 -0
- package/dist/core/resource.d.ts +299 -0
- package/dist/core/resource.js +924 -0
- package/dist/core/scheduler.d.ts +111 -0
- package/dist/core/scheduler.js +243 -0
- package/dist/core/scope.d.ts +74 -0
- package/dist/core/scope.js +171 -0
- package/dist/core/signal.d.ts +88 -0
- package/dist/core/signal.js +451 -0
- package/dist/core/store.d.ts +130 -0
- package/dist/core/store.js +234 -0
- package/dist/core/virtual.d.ts +26 -0
- package/dist/core/virtual.js +277 -0
- package/dist/core/watch-testing.d.ts +13 -0
- package/dist/core/watch-testing.js +16 -0
- package/dist/core/watch.d.ts +81 -0
- package/dist/core/watch.js +353 -0
- package/dist/core/when.d.ts +23 -0
- package/dist/core/when.js +124 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/internal/watch-testing.d.ts +1 -0
- package/dist/internal/watch-testing.js +8 -0
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.js +1 -0
- package/dist/router/route.d.ts +23 -0
- package/dist/router/route.js +48 -0
- package/dist/router/router.d.ts +23 -0
- package/dist/router/router.js +169 -0
- package/dist/runtime/bind.d.ts +59 -0
- package/dist/runtime/bind.js +340 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.js +9 -0
- package/dist/simple.d.ts +11 -0
- package/dist/simple.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { signal } from '../core/signal.js';
|
|
2
|
+
import { createScope, withScope } from '../core/scope.js';
|
|
3
|
+
import { findRoute } from './route.js';
|
|
4
|
+
let currentRouter = null;
|
|
5
|
+
export function getCurrentRouter() {
|
|
6
|
+
return currentRouter;
|
|
7
|
+
}
|
|
8
|
+
export function createRouter(config) {
|
|
9
|
+
const routes = config.routes;
|
|
10
|
+
const routeSignal = signal({
|
|
11
|
+
path: '/',
|
|
12
|
+
params: {},
|
|
13
|
+
query: new URLSearchParams(),
|
|
14
|
+
hash: ''
|
|
15
|
+
});
|
|
16
|
+
let outletElement = null;
|
|
17
|
+
let currentScope = null;
|
|
18
|
+
let currentRouteState = null;
|
|
19
|
+
let scrollPositions = {};
|
|
20
|
+
let currentLoaderController = null;
|
|
21
|
+
async function updateRoute(path, replace = false) {
|
|
22
|
+
const match = findRoute(path, routes);
|
|
23
|
+
if (!match) {
|
|
24
|
+
console.warn(`No route found for path: ${path}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const fromState = currentRouteState;
|
|
28
|
+
const toState = {
|
|
29
|
+
path: match.route.path,
|
|
30
|
+
params: match.params,
|
|
31
|
+
query: match.query,
|
|
32
|
+
hash: match.hash
|
|
33
|
+
};
|
|
34
|
+
// Save scroll position before leaving current route
|
|
35
|
+
if (fromState && outletElement) {
|
|
36
|
+
scrollPositions[fromState.path] = window.scrollY;
|
|
37
|
+
}
|
|
38
|
+
// Call beforeEnter guard
|
|
39
|
+
if (match.route.beforeEnter) {
|
|
40
|
+
const canEnter = await match.route.beforeEnter(toState, fromState || toState);
|
|
41
|
+
if (!canEnter) {
|
|
42
|
+
console.log('Navigation cancelled by beforeEnter guard');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Abort any ongoing loader
|
|
47
|
+
if (currentLoaderController) {
|
|
48
|
+
currentLoaderController.abort();
|
|
49
|
+
}
|
|
50
|
+
// Dispose of previous scope
|
|
51
|
+
if (currentScope) {
|
|
52
|
+
// Call afterLeave hook
|
|
53
|
+
if (match.route.afterLeave) {
|
|
54
|
+
await match.route.afterLeave(fromState || toState, toState);
|
|
55
|
+
}
|
|
56
|
+
currentScope.dispose();
|
|
57
|
+
}
|
|
58
|
+
// Create new scope for this route
|
|
59
|
+
currentScope = createScope();
|
|
60
|
+
// Load data if route has a loader
|
|
61
|
+
let routeData = undefined;
|
|
62
|
+
if (match.route.loader) {
|
|
63
|
+
currentLoaderController = new AbortController();
|
|
64
|
+
try {
|
|
65
|
+
routeData = await match.route.loader({
|
|
66
|
+
...toState,
|
|
67
|
+
signal: currentLoaderController.signal
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.name !== 'AbortError') {
|
|
72
|
+
console.error('Loader failed:', error);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Update route signal
|
|
78
|
+
routeSignal.set(toState);
|
|
79
|
+
currentRouteState = toState;
|
|
80
|
+
// Update browser history
|
|
81
|
+
const url = path + (match.query.toString() ? `?${match.query}` : '') + (match.hash ? `#${match.hash}` : '');
|
|
82
|
+
if (replace) {
|
|
83
|
+
history.replaceState(null, '', url);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
history.pushState(null, '', url);
|
|
87
|
+
}
|
|
88
|
+
// Mount the view if outlet exists
|
|
89
|
+
if (outletElement) {
|
|
90
|
+
await mountView(match, routeData);
|
|
91
|
+
}
|
|
92
|
+
// Restore scroll position
|
|
93
|
+
if (toState.hash) {
|
|
94
|
+
const element = document.getElementById(toState.hash.slice(1));
|
|
95
|
+
if (element) {
|
|
96
|
+
element.scrollIntoView();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (scrollPositions[toState.path] !== undefined) {
|
|
100
|
+
window.scrollTo(0, scrollPositions[toState.path]);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
window.scrollTo(0, 0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function mountView(match, routeData) {
|
|
107
|
+
if (!outletElement || !currentScope)
|
|
108
|
+
return;
|
|
109
|
+
// Clear outlet
|
|
110
|
+
outletElement.innerHTML = '';
|
|
111
|
+
// Mount view within scope
|
|
112
|
+
withScope(currentScope, () => {
|
|
113
|
+
const viewResult = match.route.view();
|
|
114
|
+
const nodes = Array.isArray(viewResult) ? viewResult : [viewResult];
|
|
115
|
+
outletElement.append(...nodes);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function handlePopState() {
|
|
119
|
+
updateRoute(window.location.pathname + window.location.search + window.location.hash, true);
|
|
120
|
+
}
|
|
121
|
+
function handleLink(event) {
|
|
122
|
+
const target = event.target;
|
|
123
|
+
const anchor = target.closest('a');
|
|
124
|
+
if (!anchor)
|
|
125
|
+
return;
|
|
126
|
+
const href = anchor.getAttribute('href');
|
|
127
|
+
if (!href || href.startsWith('http') || href.startsWith('//') || anchor.target === '_blank') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
push(href);
|
|
132
|
+
}
|
|
133
|
+
function push(path) {
|
|
134
|
+
updateRoute(path, false);
|
|
135
|
+
}
|
|
136
|
+
function replace(path) {
|
|
137
|
+
updateRoute(path, true);
|
|
138
|
+
}
|
|
139
|
+
function back() {
|
|
140
|
+
window.history.back();
|
|
141
|
+
}
|
|
142
|
+
function mount(outlet) {
|
|
143
|
+
outletElement = outlet;
|
|
144
|
+
// Set up history API listener
|
|
145
|
+
window.addEventListener('popstate', handlePopState);
|
|
146
|
+
// Initialize with current location
|
|
147
|
+
const initialPath = window.location.pathname + window.location.search + window.location.hash;
|
|
148
|
+
updateRoute(initialPath, true);
|
|
149
|
+
}
|
|
150
|
+
function outlet() {
|
|
151
|
+
if (!outletElement) {
|
|
152
|
+
outletElement = document.createElement('div');
|
|
153
|
+
}
|
|
154
|
+
return outletElement;
|
|
155
|
+
}
|
|
156
|
+
// Create router instance
|
|
157
|
+
const router = {
|
|
158
|
+
mount,
|
|
159
|
+
push,
|
|
160
|
+
replace,
|
|
161
|
+
back,
|
|
162
|
+
link: handleLink,
|
|
163
|
+
outlet,
|
|
164
|
+
route: routeSignal
|
|
165
|
+
};
|
|
166
|
+
// Set as current router
|
|
167
|
+
currentRouter = router;
|
|
168
|
+
return router;
|
|
169
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dalila Template Runtime - bind()
|
|
3
|
+
*
|
|
4
|
+
* Binds a DOM tree to a reactive context using declarative attributes.
|
|
5
|
+
* No eval, no inline JS execution - only identifier resolution from ctx.
|
|
6
|
+
*
|
|
7
|
+
* @module dalila/runtime
|
|
8
|
+
*/
|
|
9
|
+
export interface BindOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Event types to bind (default: click, input, change, submit, keydown, keyup)
|
|
12
|
+
*/
|
|
13
|
+
events?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Selectors for elements where text interpolation should be skipped
|
|
16
|
+
*/
|
|
17
|
+
rawTextSelectors?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BindContext {
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
export type DisposeFunction = () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Bind a DOM tree to a reactive context.
|
|
25
|
+
*
|
|
26
|
+
* @param root - The root element to bind
|
|
27
|
+
* @param ctx - The context object containing handlers and reactive values
|
|
28
|
+
* @param options - Binding options
|
|
29
|
+
* @returns A dispose function that removes all bindings
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { bind } from 'dalila/runtime';
|
|
34
|
+
*
|
|
35
|
+
* const ctx = {
|
|
36
|
+
* count: signal(0),
|
|
37
|
+
* increment: () => count.update(n => n + 1),
|
|
38
|
+
* };
|
|
39
|
+
*
|
|
40
|
+
* const dispose = bind(document.getElementById('app')!, ctx);
|
|
41
|
+
*
|
|
42
|
+
* // Later, to cleanup:
|
|
43
|
+
* dispose();
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function bind(root: Element, ctx: BindContext, options?: BindOptions): DisposeFunction;
|
|
47
|
+
/**
|
|
48
|
+
* Automatically bind when DOM is ready.
|
|
49
|
+
* Useful for simple pages without a build step.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```html
|
|
53
|
+
* <script type="module">
|
|
54
|
+
* import { autoBind } from 'dalila/runtime';
|
|
55
|
+
* autoBind('#app', { count: signal(0) });
|
|
56
|
+
* </script>
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function autoBind(selector: string, ctx: BindContext, options?: BindOptions): Promise<DisposeFunction>;
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dalila Template Runtime - bind()
|
|
3
|
+
*
|
|
4
|
+
* Binds a DOM tree to a reactive context using declarative attributes.
|
|
5
|
+
* No eval, no inline JS execution - only identifier resolution from ctx.
|
|
6
|
+
*
|
|
7
|
+
* @module dalila/runtime
|
|
8
|
+
*/
|
|
9
|
+
import { effect, createScope, withScope, isInDevMode } from '../core/index.js';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Utilities
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Check if a value is a Dalila signal
|
|
15
|
+
*/
|
|
16
|
+
function isSignal(value) {
|
|
17
|
+
return typeof value === 'function' && 'set' in value && 'update' in value;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a value from ctx - handles signals, functions, and plain values
|
|
21
|
+
*/
|
|
22
|
+
function resolve(value) {
|
|
23
|
+
if (isSignal(value))
|
|
24
|
+
return value();
|
|
25
|
+
if (typeof value === 'function')
|
|
26
|
+
return value();
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Normalize binding attribute value
|
|
31
|
+
* Handles both "name" and "{name}" formats
|
|
32
|
+
*/
|
|
33
|
+
function normalizeBinding(raw) {
|
|
34
|
+
if (!raw)
|
|
35
|
+
return null;
|
|
36
|
+
const trimmed = raw.trim();
|
|
37
|
+
// Match {name} format and extract name
|
|
38
|
+
const match = trimmed.match(/^\{\s*([a-zA-Z_$][\w$]*)\s*\}$/);
|
|
39
|
+
return match ? match[1] : trimmed;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Dev mode warning helper
|
|
43
|
+
*/
|
|
44
|
+
function warn(message) {
|
|
45
|
+
if (isInDevMode()) {
|
|
46
|
+
console.warn(`[Dalila] ${message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Default Options
|
|
51
|
+
// ============================================================================
|
|
52
|
+
const DEFAULT_EVENTS = ['click', 'input', 'change', 'submit', 'keydown', 'keyup'];
|
|
53
|
+
const DEFAULT_RAW_TEXT_SELECTORS = 'pre, code';
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Text Interpolation
|
|
56
|
+
// ============================================================================
|
|
57
|
+
/**
|
|
58
|
+
* Process a text node and replace {tokens} with reactive bindings
|
|
59
|
+
*/
|
|
60
|
+
function bindTextNode(node, ctx, cleanups) {
|
|
61
|
+
const text = node.data;
|
|
62
|
+
const regex = /\{\s*([a-zA-Z_$][\w$]*)\s*\}/g;
|
|
63
|
+
// Check if there are any tokens
|
|
64
|
+
if (!regex.test(text))
|
|
65
|
+
return;
|
|
66
|
+
// Reset regex
|
|
67
|
+
regex.lastIndex = 0;
|
|
68
|
+
const frag = document.createDocumentFragment();
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = regex.exec(text)) !== null) {
|
|
72
|
+
// Add text before the token
|
|
73
|
+
const before = text.slice(cursor, match.index);
|
|
74
|
+
if (before) {
|
|
75
|
+
frag.appendChild(document.createTextNode(before));
|
|
76
|
+
}
|
|
77
|
+
const key = match[1];
|
|
78
|
+
const value = ctx[key];
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
warn(`Text interpolation: "${key}" not found in context`);
|
|
81
|
+
frag.appendChild(document.createTextNode(match[0]));
|
|
82
|
+
}
|
|
83
|
+
else if (isSignal(value)) {
|
|
84
|
+
// Reactive text node
|
|
85
|
+
const textNode = document.createTextNode('');
|
|
86
|
+
const stop = effect(() => {
|
|
87
|
+
textNode.data = String(value());
|
|
88
|
+
});
|
|
89
|
+
if (typeof stop === 'function') {
|
|
90
|
+
cleanups.push(stop);
|
|
91
|
+
}
|
|
92
|
+
frag.appendChild(textNode);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Static value - render once
|
|
96
|
+
frag.appendChild(document.createTextNode(String(value)));
|
|
97
|
+
}
|
|
98
|
+
cursor = match.index + match[0].length;
|
|
99
|
+
}
|
|
100
|
+
// Add remaining text
|
|
101
|
+
const after = text.slice(cursor);
|
|
102
|
+
if (after) {
|
|
103
|
+
frag.appendChild(document.createTextNode(after));
|
|
104
|
+
}
|
|
105
|
+
// Replace original node
|
|
106
|
+
if (node.parentNode) {
|
|
107
|
+
node.parentNode.replaceChild(frag, node);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Event Binding
|
|
112
|
+
// ============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* Bind all d-on-* events within root
|
|
115
|
+
*/
|
|
116
|
+
function bindEvents(root, ctx, events, cleanups) {
|
|
117
|
+
for (const eventName of events) {
|
|
118
|
+
const attr = `d-on-${eventName}`;
|
|
119
|
+
const elements = root.querySelectorAll(`[${attr}]`);
|
|
120
|
+
elements.forEach((el) => {
|
|
121
|
+
const handlerName = normalizeBinding(el.getAttribute(attr));
|
|
122
|
+
if (!handlerName)
|
|
123
|
+
return;
|
|
124
|
+
const handler = ctx[handlerName];
|
|
125
|
+
if (handler === undefined) {
|
|
126
|
+
warn(`Event handler "${handlerName}" not found in context`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (typeof handler !== 'function') {
|
|
130
|
+
warn(`Event handler "${handlerName}" is not a function`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
el.addEventListener(eventName, handler);
|
|
134
|
+
cleanups.push(() => el.removeEventListener(eventName, handler));
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// when Directive
|
|
140
|
+
// ============================================================================
|
|
141
|
+
/**
|
|
142
|
+
* Bind all [when] directives within root
|
|
143
|
+
*/
|
|
144
|
+
function bindWhen(root, ctx, cleanups) {
|
|
145
|
+
const elements = root.querySelectorAll('[when]');
|
|
146
|
+
elements.forEach((el) => {
|
|
147
|
+
const bindingName = normalizeBinding(el.getAttribute('when'));
|
|
148
|
+
if (!bindingName)
|
|
149
|
+
return;
|
|
150
|
+
const binding = ctx[bindingName];
|
|
151
|
+
if (binding === undefined) {
|
|
152
|
+
warn(`when: "${bindingName}" not found in context`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const htmlEl = el;
|
|
156
|
+
const stop = effect(() => {
|
|
157
|
+
const value = !!resolve(binding);
|
|
158
|
+
htmlEl.style.display = value ? '' : 'none';
|
|
159
|
+
});
|
|
160
|
+
if (typeof stop === 'function') {
|
|
161
|
+
cleanups.push(stop);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// match Directive
|
|
167
|
+
// ============================================================================
|
|
168
|
+
/**
|
|
169
|
+
* Bind all [match] directives within root
|
|
170
|
+
*/
|
|
171
|
+
function bindMatch(root, ctx, cleanups) {
|
|
172
|
+
const elements = root.querySelectorAll('[match]');
|
|
173
|
+
elements.forEach((el) => {
|
|
174
|
+
const bindingName = normalizeBinding(el.getAttribute('match'));
|
|
175
|
+
if (!bindingName)
|
|
176
|
+
return;
|
|
177
|
+
const binding = ctx[bindingName];
|
|
178
|
+
if (binding === undefined) {
|
|
179
|
+
warn(`match: "${bindingName}" not found in context`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const cases = Array.from(el.querySelectorAll('[case]'));
|
|
183
|
+
const stop = effect(() => {
|
|
184
|
+
const value = String(resolve(binding));
|
|
185
|
+
let matchedEl = null;
|
|
186
|
+
let defaultEl = null;
|
|
187
|
+
// First pass: hide all and find match/default
|
|
188
|
+
for (const caseEl of cases) {
|
|
189
|
+
caseEl.style.display = 'none';
|
|
190
|
+
const caseValue = caseEl.getAttribute('case');
|
|
191
|
+
if (caseValue === 'default') {
|
|
192
|
+
defaultEl = caseEl;
|
|
193
|
+
}
|
|
194
|
+
else if (caseValue === value && !matchedEl) {
|
|
195
|
+
matchedEl = caseEl;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Second pass: show match OR default (not both)
|
|
199
|
+
if (matchedEl) {
|
|
200
|
+
matchedEl.style.display = '';
|
|
201
|
+
}
|
|
202
|
+
else if (defaultEl) {
|
|
203
|
+
defaultEl.style.display = '';
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
if (typeof stop === 'function') {
|
|
207
|
+
cleanups.push(stop);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Main bind() Function
|
|
213
|
+
// ============================================================================
|
|
214
|
+
/**
|
|
215
|
+
* Bind a DOM tree to a reactive context.
|
|
216
|
+
*
|
|
217
|
+
* @param root - The root element to bind
|
|
218
|
+
* @param ctx - The context object containing handlers and reactive values
|
|
219
|
+
* @param options - Binding options
|
|
220
|
+
* @returns A dispose function that removes all bindings
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* import { bind } from 'dalila/runtime';
|
|
225
|
+
*
|
|
226
|
+
* const ctx = {
|
|
227
|
+
* count: signal(0),
|
|
228
|
+
* increment: () => count.update(n => n + 1),
|
|
229
|
+
* };
|
|
230
|
+
*
|
|
231
|
+
* const dispose = bind(document.getElementById('app')!, ctx);
|
|
232
|
+
*
|
|
233
|
+
* // Later, to cleanup:
|
|
234
|
+
* dispose();
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export function bind(root, ctx, options = {}) {
|
|
238
|
+
const events = options.events ?? DEFAULT_EVENTS;
|
|
239
|
+
const rawTextSelectors = options.rawTextSelectors ?? DEFAULT_RAW_TEXT_SELECTORS;
|
|
240
|
+
const htmlRoot = root;
|
|
241
|
+
// HMR support: Register binding context globally in dev mode
|
|
242
|
+
if (isInDevMode()) {
|
|
243
|
+
globalThis.__dalila_hmr_context = { root, ctx, options };
|
|
244
|
+
}
|
|
245
|
+
// Create a scope for this template binding
|
|
246
|
+
const templateScope = createScope();
|
|
247
|
+
const cleanups = [];
|
|
248
|
+
// Run all bindings within the template scope
|
|
249
|
+
withScope(templateScope, () => {
|
|
250
|
+
// 1. Text interpolation
|
|
251
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
252
|
+
const textNodes = [];
|
|
253
|
+
while (walker.nextNode()) {
|
|
254
|
+
const node = walker.currentNode;
|
|
255
|
+
// Skip nodes inside raw text containers
|
|
256
|
+
const parent = node.parentElement;
|
|
257
|
+
if (parent && parent.closest(rawTextSelectors)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (node.data.includes('{')) {
|
|
261
|
+
textNodes.push(node);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Process text nodes (collect first, then process to avoid walker issues)
|
|
265
|
+
for (const node of textNodes) {
|
|
266
|
+
bindTextNode(node, ctx, cleanups);
|
|
267
|
+
}
|
|
268
|
+
// 2. Event bindings
|
|
269
|
+
bindEvents(root, ctx, events, cleanups);
|
|
270
|
+
// 3. when directive
|
|
271
|
+
bindWhen(root, ctx, cleanups);
|
|
272
|
+
// 4. match directive
|
|
273
|
+
bindMatch(root, ctx, cleanups);
|
|
274
|
+
});
|
|
275
|
+
// Bindings complete: remove loading state and mark as ready
|
|
276
|
+
// Use microtask to ensure all effects have run at least once
|
|
277
|
+
queueMicrotask(() => {
|
|
278
|
+
htmlRoot.removeAttribute('d-loading');
|
|
279
|
+
htmlRoot.setAttribute('d-ready', '');
|
|
280
|
+
});
|
|
281
|
+
// Return dispose function
|
|
282
|
+
return () => {
|
|
283
|
+
// Run manual cleanups (event listeners)
|
|
284
|
+
for (const cleanup of cleanups) {
|
|
285
|
+
if (typeof cleanup === 'function') {
|
|
286
|
+
try {
|
|
287
|
+
cleanup();
|
|
288
|
+
}
|
|
289
|
+
catch (e) {
|
|
290
|
+
if (isInDevMode()) {
|
|
291
|
+
console.warn('[Dalila] Cleanup error:', e);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
cleanups.length = 0;
|
|
297
|
+
// Dispose template scope (stops all effects)
|
|
298
|
+
try {
|
|
299
|
+
templateScope.dispose();
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
if (isInDevMode()) {
|
|
303
|
+
console.warn('[Dalila] Scope dispose error:', e);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// Convenience: Auto-bind on DOMContentLoaded
|
|
310
|
+
// ============================================================================
|
|
311
|
+
/**
|
|
312
|
+
* Automatically bind when DOM is ready.
|
|
313
|
+
* Useful for simple pages without a build step.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```html
|
|
317
|
+
* <script type="module">
|
|
318
|
+
* import { autoBind } from 'dalila/runtime';
|
|
319
|
+
* autoBind('#app', { count: signal(0) });
|
|
320
|
+
* </script>
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
export function autoBind(selector, ctx, options) {
|
|
324
|
+
return new Promise((resolve, reject) => {
|
|
325
|
+
const doBind = () => {
|
|
326
|
+
const root = document.querySelector(selector);
|
|
327
|
+
if (!root) {
|
|
328
|
+
reject(new Error(`[Dalila] Element not found: ${selector}`));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
resolve(bind(root, ctx, options));
|
|
332
|
+
};
|
|
333
|
+
if (document.readyState === 'loading') {
|
|
334
|
+
document.addEventListener('DOMContentLoaded', doBind, { once: true });
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
doBind();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dalila Runtime Module
|
|
3
|
+
*
|
|
4
|
+
* Provides declarative DOM binding with reactive updates.
|
|
5
|
+
* No eval, no inline JS - only identifier resolution.
|
|
6
|
+
*
|
|
7
|
+
* @module dalila/runtime
|
|
8
|
+
*/
|
|
9
|
+
export { bind, autoBind } from './bind.js';
|
|
10
|
+
export type { BindOptions, BindContext, DisposeFunction } from './bind.js';
|
package/dist/simple.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dalila/simple - Simplified API
|
|
3
|
+
*
|
|
4
|
+
* Just the essentials:
|
|
5
|
+
* - Global store (no Zustand needed)
|
|
6
|
+
* - Auto-scope context (menos verboso)
|
|
7
|
+
* - Core reactivity primitives
|
|
8
|
+
*
|
|
9
|
+
* Philosophy: Use HTML templates (via dev-server), not hyperscript.
|
|
10
|
+
*/
|
|
11
|
+
export * from './core/index.js';
|
package/dist/simple.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dalila/simple - Simplified API
|
|
3
|
+
*
|
|
4
|
+
* Just the essentials:
|
|
5
|
+
* - Global store (no Zustand needed)
|
|
6
|
+
* - Auto-scope context (menos verboso)
|
|
7
|
+
* - Core reactivity primitives
|
|
8
|
+
*
|
|
9
|
+
* Philosophy: Use HTML templates (via dev-server), not hyperscript.
|
|
10
|
+
*/
|
|
11
|
+
export * from './core/index.js';
|