defuss 3.4.2 → 3.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/index.cjs +63 -7
- package/dist/index.d.ts +80 -6
- package/dist/index.mjs +63 -7
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1162,8 +1162,12 @@ const setupRouter = (config = {
|
|
|
1162
1162
|
isReady: false,
|
|
1163
1163
|
pendingResolvers: [],
|
|
1164
1164
|
currentPath: "",
|
|
1165
|
-
popAttached: false
|
|
1165
|
+
popAttached: false,
|
|
1166
|
+
lifecycleHooks: { beforeUnmount: [], unmount: [] }
|
|
1166
1167
|
};
|
|
1168
|
+
if (!state.lifecycleHooks) {
|
|
1169
|
+
state.lifecycleHooks = { beforeUnmount: [], unmount: [] };
|
|
1170
|
+
}
|
|
1167
1171
|
const routeRegistrations = state.routeRegistrations;
|
|
1168
1172
|
if (typeof window !== "undefined" && !windowImpl) {
|
|
1169
1173
|
windowImpl = globalThis.__defuss_window || window;
|
|
@@ -1286,6 +1290,35 @@ const setupRouter = (config = {
|
|
|
1286
1290
|
resolve();
|
|
1287
1291
|
}
|
|
1288
1292
|
state.pendingResolvers = [];
|
|
1293
|
+
},
|
|
1294
|
+
createRouteContext(request) {
|
|
1295
|
+
return {
|
|
1296
|
+
request,
|
|
1297
|
+
onBeforeUnmount(fn) {
|
|
1298
|
+
state.lifecycleHooks.beforeUnmount.push(fn);
|
|
1299
|
+
},
|
|
1300
|
+
onUnmount(fn) {
|
|
1301
|
+
state.lifecycleHooks.unmount.push(fn);
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
},
|
|
1305
|
+
async runBeforeUnmountHooks() {
|
|
1306
|
+
for (const fn of state.lifecycleHooks.beforeUnmount) {
|
|
1307
|
+
const result = await fn();
|
|
1308
|
+
if (result === false) return false;
|
|
1309
|
+
}
|
|
1310
|
+
return true;
|
|
1311
|
+
},
|
|
1312
|
+
runUnmountHooks() {
|
|
1313
|
+
for (const fn of state.lifecycleHooks.unmount) {
|
|
1314
|
+
fn();
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
clearRouteLifecycle() {
|
|
1318
|
+
const oldUnmountHooks = [...state.lifecycleHooks.unmount];
|
|
1319
|
+
state.lifecycleHooks.beforeUnmount = [];
|
|
1320
|
+
state.lifecycleHooks.unmount = [];
|
|
1321
|
+
return { unmountHooks: oldUnmountHooks };
|
|
1289
1322
|
}
|
|
1290
1323
|
};
|
|
1291
1324
|
const handlePopState = (event) => {
|
|
@@ -1315,7 +1348,8 @@ if (!globalThis[ROUTER_STATE_KEY]) {
|
|
|
1315
1348
|
isReady: false,
|
|
1316
1349
|
pendingResolvers: [],
|
|
1317
1350
|
currentPath: "",
|
|
1318
|
-
popAttached: false
|
|
1351
|
+
popAttached: false,
|
|
1352
|
+
lifecycleHooks: { beforeUnmount: [], unmount: [] }
|
|
1319
1353
|
};
|
|
1320
1354
|
}
|
|
1321
1355
|
const getRouterState = () => globalThis[ROUTER_STATE_KEY];
|
|
@@ -1336,7 +1370,13 @@ const Route = ({
|
|
|
1336
1370
|
const req = router.match(path);
|
|
1337
1371
|
if (!req.match) return null;
|
|
1338
1372
|
if (Component) {
|
|
1339
|
-
|
|
1373
|
+
const routeContext = router.createRouteContext(req);
|
|
1374
|
+
const isAsync = Component.constructor.name === "AsyncFunction";
|
|
1375
|
+
const routeChildren = Array.isArray(children) ? children[0] : children;
|
|
1376
|
+
if (isAsync && routeChildren) {
|
|
1377
|
+
return /* @__PURE__ */ jsx(Component, { route: routeContext, fallback: routeChildren });
|
|
1378
|
+
}
|
|
1379
|
+
return /* @__PURE__ */ jsx(Component, { route: routeContext });
|
|
1340
1380
|
}
|
|
1341
1381
|
return Array.isArray(children) ? children[0] : children || null;
|
|
1342
1382
|
};
|
|
@@ -1381,11 +1421,27 @@ const RouterSlot = ({
|
|
|
1381
1421
|
router.onRouteChange(async () => {
|
|
1382
1422
|
const currentPath = router.getRequest().path;
|
|
1383
1423
|
const isSamePath = currentPath === lastPath;
|
|
1424
|
+
if (!isSamePath) {
|
|
1425
|
+
const allowed = await router.runBeforeUnmountHooks();
|
|
1426
|
+
if (!allowed) {
|
|
1427
|
+
window.history.pushState({}, "", lastPath);
|
|
1428
|
+
router.resolve(lastPath);
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const { unmountHooks } = router.clearRouteLifecycle();
|
|
1432
|
+
await dom.$(ref).update(
|
|
1433
|
+
typeof RouterOutlet === "function" ? RouterOutlet() : [],
|
|
1434
|
+
transitionConfig
|
|
1435
|
+
);
|
|
1436
|
+
for (const fn of unmountHooks) {
|
|
1437
|
+
fn();
|
|
1438
|
+
}
|
|
1439
|
+
} else {
|
|
1440
|
+
await dom.$(ref).update(
|
|
1441
|
+
typeof RouterOutlet === "function" ? RouterOutlet() : []
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1384
1444
|
lastPath = currentPath;
|
|
1385
|
-
await dom.$(ref).update(
|
|
1386
|
-
typeof RouterOutlet === "function" ? RouterOutlet() : [],
|
|
1387
|
-
isSamePath ? void 0 : transitionConfig
|
|
1388
|
-
);
|
|
1389
1445
|
});
|
|
1390
1446
|
}
|
|
1391
1447
|
if (document.getElementById(slotId)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -137,6 +137,60 @@ declare const T: ({ key, values, tag, ref, ...attrs }: TransProps<string>) => VN
|
|
|
137
137
|
type OnHandleRouteChangeFn = (newRoute: string, oldRoute: string) => void;
|
|
138
138
|
type OnRouteChangeFn = (cb: OnHandleRouteChangeFn) => void;
|
|
139
139
|
type RouterStrategy = "page-refresh" | "slot-refresh";
|
|
140
|
+
type BeforeUnmountHookFn = () => boolean | void | Promise<boolean | void>;
|
|
141
|
+
type UnmountHookFn = () => void;
|
|
142
|
+
/**
|
|
143
|
+
* Context object passed to components rendered via Route's `component` prop.
|
|
144
|
+
* Provides access to the current route request and lifecycle hooks.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```tsx
|
|
148
|
+
* const MyScreen = ({ route }: { route: RouteContext }) => {
|
|
149
|
+
* route.onBeforeUnmount(() => {
|
|
150
|
+
* // Return false to block navigation (e.g. unsaved changes)
|
|
151
|
+
* return confirm("Leave page?");
|
|
152
|
+
* });
|
|
153
|
+
* route.onUnmount(() => {
|
|
154
|
+
* console.log("Route was left");
|
|
155
|
+
* });
|
|
156
|
+
* return <div>Current path: {route.request.path}</div>;
|
|
157
|
+
* };
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
interface RouteContext {
|
|
161
|
+
/** The matched RouteRequest for the current route */
|
|
162
|
+
request: RouteRequest;
|
|
163
|
+
/**
|
|
164
|
+
* Register a hook that fires before the route is unmounted.
|
|
165
|
+
* Returning `false` (or a Promise resolving to `false`) blocks navigation,
|
|
166
|
+
* allowing implementation of confirmation dialogs.
|
|
167
|
+
*/
|
|
168
|
+
onBeforeUnmount(fn: BeforeUnmountHookFn): void;
|
|
169
|
+
/**
|
|
170
|
+
* Register a hook that fires after the route has been unmounted
|
|
171
|
+
* (navigation completed, new route is rendered).
|
|
172
|
+
*/
|
|
173
|
+
onUnmount(fn: UnmountHookFn): void;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Props mixin for screen components rendered by `<Route component={...} />`.
|
|
177
|
+
* Extend your component props with this to get typed access to route context.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```tsx
|
|
181
|
+
* import { Router, type Props, type RouteProps } from "defuss";
|
|
182
|
+
*
|
|
183
|
+
* export interface ProjectDetailsProps extends Props, RouteProps {}
|
|
184
|
+
*
|
|
185
|
+
* export function ProjectDetailsScreen({ route }: ProjectDetailsProps) {
|
|
186
|
+
* const { projectName } = route.request.params;
|
|
187
|
+
* return <h1>Project: {projectName}</h1>;
|
|
188
|
+
* }
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
interface RouteProps {
|
|
192
|
+
route: RouteContext;
|
|
193
|
+
}
|
|
140
194
|
interface RouterConfig {
|
|
141
195
|
strategy?: RouterStrategy;
|
|
142
196
|
}
|
|
@@ -268,6 +322,10 @@ interface RouterState {
|
|
|
268
322
|
pendingResolvers: Array<() => void>;
|
|
269
323
|
currentPath: string;
|
|
270
324
|
popAttached: boolean;
|
|
325
|
+
lifecycleHooks: {
|
|
326
|
+
beforeUnmount: Array<BeforeUnmountHookFn>;
|
|
327
|
+
unmount: Array<UnmountHookFn>;
|
|
328
|
+
};
|
|
271
329
|
}
|
|
272
330
|
declare global {
|
|
273
331
|
var __defuss_router__: Router | undefined;
|
|
@@ -301,10 +359,16 @@ interface Router {
|
|
|
301
359
|
* ```
|
|
302
360
|
*/
|
|
303
361
|
ready(): Promise<void>;
|
|
362
|
+
/**
|
|
363
|
+
* Create a RouteContext object for a matched route request.
|
|
364
|
+
* The context provides lifecycle hooks (onBeforeUnmount, onUnmount)
|
|
365
|
+
* and is passed to components rendered via Route's `component` prop.
|
|
366
|
+
*/
|
|
367
|
+
createRouteContext(request: RouteRequest): RouteContext;
|
|
304
368
|
}
|
|
305
369
|
declare const Router: Router;
|
|
306
370
|
|
|
307
|
-
interface
|
|
371
|
+
interface RouteComponentProps extends Props {
|
|
308
372
|
path: string;
|
|
309
373
|
router?: Router;
|
|
310
374
|
exact?: boolean;
|
|
@@ -315,19 +379,29 @@ interface RouteProps extends Props {
|
|
|
315
379
|
* has been registered and matched. This ensures `Router.getRequest()`
|
|
316
380
|
* returns the correct params inside the component.
|
|
317
381
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
382
|
+
* The component receives a `route` prop (RouteContext) with:
|
|
383
|
+
* - `route.request` — the matched RouteRequest with params, query, etc.
|
|
384
|
+
* - `route.onBeforeUnmount(fn)` — register a hook before route leaves; return `false` to block
|
|
385
|
+
* - `route.onUnmount(fn)` — register a hook after route has been left
|
|
386
|
+
*
|
|
387
|
+
* When the component is async and Route has children, the children are
|
|
388
|
+
* shown as a loading fallback until the async component resolves.
|
|
320
389
|
*
|
|
321
390
|
* @example
|
|
322
391
|
* ```tsx
|
|
323
392
|
* <Route path="/project/:projectName" component={ProjectDetailsScreen} />
|
|
393
|
+
*
|
|
394
|
+
* // With loading fallback for async components:
|
|
395
|
+
* <Route path="/dashboard" component={AsyncDashboard}>
|
|
396
|
+
* <Spinner />
|
|
397
|
+
* </Route>
|
|
324
398
|
* ```
|
|
325
399
|
*/
|
|
326
400
|
component?: FC<any>;
|
|
327
401
|
}
|
|
328
|
-
declare const Route: FC<
|
|
402
|
+
declare const Route: FC<RouteComponentProps>;
|
|
329
403
|
|
|
330
|
-
interface RedirectProps extends
|
|
404
|
+
interface RedirectProps extends RouteComponentProps {
|
|
331
405
|
to: string;
|
|
332
406
|
}
|
|
333
407
|
declare const Redirect: FC<RedirectProps>;
|
|
@@ -412,4 +486,4 @@ declare const Suspense: ({ fallback, ref, children, class: _class, className, id
|
|
|
412
486
|
};
|
|
413
487
|
|
|
414
488
|
export { Async, AsyncDefussChild, DOMElement, FC, Globals, NodeType, PersistenceProviderImpl, PersistenceProviderOptions, PersistenceProviderType, Props, Redirect, Ref, RenderInput, Route, Router, RouterSlot, RouterSlotId, Suspense, T, Trans, TransitionConfig, VNode, VNodeAttributes, VNodeChild, addElementEvent, areDomNodesEqual, changeLanguage, checkElementVisibility, clearElementEvents, createI18n, createTrans, domNodeToVNode, getEventMap, getLanguage, getMimeType, getRouterState, htmlStringToVNodes, i18n, inDevMode, isHTML, isMarkup, isSVG, loadLanguage, matchRouteRegistrations, parseDOM, processAllFormElements, queueCallback, removeElementEvent, renderMarkup, replaceDomWithVdom, setupRouter, t, tokenizePath, updateDomWithVdom, waitForDOM, webstorage };
|
|
415
|
-
export type { AsyncProps, AsyncState, AsyncStateRef, I18nStore, MatchRouteRegistrationsOpts, OnHandleRouteChangeFn, OnLanguageChangeListener, OnRouteChangeFn, RedirectProps, Replacements, Resolve, RouteHandler, RouteParams, RouteProps, RouteRegistration, RouteRequest, RouterConfig, RouterSlotProps, RouterStrategy, TokenizedPath, TransProps, TransRef, TranslationKeys, TranslationObject, Translations, ValidChild };
|
|
489
|
+
export type { AsyncProps, AsyncState, AsyncStateRef, BeforeUnmountHookFn, I18nStore, MatchRouteRegistrationsOpts, OnHandleRouteChangeFn, OnLanguageChangeListener, OnRouteChangeFn, RedirectProps, Replacements, Resolve, RouteComponentProps, RouteContext, RouteHandler, RouteParams, RouteProps, RouteRegistration, RouteRequest, RouterConfig, RouterSlotProps, RouterStrategy, TokenizedPath, TransProps, TransRef, TranslationKeys, TranslationObject, Translations, UnmountHookFn, ValidChild };
|
package/dist/index.mjs
CHANGED
|
@@ -1161,8 +1161,12 @@ const setupRouter = (config = {
|
|
|
1161
1161
|
isReady: false,
|
|
1162
1162
|
pendingResolvers: [],
|
|
1163
1163
|
currentPath: "",
|
|
1164
|
-
popAttached: false
|
|
1164
|
+
popAttached: false,
|
|
1165
|
+
lifecycleHooks: { beforeUnmount: [], unmount: [] }
|
|
1165
1166
|
};
|
|
1167
|
+
if (!state.lifecycleHooks) {
|
|
1168
|
+
state.lifecycleHooks = { beforeUnmount: [], unmount: [] };
|
|
1169
|
+
}
|
|
1166
1170
|
const routeRegistrations = state.routeRegistrations;
|
|
1167
1171
|
if (typeof window !== "undefined" && !windowImpl) {
|
|
1168
1172
|
windowImpl = globalThis.__defuss_window || window;
|
|
@@ -1285,6 +1289,35 @@ const setupRouter = (config = {
|
|
|
1285
1289
|
resolve();
|
|
1286
1290
|
}
|
|
1287
1291
|
state.pendingResolvers = [];
|
|
1292
|
+
},
|
|
1293
|
+
createRouteContext(request) {
|
|
1294
|
+
return {
|
|
1295
|
+
request,
|
|
1296
|
+
onBeforeUnmount(fn) {
|
|
1297
|
+
state.lifecycleHooks.beforeUnmount.push(fn);
|
|
1298
|
+
},
|
|
1299
|
+
onUnmount(fn) {
|
|
1300
|
+
state.lifecycleHooks.unmount.push(fn);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
},
|
|
1304
|
+
async runBeforeUnmountHooks() {
|
|
1305
|
+
for (const fn of state.lifecycleHooks.beforeUnmount) {
|
|
1306
|
+
const result = await fn();
|
|
1307
|
+
if (result === false) return false;
|
|
1308
|
+
}
|
|
1309
|
+
return true;
|
|
1310
|
+
},
|
|
1311
|
+
runUnmountHooks() {
|
|
1312
|
+
for (const fn of state.lifecycleHooks.unmount) {
|
|
1313
|
+
fn();
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
clearRouteLifecycle() {
|
|
1317
|
+
const oldUnmountHooks = [...state.lifecycleHooks.unmount];
|
|
1318
|
+
state.lifecycleHooks.beforeUnmount = [];
|
|
1319
|
+
state.lifecycleHooks.unmount = [];
|
|
1320
|
+
return { unmountHooks: oldUnmountHooks };
|
|
1288
1321
|
}
|
|
1289
1322
|
};
|
|
1290
1323
|
const handlePopState = (event) => {
|
|
@@ -1314,7 +1347,8 @@ if (!globalThis[ROUTER_STATE_KEY]) {
|
|
|
1314
1347
|
isReady: false,
|
|
1315
1348
|
pendingResolvers: [],
|
|
1316
1349
|
currentPath: "",
|
|
1317
|
-
popAttached: false
|
|
1350
|
+
popAttached: false,
|
|
1351
|
+
lifecycleHooks: { beforeUnmount: [], unmount: [] }
|
|
1318
1352
|
};
|
|
1319
1353
|
}
|
|
1320
1354
|
const getRouterState = () => globalThis[ROUTER_STATE_KEY];
|
|
@@ -1335,7 +1369,13 @@ const Route = ({
|
|
|
1335
1369
|
const req = router.match(path);
|
|
1336
1370
|
if (!req.match) return null;
|
|
1337
1371
|
if (Component) {
|
|
1338
|
-
|
|
1372
|
+
const routeContext = router.createRouteContext(req);
|
|
1373
|
+
const isAsync = Component.constructor.name === "AsyncFunction";
|
|
1374
|
+
const routeChildren = Array.isArray(children) ? children[0] : children;
|
|
1375
|
+
if (isAsync && routeChildren) {
|
|
1376
|
+
return /* @__PURE__ */ jsx(Component, { route: routeContext, fallback: routeChildren });
|
|
1377
|
+
}
|
|
1378
|
+
return /* @__PURE__ */ jsx(Component, { route: routeContext });
|
|
1339
1379
|
}
|
|
1340
1380
|
return Array.isArray(children) ? children[0] : children || null;
|
|
1341
1381
|
};
|
|
@@ -1380,11 +1420,27 @@ const RouterSlot = ({
|
|
|
1380
1420
|
router.onRouteChange(async () => {
|
|
1381
1421
|
const currentPath = router.getRequest().path;
|
|
1382
1422
|
const isSamePath = currentPath === lastPath;
|
|
1423
|
+
if (!isSamePath) {
|
|
1424
|
+
const allowed = await router.runBeforeUnmountHooks();
|
|
1425
|
+
if (!allowed) {
|
|
1426
|
+
window.history.pushState({}, "", lastPath);
|
|
1427
|
+
router.resolve(lastPath);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const { unmountHooks } = router.clearRouteLifecycle();
|
|
1431
|
+
await $(ref).update(
|
|
1432
|
+
typeof RouterOutlet === "function" ? RouterOutlet() : [],
|
|
1433
|
+
transitionConfig
|
|
1434
|
+
);
|
|
1435
|
+
for (const fn of unmountHooks) {
|
|
1436
|
+
fn();
|
|
1437
|
+
}
|
|
1438
|
+
} else {
|
|
1439
|
+
await $(ref).update(
|
|
1440
|
+
typeof RouterOutlet === "function" ? RouterOutlet() : []
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1383
1443
|
lastPath = currentPath;
|
|
1384
|
-
await $(ref).update(
|
|
1385
|
-
typeof RouterOutlet === "function" ? RouterOutlet() : [],
|
|
1386
|
-
isSamePath ? void 0 : transitionConfig
|
|
1387
|
-
);
|
|
1388
1444
|
});
|
|
1389
1445
|
}
|
|
1390
1446
|
if (document.getElementById(slotId)) {
|