dalila 1.9.3 → 1.9.5
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 +32 -21
- package/dist/cli/check.js +1 -1
- package/dist/cli/index.js +5 -5
- package/dist/cli/routes-generator.js +23 -2
- package/dist/router/route-tables.d.ts +5 -1
- 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 +34 -1
- package/dist/runtime/bind.js +756 -25
- package/dist/runtime/component.d.ts +74 -0
- package/dist/runtime/component.js +40 -0
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.js +2 -1
- package/package.json +1 -1
- package/scripts/dev-server.cjs +47 -7
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Dalila
|
|
1
|
+
# 🐰✂️ Dalila
|
|
2
2
|
|
|
3
3
|
**DOM-first reactivity without the re-renders.**
|
|
4
4
|
|
|
@@ -58,7 +58,8 @@ bind(document.getElementById('app')!, ctx);
|
|
|
58
58
|
|
|
59
59
|
### Runtime
|
|
60
60
|
|
|
61
|
-
- [Template Binding](./docs/runtime/bind.md) — `bind()`, text interpolation, events
|
|
61
|
+
- [Template Binding](./docs/runtime/bind.md) — `bind()`, `mount()`, `configure()`, text interpolation, events
|
|
62
|
+
- [Components](./docs/runtime/component.md) — `defineComponent`, typed props/emits/refs, slots
|
|
62
63
|
- [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
|
|
63
64
|
|
|
64
65
|
### Routing
|
|
@@ -107,7 +108,7 @@ Firefox extension workflows:
|
|
|
107
108
|
|
|
108
109
|
```
|
|
109
110
|
dalila → signal, computed, effect, batch, ...
|
|
110
|
-
dalila/runtime → bind()
|
|
111
|
+
dalila/runtime → bind(), mount(), configure(), defineComponent()
|
|
111
112
|
dalila/context → createContext, provide, inject
|
|
112
113
|
dalila/http → createHttpClient with XSRF protection
|
|
113
114
|
```
|
|
@@ -130,10 +131,13 @@ count.set(5); // logs: Count is 5
|
|
|
130
131
|
### Template Binding
|
|
131
132
|
|
|
132
133
|
```ts
|
|
133
|
-
import {
|
|
134
|
+
import { configure, mount } from 'dalila/runtime';
|
|
135
|
+
|
|
136
|
+
// Global component registry and options
|
|
137
|
+
configure({ components: [MyComponent] });
|
|
134
138
|
|
|
135
|
-
//
|
|
136
|
-
const dispose =
|
|
139
|
+
// Bind a selector to a reactive view-model
|
|
140
|
+
const dispose = mount('.app', { count: signal(0) });
|
|
137
141
|
|
|
138
142
|
// Cleanup when done
|
|
139
143
|
dispose();
|
|
@@ -279,39 +283,46 @@ async function handleSubmit(data, { signal }) {
|
|
|
279
283
|
|
|
280
284
|
```ts
|
|
281
285
|
// page.ts
|
|
282
|
-
import { signal } from 'dalila';
|
|
286
|
+
import { computed, signal } from 'dalila';
|
|
283
287
|
import { createDialog, mountUI } from 'dalila/components/ui';
|
|
284
288
|
|
|
285
|
-
|
|
289
|
+
class HomePageVM {
|
|
290
|
+
count = signal(0);
|
|
291
|
+
status = computed(() => `Count: ${this.count()}`);
|
|
292
|
+
dialog = createDialog({ closeOnBackdrop: true, closeOnEscape: true });
|
|
293
|
+
increment = () => this.count.update(n => n + 1);
|
|
294
|
+
openModal = () => this.dialog.show();
|
|
295
|
+
closeModal = () => this.dialog.close();
|
|
296
|
+
}
|
|
286
297
|
|
|
287
298
|
export function loader() {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
count,
|
|
292
|
-
increment: () => count.update(n => n + 1),
|
|
293
|
-
openDialog: () => dialog.show(),
|
|
294
|
-
};
|
|
299
|
+
return new HomePageVM();
|
|
295
300
|
}
|
|
296
301
|
|
|
297
302
|
// Called after view is mounted
|
|
298
|
-
export function onMount(root: HTMLElement) {
|
|
299
|
-
mountUI(root, {
|
|
300
|
-
dialogs: { dialog }
|
|
303
|
+
export function onMount(root: HTMLElement, data: HomePageVM) {
|
|
304
|
+
return mountUI(root, {
|
|
305
|
+
dialogs: { dialog: data.dialog },
|
|
306
|
+
events: []
|
|
301
307
|
});
|
|
302
308
|
}
|
|
309
|
+
|
|
310
|
+
// Optional extra hook on route leave
|
|
311
|
+
export function onUnmount(_root: HTMLElement) {}
|
|
303
312
|
```
|
|
304
313
|
|
|
305
314
|
```html
|
|
306
315
|
<!-- page.html -->
|
|
307
|
-
<d-button d-on-click="
|
|
316
|
+
<d-button d-on-click="increment">Increment</d-button>
|
|
317
|
+
<d-button d-on-click="openModal">Open Dialog</d-button>
|
|
308
318
|
|
|
309
319
|
<d-dialog d-ui="dialog">
|
|
310
320
|
<d-dialog-header>
|
|
311
|
-
<d-dialog-title>
|
|
321
|
+
<d-dialog-title>{status}</d-dialog-title>
|
|
322
|
+
<d-dialog-close d-on-click="closeModal">×</d-dialog-close>
|
|
312
323
|
</d-dialog-header>
|
|
313
324
|
<d-dialog-body>
|
|
314
|
-
<
|
|
325
|
+
<p>Modal controlled by the official route + UI pattern.</p>
|
|
315
326
|
</d-dialog-body>
|
|
316
327
|
</d-dialog>
|
|
317
328
|
```
|
package/dist/cli/check.js
CHANGED
|
@@ -520,7 +520,7 @@ function extractTemplateIdentifiers(html) {
|
|
|
520
520
|
i++;
|
|
521
521
|
}
|
|
522
522
|
// --- 2. Directive scanning (supports single and double quotes) ---
|
|
523
|
-
const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-overscan|d-if|d-when|d-match|d-html|d-attr-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
|
|
523
|
+
const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-overscan|d-if|d-when|d-match|d-html|d-attr-[a-zA-Z][\w-]*|d-bind-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
|
|
524
524
|
DIRECTIVE_RE.lastIndex = 0;
|
|
525
525
|
let match;
|
|
526
526
|
while ((match = DIRECTIVE_RE.exec(html))) {
|
package/dist/cli/index.js
CHANGED
|
@@ -9,7 +9,7 @@ const routeArgs = args.slice(2);
|
|
|
9
9
|
const WATCH_DEBOUNCE_MS = 120;
|
|
10
10
|
function showHelp() {
|
|
11
11
|
console.log(`
|
|
12
|
-
Dalila CLI
|
|
12
|
+
🐰✂️ Dalila CLI
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
15
15
|
dalila routes generate [options] Generate routes + manifest from app file structure
|
|
@@ -32,7 +32,7 @@ Examples:
|
|
|
32
32
|
}
|
|
33
33
|
function showRoutesHelp() {
|
|
34
34
|
console.log(`
|
|
35
|
-
Dalila CLI - Routes
|
|
35
|
+
🐰✂️ Dalila CLI - Routes
|
|
36
36
|
|
|
37
37
|
Usage:
|
|
38
38
|
dalila routes generate [options] Generate routes + manifest from app file structure
|
|
@@ -52,7 +52,7 @@ Examples:
|
|
|
52
52
|
}
|
|
53
53
|
function showCheckHelp() {
|
|
54
54
|
console.log(`
|
|
55
|
-
Dalila CLI - Check
|
|
55
|
+
🐰✂️ Dalila CLI - Check
|
|
56
56
|
|
|
57
57
|
Usage:
|
|
58
58
|
dalila check [path] [options] Static analysis of HTML templates
|
|
@@ -182,7 +182,7 @@ function resolveGenerateConfig(cliArgs, cwd = process.cwd()) {
|
|
|
182
182
|
async function generateRoutes(cliArgs) {
|
|
183
183
|
const { appDir, outputPath } = resolveGenerateConfig(cliArgs);
|
|
184
184
|
console.log('');
|
|
185
|
-
console.log('
|
|
185
|
+
console.log('🐰✂️ Dalila Routes Generator');
|
|
186
186
|
console.log('');
|
|
187
187
|
try {
|
|
188
188
|
await generateRoutesFile(appDir, outputPath);
|
|
@@ -243,7 +243,7 @@ function watchRoutes(cliArgs) {
|
|
|
243
243
|
process.exit(1);
|
|
244
244
|
}
|
|
245
245
|
console.log('');
|
|
246
|
-
console.log('
|
|
246
|
+
console.log('🐰✂️ Dalila Routes Watch');
|
|
247
247
|
console.log(` app: ${appDir}`);
|
|
248
248
|
console.log(` output: ${outputPath}`);
|
|
249
249
|
console.log('');
|
|
@@ -467,16 +467,31 @@ function pageProps(pageHtml, pageTs) {
|
|
|
467
467
|
if (hasNamedExport(pageTs, 'onMount')) {
|
|
468
468
|
if (pageTs.lazy) {
|
|
469
469
|
const lazyLoader = `${pageTs.importName}_lazy`;
|
|
470
|
-
props.push(`onMount: (root: HTMLElement) => ${lazyLoader}().then(mod => {
|
|
470
|
+
props.push(`onMount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
|
|
471
471
|
if (typeof (mod as any).onMount === 'function') {
|
|
472
|
-
(mod as any).onMount(root);
|
|
472
|
+
return (mod as any).onMount(root, data, ctx);
|
|
473
473
|
}
|
|
474
|
+
return undefined;
|
|
474
475
|
})`);
|
|
475
476
|
}
|
|
476
477
|
else {
|
|
477
478
|
props.push(`onMount: ${moduleExport(pageTs, 'onMount')}`);
|
|
478
479
|
}
|
|
479
480
|
}
|
|
481
|
+
if (hasNamedExport(pageTs, 'onUnmount')) {
|
|
482
|
+
if (pageTs.lazy) {
|
|
483
|
+
const lazyLoader = `${pageTs.importName}_lazy`;
|
|
484
|
+
props.push(`onUnmount: (root: HTMLElement, data: unknown, ctx: unknown) => ${lazyLoader}().then(mod => {
|
|
485
|
+
if (typeof (mod as any).onUnmount === 'function') {
|
|
486
|
+
return (mod as any).onUnmount(root, data, ctx);
|
|
487
|
+
}
|
|
488
|
+
return undefined;
|
|
489
|
+
})`);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
props.push(`onUnmount: ${moduleExport(pageTs, 'onUnmount')}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
480
495
|
}
|
|
481
496
|
else {
|
|
482
497
|
props.push(`view: (ctx) => fromHtml(${viewConst}, { data: ${routeDataExpr()}, scope: ctx.scope })`);
|
|
@@ -501,6 +516,12 @@ function pageProps(pageHtml, pageTs) {
|
|
|
501
516
|
if (hasNamedExport(pageTs, 'validation')) {
|
|
502
517
|
props.push(`validation: ${moduleExport(pageTs, 'validation', { allowValue: true })}`);
|
|
503
518
|
}
|
|
519
|
+
if (hasNamedExport(pageTs, 'onMount')) {
|
|
520
|
+
props.push(`onMount: ${moduleExport(pageTs, 'onMount')}`);
|
|
521
|
+
}
|
|
522
|
+
if (hasNamedExport(pageTs, 'onUnmount')) {
|
|
523
|
+
props.push(`onUnmount: ${moduleExport(pageTs, 'onUnmount')}`);
|
|
524
|
+
}
|
|
504
525
|
return props;
|
|
505
526
|
}
|
|
506
527
|
function layoutProps(indent, layoutHtml, layoutTs) {
|
|
@@ -33,6 +33,9 @@ export type RouteRedirectResult = string | null | undefined | Promise<string | n
|
|
|
33
33
|
export type RouteMiddlewareResult = RouteGuardResult;
|
|
34
34
|
export type RouteMiddleware = (ctx: RouteCtx) => RouteMiddlewareResult;
|
|
35
35
|
export type RouteMiddlewareResolver = RouteMiddleware[] | ((ctx: RouteCtx) => RouteMiddleware[] | null | undefined | Promise<RouteMiddleware[] | null | undefined>);
|
|
36
|
+
export type RouteMountCleanup = () => void;
|
|
37
|
+
export type RouteMountResult = void | RouteMountCleanup | Promise<void | RouteMountCleanup>;
|
|
38
|
+
export type RouteUnmountResult = void | Promise<void>;
|
|
36
39
|
/**
|
|
37
40
|
* Route definition.
|
|
38
41
|
*
|
|
@@ -50,7 +53,8 @@ export interface RouteTable<T = any> {
|
|
|
50
53
|
layout?: (ctx: RouteCtx, child: Node | DocumentFragment | Node[], data: T) => Node | DocumentFragment | Node[];
|
|
51
54
|
loader?: (ctx: RouteCtx) => Promise<T>;
|
|
52
55
|
preload?: (ctx: RouteCtx) => Promise<T>;
|
|
53
|
-
onMount?: (root: HTMLElement) =>
|
|
56
|
+
onMount?: (root: HTMLElement, data: T, ctx: RouteCtx) => RouteMountResult;
|
|
57
|
+
onUnmount?: (root: HTMLElement, data: T, ctx: RouteCtx) => RouteUnmountResult;
|
|
54
58
|
pending?: (ctx: RouteCtx) => Node | DocumentFragment | Node[];
|
|
55
59
|
error?: (ctx: RouteCtx, error: unknown, data?: T) => Node | DocumentFragment | Node[];
|
|
56
60
|
notFound?: (ctx: RouteCtx) => Node | DocumentFragment | Node[];
|
package/dist/router/router.js
CHANGED
|
@@ -109,6 +109,11 @@ export function createRouter(config) {
|
|
|
109
109
|
let currentLoaderController = null;
|
|
110
110
|
let started = false;
|
|
111
111
|
let navigationToken = 0;
|
|
112
|
+
let activeLeafRoute = null;
|
|
113
|
+
let activeRouteMountCleanup = null;
|
|
114
|
+
let activeRouteUnmountHook = null;
|
|
115
|
+
let activeLeafData = undefined;
|
|
116
|
+
let activeLeafCtx = null;
|
|
112
117
|
const transitionCoalescing = new Map();
|
|
113
118
|
const scrollPositions = new LRUCache(config.scrollPositionsCacheSize ?? 100);
|
|
114
119
|
const preloadTagsByKey = new Map();
|
|
@@ -582,10 +587,43 @@ export function createRouter(config) {
|
|
|
582
587
|
replace: true
|
|
583
588
|
};
|
|
584
589
|
}
|
|
590
|
+
function runRouteUnmountLifecycle() {
|
|
591
|
+
const cleanup = activeRouteMountCleanup;
|
|
592
|
+
const onUnmount = activeRouteUnmountHook;
|
|
593
|
+
const data = activeLeafData;
|
|
594
|
+
const ctx = activeLeafCtx;
|
|
595
|
+
activeRouteMountCleanup = null;
|
|
596
|
+
activeRouteUnmountHook = null;
|
|
597
|
+
activeLeafRoute = null;
|
|
598
|
+
activeLeafData = undefined;
|
|
599
|
+
activeLeafCtx = null;
|
|
600
|
+
if (cleanup) {
|
|
601
|
+
try {
|
|
602
|
+
cleanup();
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.error('[Dalila] Error in onMount cleanup lifecycle hook:', error);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (onUnmount) {
|
|
609
|
+
try {
|
|
610
|
+
const result = onUnmount(outletElement, data, ctx);
|
|
611
|
+
if (result && typeof result.then === 'function') {
|
|
612
|
+
void result.catch((error) => {
|
|
613
|
+
console.error('[Dalila] Error in onUnmount lifecycle hook:', error);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
console.error('[Dalila] Error in onUnmount lifecycle hook:', error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
585
622
|
/**
|
|
586
623
|
* Mount nodes into outlet and remove loading state
|
|
587
624
|
*/
|
|
588
625
|
function mountToOutlet(...nodes) {
|
|
626
|
+
runRouteUnmountLifecycle();
|
|
589
627
|
outletElement.replaceChildren(...nodes);
|
|
590
628
|
queueMicrotask(() => {
|
|
591
629
|
outletElement.removeAttribute('d-loading');
|
|
@@ -950,7 +988,7 @@ export function createRouter(config) {
|
|
|
950
988
|
}
|
|
951
989
|
commitRouteState();
|
|
952
990
|
try {
|
|
953
|
-
mountViewStack(matchStack, ctx, dataStack);
|
|
991
|
+
mountViewStack(matchStack, ctx, dataStack, token);
|
|
954
992
|
await finishSuccessfulTransition();
|
|
955
993
|
}
|
|
956
994
|
catch (error) {
|
|
@@ -986,16 +1024,18 @@ export function createRouter(config) {
|
|
|
986
1024
|
return promise;
|
|
987
1025
|
}
|
|
988
1026
|
/** Compose and mount the view stack (leaf view wrapped by parent layouts). */
|
|
989
|
-
function mountViewStack(matchStack, ctx, dataStack) {
|
|
1027
|
+
function mountViewStack(matchStack, ctx, dataStack, navToken) {
|
|
990
1028
|
try {
|
|
991
1029
|
let content = null;
|
|
992
1030
|
let leafRoute = null;
|
|
1031
|
+
let leafData = undefined;
|
|
993
1032
|
for (let i = matchStack.length - 1; i >= 0; i--) {
|
|
994
1033
|
const match = matchStack[i];
|
|
995
1034
|
const data = dataStack[i];
|
|
996
1035
|
const route = match.route;
|
|
997
1036
|
if (i === matchStack.length - 1) {
|
|
998
1037
|
leafRoute = route;
|
|
1038
|
+
leafData = data;
|
|
999
1039
|
if (!route.view) {
|
|
1000
1040
|
console.warn(`[Dalila] Leaf route ${match.path} has no view function`);
|
|
1001
1041
|
return;
|
|
@@ -1027,11 +1067,54 @@ export function createRouter(config) {
|
|
|
1027
1067
|
if (content) {
|
|
1028
1068
|
const nodes = Array.isArray(content) ? content : [content];
|
|
1029
1069
|
mountToOutlet(...nodes);
|
|
1070
|
+
activeLeafRoute = leafRoute ?? null;
|
|
1071
|
+
activeRouteUnmountHook = leafRoute?.onUnmount ?? null;
|
|
1072
|
+
activeLeafData = leafData;
|
|
1073
|
+
activeLeafCtx = ctx;
|
|
1030
1074
|
// Call onMount lifecycle hook if present
|
|
1031
1075
|
if (leafRoute?.onMount) {
|
|
1032
1076
|
queueMicrotask(() => {
|
|
1077
|
+
if (navigationToken !== navToken)
|
|
1078
|
+
return;
|
|
1079
|
+
if (activeLeafRoute !== leafRoute)
|
|
1080
|
+
return;
|
|
1033
1081
|
try {
|
|
1034
|
-
leafRoute.onMount(outletElement);
|
|
1082
|
+
const result = leafRoute.onMount(outletElement, leafData, ctx);
|
|
1083
|
+
if (typeof result === 'function') {
|
|
1084
|
+
if (navigationToken === navToken && activeLeafRoute === leafRoute) {
|
|
1085
|
+
activeRouteMountCleanup = result;
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
try {
|
|
1089
|
+
result();
|
|
1090
|
+
}
|
|
1091
|
+
catch (cleanupError) {
|
|
1092
|
+
console.error('[Dalila] Error in stale onMount cleanup lifecycle hook:', cleanupError);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (result && typeof result.then === 'function') {
|
|
1098
|
+
void result
|
|
1099
|
+
.then((resolved) => {
|
|
1100
|
+
if (typeof resolved !== 'function')
|
|
1101
|
+
return;
|
|
1102
|
+
if (navigationToken === navToken && activeLeafRoute === leafRoute) {
|
|
1103
|
+
activeRouteMountCleanup = resolved;
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
try {
|
|
1107
|
+
resolved();
|
|
1108
|
+
}
|
|
1109
|
+
catch (cleanupError) {
|
|
1110
|
+
console.error('[Dalila] Error in stale async onMount cleanup lifecycle hook:', cleanupError);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
})
|
|
1114
|
+
.catch((error) => {
|
|
1115
|
+
console.error('[Dalila] Error in onMount lifecycle hook:', error);
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1035
1118
|
}
|
|
1036
1119
|
catch (error) {
|
|
1037
1120
|
console.error('[Dalila] Error in onMount lifecycle hook:', error);
|
|
@@ -1413,6 +1496,7 @@ export function createRouter(config) {
|
|
|
1413
1496
|
document.removeEventListener('click', handleLink);
|
|
1414
1497
|
document.removeEventListener('pointerover', handleLinkIntent);
|
|
1415
1498
|
document.removeEventListener('focusin', handleLinkIntent);
|
|
1499
|
+
runRouteUnmountLifecycle();
|
|
1416
1500
|
if (currentLoaderController) {
|
|
1417
1501
|
currentLoaderController.abort();
|
|
1418
1502
|
currentLoaderController = null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// This file is auto-generated by 'dalila routes generate'
|
|
2
|
+
// Do not edit manually - your changes will be overwritten
|
|
3
|
+
import { fromHtml } from 'dalila';
|
|
4
|
+
const layout_html = `<div class="app-shell">
|
|
5
|
+
<h1>Router + PageVM + Componente</h1>
|
|
6
|
+
<nav>
|
|
7
|
+
<a href="/examples/router-simple/" d-link>Home</a>
|
|
8
|
+
<a href="/examples/router-simple/about" d-link>Sobre</a>
|
|
9
|
+
</nav>
|
|
10
|
+
<main id="outlet-slot" data-slot="children"></main>
|
|
11
|
+
</div>
|
|
12
|
+
`;
|
|
13
|
+
const page_html = `<section>
|
|
14
|
+
<h2>Home</h2>
|
|
15
|
+
<p>PageVM da rota: <strong>HomePageVM</strong></p>
|
|
16
|
+
<status-badge d-props-text="status"></status-badge>
|
|
17
|
+
<div style="display:flex; gap:.5rem; margin-top:.75rem;">
|
|
18
|
+
<button d-on-click="increment">Incrementar</button>
|
|
19
|
+
<button d-on-click="openModal">Abrir modal</button>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<d-dialog d-ui="infoDialog" class="d-dialog">
|
|
23
|
+
<d-dialog-header>
|
|
24
|
+
<d-dialog-title>Modal Dalila</d-dialog-title>
|
|
25
|
+
<d-dialog-close d-on-click="closeModal">×</d-dialog-close>
|
|
26
|
+
</d-dialog-header>
|
|
27
|
+
<d-dialog-body>
|
|
28
|
+
<p>This modal is mounted with <code>mountUI()</code> in the route <code>onMount</code>.</p>
|
|
29
|
+
<p>Status atual: <strong d-text="status"></strong></p>
|
|
30
|
+
</d-dialog-body>
|
|
31
|
+
<d-dialog-footer>
|
|
32
|
+
<button d-on-click="closeModal">Fechar</button>
|
|
33
|
+
</d-dialog-footer>
|
|
34
|
+
</d-dialog>
|
|
35
|
+
</section>
|
|
36
|
+
`;
|
|
37
|
+
const about_page_html = `<section>
|
|
38
|
+
<h2>Sobre</h2>
|
|
39
|
+
<p>PageVM da rota: <strong>AboutPageVM</strong></p>
|
|
40
|
+
<p>Nome atual: <strong d-text="nomeUpper"></strong></p>
|
|
41
|
+
<input d-bind-value="nome" placeholder="Digite um nome" />
|
|
42
|
+
</section>
|
|
43
|
+
`;
|
|
44
|
+
const page_lazy = () => import('./app/page.js');
|
|
45
|
+
const about_page_lazy = () => import('./app/about/page.js');
|
|
46
|
+
export const routes = [
|
|
47
|
+
{
|
|
48
|
+
path: '/',
|
|
49
|
+
layout: (ctx, children) => fromHtml(layout_html, { data: { ...ctx.params, params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, children, scope: ctx.scope }),
|
|
50
|
+
children: [
|
|
51
|
+
{ path: '', view: (ctx, data) => fromHtml(page_html, { data: { ...ctx.params, ...(data ?? {}), params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, scope: ctx.scope }), loader: (...args) => page_lazy().then(mod => {
|
|
52
|
+
const exported = mod.loader;
|
|
53
|
+
if (typeof exported === 'function') {
|
|
54
|
+
return exported(...args);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}), onMount: (root, data, ctx) => page_lazy().then(mod => {
|
|
58
|
+
if (typeof mod.onMount === 'function') {
|
|
59
|
+
return mod.onMount(root, data, ctx);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}) },
|
|
63
|
+
{
|
|
64
|
+
path: 'about',
|
|
65
|
+
view: (ctx, data) => fromHtml(about_page_html, { data: { ...ctx.params, ...(data ?? {}), params: ctx.params, query: ctx.query.toString(), path: ctx.path, fullPath: ctx.fullPath }, scope: ctx.scope }),
|
|
66
|
+
loader: (...args) => about_page_lazy().then(mod => {
|
|
67
|
+
const exported = mod.loader;
|
|
68
|
+
if (typeof exported === 'function') {
|
|
69
|
+
return exported(...args);
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}),
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RouteManifestEntry } from 'dalila/router';
|
|
2
|
+
export declare const routeManifest: RouteManifestEntry[];
|
|
3
|
+
export declare function getRouteManifestEntry(id: string): RouteManifestEntry | undefined;
|
|
4
|
+
export declare function prefetchRouteById(id: string): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// This file is auto-generated by 'dalila routes generate'
|
|
2
|
+
// Do not edit manually - your changes will be overwritten
|
|
3
|
+
export const routeManifest = [
|
|
4
|
+
{
|
|
5
|
+
id: 'root',
|
|
6
|
+
pattern: '/',
|
|
7
|
+
score: 1000,
|
|
8
|
+
paramKeys: [],
|
|
9
|
+
tags: [],
|
|
10
|
+
modules: ["./app/page.js"],
|
|
11
|
+
load: () => import('./app/page.js').then(() => undefined)
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'about',
|
|
15
|
+
pattern: '/about',
|
|
16
|
+
score: 301,
|
|
17
|
+
paramKeys: [],
|
|
18
|
+
tags: [],
|
|
19
|
+
modules: ["./app/page.js", "./app/about/page.js"],
|
|
20
|
+
load: () => Promise.all([import('./app/page.js'), import('./app/about/page.js')]).then(() => undefined)
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const manifestById = new Map(routeManifest.map(route => [route.id, route]));
|
|
24
|
+
export function getRouteManifestEntry(id) {
|
|
25
|
+
return manifestById.get(id);
|
|
26
|
+
}
|
|
27
|
+
export async function prefetchRouteById(id) {
|
|
28
|
+
const entry = manifestById.get(id);
|
|
29
|
+
if (!entry)
|
|
30
|
+
return;
|
|
31
|
+
await entry.load();
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RoutePattern = '/' | '/about';
|
|
2
|
+
export type RouteParamsByPattern = {
|
|
3
|
+
'/': {};
|
|
4
|
+
'/about': {};
|
|
5
|
+
};
|
|
6
|
+
export type RouteSearchByPattern = {
|
|
7
|
+
[P in RoutePattern]: Record<string, string | string[]>;
|
|
8
|
+
};
|
|
9
|
+
export type RouteParams<P extends RoutePattern> = RouteParamsByPattern[P];
|
|
10
|
+
export type RouteSearch<P extends RoutePattern> = RouteSearchByPattern[P];
|
|
11
|
+
export declare function buildRoutePath<P extends RoutePattern>(pattern: P, params: RouteParams<P>): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// This file is auto-generated by 'dalila routes generate'
|
|
2
|
+
// Do not edit manually - your changes will be overwritten
|
|
3
|
+
export function buildRoutePath(pattern, params) {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const segment of pattern.split('/').filter(Boolean)) {
|
|
6
|
+
if (!segment.startsWith(':')) {
|
|
7
|
+
out.push(segment);
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const isOptionalCatchAll = segment.endsWith('*?');
|
|
11
|
+
const isCatchAll = isOptionalCatchAll || segment.endsWith('*');
|
|
12
|
+
const key = segment.slice(1, isCatchAll ? (isOptionalCatchAll ? -2 : -1) : undefined);
|
|
13
|
+
const value = params[key];
|
|
14
|
+
if (isCatchAll) {
|
|
15
|
+
if (value === undefined || value === null) {
|
|
16
|
+
if (isOptionalCatchAll)
|
|
17
|
+
continue;
|
|
18
|
+
throw new Error(`Missing route param: ${key}`);
|
|
19
|
+
}
|
|
20
|
+
if (!Array.isArray(value)) {
|
|
21
|
+
throw new Error(`Route param "${key}" must be an array`);
|
|
22
|
+
}
|
|
23
|
+
if (value.length === 0) {
|
|
24
|
+
if (isOptionalCatchAll)
|
|
25
|
+
continue;
|
|
26
|
+
throw new Error(`Route param "${key}" cannot be empty`);
|
|
27
|
+
}
|
|
28
|
+
out.push(...value.map(v => encodeURIComponent(String(v))));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (value === undefined || value === null) {
|
|
32
|
+
throw new Error(`Missing route param: ${key}`);
|
|
33
|
+
}
|
|
34
|
+
out.push(encodeURIComponent(String(value)));
|
|
35
|
+
}
|
|
36
|
+
return out.length === 0 ? '/' : `/${out.join('/')}`;
|
|
37
|
+
}
|
package/dist/runtime/bind.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
|
+
import type { Component } from './component.js';
|
|
9
10
|
export interface BindOptions {
|
|
10
11
|
/**
|
|
11
12
|
* Event types to bind (default: click, input, change, submit, keydown, keyup)
|
|
@@ -25,6 +26,10 @@ export interface BindOptions {
|
|
|
25
26
|
/** Time-to-live (ms) per plan, refreshed on hit (0 disables cache). */
|
|
26
27
|
ttlMs?: number;
|
|
27
28
|
};
|
|
29
|
+
/** Component registry — accepts map `{ tag: component }` or array `[component]` */
|
|
30
|
+
components?: Record<string, Component> | Component[];
|
|
31
|
+
/** Error policy for component `ctx.onMount()` callbacks. Default: 'log'. */
|
|
32
|
+
onMountError?: 'log' | 'throw';
|
|
28
33
|
/**
|
|
29
34
|
* Internal flag — set by fromHtml for router/template rendering.
|
|
30
35
|
* Skips HMR context registration but KEEPS d-ready/d-loading lifecycle.
|
|
@@ -54,6 +59,23 @@ export interface BindHandle {
|
|
|
54
59
|
getRef(name: string): Element | null;
|
|
55
60
|
getRefs(): Readonly<Record<string, Element>>;
|
|
56
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Set global defaults for all `bind()` / `mount()` calls.
|
|
64
|
+
*
|
|
65
|
+
* Options set here are merged with per-call options (per-call wins).
|
|
66
|
+
* Call with an empty object to reset.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { configure } from 'dalila/runtime';
|
|
71
|
+
*
|
|
72
|
+
* configure({
|
|
73
|
+
* components: [FruitPicker],
|
|
74
|
+
* onMountError: 'log',
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function configure(config: BindOptions): void;
|
|
57
79
|
/**
|
|
58
80
|
* Bind a DOM tree to a reactive context.
|
|
59
81
|
*
|
|
@@ -77,7 +99,7 @@ export interface BindHandle {
|
|
|
77
99
|
* dispose();
|
|
78
100
|
* ```
|
|
79
101
|
*/
|
|
80
|
-
export declare function bind<T extends Record<string, unknown> = BindContext>(root: Element, ctx: T, options?: BindOptions): BindHandle;
|
|
102
|
+
export declare function bind<T extends Record<string, unknown> = BindContext>(root: Element | string, ctx: T, options?: BindOptions): BindHandle;
|
|
81
103
|
/**
|
|
82
104
|
* Automatically bind when DOM is ready.
|
|
83
105
|
* Useful for simple pages without a build step.
|
|
@@ -91,3 +113,14 @@ export declare function bind<T extends Record<string, unknown> = BindContext>(ro
|
|
|
91
113
|
* ```
|
|
92
114
|
*/
|
|
93
115
|
export declare function autoBind<T extends Record<string, unknown> = BindContext>(selector: string, ctx: T, options?: BindOptions): Promise<BindHandle>;
|
|
116
|
+
/**
|
|
117
|
+
* Mount a component imperatively, or bind a selector to a view-model.
|
|
118
|
+
*
|
|
119
|
+
* Overload 1 — `mount(selector, vm, options?)`:
|
|
120
|
+
* Shorthand for `bind(selector, vm, options)`.
|
|
121
|
+
*
|
|
122
|
+
* Overload 2 — `mount(component, target, props?)`:
|
|
123
|
+
* Mount a component created with defineComponent() into a target element.
|
|
124
|
+
*/
|
|
125
|
+
export declare function mount<T extends object>(selector: string, vm: T, options?: BindOptions): BindHandle;
|
|
126
|
+
export declare function mount(component: Component, target: Element, props?: Record<string, unknown>): BindHandle;
|