@hybridly/core 0.4.4 → 0.5.0

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 CHANGED
@@ -18,14 +18,12 @@ const ONLY_DATA_HEADER = `${HYBRIDLY_HEADER}-only-data`;
18
18
  const DIALOG_KEY_HEADER = `${HYBRIDLY_HEADER}-dialog-key`;
19
19
  const DIALOG_REDIRECT_HEADER = `${HYBRIDLY_HEADER}-dialog-redirect`;
20
20
  const EXCEPT_DATA_HEADER = `${HYBRIDLY_HEADER}-except-data`;
21
- const CONTEXT_HEADER = `${HYBRIDLY_HEADER}-context`;
22
21
  const VERSION_HEADER = `${HYBRIDLY_HEADER}-version`;
23
22
  const ERROR_BAG_HEADER = `${HYBRIDLY_HEADER}-error-bag`;
24
23
  const SCROLL_REGION_ATTRIBUTE = "scroll-region";
25
24
 
26
25
  const constants = {
27
26
  __proto__: null,
28
- CONTEXT_HEADER: CONTEXT_HEADER,
29
27
  DIALOG_KEY_HEADER: DIALOG_KEY_HEADER,
30
28
  DIALOG_REDIRECT_HEADER: DIALOG_REDIRECT_HEADER,
31
29
  ERROR_BAG_HEADER: ERROR_BAG_HEADER,
@@ -273,6 +271,7 @@ async function registerEventListeners() {
273
271
  if (!state) {
274
272
  utils.debug.history("There is no state. Adding hash if any and restoring scroll positions.");
275
273
  return await navigate({
274
+ type: "initial",
276
275
  payload: {
277
276
  ...context,
278
277
  url: makeUrl(context.url, { hash: window.location.hash }).toString()
@@ -283,11 +282,11 @@ async function registerEventListeners() {
283
282
  });
284
283
  }
285
284
  await navigate({
285
+ type: "back-forward",
286
286
  payload: state,
287
287
  preserveScroll: true,
288
288
  preserveState: !!getInternalRouterContext().dialog || !!state.dialog,
289
- updateHistoryState: false,
290
- isBackForward: true
289
+ updateHistoryState: false
291
290
  });
292
291
  });
293
292
  window?.addEventListener("scroll", (event) => utils.debounce(() => {
@@ -310,14 +309,14 @@ async function handleBackForwardNavigation() {
310
309
  throw new Error("Tried to handling a back/forward navigation, but there was no state in the history. This should not happen.");
311
310
  }
312
311
  await navigate({
312
+ type: "back-forward",
313
313
  payload: {
314
314
  ...state,
315
315
  version: context.version
316
316
  },
317
317
  preserveScroll: true,
318
318
  preserveState: false,
319
- updateHistoryState: false,
320
- isBackForward: true
319
+ updateHistoryState: false
321
320
  });
322
321
  }
323
322
  function remember(key, value) {
@@ -607,8 +606,9 @@ async function handleExternalNavigation() {
607
606
  url: makeUrl(getRouterContext().url, { hash: window.location.hash }).toString()
608
607
  });
609
608
  await navigate({
610
- preserveScroll: options.preserveScroll,
611
- preserveState: true
609
+ type: "initial",
610
+ preserveState: true,
611
+ preserveScroll: options.preserveScroll
612
612
  });
613
613
  }
614
614
  function isExternalNavigation() {
@@ -644,6 +644,27 @@ async function closeDialog(options) {
644
644
  });
645
645
  }
646
646
 
647
+ function isDownloadResponse(response) {
648
+ return response.status === 200 && !!response.headers["content-disposition"];
649
+ }
650
+ async function handleDownloadResponse(response) {
651
+ const blob = new Blob([response.data], { type: "application/octet-stream" });
652
+ const urlObject = window.webkitURL || window.URL;
653
+ const link = document.createElement("a");
654
+ link.style.display = "none";
655
+ link.href = urlObject.createObjectURL(blob);
656
+ link.download = getFileNameFromContentDispositionHeader(response.headers["content-disposition"]);
657
+ link.click();
658
+ setTimeout(() => {
659
+ urlObject.revokeObjectURL(link.href);
660
+ link.remove();
661
+ }, 0);
662
+ }
663
+ function getFileNameFromContentDispositionHeader(header) {
664
+ const result = header.split(";")[1]?.trim().split("=")[1];
665
+ return result?.replace(/^"(.*)"$/, "$1") ?? "";
666
+ }
667
+
647
668
  function isPreloaded(targetUrl) {
648
669
  const context = getInternalRouterContext();
649
670
  return context.preloadCache.has(targetUrl.toString()) ?? false;
@@ -765,10 +786,7 @@ async function performHybridNavigation(options) {
765
786
  context.pendingNavigation?.controller?.abort();
766
787
  }
767
788
  saveScrollPositions();
768
- if (options.url && options.transformUrl) {
769
- options.url = makeUrl(options.url, options.transformUrl);
770
- }
771
- const targetUrl = makeUrl(options.url ?? context.url);
789
+ const targetUrl = makeUrl(options.url ?? context.url, options.transformUrl);
772
790
  const abortController = new AbortController();
773
791
  setContext({
774
792
  pendingNavigation: {
@@ -782,7 +800,10 @@ async function performHybridNavigation(options) {
782
800
  await runHooks("start", options.hooks, context);
783
801
  utils.debug.router("Making request with axios.");
784
802
  const response = await performHybridRequest(targetUrl, options, abortController);
785
- await runHooks("data", options.hooks, response, context);
803
+ const result = await runHooks("data", options.hooks, response, context);
804
+ if (result === false) {
805
+ return { response };
806
+ }
786
807
  if (isExternalResponse(response)) {
787
808
  utils.debug.router("The response is explicitely external.");
788
809
  await performExternalNavigation({
@@ -791,6 +812,11 @@ async function performHybridNavigation(options) {
791
812
  });
792
813
  return { response };
793
814
  }
815
+ if (isDownloadResponse(response)) {
816
+ utils.debug.router("The response returns a file to download.");
817
+ await handleDownloadResponse(response);
818
+ return { response };
819
+ }
794
820
  if (!isHybridResponse(response)) {
795
821
  throw new NotAHybridResponseError(response);
796
822
  }
@@ -802,6 +828,7 @@ async function performHybridNavigation(options) {
802
828
  utils.debug.router("Merged properties:", payload.view.properties);
803
829
  }
804
830
  await navigate({
831
+ type: "server",
805
832
  payload: {
806
833
  ...payload,
807
834
  url: fillHash(targetUrl, payload.url)
@@ -885,6 +912,7 @@ function isHybridResponse(response) {
885
912
  }
886
913
  async function navigate(options) {
887
914
  const context = getRouterContext();
915
+ options.hasDialog ?? (options.hasDialog = !!options.payload?.dialog);
888
916
  utils.debug.router("Making an internal navigation:", { context, options });
889
917
  await runHooks("navigating", {}, options, context);
890
918
  options.payload ?? (options.payload = payloadFromContext());
@@ -895,6 +923,7 @@ async function navigate(options) {
895
923
  const shouldPreserveScroll = evaluateConditionalOption(options.preserveScroll);
896
924
  const shouldReplaceHistory = evaluateConditionalOption(options.replace);
897
925
  const shouldReplaceUrl = evaluateConditionalOption(options.preserveUrl);
926
+ const shouldPreserveView = !options.payload.view.component;
898
927
  if (shouldPreserveState && getHistoryMemo() && options.payload.view.component === context.view.component) {
899
928
  utils.debug.history("Setting the memo from this history entry into the current context.");
900
929
  setContext({ memo: getHistoryMemo() });
@@ -903,26 +932,44 @@ async function navigate(options) {
903
932
  utils.debug.router(`Preserving the current URL (${context.url}) instead of navigating to ${options.payload.url}`);
904
933
  options.payload.url = context.url;
905
934
  }
906
- setContext({
907
- ...options.payload,
908
- memo: {}
909
- });
935
+ const payload = shouldPreserveView ? {
936
+ view: {
937
+ component: context.view.component,
938
+ properties: utils.merge(context.view.properties, options.payload.view.properties),
939
+ deferred: context.view.deferred
940
+ },
941
+ url: context.url,
942
+ version: options.payload.version,
943
+ dialog: context.dialog
944
+ } : options.payload;
945
+ setContext({ ...payload, memo: {} });
910
946
  if (options.updateHistoryState !== false) {
911
947
  utils.debug.router(`Target URL is ${context.url}, current window URL is ${window.location.href}.`, { shouldReplaceHistory });
912
948
  setHistoryState({ replace: shouldReplaceHistory });
913
949
  }
914
- context.adapter.executeOnMounted(() => {
915
- runHooks("mounted", {}, context);
916
- });
917
- const viewComponent = await context.adapter.resolveComponent(context.view.component);
918
- utils.debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
950
+ if (context.view.deferred?.length) {
951
+ utils.debug.router("Request has deferred properties, queueing a partial reload:", context.view.deferred);
952
+ context.adapter.executeOnMounted(async () => {
953
+ await performHybridNavigation({
954
+ preserveScroll: true,
955
+ preserveState: true,
956
+ replace: true,
957
+ only: context.view.deferred
958
+ });
959
+ });
960
+ }
961
+ const viewComponent = !shouldPreserveView ? await context.adapter.resolveComponent(context.view.component) : void 0;
962
+ if (viewComponent) {
963
+ utils.debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
964
+ }
919
965
  await context.adapter.onViewSwap({
920
966
  component: viewComponent,
921
967
  dialog: context.dialog,
922
968
  properties: options.payload?.view?.properties,
923
- preserveState: shouldPreserveState
969
+ preserveState: shouldPreserveState,
970
+ onMounted: (hookOptions) => runHooks("mounted", {}, { ...options, ...hookOptions }, context)
924
971
  });
925
- if (options.isBackForward) {
972
+ if (options.type === "back-forward") {
926
973
  restoreScrollPositions();
927
974
  } else if (!shouldPreserveScroll) {
928
975
  resetScrollPositions();
@@ -974,12 +1021,12 @@ async function initializeRouter() {
974
1021
  } else if (isExternalNavigation()) {
975
1022
  handleExternalNavigation();
976
1023
  } else {
977
- utils.debug.router("Handling the initial page navigation.");
1024
+ utils.debug.router("Handling the initial navigation.");
978
1025
  setContext({
979
1026
  url: makeUrl(context.url, { hash: window.location.hash }).toString()
980
1027
  });
981
1028
  await navigate({
982
- isInitial: true,
1029
+ type: "initial",
983
1030
  preserveState: true,
984
1031
  replace: sameUrls(context.url, window.location.href)
985
1032
  });
@@ -993,13 +1040,15 @@ async function performLocalNavigation(targetUrl, options) {
993
1040
  const url = normalizeUrl(targetUrl);
994
1041
  return await navigate({
995
1042
  ...options,
1043
+ type: "local",
996
1044
  payload: {
997
1045
  version: context.version,
998
1046
  dialog: options?.dialog === false ? void 0 : options?.dialog ?? context.dialog,
999
1047
  url,
1000
1048
  view: {
1001
1049
  component: options?.component ?? context.view.component,
1002
- properties: options?.properties ?? {}
1050
+ properties: options?.properties ?? {},
1051
+ deferred: []
1003
1052
  }
1004
1053
  }
1005
1054
  });
package/dist/index.d.cts CHANGED
@@ -55,7 +55,7 @@ interface Hooks extends RequestHooks {
55
55
  */
56
56
  initialized: (context: InternalRouterContext) => MaybePromise<any>;
57
57
  /**
58
- * Called after Hybridly's initial page load.
58
+ * Called after Hybridly's initial load.
59
59
  */
60
60
  ready: (context: InternalRouterContext) => MaybePromise<any>;
61
61
  /**
@@ -65,15 +65,21 @@ interface Hooks extends RequestHooks {
65
65
  /**
66
66
  * Called when a component navigation is being made.
67
67
  */
68
- navigating: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
68
+ navigating: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
69
69
  /**
70
70
  * Called when a component has been navigated to.
71
71
  */
72
- navigated: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
72
+ navigated: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
73
73
  /**
74
74
  * Called when a component has been navigated to and was mounted by the adapter.
75
75
  */
76
- mounted: (context: InternalRouterContext) => MaybePromise<any>;
76
+ mounted: (options: InternalNavigationOptions & MountedHookOptions, context: InternalRouterContext) => MaybePromise<any>;
77
+ }
78
+ interface MountedHookOptions {
79
+ /**
80
+ * Whether the component being mounted is a dialog.
81
+ */
82
+ isDialog: boolean;
77
83
  }
78
84
  interface HookOptions {
79
85
  /** Executes the hook only once. */
@@ -128,7 +134,7 @@ interface ComponentNavigationOptions {
128
134
  replace?: ConditionalNavigationOption<boolean>;
129
135
  /** Whether to preserve the current scrollbar position. */
130
136
  preserveScroll?: ConditionalNavigationOption<boolean>;
131
- /** Whether to preserve the current page component state. */
137
+ /** Whether to preserve the current view component state. */
132
138
  preserveState?: ConditionalNavigationOption<boolean>;
133
139
  }
134
140
  interface NavigationOptions {
@@ -139,9 +145,9 @@ interface NavigationOptions {
139
145
  * one. This affects the browser's "back" and "forward" features.
140
146
  */
141
147
  replace?: ConditionalNavigationOption<boolean>;
142
- /** Whether to preserve the scrollbars positions on the page. */
148
+ /** Whether to preserve the scrollbars positions on the view. */
143
149
  preserveScroll?: ConditionalNavigationOption<boolean>;
144
- /** Whether to preserve the current page component's state. */
150
+ /** Whether to preserve the current view component's state. */
145
151
  preserveState?: ConditionalNavigationOption<boolean>;
146
152
  /** Whether to preserve the current URL. */
147
153
  preserveUrl?: ConditionalNavigationOption<boolean>;
@@ -160,16 +166,22 @@ interface NavigationOptions {
160
166
  * @internal This is an advanced property meant to be used internally.
161
167
  */
162
168
  updateHistoryState?: boolean;
169
+ }
170
+ interface InternalNavigationOptions extends NavigationOptions {
163
171
  /**
164
- * Defines whether this navigation is a back/forward navigation from the popstate event.
165
- * @internal This is an advanced property meant to be used internally.
172
+ * Defines the kind of navigation being performed.
173
+ * - initial: the initial load's navigation
174
+ * - server: a navigation initiated by a server round-trip
175
+ * - local: a navigation initiated by `router.local`
176
+ * - back-forward: a navigation initiated by the browser's `popstate` event
177
+ * @internal
166
178
  */
167
- isBackForward?: boolean;
179
+ type: 'initial' | 'local' | 'back-forward' | 'server';
168
180
  /**
169
- * Defines whether this navigation is the first to happen after a direct page load.
170
- * @internal This is an advanced property meant to be used internally.
181
+ * Defines whether this navigation opens a dialog.
182
+ * @internal
171
183
  */
172
- isInitial?: boolean;
184
+ hasDialog?: boolean;
173
185
  }
174
186
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
175
187
  interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
@@ -266,15 +278,17 @@ interface PendingNavigation {
266
278
  /** Current status. */
267
279
  status: 'pending' | 'success' | 'error';
268
280
  }
269
- /** A page or dialog component. */
281
+ /** A view or dialog component. */
270
282
  interface View {
271
283
  /** Name of the component to use. */
272
- component: string;
284
+ component?: string;
273
285
  /** Properties to apply to the component. */
274
286
  properties: Properties;
287
+ /** Deferred properties for this view. */
288
+ deferred: string[];
275
289
  }
276
- interface Dialog extends View {
277
- /** URL that is the base background page when navigating to the dialog directly. */
290
+ interface Dialog extends Required<View> {
291
+ /** URL that is the base background view when navigating to the dialog directly. */
278
292
  baseUrl: string;
279
293
  /** URL to which the dialog should redirect when closed. */
280
294
  redirectUrl: string;
@@ -294,6 +308,8 @@ interface SwapOptions<T> {
294
308
  preserveState?: boolean;
295
309
  /** Current dialog. */
296
310
  dialog?: Dialog;
311
+ /** On mounted callback. */
312
+ onMounted?: (options: MountedHookOptions) => void;
297
313
  }
298
314
  type ViewComponent = any;
299
315
  type ResolveComponent = (name: string) => Promise<ViewComponent>;
@@ -471,12 +487,10 @@ declare const ONLY_DATA_HEADER = "x-hybrid-only-data";
471
487
  declare const DIALOG_KEY_HEADER = "x-hybrid-dialog-key";
472
488
  declare const DIALOG_REDIRECT_HEADER = "x-hybrid-dialog-redirect";
473
489
  declare const EXCEPT_DATA_HEADER = "x-hybrid-except-data";
474
- declare const CONTEXT_HEADER = "x-hybrid-context";
475
490
  declare const VERSION_HEADER = "x-hybrid-version";
476
491
  declare const ERROR_BAG_HEADER = "x-hybrid-error-bag";
477
492
  declare const SCROLL_REGION_ATTRIBUTE = "scroll-region";
478
493
 
479
- declare const constants_CONTEXT_HEADER: typeof CONTEXT_HEADER;
480
494
  declare const constants_DIALOG_KEY_HEADER: typeof DIALOG_KEY_HEADER;
481
495
  declare const constants_DIALOG_REDIRECT_HEADER: typeof DIALOG_REDIRECT_HEADER;
482
496
  declare const constants_ERROR_BAG_HEADER: typeof ERROR_BAG_HEADER;
@@ -489,7 +503,7 @@ declare const constants_SCROLL_REGION_ATTRIBUTE: typeof SCROLL_REGION_ATTRIBUTE;
489
503
  declare const constants_STORAGE_EXTERNAL_KEY: typeof STORAGE_EXTERNAL_KEY;
490
504
  declare const constants_VERSION_HEADER: typeof VERSION_HEADER;
491
505
  declare namespace constants {
492
- export { constants_CONTEXT_HEADER as CONTEXT_HEADER, constants_DIALOG_KEY_HEADER as DIALOG_KEY_HEADER, constants_DIALOG_REDIRECT_HEADER as DIALOG_REDIRECT_HEADER, constants_ERROR_BAG_HEADER as ERROR_BAG_HEADER, constants_EXCEPT_DATA_HEADER as EXCEPT_DATA_HEADER, constants_EXTERNAL_NAVIGATION_HEADER as EXTERNAL_NAVIGATION_HEADER, constants_HYBRIDLY_HEADER as HYBRIDLY_HEADER, constants_ONLY_DATA_HEADER as ONLY_DATA_HEADER, constants_PARTIAL_COMPONENT_HEADER as PARTIAL_COMPONENT_HEADER, constants_SCROLL_REGION_ATTRIBUTE as SCROLL_REGION_ATTRIBUTE, constants_STORAGE_EXTERNAL_KEY as STORAGE_EXTERNAL_KEY, constants_VERSION_HEADER as VERSION_HEADER };
506
+ export { constants_DIALOG_KEY_HEADER as DIALOG_KEY_HEADER, constants_DIALOG_REDIRECT_HEADER as DIALOG_REDIRECT_HEADER, constants_ERROR_BAG_HEADER as ERROR_BAG_HEADER, constants_EXCEPT_DATA_HEADER as EXCEPT_DATA_HEADER, constants_EXTERNAL_NAVIGATION_HEADER as EXTERNAL_NAVIGATION_HEADER, constants_HYBRIDLY_HEADER as HYBRIDLY_HEADER, constants_ONLY_DATA_HEADER as ONLY_DATA_HEADER, constants_PARTIAL_COMPONENT_HEADER as PARTIAL_COMPONENT_HEADER, constants_SCROLL_REGION_ATTRIBUTE as SCROLL_REGION_ATTRIBUTE, constants_STORAGE_EXTERNAL_KEY as STORAGE_EXTERNAL_KEY, constants_VERSION_HEADER as VERSION_HEADER };
493
507
  }
494
508
 
495
509
  export { type Authorizable, type DynamicConfiguration, type GlobalRouteCollection, type HybridPayload, type HybridRequestOptions, type MaybePromise, type Method, type NavigationResponse, type Plugin, type Progress, type ResolveComponent, type RouteDefinition, type RouteName, type RouteParameters, type Router, type RouterContext, type RouterContextOptions, type RoutingConfiguration, type UrlResolvable, can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
package/dist/index.d.mts CHANGED
@@ -55,7 +55,7 @@ interface Hooks extends RequestHooks {
55
55
  */
56
56
  initialized: (context: InternalRouterContext) => MaybePromise<any>;
57
57
  /**
58
- * Called after Hybridly's initial page load.
58
+ * Called after Hybridly's initial load.
59
59
  */
60
60
  ready: (context: InternalRouterContext) => MaybePromise<any>;
61
61
  /**
@@ -65,15 +65,21 @@ interface Hooks extends RequestHooks {
65
65
  /**
66
66
  * Called when a component navigation is being made.
67
67
  */
68
- navigating: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
68
+ navigating: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
69
69
  /**
70
70
  * Called when a component has been navigated to.
71
71
  */
72
- navigated: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
72
+ navigated: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
73
73
  /**
74
74
  * Called when a component has been navigated to and was mounted by the adapter.
75
75
  */
76
- mounted: (context: InternalRouterContext) => MaybePromise<any>;
76
+ mounted: (options: InternalNavigationOptions & MountedHookOptions, context: InternalRouterContext) => MaybePromise<any>;
77
+ }
78
+ interface MountedHookOptions {
79
+ /**
80
+ * Whether the component being mounted is a dialog.
81
+ */
82
+ isDialog: boolean;
77
83
  }
78
84
  interface HookOptions {
79
85
  /** Executes the hook only once. */
@@ -128,7 +134,7 @@ interface ComponentNavigationOptions {
128
134
  replace?: ConditionalNavigationOption<boolean>;
129
135
  /** Whether to preserve the current scrollbar position. */
130
136
  preserveScroll?: ConditionalNavigationOption<boolean>;
131
- /** Whether to preserve the current page component state. */
137
+ /** Whether to preserve the current view component state. */
132
138
  preserveState?: ConditionalNavigationOption<boolean>;
133
139
  }
134
140
  interface NavigationOptions {
@@ -139,9 +145,9 @@ interface NavigationOptions {
139
145
  * one. This affects the browser's "back" and "forward" features.
140
146
  */
141
147
  replace?: ConditionalNavigationOption<boolean>;
142
- /** Whether to preserve the scrollbars positions on the page. */
148
+ /** Whether to preserve the scrollbars positions on the view. */
143
149
  preserveScroll?: ConditionalNavigationOption<boolean>;
144
- /** Whether to preserve the current page component's state. */
150
+ /** Whether to preserve the current view component's state. */
145
151
  preserveState?: ConditionalNavigationOption<boolean>;
146
152
  /** Whether to preserve the current URL. */
147
153
  preserveUrl?: ConditionalNavigationOption<boolean>;
@@ -160,16 +166,22 @@ interface NavigationOptions {
160
166
  * @internal This is an advanced property meant to be used internally.
161
167
  */
162
168
  updateHistoryState?: boolean;
169
+ }
170
+ interface InternalNavigationOptions extends NavigationOptions {
163
171
  /**
164
- * Defines whether this navigation is a back/forward navigation from the popstate event.
165
- * @internal This is an advanced property meant to be used internally.
172
+ * Defines the kind of navigation being performed.
173
+ * - initial: the initial load's navigation
174
+ * - server: a navigation initiated by a server round-trip
175
+ * - local: a navigation initiated by `router.local`
176
+ * - back-forward: a navigation initiated by the browser's `popstate` event
177
+ * @internal
166
178
  */
167
- isBackForward?: boolean;
179
+ type: 'initial' | 'local' | 'back-forward' | 'server';
168
180
  /**
169
- * Defines whether this navigation is the first to happen after a direct page load.
170
- * @internal This is an advanced property meant to be used internally.
181
+ * Defines whether this navigation opens a dialog.
182
+ * @internal
171
183
  */
172
- isInitial?: boolean;
184
+ hasDialog?: boolean;
173
185
  }
174
186
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
175
187
  interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
@@ -266,15 +278,17 @@ interface PendingNavigation {
266
278
  /** Current status. */
267
279
  status: 'pending' | 'success' | 'error';
268
280
  }
269
- /** A page or dialog component. */
281
+ /** A view or dialog component. */
270
282
  interface View {
271
283
  /** Name of the component to use. */
272
- component: string;
284
+ component?: string;
273
285
  /** Properties to apply to the component. */
274
286
  properties: Properties;
287
+ /** Deferred properties for this view. */
288
+ deferred: string[];
275
289
  }
276
- interface Dialog extends View {
277
- /** URL that is the base background page when navigating to the dialog directly. */
290
+ interface Dialog extends Required<View> {
291
+ /** URL that is the base background view when navigating to the dialog directly. */
278
292
  baseUrl: string;
279
293
  /** URL to which the dialog should redirect when closed. */
280
294
  redirectUrl: string;
@@ -294,6 +308,8 @@ interface SwapOptions<T> {
294
308
  preserveState?: boolean;
295
309
  /** Current dialog. */
296
310
  dialog?: Dialog;
311
+ /** On mounted callback. */
312
+ onMounted?: (options: MountedHookOptions) => void;
297
313
  }
298
314
  type ViewComponent = any;
299
315
  type ResolveComponent = (name: string) => Promise<ViewComponent>;
@@ -471,12 +487,10 @@ declare const ONLY_DATA_HEADER = "x-hybrid-only-data";
471
487
  declare const DIALOG_KEY_HEADER = "x-hybrid-dialog-key";
472
488
  declare const DIALOG_REDIRECT_HEADER = "x-hybrid-dialog-redirect";
473
489
  declare const EXCEPT_DATA_HEADER = "x-hybrid-except-data";
474
- declare const CONTEXT_HEADER = "x-hybrid-context";
475
490
  declare const VERSION_HEADER = "x-hybrid-version";
476
491
  declare const ERROR_BAG_HEADER = "x-hybrid-error-bag";
477
492
  declare const SCROLL_REGION_ATTRIBUTE = "scroll-region";
478
493
 
479
- declare const constants_CONTEXT_HEADER: typeof CONTEXT_HEADER;
480
494
  declare const constants_DIALOG_KEY_HEADER: typeof DIALOG_KEY_HEADER;
481
495
  declare const constants_DIALOG_REDIRECT_HEADER: typeof DIALOG_REDIRECT_HEADER;
482
496
  declare const constants_ERROR_BAG_HEADER: typeof ERROR_BAG_HEADER;
@@ -489,7 +503,7 @@ declare const constants_SCROLL_REGION_ATTRIBUTE: typeof SCROLL_REGION_ATTRIBUTE;
489
503
  declare const constants_STORAGE_EXTERNAL_KEY: typeof STORAGE_EXTERNAL_KEY;
490
504
  declare const constants_VERSION_HEADER: typeof VERSION_HEADER;
491
505
  declare namespace constants {
492
- export { constants_CONTEXT_HEADER as CONTEXT_HEADER, constants_DIALOG_KEY_HEADER as DIALOG_KEY_HEADER, constants_DIALOG_REDIRECT_HEADER as DIALOG_REDIRECT_HEADER, constants_ERROR_BAG_HEADER as ERROR_BAG_HEADER, constants_EXCEPT_DATA_HEADER as EXCEPT_DATA_HEADER, constants_EXTERNAL_NAVIGATION_HEADER as EXTERNAL_NAVIGATION_HEADER, constants_HYBRIDLY_HEADER as HYBRIDLY_HEADER, constants_ONLY_DATA_HEADER as ONLY_DATA_HEADER, constants_PARTIAL_COMPONENT_HEADER as PARTIAL_COMPONENT_HEADER, constants_SCROLL_REGION_ATTRIBUTE as SCROLL_REGION_ATTRIBUTE, constants_STORAGE_EXTERNAL_KEY as STORAGE_EXTERNAL_KEY, constants_VERSION_HEADER as VERSION_HEADER };
506
+ export { constants_DIALOG_KEY_HEADER as DIALOG_KEY_HEADER, constants_DIALOG_REDIRECT_HEADER as DIALOG_REDIRECT_HEADER, constants_ERROR_BAG_HEADER as ERROR_BAG_HEADER, constants_EXCEPT_DATA_HEADER as EXCEPT_DATA_HEADER, constants_EXTERNAL_NAVIGATION_HEADER as EXTERNAL_NAVIGATION_HEADER, constants_HYBRIDLY_HEADER as HYBRIDLY_HEADER, constants_ONLY_DATA_HEADER as ONLY_DATA_HEADER, constants_PARTIAL_COMPONENT_HEADER as PARTIAL_COMPONENT_HEADER, constants_SCROLL_REGION_ATTRIBUTE as SCROLL_REGION_ATTRIBUTE, constants_STORAGE_EXTERNAL_KEY as STORAGE_EXTERNAL_KEY, constants_VERSION_HEADER as VERSION_HEADER };
493
507
  }
494
508
 
495
509
  export { type Authorizable, type DynamicConfiguration, type GlobalRouteCollection, type HybridPayload, type HybridRequestOptions, type MaybePromise, type Method, type NavigationResponse, type Plugin, type Progress, type ResolveComponent, type RouteDefinition, type RouteName, type RouteParameters, type Router, type RouterContext, type RouterContextOptions, type RoutingConfiguration, type UrlResolvable, can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
package/dist/index.d.ts CHANGED
@@ -55,7 +55,7 @@ interface Hooks extends RequestHooks {
55
55
  */
56
56
  initialized: (context: InternalRouterContext) => MaybePromise<any>;
57
57
  /**
58
- * Called after Hybridly's initial page load.
58
+ * Called after Hybridly's initial load.
59
59
  */
60
60
  ready: (context: InternalRouterContext) => MaybePromise<any>;
61
61
  /**
@@ -65,15 +65,21 @@ interface Hooks extends RequestHooks {
65
65
  /**
66
66
  * Called when a component navigation is being made.
67
67
  */
68
- navigating: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
68
+ navigating: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
69
69
  /**
70
70
  * Called when a component has been navigated to.
71
71
  */
72
- navigated: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
72
+ navigated: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
73
73
  /**
74
74
  * Called when a component has been navigated to and was mounted by the adapter.
75
75
  */
76
- mounted: (context: InternalRouterContext) => MaybePromise<any>;
76
+ mounted: (options: InternalNavigationOptions & MountedHookOptions, context: InternalRouterContext) => MaybePromise<any>;
77
+ }
78
+ interface MountedHookOptions {
79
+ /**
80
+ * Whether the component being mounted is a dialog.
81
+ */
82
+ isDialog: boolean;
77
83
  }
78
84
  interface HookOptions {
79
85
  /** Executes the hook only once. */
@@ -128,7 +134,7 @@ interface ComponentNavigationOptions {
128
134
  replace?: ConditionalNavigationOption<boolean>;
129
135
  /** Whether to preserve the current scrollbar position. */
130
136
  preserveScroll?: ConditionalNavigationOption<boolean>;
131
- /** Whether to preserve the current page component state. */
137
+ /** Whether to preserve the current view component state. */
132
138
  preserveState?: ConditionalNavigationOption<boolean>;
133
139
  }
134
140
  interface NavigationOptions {
@@ -139,9 +145,9 @@ interface NavigationOptions {
139
145
  * one. This affects the browser's "back" and "forward" features.
140
146
  */
141
147
  replace?: ConditionalNavigationOption<boolean>;
142
- /** Whether to preserve the scrollbars positions on the page. */
148
+ /** Whether to preserve the scrollbars positions on the view. */
143
149
  preserveScroll?: ConditionalNavigationOption<boolean>;
144
- /** Whether to preserve the current page component's state. */
150
+ /** Whether to preserve the current view component's state. */
145
151
  preserveState?: ConditionalNavigationOption<boolean>;
146
152
  /** Whether to preserve the current URL. */
147
153
  preserveUrl?: ConditionalNavigationOption<boolean>;
@@ -160,16 +166,22 @@ interface NavigationOptions {
160
166
  * @internal This is an advanced property meant to be used internally.
161
167
  */
162
168
  updateHistoryState?: boolean;
169
+ }
170
+ interface InternalNavigationOptions extends NavigationOptions {
163
171
  /**
164
- * Defines whether this navigation is a back/forward navigation from the popstate event.
165
- * @internal This is an advanced property meant to be used internally.
172
+ * Defines the kind of navigation being performed.
173
+ * - initial: the initial load's navigation
174
+ * - server: a navigation initiated by a server round-trip
175
+ * - local: a navigation initiated by `router.local`
176
+ * - back-forward: a navigation initiated by the browser's `popstate` event
177
+ * @internal
166
178
  */
167
- isBackForward?: boolean;
179
+ type: 'initial' | 'local' | 'back-forward' | 'server';
168
180
  /**
169
- * Defines whether this navigation is the first to happen after a direct page load.
170
- * @internal This is an advanced property meant to be used internally.
181
+ * Defines whether this navigation opens a dialog.
182
+ * @internal
171
183
  */
172
- isInitial?: boolean;
184
+ hasDialog?: boolean;
173
185
  }
174
186
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
175
187
  interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
@@ -266,15 +278,17 @@ interface PendingNavigation {
266
278
  /** Current status. */
267
279
  status: 'pending' | 'success' | 'error';
268
280
  }
269
- /** A page or dialog component. */
281
+ /** A view or dialog component. */
270
282
  interface View {
271
283
  /** Name of the component to use. */
272
- component: string;
284
+ component?: string;
273
285
  /** Properties to apply to the component. */
274
286
  properties: Properties;
287
+ /** Deferred properties for this view. */
288
+ deferred: string[];
275
289
  }
276
- interface Dialog extends View {
277
- /** URL that is the base background page when navigating to the dialog directly. */
290
+ interface Dialog extends Required<View> {
291
+ /** URL that is the base background view when navigating to the dialog directly. */
278
292
  baseUrl: string;
279
293
  /** URL to which the dialog should redirect when closed. */
280
294
  redirectUrl: string;
@@ -294,6 +308,8 @@ interface SwapOptions<T> {
294
308
  preserveState?: boolean;
295
309
  /** Current dialog. */
296
310
  dialog?: Dialog;
311
+ /** On mounted callback. */
312
+ onMounted?: (options: MountedHookOptions) => void;
297
313
  }
298
314
  type ViewComponent = any;
299
315
  type ResolveComponent = (name: string) => Promise<ViewComponent>;
@@ -471,12 +487,10 @@ declare const ONLY_DATA_HEADER = "x-hybrid-only-data";
471
487
  declare const DIALOG_KEY_HEADER = "x-hybrid-dialog-key";
472
488
  declare const DIALOG_REDIRECT_HEADER = "x-hybrid-dialog-redirect";
473
489
  declare const EXCEPT_DATA_HEADER = "x-hybrid-except-data";
474
- declare const CONTEXT_HEADER = "x-hybrid-context";
475
490
  declare const VERSION_HEADER = "x-hybrid-version";
476
491
  declare const ERROR_BAG_HEADER = "x-hybrid-error-bag";
477
492
  declare const SCROLL_REGION_ATTRIBUTE = "scroll-region";
478
493
 
479
- declare const constants_CONTEXT_HEADER: typeof CONTEXT_HEADER;
480
494
  declare const constants_DIALOG_KEY_HEADER: typeof DIALOG_KEY_HEADER;
481
495
  declare const constants_DIALOG_REDIRECT_HEADER: typeof DIALOG_REDIRECT_HEADER;
482
496
  declare const constants_ERROR_BAG_HEADER: typeof ERROR_BAG_HEADER;
@@ -489,7 +503,7 @@ declare const constants_SCROLL_REGION_ATTRIBUTE: typeof SCROLL_REGION_ATTRIBUTE;
489
503
  declare const constants_STORAGE_EXTERNAL_KEY: typeof STORAGE_EXTERNAL_KEY;
490
504
  declare const constants_VERSION_HEADER: typeof VERSION_HEADER;
491
505
  declare namespace constants {
492
- export { constants_CONTEXT_HEADER as CONTEXT_HEADER, constants_DIALOG_KEY_HEADER as DIALOG_KEY_HEADER, constants_DIALOG_REDIRECT_HEADER as DIALOG_REDIRECT_HEADER, constants_ERROR_BAG_HEADER as ERROR_BAG_HEADER, constants_EXCEPT_DATA_HEADER as EXCEPT_DATA_HEADER, constants_EXTERNAL_NAVIGATION_HEADER as EXTERNAL_NAVIGATION_HEADER, constants_HYBRIDLY_HEADER as HYBRIDLY_HEADER, constants_ONLY_DATA_HEADER as ONLY_DATA_HEADER, constants_PARTIAL_COMPONENT_HEADER as PARTIAL_COMPONENT_HEADER, constants_SCROLL_REGION_ATTRIBUTE as SCROLL_REGION_ATTRIBUTE, constants_STORAGE_EXTERNAL_KEY as STORAGE_EXTERNAL_KEY, constants_VERSION_HEADER as VERSION_HEADER };
506
+ export { constants_DIALOG_KEY_HEADER as DIALOG_KEY_HEADER, constants_DIALOG_REDIRECT_HEADER as DIALOG_REDIRECT_HEADER, constants_ERROR_BAG_HEADER as ERROR_BAG_HEADER, constants_EXCEPT_DATA_HEADER as EXCEPT_DATA_HEADER, constants_EXTERNAL_NAVIGATION_HEADER as EXTERNAL_NAVIGATION_HEADER, constants_HYBRIDLY_HEADER as HYBRIDLY_HEADER, constants_ONLY_DATA_HEADER as ONLY_DATA_HEADER, constants_PARTIAL_COMPONENT_HEADER as PARTIAL_COMPONENT_HEADER, constants_SCROLL_REGION_ATTRIBUTE as SCROLL_REGION_ATTRIBUTE, constants_STORAGE_EXTERNAL_KEY as STORAGE_EXTERNAL_KEY, constants_VERSION_HEADER as VERSION_HEADER };
493
507
  }
494
508
 
495
509
  export { type Authorizable, type DynamicConfiguration, type GlobalRouteCollection, type HybridPayload, type HybridRequestOptions, type MaybePromise, type Method, type NavigationResponse, type Plugin, type Progress, type ResolveComponent, type RouteDefinition, type RouteName, type RouteParameters, type Router, type RouterContext, type RouterContextOptions, type RoutingConfiguration, type UrlResolvable, can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
package/dist/index.mjs CHANGED
@@ -11,14 +11,12 @@ const ONLY_DATA_HEADER = `${HYBRIDLY_HEADER}-only-data`;
11
11
  const DIALOG_KEY_HEADER = `${HYBRIDLY_HEADER}-dialog-key`;
12
12
  const DIALOG_REDIRECT_HEADER = `${HYBRIDLY_HEADER}-dialog-redirect`;
13
13
  const EXCEPT_DATA_HEADER = `${HYBRIDLY_HEADER}-except-data`;
14
- const CONTEXT_HEADER = `${HYBRIDLY_HEADER}-context`;
15
14
  const VERSION_HEADER = `${HYBRIDLY_HEADER}-version`;
16
15
  const ERROR_BAG_HEADER = `${HYBRIDLY_HEADER}-error-bag`;
17
16
  const SCROLL_REGION_ATTRIBUTE = "scroll-region";
18
17
 
19
18
  const constants = {
20
19
  __proto__: null,
21
- CONTEXT_HEADER: CONTEXT_HEADER,
22
20
  DIALOG_KEY_HEADER: DIALOG_KEY_HEADER,
23
21
  DIALOG_REDIRECT_HEADER: DIALOG_REDIRECT_HEADER,
24
22
  ERROR_BAG_HEADER: ERROR_BAG_HEADER,
@@ -266,6 +264,7 @@ async function registerEventListeners() {
266
264
  if (!state) {
267
265
  debug.history("There is no state. Adding hash if any and restoring scroll positions.");
268
266
  return await navigate({
267
+ type: "initial",
269
268
  payload: {
270
269
  ...context,
271
270
  url: makeUrl(context.url, { hash: window.location.hash }).toString()
@@ -276,11 +275,11 @@ async function registerEventListeners() {
276
275
  });
277
276
  }
278
277
  await navigate({
278
+ type: "back-forward",
279
279
  payload: state,
280
280
  preserveScroll: true,
281
281
  preserveState: !!getInternalRouterContext().dialog || !!state.dialog,
282
- updateHistoryState: false,
283
- isBackForward: true
282
+ updateHistoryState: false
284
283
  });
285
284
  });
286
285
  window?.addEventListener("scroll", (event) => debounce(() => {
@@ -303,14 +302,14 @@ async function handleBackForwardNavigation() {
303
302
  throw new Error("Tried to handling a back/forward navigation, but there was no state in the history. This should not happen.");
304
303
  }
305
304
  await navigate({
305
+ type: "back-forward",
306
306
  payload: {
307
307
  ...state,
308
308
  version: context.version
309
309
  },
310
310
  preserveScroll: true,
311
311
  preserveState: false,
312
- updateHistoryState: false,
313
- isBackForward: true
312
+ updateHistoryState: false
314
313
  });
315
314
  }
316
315
  function remember(key, value) {
@@ -600,8 +599,9 @@ async function handleExternalNavigation() {
600
599
  url: makeUrl(getRouterContext().url, { hash: window.location.hash }).toString()
601
600
  });
602
601
  await navigate({
603
- preserveScroll: options.preserveScroll,
604
- preserveState: true
602
+ type: "initial",
603
+ preserveState: true,
604
+ preserveScroll: options.preserveScroll
605
605
  });
606
606
  }
607
607
  function isExternalNavigation() {
@@ -637,6 +637,27 @@ async function closeDialog(options) {
637
637
  });
638
638
  }
639
639
 
640
+ function isDownloadResponse(response) {
641
+ return response.status === 200 && !!response.headers["content-disposition"];
642
+ }
643
+ async function handleDownloadResponse(response) {
644
+ const blob = new Blob([response.data], { type: "application/octet-stream" });
645
+ const urlObject = window.webkitURL || window.URL;
646
+ const link = document.createElement("a");
647
+ link.style.display = "none";
648
+ link.href = urlObject.createObjectURL(blob);
649
+ link.download = getFileNameFromContentDispositionHeader(response.headers["content-disposition"]);
650
+ link.click();
651
+ setTimeout(() => {
652
+ urlObject.revokeObjectURL(link.href);
653
+ link.remove();
654
+ }, 0);
655
+ }
656
+ function getFileNameFromContentDispositionHeader(header) {
657
+ const result = header.split(";")[1]?.trim().split("=")[1];
658
+ return result?.replace(/^"(.*)"$/, "$1") ?? "";
659
+ }
660
+
640
661
  function isPreloaded(targetUrl) {
641
662
  const context = getInternalRouterContext();
642
663
  return context.preloadCache.has(targetUrl.toString()) ?? false;
@@ -758,10 +779,7 @@ async function performHybridNavigation(options) {
758
779
  context.pendingNavigation?.controller?.abort();
759
780
  }
760
781
  saveScrollPositions();
761
- if (options.url && options.transformUrl) {
762
- options.url = makeUrl(options.url, options.transformUrl);
763
- }
764
- const targetUrl = makeUrl(options.url ?? context.url);
782
+ const targetUrl = makeUrl(options.url ?? context.url, options.transformUrl);
765
783
  const abortController = new AbortController();
766
784
  setContext({
767
785
  pendingNavigation: {
@@ -775,7 +793,10 @@ async function performHybridNavigation(options) {
775
793
  await runHooks("start", options.hooks, context);
776
794
  debug.router("Making request with axios.");
777
795
  const response = await performHybridRequest(targetUrl, options, abortController);
778
- await runHooks("data", options.hooks, response, context);
796
+ const result = await runHooks("data", options.hooks, response, context);
797
+ if (result === false) {
798
+ return { response };
799
+ }
779
800
  if (isExternalResponse(response)) {
780
801
  debug.router("The response is explicitely external.");
781
802
  await performExternalNavigation({
@@ -784,6 +805,11 @@ async function performHybridNavigation(options) {
784
805
  });
785
806
  return { response };
786
807
  }
808
+ if (isDownloadResponse(response)) {
809
+ debug.router("The response returns a file to download.");
810
+ await handleDownloadResponse(response);
811
+ return { response };
812
+ }
787
813
  if (!isHybridResponse(response)) {
788
814
  throw new NotAHybridResponseError(response);
789
815
  }
@@ -795,6 +821,7 @@ async function performHybridNavigation(options) {
795
821
  debug.router("Merged properties:", payload.view.properties);
796
822
  }
797
823
  await navigate({
824
+ type: "server",
798
825
  payload: {
799
826
  ...payload,
800
827
  url: fillHash(targetUrl, payload.url)
@@ -878,6 +905,7 @@ function isHybridResponse(response) {
878
905
  }
879
906
  async function navigate(options) {
880
907
  const context = getRouterContext();
908
+ options.hasDialog ?? (options.hasDialog = !!options.payload?.dialog);
881
909
  debug.router("Making an internal navigation:", { context, options });
882
910
  await runHooks("navigating", {}, options, context);
883
911
  options.payload ?? (options.payload = payloadFromContext());
@@ -888,6 +916,7 @@ async function navigate(options) {
888
916
  const shouldPreserveScroll = evaluateConditionalOption(options.preserveScroll);
889
917
  const shouldReplaceHistory = evaluateConditionalOption(options.replace);
890
918
  const shouldReplaceUrl = evaluateConditionalOption(options.preserveUrl);
919
+ const shouldPreserveView = !options.payload.view.component;
891
920
  if (shouldPreserveState && getHistoryMemo() && options.payload.view.component === context.view.component) {
892
921
  debug.history("Setting the memo from this history entry into the current context.");
893
922
  setContext({ memo: getHistoryMemo() });
@@ -896,26 +925,44 @@ async function navigate(options) {
896
925
  debug.router(`Preserving the current URL (${context.url}) instead of navigating to ${options.payload.url}`);
897
926
  options.payload.url = context.url;
898
927
  }
899
- setContext({
900
- ...options.payload,
901
- memo: {}
902
- });
928
+ const payload = shouldPreserveView ? {
929
+ view: {
930
+ component: context.view.component,
931
+ properties: merge(context.view.properties, options.payload.view.properties),
932
+ deferred: context.view.deferred
933
+ },
934
+ url: context.url,
935
+ version: options.payload.version,
936
+ dialog: context.dialog
937
+ } : options.payload;
938
+ setContext({ ...payload, memo: {} });
903
939
  if (options.updateHistoryState !== false) {
904
940
  debug.router(`Target URL is ${context.url}, current window URL is ${window.location.href}.`, { shouldReplaceHistory });
905
941
  setHistoryState({ replace: shouldReplaceHistory });
906
942
  }
907
- context.adapter.executeOnMounted(() => {
908
- runHooks("mounted", {}, context);
909
- });
910
- const viewComponent = await context.adapter.resolveComponent(context.view.component);
911
- debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
943
+ if (context.view.deferred?.length) {
944
+ debug.router("Request has deferred properties, queueing a partial reload:", context.view.deferred);
945
+ context.adapter.executeOnMounted(async () => {
946
+ await performHybridNavigation({
947
+ preserveScroll: true,
948
+ preserveState: true,
949
+ replace: true,
950
+ only: context.view.deferred
951
+ });
952
+ });
953
+ }
954
+ const viewComponent = !shouldPreserveView ? await context.adapter.resolveComponent(context.view.component) : void 0;
955
+ if (viewComponent) {
956
+ debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
957
+ }
912
958
  await context.adapter.onViewSwap({
913
959
  component: viewComponent,
914
960
  dialog: context.dialog,
915
961
  properties: options.payload?.view?.properties,
916
- preserveState: shouldPreserveState
962
+ preserveState: shouldPreserveState,
963
+ onMounted: (hookOptions) => runHooks("mounted", {}, { ...options, ...hookOptions }, context)
917
964
  });
918
- if (options.isBackForward) {
965
+ if (options.type === "back-forward") {
919
966
  restoreScrollPositions();
920
967
  } else if (!shouldPreserveScroll) {
921
968
  resetScrollPositions();
@@ -967,12 +1014,12 @@ async function initializeRouter() {
967
1014
  } else if (isExternalNavigation()) {
968
1015
  handleExternalNavigation();
969
1016
  } else {
970
- debug.router("Handling the initial page navigation.");
1017
+ debug.router("Handling the initial navigation.");
971
1018
  setContext({
972
1019
  url: makeUrl(context.url, { hash: window.location.hash }).toString()
973
1020
  });
974
1021
  await navigate({
975
- isInitial: true,
1022
+ type: "initial",
976
1023
  preserveState: true,
977
1024
  replace: sameUrls(context.url, window.location.href)
978
1025
  });
@@ -986,13 +1033,15 @@ async function performLocalNavigation(targetUrl, options) {
986
1033
  const url = normalizeUrl(targetUrl);
987
1034
  return await navigate({
988
1035
  ...options,
1036
+ type: "local",
989
1037
  payload: {
990
1038
  version: context.version,
991
1039
  dialog: options?.dialog === false ? void 0 : options?.dialog ?? context.dialog,
992
1040
  url,
993
1041
  view: {
994
1042
  component: options?.component ?? context.view.component,
995
- properties: options?.properties ?? {}
1043
+ properties: options?.properties ?? {},
1044
+ deferred: []
996
1045
  }
997
1046
  }
998
1047
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridly/core",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Core functionality of Hybridly",
5
5
  "keywords": [
6
6
  "hybridly",
@@ -38,7 +38,7 @@
38
38
  "dependencies": {
39
39
  "qs": "^6.11.2",
40
40
  "superjson": "^1.13.1",
41
- "@hybridly/utils": "0.4.4"
41
+ "@hybridly/utils": "0.5.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "defu": "^6.1.2"