@solidjs/router 0.15.1 → 0.15.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/README.md CHANGED
@@ -818,7 +818,7 @@ if (unauthorized) {
818
818
 
819
819
  ### useLocation
820
820
 
821
- Retrieves reactive `location` object useful for getting things like `pathname`
821
+ Retrieves reactive `location` object useful for getting things like `pathname`.
822
822
 
823
823
  ```js
824
824
  const location = useLocation();
@@ -880,7 +880,8 @@ return <div classList={{ active: Boolean(match()) }} />;
880
880
  For example if you stored breadcrumbs on your route definition you could retrieve them like so:
881
881
  ```js
882
882
  const matches = useCurrentMatches();
883
- const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
883
+
884
+ const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb));
884
885
  ```
885
886
 
886
887
  ### usePreloadRoute
@@ -898,11 +899,11 @@ preload(`/users/settings`, { preloadData: true });
898
899
  `useBeforeLeave` takes a function that will be called prior to leaving a route. The function will be called with:
899
900
 
900
901
  - from (_Location_): current location (before change).
901
- - to (_string | number_}: path passed to `navigate`.
902
- - options (_NavigateOptions_}: options passed to `navigate`.
903
- - preventDefault (_void function_): call to block the route change.
904
- - defaultPrevented (_readonly boolean_): true if any previously called leave handlers called preventDefault().
905
- - retry (_void function_, _force?: boolean_ ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (ie force navigate without confirming).
902
+ - to (_string | number_): path passed to `navigate`.
903
+ - options (_NavigateOptions_): options passed to `navigate`.
904
+ - preventDefault (_function_): call to block the route change.
905
+ - defaultPrevented (_readonly boolean_): `true` if any previously called leave handlers called `preventDefault`.
906
+ - retry (_function_, _force?: boolean_ ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (i.e. force navigate without confirming).
906
907
 
907
908
  Example usage:
908
909
 
@@ -1,4 +1,7 @@
1
1
  import type { CacheEntry, NarrowResponse } from "../types.js";
2
+ /**
3
+ * Revalidates the given cache entry/entries.
4
+ */
2
5
  export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
3
6
  export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
4
7
  export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {
@@ -24,6 +24,9 @@ function getCache() {
24
24
  throw new Error("Cannot find cache context");
25
25
  return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
26
26
  }
27
+ /**
28
+ * Revalidates the given cache entry/entries.
29
+ */
27
30
  export function revalidate(key, force = true) {
28
31
  return startTransition(() => {
29
32
  const now = Date.now();
@@ -97,9 +100,14 @@ export function query(fn, name) {
97
100
  inPreloadFn && "then" in res && res.catch(() => { });
98
101
  return res;
99
102
  }
100
- let res = !isServer && sharedConfig.context && sharedConfig.has(key)
101
- ? sharedConfig.load(key) // hydrating
102
- : fn(...args);
103
+ let res;
104
+ if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
105
+ res = sharedConfig.load(key); // hydrating
106
+ // @ts-ignore at least until we add a delete method to sharedConfig
107
+ delete globalThis._$HY.r[key];
108
+ }
109
+ else
110
+ res = fn(...args);
103
111
  if (cached) {
104
112
  cached[0] = now;
105
113
  cached[1] = res;
package/dist/index.js CHANGED
@@ -251,13 +251,84 @@ const useHref = to => {
251
251
  return to_ !== undefined ? router.renderPath(to_) : to_;
252
252
  });
253
253
  };
254
+
255
+ /**
256
+ * Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options:
257
+ *
258
+ * - resolve (*boolean*, default `true`): resolve the path against the current route
259
+ * - replace (*boolean*, default `false`): replace the history entry
260
+ * - scroll (*boolean*, default `true`): scroll to top after navigation
261
+ * - state (*any*, default `undefined`): pass custom state to `location.state`
262
+ *
263
+ * **Note**: The state is serialized using the structured clone algorithm which does not support all object types.
264
+ *
265
+ * @example
266
+ * ```js
267
+ * const navigate = useNavigate();
268
+ *
269
+ * if (unauthorized) {
270
+ * navigate("/login", { replace: true });
271
+ * }
272
+ * ```
273
+ */
254
274
  const useNavigate = () => useRouter().navigatorFactory();
275
+
276
+ /**
277
+ * Retrieves reactive `location` object useful for getting things like `pathname`.
278
+ *
279
+ * @example
280
+ * ```js
281
+ * const location = useLocation();
282
+ *
283
+ * const pathname = createMemo(() => parsePath(location.pathname));
284
+ * ```
285
+ */
255
286
  const useLocation = () => useRouter().location;
287
+
288
+ /**
289
+ * Retrieves signal that indicates whether the route is currently in a *Transition*.
290
+ * Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering.
291
+ *
292
+ * @example
293
+ * ```js
294
+ * const isRouting = useIsRouting();
295
+ *
296
+ * return (
297
+ * <div classList={{ "grey-out": isRouting() }}>
298
+ * <MyAwesomeContent />
299
+ * </div>
300
+ * );
301
+ * ```
302
+ */
256
303
  const useIsRouting = () => useRouter().isRouting;
304
+
305
+ /**
306
+ * usePreloadRoute returns a function that can be used to preload a route manual.
307
+ * This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
308
+ *
309
+ * @example
310
+ * ```js
311
+ * const preload = usePreloadRoute();
312
+ *
313
+ * preload(`/users/settings`, { preloadData: true });
314
+ * ```
315
+ */
257
316
  const usePreloadRoute = () => {
258
317
  const pre = useRouter().preloadRoute;
259
318
  return (url, options = {}) => pre(url instanceof URL ? url : new URL(url, mockBase), options.preloadData);
260
319
  };
320
+
321
+ /**
322
+ * `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
323
+ * Useful for determining if a given path matches the current route.
324
+ *
325
+ * @example
326
+ * ```js
327
+ * const match = useMatch(() => props.href);
328
+ *
329
+ * return <div classList={{ active: Boolean(match()) }} />;
330
+ * ```
331
+ */
261
332
  const useMatch = (path, matchFilters) => {
262
333
  const location = useLocation();
263
334
  const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
@@ -268,8 +339,60 @@ const useMatch = (path, matchFilters) => {
268
339
  }
269
340
  });
270
341
  };
342
+
343
+ /**
344
+ * `useCurrentMatches` returns all the matches for the current matched route.
345
+ * Useful for getting all the route information.
346
+ *
347
+ * @example
348
+ * ```js
349
+ * const matches = useCurrentMatches();
350
+ *
351
+ * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
352
+ * ```
353
+ */
271
354
  const useCurrentMatches = () => useRouter().matches;
355
+
356
+ /**
357
+ * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
358
+ *
359
+ * @example
360
+ * ```js
361
+ * const params = useParams();
362
+ *
363
+ * // fetch user based on the id path parameter
364
+ * const [user] = createResource(() => params.id, fetchUser);
365
+ * ```
366
+ */
272
367
  const useParams = () => useRouter().params;
368
+
369
+ /**
370
+ * Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
371
+ * The object is a proxy so you must access properties to subscribe to reactive updates.
372
+ * **Note** that values will be strings and property names will retain their casing.
373
+ *
374
+ * The setter method accepts an object whose entries will be merged into the current query string.
375
+ * Values `''`, `undefined` and `null` will remove the key from the resulting query string.
376
+ * Updates will behave just like a navigation and the setter accepts the same optional second parameter as `navigate` and auto-scrolling is disabled by default.
377
+ *
378
+ * @examples
379
+ * ```js
380
+ * const [searchParams, setSearchParams] = useSearchParams();
381
+ *
382
+ * return (
383
+ * <div>
384
+ * <span>Page: {searchParams.page}</span>
385
+ * <button
386
+ * onClick={() =>
387
+ * setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
388
+ * }
389
+ * >
390
+ * Next Page
391
+ * </button>
392
+ * </div>
393
+ * );
394
+ * ```
395
+ */
273
396
  const useSearchParams = () => {
274
397
  const location = useLocation();
275
398
  const navigate = useNavigate();
@@ -283,6 +406,34 @@ const useSearchParams = () => {
283
406
  };
284
407
  return [location.query, setSearchParams];
285
408
  };
409
+
410
+ /**
411
+ * useBeforeLeave takes a function that will be called prior to leaving a route.
412
+ * The function will be called with:
413
+ *
414
+ * - from (*Location*): current location (before change).
415
+ * - to (*string | number*): path passed to `navigate`.
416
+ * - options (*NavigateOptions*): options passed to navigate.
417
+ * - preventDefault (*function*): call to block the route change.
418
+ * - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`.
419
+ * - retry (*function*, force?: boolean ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (i.e. force navigate without confirming).
420
+ *
421
+ * @example
422
+ * ```js
423
+ * useBeforeLeave((e: BeforeLeaveEventArgs) => {
424
+ * if (form.isDirty && !e.defaultPrevented) {
425
+ * // preventDefault to block immediately and prompt user async
426
+ * e.preventDefault();
427
+ * setTimeout(() => {
428
+ * if (window.confirm("Discard unsaved changes - are you sure?")) {
429
+ * // user wants to proceed anyway so retry with force=true
430
+ * e.retry(true);
431
+ * }
432
+ * }, 100);
433
+ * }
434
+ * });
435
+ * ```
436
+ */
286
437
  const useBeforeLeave = listener => {
287
438
  const s = useRouter().beforeLeave.subscribe({
288
439
  listener,
@@ -462,7 +613,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
462
613
  setReference(lastTransitionTarget.value);
463
614
  setState(lastTransitionTarget.state);
464
615
  resetErrorBoundaries();
465
- if (!isServer) submissions[1]([]);
616
+ if (!isServer) submissions[1](subs => subs.filter(s => s.pending));
466
617
  }).finally(() => {
467
618
  if (lastTransitionTarget !== newTarget) return;
468
619
 
@@ -832,7 +983,7 @@ function dataOnly(event, routerState, branches) {
832
983
  }
833
984
 
834
985
  function intercept([value, setValue], get, set) {
835
- return [get ? () => get(value()) : value, set ? v => setValue(set(v)) : setValue];
986
+ return [value, set ? v => setValue(set(v)) : setValue];
836
987
  }
837
988
  function createRouter(config) {
838
989
  let ignore = false;
@@ -906,6 +1057,10 @@ function getCache() {
906
1057
  if (!req) throw new Error("Cannot find cache context");
907
1058
  return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
908
1059
  }
1060
+
1061
+ /**
1062
+ * Revalidates the given cache entry/entries.
1063
+ */
909
1064
  function revalidate(key, force = true) {
910
1065
  return startTransition(() => {
911
1066
  const now = Date.now();
@@ -968,8 +1123,12 @@ function query(fn, name) {
968
1123
  inPreloadFn && "then" in res && res.catch(() => {});
969
1124
  return res;
970
1125
  }
971
- let res = !isServer && sharedConfig.context && sharedConfig.has(key) ? sharedConfig.load(key) // hydrating
972
- : fn(...args);
1126
+ let res;
1127
+ if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
1128
+ res = sharedConfig.load(key); // hydrating
1129
+ // @ts-ignore at least until we add a delete method to sharedConfig
1130
+ delete globalThis._$HY.r[key];
1131
+ } else res = fn(...args);
973
1132
  if (cached) {
974
1133
  cached[0] = now;
975
1134
  cached[1] = res;
package/dist/routing.d.ts CHANGED
@@ -6,16 +6,158 @@ export declare const useRouter: () => RouterContext;
6
6
  export declare const useRoute: () => RouteContext;
7
7
  export declare const useResolvedPath: (path: () => string) => Accessor<string | undefined>;
8
8
  export declare const useHref: (to: () => string | undefined) => Accessor<string | undefined>;
9
+ /**
10
+ * Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options:
11
+ *
12
+ * - resolve (*boolean*, default `true`): resolve the path against the current route
13
+ * - replace (*boolean*, default `false`): replace the history entry
14
+ * - scroll (*boolean*, default `true`): scroll to top after navigation
15
+ * - state (*any*, default `undefined`): pass custom state to `location.state`
16
+ *
17
+ * **Note**: The state is serialized using the structured clone algorithm which does not support all object types.
18
+ *
19
+ * @example
20
+ * ```js
21
+ * const navigate = useNavigate();
22
+ *
23
+ * if (unauthorized) {
24
+ * navigate("/login", { replace: true });
25
+ * }
26
+ * ```
27
+ */
9
28
  export declare const useNavigate: () => Navigator;
29
+ /**
30
+ * Retrieves reactive `location` object useful for getting things like `pathname`.
31
+ *
32
+ * @example
33
+ * ```js
34
+ * const location = useLocation();
35
+ *
36
+ * const pathname = createMemo(() => parsePath(location.pathname));
37
+ * ```
38
+ */
10
39
  export declare const useLocation: <S = unknown>() => Location<S>;
40
+ /**
41
+ * Retrieves signal that indicates whether the route is currently in a *Transition*.
42
+ * Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering.
43
+ *
44
+ * @example
45
+ * ```js
46
+ * const isRouting = useIsRouting();
47
+ *
48
+ * return (
49
+ * <div classList={{ "grey-out": isRouting() }}>
50
+ * <MyAwesomeContent />
51
+ * </div>
52
+ * );
53
+ * ```
54
+ */
11
55
  export declare const useIsRouting: () => () => boolean;
56
+ /**
57
+ * usePreloadRoute returns a function that can be used to preload a route manual.
58
+ * This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
59
+ *
60
+ * @example
61
+ * ```js
62
+ * const preload = usePreloadRoute();
63
+ *
64
+ * preload(`/users/settings`, { preloadData: true });
65
+ * ```
66
+ */
12
67
  export declare const usePreloadRoute: () => (url: string | URL, options?: {
13
68
  preloadData?: boolean;
14
69
  }) => void;
15
- export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S> | undefined) => Accessor<import("./types.js").PathMatch | undefined>;
70
+ /**
71
+ * `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
72
+ * Useful for determining if a given path matches the current route.
73
+ *
74
+ * @example
75
+ * ```js
76
+ * const match = useMatch(() => props.href);
77
+ *
78
+ * return <div classList={{ active: Boolean(match()) }} />;
79
+ * ```
80
+ */
81
+ export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S>) => Accessor<import("./types.js").PathMatch | undefined>;
82
+ /**
83
+ * `useCurrentMatches` returns all the matches for the current matched route.
84
+ * Useful for getting all the route information.
85
+ *
86
+ * @example
87
+ * ```js
88
+ * const matches = useCurrentMatches();
89
+ *
90
+ * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
91
+ * ```
92
+ */
16
93
  export declare const useCurrentMatches: () => () => RouteMatch[];
94
+ /**
95
+ * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
96
+ *
97
+ * @example
98
+ * ```js
99
+ * const params = useParams();
100
+ *
101
+ * // fetch user based on the id path parameter
102
+ * const [user] = createResource(() => params.id, fetchUser);
103
+ * ```
104
+ */
17
105
  export declare const useParams: <T extends Params>() => T;
106
+ /**
107
+ * Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
108
+ * The object is a proxy so you must access properties to subscribe to reactive updates.
109
+ * **Note** that values will be strings and property names will retain their casing.
110
+ *
111
+ * The setter method accepts an object whose entries will be merged into the current query string.
112
+ * Values `''`, `undefined` and `null` will remove the key from the resulting query string.
113
+ * Updates will behave just like a navigation and the setter accepts the same optional second parameter as `navigate` and auto-scrolling is disabled by default.
114
+ *
115
+ * @examples
116
+ * ```js
117
+ * const [searchParams, setSearchParams] = useSearchParams();
118
+ *
119
+ * return (
120
+ * <div>
121
+ * <span>Page: {searchParams.page}</span>
122
+ * <button
123
+ * onClick={() =>
124
+ * setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
125
+ * }
126
+ * >
127
+ * Next Page
128
+ * </button>
129
+ * </div>
130
+ * );
131
+ * ```
132
+ */
18
133
  export declare const useSearchParams: <T extends SearchParams>() => [Partial<T>, (params: SetSearchParams, options?: Partial<NavigateOptions>) => void];
134
+ /**
135
+ * useBeforeLeave takes a function that will be called prior to leaving a route.
136
+ * The function will be called with:
137
+ *
138
+ * - from (*Location*): current location (before change).
139
+ * - to (*string | number*): path passed to `navigate`.
140
+ * - options (*NavigateOptions*): options passed to navigate.
141
+ * - preventDefault (*function*): call to block the route change.
142
+ * - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`.
143
+ * - retry (*function*, force?: boolean ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (i.e. force navigate without confirming).
144
+ *
145
+ * @example
146
+ * ```js
147
+ * useBeforeLeave((e: BeforeLeaveEventArgs) => {
148
+ * if (form.isDirty && !e.defaultPrevented) {
149
+ * // preventDefault to block immediately and prompt user async
150
+ * e.preventDefault();
151
+ * setTimeout(() => {
152
+ * if (window.confirm("Discard unsaved changes - are you sure?")) {
153
+ * // user wants to proceed anyway so retry with force=true
154
+ * e.retry(true);
155
+ * }
156
+ * }, 100);
157
+ * }
158
+ * });
159
+ * ```
160
+ */
19
161
  export declare const useBeforeLeave: (listener: (e: BeforeLeaveEventArgs) => void) => void;
20
162
  export declare function createRoutes(routeDef: RouteDefinition, base?: string): RouteDescription[];
21
163
  export declare function createBranch(routes: RouteDescription[], index?: number): Branch;
package/dist/routing.js CHANGED
@@ -20,13 +20,79 @@ export const useHref = (to) => {
20
20
  return to_ !== undefined ? router.renderPath(to_) : to_;
21
21
  });
22
22
  };
23
+ /**
24
+ * Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options:
25
+ *
26
+ * - resolve (*boolean*, default `true`): resolve the path against the current route
27
+ * - replace (*boolean*, default `false`): replace the history entry
28
+ * - scroll (*boolean*, default `true`): scroll to top after navigation
29
+ * - state (*any*, default `undefined`): pass custom state to `location.state`
30
+ *
31
+ * **Note**: The state is serialized using the structured clone algorithm which does not support all object types.
32
+ *
33
+ * @example
34
+ * ```js
35
+ * const navigate = useNavigate();
36
+ *
37
+ * if (unauthorized) {
38
+ * navigate("/login", { replace: true });
39
+ * }
40
+ * ```
41
+ */
23
42
  export const useNavigate = () => useRouter().navigatorFactory();
43
+ /**
44
+ * Retrieves reactive `location` object useful for getting things like `pathname`.
45
+ *
46
+ * @example
47
+ * ```js
48
+ * const location = useLocation();
49
+ *
50
+ * const pathname = createMemo(() => parsePath(location.pathname));
51
+ * ```
52
+ */
24
53
  export const useLocation = () => useRouter().location;
54
+ /**
55
+ * Retrieves signal that indicates whether the route is currently in a *Transition*.
56
+ * Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering.
57
+ *
58
+ * @example
59
+ * ```js
60
+ * const isRouting = useIsRouting();
61
+ *
62
+ * return (
63
+ * <div classList={{ "grey-out": isRouting() }}>
64
+ * <MyAwesomeContent />
65
+ * </div>
66
+ * );
67
+ * ```
68
+ */
25
69
  export const useIsRouting = () => useRouter().isRouting;
70
+ /**
71
+ * usePreloadRoute returns a function that can be used to preload a route manual.
72
+ * This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
73
+ *
74
+ * @example
75
+ * ```js
76
+ * const preload = usePreloadRoute();
77
+ *
78
+ * preload(`/users/settings`, { preloadData: true });
79
+ * ```
80
+ */
26
81
  export const usePreloadRoute = () => {
27
82
  const pre = useRouter().preloadRoute;
28
83
  return (url, options = {}) => pre(url instanceof URL ? url : new URL(url, mockBase), options.preloadData);
29
84
  };
85
+ /**
86
+ * `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
87
+ * Useful for determining if a given path matches the current route.
88
+ *
89
+ * @example
90
+ * ```js
91
+ * const match = useMatch(() => props.href);
92
+ *
93
+ * return <div classList={{ active: Boolean(match()) }} />;
94
+ * ```
95
+ */
30
96
  export const useMatch = (path, matchFilters) => {
31
97
  const location = useLocation();
32
98
  const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
@@ -38,8 +104,57 @@ export const useMatch = (path, matchFilters) => {
38
104
  }
39
105
  });
40
106
  };
107
+ /**
108
+ * `useCurrentMatches` returns all the matches for the current matched route.
109
+ * Useful for getting all the route information.
110
+ *
111
+ * @example
112
+ * ```js
113
+ * const matches = useCurrentMatches();
114
+ *
115
+ * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
116
+ * ```
117
+ */
41
118
  export const useCurrentMatches = () => useRouter().matches;
119
+ /**
120
+ * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
121
+ *
122
+ * @example
123
+ * ```js
124
+ * const params = useParams();
125
+ *
126
+ * // fetch user based on the id path parameter
127
+ * const [user] = createResource(() => params.id, fetchUser);
128
+ * ```
129
+ */
42
130
  export const useParams = () => useRouter().params;
131
+ /**
132
+ * Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
133
+ * The object is a proxy so you must access properties to subscribe to reactive updates.
134
+ * **Note** that values will be strings and property names will retain their casing.
135
+ *
136
+ * The setter method accepts an object whose entries will be merged into the current query string.
137
+ * Values `''`, `undefined` and `null` will remove the key from the resulting query string.
138
+ * Updates will behave just like a navigation and the setter accepts the same optional second parameter as `navigate` and auto-scrolling is disabled by default.
139
+ *
140
+ * @examples
141
+ * ```js
142
+ * const [searchParams, setSearchParams] = useSearchParams();
143
+ *
144
+ * return (
145
+ * <div>
146
+ * <span>Page: {searchParams.page}</span>
147
+ * <button
148
+ * onClick={() =>
149
+ * setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
150
+ * }
151
+ * >
152
+ * Next Page
153
+ * </button>
154
+ * </div>
155
+ * );
156
+ * ```
157
+ */
43
158
  export const useSearchParams = () => {
44
159
  const location = useLocation();
45
160
  const navigate = useNavigate();
@@ -53,6 +168,33 @@ export const useSearchParams = () => {
53
168
  };
54
169
  return [location.query, setSearchParams];
55
170
  };
171
+ /**
172
+ * useBeforeLeave takes a function that will be called prior to leaving a route.
173
+ * The function will be called with:
174
+ *
175
+ * - from (*Location*): current location (before change).
176
+ * - to (*string | number*): path passed to `navigate`.
177
+ * - options (*NavigateOptions*): options passed to navigate.
178
+ * - preventDefault (*function*): call to block the route change.
179
+ * - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`.
180
+ * - retry (*function*, force?: boolean ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (i.e. force navigate without confirming).
181
+ *
182
+ * @example
183
+ * ```js
184
+ * useBeforeLeave((e: BeforeLeaveEventArgs) => {
185
+ * if (form.isDirty && !e.defaultPrevented) {
186
+ * // preventDefault to block immediately and prompt user async
187
+ * e.preventDefault();
188
+ * setTimeout(() => {
189
+ * if (window.confirm("Discard unsaved changes - are you sure?")) {
190
+ * // user wants to proceed anyway so retry with force=true
191
+ * e.retry(true);
192
+ * }
193
+ * }, 100);
194
+ * }
195
+ * });
196
+ * ```
197
+ */
56
198
  export const useBeforeLeave = (listener) => {
57
199
  const s = useRouter().beforeLeave.subscribe({
58
200
  listener,
@@ -227,7 +369,7 @@ export function createRouterContext(integration, branches, getContext, options =
227
369
  setState(lastTransitionTarget.state);
228
370
  resetErrorBoundaries();
229
371
  if (!isServer)
230
- submissions[1]([]);
372
+ submissions[1](subs => subs.filter(s => s.pending));
231
373
  }).finally(() => {
232
374
  if (lastTransitionTarget !== newTarget)
233
375
  return;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.15.1",
9
+ "version": "0.15.3",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -29,23 +29,23 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "devDependencies": {
32
- "@babel/core": "^7.23.9",
33
- "@babel/preset-typescript": "^7.23.3",
34
- "@changesets/cli": "^2.27.1",
32
+ "@babel/core": "^7.26.0",
33
+ "@babel/preset-typescript": "^7.26.0",
34
+ "@changesets/cli": "^2.27.10",
35
35
  "@rollup/plugin-babel": "6.0.4",
36
- "@rollup/plugin-node-resolve": "15.2.3",
36
+ "@rollup/plugin-node-resolve": "15.3.0",
37
37
  "@rollup/plugin-terser": "0.4.4",
38
- "@types/jest": "^29.5.11",
39
- "@types/node": "^20.11.14",
40
- "babel-preset-solid": "^1.9.2",
41
- "jsdom": "^24.0.0",
42
- "prettier": "^2.7.0",
43
- "rollup": "^4.9.6",
44
- "solid-js": "^1.9.2",
45
- "typescript": "^5.3.3",
46
- "vite": "^5.4.8",
47
- "vite-plugin-solid": "^2.9.1",
48
- "vitest": "^2.1.2"
38
+ "@types/jest": "^29.5.14",
39
+ "@types/node": "^22.10.0",
40
+ "babel-preset-solid": "^1.9.3",
41
+ "jsdom": "^25.0.1",
42
+ "prettier": "^3.4.1",
43
+ "rollup": "^4.27.4",
44
+ "solid-js": "^1.9.3",
45
+ "typescript": "^5.7.2",
46
+ "vite": "^6.0.0",
47
+ "vite-plugin-solid": "^2.11.0",
48
+ "vitest": "^2.1.6"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "solid-js": "^1.8.6"
@@ -1,15 +0,0 @@
1
- import type { CacheEntry, NarrowResponse } from "../types.js";
2
- export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
3
- export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
4
- export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {
5
- [K in keyof A]-?: A[K];
6
- } ? (...args: never[]) => R extends Promise<infer P> ? Promise<NarrowResponse<P>> : NarrowResponse<R> : (...args: A) => R extends Promise<infer P> ? Promise<NarrowResponse<P>> : NarrowResponse<R>) & {
7
- keyFor: (...args: A) => string;
8
- key: string;
9
- } : never;
10
- export declare function cache<T extends (...args: any) => any>(fn: T, name: string): CachedFunction<T>;
11
- export declare namespace cache {
12
- var set: (key: string, value: any) => void;
13
- var clear: () => void;
14
- }
15
- export declare function hashKey<T extends Array<any>>(args: T): string;
@@ -1,209 +0,0 @@
1
- import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
2
- import { getRequestEvent, isServer } from "solid-js/web";
3
- import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
4
- const LocationHeader = "Location";
5
- const PRELOAD_TIMEOUT = 5000;
6
- const CACHE_TIMEOUT = 180000;
7
- let cacheMap = new Map();
8
- // cleanup forward/back cache
9
- if (!isServer) {
10
- setInterval(() => {
11
- const now = Date.now();
12
- for (let [k, v] of cacheMap.entries()) {
13
- if (!v[3].count && now - v[0] > CACHE_TIMEOUT) {
14
- cacheMap.delete(k);
15
- }
16
- }
17
- }, 300000);
18
- }
19
- function getCache() {
20
- if (!isServer)
21
- return cacheMap;
22
- const req = getRequestEvent();
23
- if (!req)
24
- throw new Error("Cannot find cache context");
25
- return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
26
- }
27
- export function revalidate(key, force = true) {
28
- return startTransition(() => {
29
- const now = Date.now();
30
- cacheKeyOp(key, entry => {
31
- force && (entry[0] = 0); //force cache miss
32
- entry[3][1](now); // retrigger live signals
33
- });
34
- });
35
- }
36
- export function cacheKeyOp(key, fn) {
37
- key && !Array.isArray(key) && (key = [key]);
38
- for (let k of cacheMap.keys()) {
39
- if (key === undefined || matchKey(k, key))
40
- fn(cacheMap.get(k));
41
- }
42
- }
43
- export function cache(fn, name) {
44
- // prioritize GET for server functions
45
- if (fn.GET)
46
- fn = fn.GET;
47
- const cachedFn = ((...args) => {
48
- const cache = getCache();
49
- const intent = getIntent();
50
- const inPreloadFn = getInPreloadFn();
51
- const owner = getOwner();
52
- const navigate = owner ? useNavigate() : undefined;
53
- const now = Date.now();
54
- const key = name + hashKey(args);
55
- let cached = cache.get(key);
56
- let tracking;
57
- if (isServer) {
58
- const e = getRequestEvent();
59
- if (e) {
60
- const dataOnly = (e.router || (e.router = {})).dataOnly;
61
- if (dataOnly) {
62
- const data = e && (e.router.data || (e.router.data = {}));
63
- if (data && key in data)
64
- return data[key];
65
- if (Array.isArray(dataOnly) && !matchKey(key, dataOnly)) {
66
- data[key] = undefined;
67
- return Promise.resolve();
68
- }
69
- }
70
- }
71
- }
72
- if (getListener() && !isServer) {
73
- tracking = true;
74
- onCleanup(() => cached[3].count--);
75
- }
76
- if (cached &&
77
- cached[0] &&
78
- (isServer ||
79
- intent === "native" ||
80
- cached[3].count ||
81
- Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
82
- if (tracking) {
83
- cached[3].count++;
84
- cached[3][0](); // track
85
- }
86
- if (cached[2] === "preload" && intent !== "preload") {
87
- cached[0] = now;
88
- }
89
- let res = cached[1];
90
- if (intent !== "preload") {
91
- res =
92
- "then" in cached[1]
93
- ? cached[1].then(handleResponse(false), handleResponse(true))
94
- : handleResponse(false)(cached[1]);
95
- !isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
96
- }
97
- inPreloadFn && "then" in res && res.catch(() => { });
98
- return res;
99
- }
100
- let res = !isServer && sharedConfig.context && sharedConfig.has(key)
101
- ? sharedConfig.load(key) // hydrating
102
- : fn(...args);
103
- if (cached) {
104
- cached[0] = now;
105
- cached[1] = res;
106
- cached[2] = intent;
107
- !isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
108
- }
109
- else {
110
- cache.set(key, (cached = [now, res, intent, createSignal(now)]));
111
- cached[3].count = 0;
112
- }
113
- if (tracking) {
114
- cached[3].count++;
115
- cached[3][0](); // track
116
- }
117
- if (isServer) {
118
- const e = getRequestEvent();
119
- if (e && e.router.dataOnly)
120
- return (e.router.data[key] = res);
121
- }
122
- if (intent !== "preload") {
123
- res =
124
- "then" in res
125
- ? res.then(handleResponse(false), handleResponse(true))
126
- : handleResponse(false)(res);
127
- }
128
- inPreloadFn && "then" in res && res.catch(() => { });
129
- // serialize on server
130
- if (isServer &&
131
- sharedConfig.context &&
132
- sharedConfig.context.async &&
133
- !sharedConfig.context.noHydrate) {
134
- const e = getRequestEvent();
135
- (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
136
- }
137
- return res;
138
- function handleResponse(error) {
139
- return async (v) => {
140
- if (v instanceof Response) {
141
- const url = v.headers.get(LocationHeader);
142
- if (url !== null) {
143
- // client + server relative redirect
144
- if (navigate && url.startsWith("/"))
145
- startTransition(() => {
146
- navigate(url, { replace: true });
147
- });
148
- else if (!isServer)
149
- window.location.href = url;
150
- else if (isServer) {
151
- const e = getRequestEvent();
152
- if (e)
153
- e.response = { status: 302, headers: new Headers({ Location: url }) };
154
- }
155
- return;
156
- }
157
- if (v.customBody)
158
- v = await v.customBody();
159
- }
160
- if (error)
161
- throw v;
162
- return v;
163
- };
164
- }
165
- });
166
- cachedFn.keyFor = (...args) => name + hashKey(args);
167
- cachedFn.key = name;
168
- return cachedFn;
169
- }
170
- cache.set = (key, value) => {
171
- const cache = getCache();
172
- const now = Date.now();
173
- let cached = cache.get(key);
174
- if (cached) {
175
- cached[0] = now;
176
- cached[1] = value;
177
- cached[2] = "preload";
178
- }
179
- else {
180
- cache.set(key, (cached = [now, value, , createSignal(now)]));
181
- cached[3].count = 0;
182
- }
183
- };
184
- cache.clear = () => getCache().clear();
185
- function matchKey(key, keys) {
186
- for (let k of keys) {
187
- if (k && key.startsWith(k))
188
- return true;
189
- }
190
- return false;
191
- }
192
- // Modified from the amazing Tanstack Query library (MIT)
193
- // https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts#L168
194
- export function hashKey(args) {
195
- return JSON.stringify(args, (_, val) => isPlainObject(val)
196
- ? Object.keys(val)
197
- .sort()
198
- .reduce((result, key) => {
199
- result[key] = val[key];
200
- return result;
201
- }, {})
202
- : val);
203
- }
204
- function isPlainObject(obj) {
205
- let proto;
206
- return (obj != null &&
207
- typeof obj === "object" &&
208
- (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype));
209
- }
@@ -1,4 +0,0 @@
1
- import type { LocationChange, RouterContext, RouterUtils } from "../types";
2
- export declare function createIntegration(get: () => string | LocationChange, set: (next: LocationChange) => void, init?: (notify: (value?: string | LocationChange) => void) => () => void, create?: (router: RouterContext) => void, utils?: Partial<RouterUtils>): (props: import("./components").RouterProps) => import("solid-js").JSX.Element;
3
- export declare function bindEvent(target: EventTarget, type: string, handler: EventListener): () => void;
4
- export declare function scrollToHash(hash: string, fallbackTop?: boolean): void;
@@ -1,49 +0,0 @@
1
- import { createSignal, onCleanup } from "solid-js";
2
- import { createRouter } from "./components";
3
- function intercept([value, setValue], get, set) {
4
- return [get ? () => get(value()) : value, set ? (v) => setValue(set(v)) : setValue];
5
- }
6
- function querySelector(selector) {
7
- if (selector === "#") {
8
- return null;
9
- }
10
- // Guard against selector being an invalid CSS selector
11
- try {
12
- return document.querySelector(selector);
13
- }
14
- catch (e) {
15
- return null;
16
- }
17
- }
18
- export function createIntegration(get, set, init, create, utils) {
19
- let ignore = false;
20
- const wrap = (value) => (typeof value === "string" ? { value } : value);
21
- const signal = intercept(createSignal(wrap(get()), { equals: (a, b) => a.value === b.value }), undefined, next => {
22
- !ignore && set(next);
23
- return next;
24
- });
25
- init &&
26
- onCleanup(init((value = get()) => {
27
- ignore = true;
28
- signal[1](wrap(value));
29
- ignore = false;
30
- }));
31
- return createRouter({
32
- signal,
33
- create,
34
- utils
35
- });
36
- }
37
- export function bindEvent(target, type, handler) {
38
- target.addEventListener(type, handler);
39
- return () => target.removeEventListener(type, handler);
40
- }
41
- export function scrollToHash(hash, fallbackTop) {
42
- const el = querySelector(`#${hash}`);
43
- if (el) {
44
- el.scrollIntoView();
45
- }
46
- else if (fallbackTop) {
47
- window.scrollTo(0, 0);
48
- }
49
- }
@@ -1,48 +0,0 @@
1
- import { createSignal, onCleanup } from "solid-js";
2
- function intercept([value, setValue], get, set) {
3
- return [get ? () => get(value()) : value, set ? (v) => setValue(set(v)) : setValue];
4
- }
5
- function querySelector(selector) {
6
- if (selector === "#") {
7
- return null;
8
- }
9
- // Guard against selector being an invalid CSS selector
10
- try {
11
- return document.querySelector(selector);
12
- }
13
- catch (e) {
14
- return null;
15
- }
16
- }
17
- export function createIntegration(get, set, init, create, utils) {
18
- let ignore = false;
19
- const wrap = (value) => (typeof value === "string" ? { value } : value);
20
- const signal = intercept(createSignal(wrap(get()), { equals: (a, b) => a.value === b.value }), undefined, next => {
21
- !ignore && set(next);
22
- return next;
23
- });
24
- init &&
25
- onCleanup(init((value = get()) => {
26
- ignore = true;
27
- signal[1](wrap(value));
28
- ignore = false;
29
- }));
30
- return {
31
- signal,
32
- create,
33
- utils
34
- };
35
- }
36
- export function bindEvent(target, type, handler) {
37
- target.addEventListener(type, handler);
38
- return () => target.removeEventListener(type, handler);
39
- }
40
- export function scrollToHash(hash, fallbackTop) {
41
- const el = querySelector(`#${hash}`);
42
- if (el) {
43
- el.scrollIntoView();
44
- }
45
- else if (fallbackTop) {
46
- window.scrollTo(0, 0);
47
- }
48
- }