@hybridly/core 0.4.0 → 0.4.1

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
@@ -160,7 +160,7 @@ async function restoreScrollPositions() {
160
160
  utils.debug.scroll("No region found to restore.");
161
161
  return;
162
162
  }
163
- context.adapter.onWaitingForMount(() => {
163
+ context.adapter.executeOnMounted(() => {
164
164
  utils.debug.scroll(`Restoring ${regions.length}/${context.scrollRegions.length} region(s).`);
165
165
  regions.forEach((el, i) => el.scrollTo({
166
166
  top: context.scrollRegions.at(i)?.top ?? el.scrollTop,
@@ -500,6 +500,7 @@ async function initializeContext(options) {
500
500
  plugins: options.plugins ?? [],
501
501
  axios: options.axios ?? axios__default.create(),
502
502
  routing: options.routing,
503
+ preloadCache: /* @__PURE__ */ new Map(),
503
504
  hooks: {},
504
505
  memo: {}
505
506
  };
@@ -590,6 +591,52 @@ async function closeDialog(options) {
590
591
  });
591
592
  }
592
593
 
594
+ function isPreloaded(targetUrl) {
595
+ const context = getInternalRouterContext();
596
+ return context.preloadCache.has(targetUrl.toString()) ?? false;
597
+ }
598
+ function getPreloadedRequest(targetUrl) {
599
+ const context = getInternalRouterContext();
600
+ return context.preloadCache.get(targetUrl.toString());
601
+ }
602
+ function storePreloadRequest(targetUrl, response) {
603
+ const context = getInternalRouterContext();
604
+ context.preloadCache.set(targetUrl.toString(), response);
605
+ }
606
+ function discardPreloadedRequest(targetUrl) {
607
+ const context = getInternalRouterContext();
608
+ return context.preloadCache.delete(targetUrl.toString());
609
+ }
610
+ async function performPreloadRequest(options) {
611
+ const context = getRouterContext();
612
+ const url = makeUrl(options.url ?? context.url);
613
+ if (isPreloaded(url)) {
614
+ utils.debug.router("This request is already preloaded.");
615
+ return false;
616
+ }
617
+ if (context.pendingNavigation) {
618
+ utils.debug.router("A navigation is pending, preload aborted.");
619
+ return false;
620
+ }
621
+ if (options.method !== "GET") {
622
+ utils.debug.router("Cannot preload non-GET requests.");
623
+ return false;
624
+ }
625
+ utils.debug.router(`Preloading response for [${url.toString()}]`);
626
+ try {
627
+ const response = await performHybridRequest(url, options);
628
+ if (!isHybridResponse(response)) {
629
+ utils.debug.router("Preload result was invalid.");
630
+ return false;
631
+ }
632
+ storePreloadRequest(url, response);
633
+ return true;
634
+ } catch (error) {
635
+ utils.debug.router("Preloading failed.");
636
+ return false;
637
+ }
638
+ }
639
+
593
640
  const router = {
594
641
  abort: async () => getRouterContext().pendingNavigation?.controller.abort(),
595
642
  active: () => !!getRouterContext().pendingNavigation,
@@ -601,6 +648,7 @@ const router = {
601
648
  patch: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "PATCH" }),
602
649
  delete: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "DELETE" }),
603
650
  local: async (url, options = {}) => await performLocalNavigation(url, options),
651
+ preload: async (url, options = {}) => await performPreloadRequest({ ...options, url, method: "GET" }),
604
652
  external: (url, data = {}) => navigateToExternalUrl(url, data),
605
653
  to: async (name, parameters, options) => {
606
654
  const url = generateRouteFromName(name, parameters);
@@ -632,11 +680,6 @@ async function performHybridNavigation(options) {
632
680
  if ((utils.hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
633
681
  options.data = utils.objectToFormData(options.data);
634
682
  utils.debug.router("Converted data to FormData.", options.data);
635
- if (options.method && ["PUT", "PATCH", "DELETE"].includes(options.method) && options.spoof !== false) {
636
- utils.debug.router(`Automatically spoofing method ${options.method}.`);
637
- options.data.append("_method", options.method);
638
- options.method = "POST";
639
- }
640
683
  }
641
684
  if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
642
685
  utils.debug.router("Transforming data to query parameters.", options.data);
@@ -645,6 +688,19 @@ async function performHybridNavigation(options) {
645
688
  });
646
689
  options.data = {};
647
690
  }
691
+ if (["PUT", "PATCH", "DELETE"].includes(options.method) && options.spoof !== false) {
692
+ utils.debug.router(`Automatically spoofing method ${options.method}.`);
693
+ if (options.data instanceof FormData) {
694
+ options.data.append("_method", options.method);
695
+ } else if (Object.keys(options.data ?? {}).length) {
696
+ Object.assign(options.data, { _method: options.method });
697
+ } else if (typeof options.data === "undefined") {
698
+ options.data = { _method: options.method };
699
+ } else {
700
+ utils.debug.router("Could not spoof method because body type is not supported.", options.data);
701
+ }
702
+ options.method = "POST";
703
+ }
648
704
  if (!await runHooks("before", options.hooks, options, context)) {
649
705
  utils.debug.router('"before" event returned false, aborting the navigation.');
650
706
  throw new NavigationCancelledError('The navigation was cancelled by the "before" event.');
@@ -670,35 +726,7 @@ async function performHybridNavigation(options) {
670
726
  });
671
727
  await runHooks("start", options.hooks, context);
672
728
  utils.debug.router("Making request with axios.");
673
- const response = await context.axios.request({
674
- url: targetUrl.toString(),
675
- method: options.method,
676
- data: options.method === "GET" ? {} : options.data,
677
- params: options.method === "GET" ? options.data : {},
678
- signal: abortController.signal,
679
- headers: {
680
- ...options.headers,
681
- ...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
682
- ...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
683
- ...utils.when(options.only !== void 0 || options.except !== void 0, {
684
- [PARTIAL_COMPONENT_HEADER]: context.view.component,
685
- ...utils.when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
686
- ...utils.when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
687
- }, {}),
688
- ...utils.when(options.errorBag, { [ERROR_BAG_HEADER]: options.errorBag }, {}),
689
- ...utils.when(context.version, { [VERSION_HEADER]: context.version }, {}),
690
- [HYBRIDLY_HEADER]: true,
691
- "X-Requested-With": "XMLHttpRequest",
692
- "Accept": "text/html, application/xhtml+xml"
693
- },
694
- validateStatus: () => true,
695
- onUploadProgress: async (event) => {
696
- await runHooks("progress", options.hooks, {
697
- event,
698
- percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
699
- }, context);
700
- }
701
- });
729
+ const response = await performHybridRequest(targetUrl, options, abortController);
702
730
  await runHooks("data", options.hooks, response, context);
703
731
  if (isExternalResponse(response)) {
704
732
  utils.debug.router("The response is explicitely external.");
@@ -842,6 +870,47 @@ async function navigate(options) {
842
870
  resetScrollPositions();
843
871
  }
844
872
  await runHooks("navigated", {}, options, context);
873
+ context.adapter.executeOnMounted(() => {
874
+ runHooks("mounted", {}, context);
875
+ });
876
+ }
877
+ async function performHybridRequest(targetUrl, options, abortController) {
878
+ const context = getInternalRouterContext();
879
+ const preloaded = getPreloadedRequest(targetUrl);
880
+ if (preloaded) {
881
+ utils.debug.router(`Found a pre-loaded request for [${targetUrl}]`);
882
+ discardPreloadedRequest(targetUrl);
883
+ return preloaded;
884
+ }
885
+ return await context.axios.request({
886
+ url: targetUrl.toString(),
887
+ method: options.method,
888
+ data: options.method === "GET" ? {} : options.data,
889
+ params: options.method === "GET" ? options.data : {},
890
+ signal: abortController?.signal,
891
+ headers: {
892
+ ...options.headers,
893
+ ...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
894
+ ...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
895
+ ...utils.when(options.only !== void 0 || options.except !== void 0, {
896
+ [PARTIAL_COMPONENT_HEADER]: context.view.component,
897
+ ...utils.when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
898
+ ...utils.when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
899
+ }, {}),
900
+ ...utils.when(options.errorBag, { [ERROR_BAG_HEADER]: options.errorBag }, {}),
901
+ ...utils.when(context.version, { [VERSION_HEADER]: context.version }, {}),
902
+ [HYBRIDLY_HEADER]: true,
903
+ "X-Requested-With": "XMLHttpRequest",
904
+ "Accept": "text/html, application/xhtml+xml"
905
+ },
906
+ validateStatus: () => true,
907
+ onUploadProgress: async (event) => {
908
+ await runHooks("progress", options.hooks, {
909
+ event,
910
+ percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
911
+ }, context);
912
+ }
913
+ });
845
914
  }
846
915
  async function initializeRouter() {
847
916
  const context = getRouterContext();
package/dist/index.d.ts CHANGED
@@ -70,6 +70,10 @@ interface Hooks extends RequestHooks {
70
70
  * Called when a component has been navigated to.
71
71
  */
72
72
  navigated: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
73
+ /**
74
+ * Called when a component has been navigated to and was mounted by the adapter.
75
+ */
76
+ mounted: (context: InternalRouterContext) => MaybePromise<any>;
73
77
  }
74
78
  interface HookOptions {
75
79
  /** Executes the hook only once. */
@@ -228,6 +232,8 @@ interface Router {
228
232
  external: (url: UrlResolvable, data?: HybridRequestOptions['data']) => void;
229
233
  /** Navigates to the given URL without a server round-trip. */
230
234
  local: (url: UrlResolvable, options: ComponentNavigationOptions) => Promise<void>;
235
+ /** Preloads the given URL. The next time this URL is navigated to, it will be loaded from the cache. */
236
+ preload: (url: UrlResolvable, options?: Omit<HybridRequestOptions, 'method' | 'url'>) => Promise<boolean>;
231
237
  /** Access the dialog router. */
232
238
  dialog: DialogRouter;
233
239
  /** Access the history state. */
@@ -368,6 +374,8 @@ interface InternalRouterContext {
368
374
  routing?: RoutingConfiguration;
369
375
  /** Whether to display response error modals. */
370
376
  responseErrorModals?: boolean;
377
+ /** Cache of preload requests. */
378
+ preloadCache: Map<string, AxiosResponse>;
371
379
  }
372
380
  /** Router context. */
373
381
  type RouterContext = Readonly<InternalRouterContext>;
@@ -382,7 +390,7 @@ interface Adapter {
382
390
  /** Called when a dialog is closed. */
383
391
  onDialogClose?: (context: InternalRouterContext) => void;
384
392
  /** Called when Hybridly is waiting for a component to be mounted. The given callback should be executed after the view component is mounted. */
385
- onWaitingForMount: (callback: Function) => void;
393
+ executeOnMounted: (callback: Function) => void;
386
394
  }
387
395
  interface ResolvedAdapter extends Adapter {
388
396
  updateRoutingConfiguration: (routing?: RoutingConfiguration) => void;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { debug, merge, removeTrailingSlash, debounce, random, hasFiles, objectToFormData, when, match, showResponseErrorModal } from '@hybridly/utils';
1
+ import { debug, merge, removeTrailingSlash, debounce, random, hasFiles, objectToFormData, match, showResponseErrorModal, when } from '@hybridly/utils';
2
2
  import axios from 'axios';
3
3
  import { stringify, parse } from 'superjson';
4
4
  import qs from 'qs';
@@ -151,7 +151,7 @@ async function restoreScrollPositions() {
151
151
  debug.scroll("No region found to restore.");
152
152
  return;
153
153
  }
154
- context.adapter.onWaitingForMount(() => {
154
+ context.adapter.executeOnMounted(() => {
155
155
  debug.scroll(`Restoring ${regions.length}/${context.scrollRegions.length} region(s).`);
156
156
  regions.forEach((el, i) => el.scrollTo({
157
157
  top: context.scrollRegions.at(i)?.top ?? el.scrollTop,
@@ -491,6 +491,7 @@ async function initializeContext(options) {
491
491
  plugins: options.plugins ?? [],
492
492
  axios: options.axios ?? axios.create(),
493
493
  routing: options.routing,
494
+ preloadCache: /* @__PURE__ */ new Map(),
494
495
  hooks: {},
495
496
  memo: {}
496
497
  };
@@ -581,6 +582,52 @@ async function closeDialog(options) {
581
582
  });
582
583
  }
583
584
 
585
+ function isPreloaded(targetUrl) {
586
+ const context = getInternalRouterContext();
587
+ return context.preloadCache.has(targetUrl.toString()) ?? false;
588
+ }
589
+ function getPreloadedRequest(targetUrl) {
590
+ const context = getInternalRouterContext();
591
+ return context.preloadCache.get(targetUrl.toString());
592
+ }
593
+ function storePreloadRequest(targetUrl, response) {
594
+ const context = getInternalRouterContext();
595
+ context.preloadCache.set(targetUrl.toString(), response);
596
+ }
597
+ function discardPreloadedRequest(targetUrl) {
598
+ const context = getInternalRouterContext();
599
+ return context.preloadCache.delete(targetUrl.toString());
600
+ }
601
+ async function performPreloadRequest(options) {
602
+ const context = getRouterContext();
603
+ const url = makeUrl(options.url ?? context.url);
604
+ if (isPreloaded(url)) {
605
+ debug.router("This request is already preloaded.");
606
+ return false;
607
+ }
608
+ if (context.pendingNavigation) {
609
+ debug.router("A navigation is pending, preload aborted.");
610
+ return false;
611
+ }
612
+ if (options.method !== "GET") {
613
+ debug.router("Cannot preload non-GET requests.");
614
+ return false;
615
+ }
616
+ debug.router(`Preloading response for [${url.toString()}]`);
617
+ try {
618
+ const response = await performHybridRequest(url, options);
619
+ if (!isHybridResponse(response)) {
620
+ debug.router("Preload result was invalid.");
621
+ return false;
622
+ }
623
+ storePreloadRequest(url, response);
624
+ return true;
625
+ } catch (error) {
626
+ debug.router("Preloading failed.");
627
+ return false;
628
+ }
629
+ }
630
+
584
631
  const router = {
585
632
  abort: async () => getRouterContext().pendingNavigation?.controller.abort(),
586
633
  active: () => !!getRouterContext().pendingNavigation,
@@ -592,6 +639,7 @@ const router = {
592
639
  patch: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "PATCH" }),
593
640
  delete: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "DELETE" }),
594
641
  local: async (url, options = {}) => await performLocalNavigation(url, options),
642
+ preload: async (url, options = {}) => await performPreloadRequest({ ...options, url, method: "GET" }),
595
643
  external: (url, data = {}) => navigateToExternalUrl(url, data),
596
644
  to: async (name, parameters, options) => {
597
645
  const url = generateRouteFromName(name, parameters);
@@ -623,11 +671,6 @@ async function performHybridNavigation(options) {
623
671
  if ((hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
624
672
  options.data = objectToFormData(options.data);
625
673
  debug.router("Converted data to FormData.", options.data);
626
- if (options.method && ["PUT", "PATCH", "DELETE"].includes(options.method) && options.spoof !== false) {
627
- debug.router(`Automatically spoofing method ${options.method}.`);
628
- options.data.append("_method", options.method);
629
- options.method = "POST";
630
- }
631
674
  }
632
675
  if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
633
676
  debug.router("Transforming data to query parameters.", options.data);
@@ -636,6 +679,19 @@ async function performHybridNavigation(options) {
636
679
  });
637
680
  options.data = {};
638
681
  }
682
+ if (["PUT", "PATCH", "DELETE"].includes(options.method) && options.spoof !== false) {
683
+ debug.router(`Automatically spoofing method ${options.method}.`);
684
+ if (options.data instanceof FormData) {
685
+ options.data.append("_method", options.method);
686
+ } else if (Object.keys(options.data ?? {}).length) {
687
+ Object.assign(options.data, { _method: options.method });
688
+ } else if (typeof options.data === "undefined") {
689
+ options.data = { _method: options.method };
690
+ } else {
691
+ debug.router("Could not spoof method because body type is not supported.", options.data);
692
+ }
693
+ options.method = "POST";
694
+ }
639
695
  if (!await runHooks("before", options.hooks, options, context)) {
640
696
  debug.router('"before" event returned false, aborting the navigation.');
641
697
  throw new NavigationCancelledError('The navigation was cancelled by the "before" event.');
@@ -661,35 +717,7 @@ async function performHybridNavigation(options) {
661
717
  });
662
718
  await runHooks("start", options.hooks, context);
663
719
  debug.router("Making request with axios.");
664
- const response = await context.axios.request({
665
- url: targetUrl.toString(),
666
- method: options.method,
667
- data: options.method === "GET" ? {} : options.data,
668
- params: options.method === "GET" ? options.data : {},
669
- signal: abortController.signal,
670
- headers: {
671
- ...options.headers,
672
- ...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
673
- ...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
674
- ...when(options.only !== void 0 || options.except !== void 0, {
675
- [PARTIAL_COMPONENT_HEADER]: context.view.component,
676
- ...when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
677
- ...when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
678
- }, {}),
679
- ...when(options.errorBag, { [ERROR_BAG_HEADER]: options.errorBag }, {}),
680
- ...when(context.version, { [VERSION_HEADER]: context.version }, {}),
681
- [HYBRIDLY_HEADER]: true,
682
- "X-Requested-With": "XMLHttpRequest",
683
- "Accept": "text/html, application/xhtml+xml"
684
- },
685
- validateStatus: () => true,
686
- onUploadProgress: async (event) => {
687
- await runHooks("progress", options.hooks, {
688
- event,
689
- percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
690
- }, context);
691
- }
692
- });
720
+ const response = await performHybridRequest(targetUrl, options, abortController);
693
721
  await runHooks("data", options.hooks, response, context);
694
722
  if (isExternalResponse(response)) {
695
723
  debug.router("The response is explicitely external.");
@@ -833,6 +861,47 @@ async function navigate(options) {
833
861
  resetScrollPositions();
834
862
  }
835
863
  await runHooks("navigated", {}, options, context);
864
+ context.adapter.executeOnMounted(() => {
865
+ runHooks("mounted", {}, context);
866
+ });
867
+ }
868
+ async function performHybridRequest(targetUrl, options, abortController) {
869
+ const context = getInternalRouterContext();
870
+ const preloaded = getPreloadedRequest(targetUrl);
871
+ if (preloaded) {
872
+ debug.router(`Found a pre-loaded request for [${targetUrl}]`);
873
+ discardPreloadedRequest(targetUrl);
874
+ return preloaded;
875
+ }
876
+ return await context.axios.request({
877
+ url: targetUrl.toString(),
878
+ method: options.method,
879
+ data: options.method === "GET" ? {} : options.data,
880
+ params: options.method === "GET" ? options.data : {},
881
+ signal: abortController?.signal,
882
+ headers: {
883
+ ...options.headers,
884
+ ...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
885
+ ...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
886
+ ...when(options.only !== void 0 || options.except !== void 0, {
887
+ [PARTIAL_COMPONENT_HEADER]: context.view.component,
888
+ ...when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
889
+ ...when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
890
+ }, {}),
891
+ ...when(options.errorBag, { [ERROR_BAG_HEADER]: options.errorBag }, {}),
892
+ ...when(context.version, { [VERSION_HEADER]: context.version }, {}),
893
+ [HYBRIDLY_HEADER]: true,
894
+ "X-Requested-With": "XMLHttpRequest",
895
+ "Accept": "text/html, application/xhtml+xml"
896
+ },
897
+ validateStatus: () => true,
898
+ onUploadProgress: async (event) => {
899
+ await runHooks("progress", options.hooks, {
900
+ event,
901
+ percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
902
+ }, context);
903
+ }
904
+ });
836
905
  }
837
906
  async function initializeRouter() {
838
907
  const context = getRouterContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridly/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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.12.3",
41
- "@hybridly/utils": "0.4.0"
41
+ "@hybridly/utils": "0.4.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "defu": "^6.1.2"