@solidjs/router 0.5.0 → 0.6.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/README.md CHANGED
@@ -28,6 +28,7 @@ It supports all of Solid's SSR methods and has Solid's transitions baked in, so
28
28
  - [useRouteData](#useroutedata)
29
29
  - [useMatch](#usematch)
30
30
  - [useRoutes](#useroutes)
31
+ - [useBeforeLeave](#usebeforeleave)
31
32
 
32
33
  ## Getting Started
33
34
 
@@ -538,3 +539,32 @@ return <div classList={{ active: Boolean(match()) }} />;
538
539
  ### useRoutes
539
540
 
540
541
  Used to define routes via a config object instead of JSX. See [Config Based Routing](#config-based-routing).
542
+
543
+ ### useBeforeLeave
544
+
545
+ `useBeforeLeave` takes a function that will be called prior to leaving a route. The function will be called with:
546
+
547
+ - from (_Location_): current location (before change).
548
+ - to (_string | number_}: path passed to `navigate`.
549
+ - options (_NavigateOptions_}: options passed to `navigate`.
550
+ - preventDefault (_void function_): call to block the route change.
551
+ - defaultPrevented (_readonly boolean_): true if any previously called leave handlers called preventDefault().
552
+ - 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).
553
+
554
+ Example usage:
555
+ ```js
556
+ useBeforeLeave((e: BeforeLeaveEventArgs) => {
557
+ if (form.isDirty && !e.defaultPrevented) {
558
+ // preventDefault to block immediately and prompt user async
559
+ e.preventDefault();
560
+ setTimeout(() => {
561
+ if (window.confirm("Discard unsaved changes - are you sure?")) {
562
+ // user wants to proceed anyway so retry with force=true
563
+ e.retry(true);
564
+ }
565
+ }, 100);
566
+ }
567
+ });
568
+ ```
569
+
570
+
@@ -10,7 +10,7 @@ declare module "solid-js" {
10
10
  }
11
11
  }
12
12
  }
13
- export declare type RouterProps = {
13
+ export type RouterProps = {
14
14
  base?: string;
15
15
  data?: RouteDataFunc;
16
16
  children: JSX.Element;
@@ -29,7 +29,7 @@ export interface RoutesProps {
29
29
  }
30
30
  export declare const Routes: (props: RoutesProps) => JSX.Element;
31
31
  export declare const useRoutes: (routes: RouteDefinition | RouteDefinition[], base?: string) => () => JSX.Element;
32
- export declare type RouteProps = {
32
+ export type RouteProps = {
33
33
  path: string | string[];
34
34
  children?: JSX.Element;
35
35
  data?: RouteDataFunc;
@@ -3,7 +3,7 @@ import { children, createMemo, createRoot, mergeProps, on, Show, splitProps } fr
3
3
  import { isServer } from "solid-js/web";
4
4
  import { pathIntegration, staticIntegration } from "./integration";
5
5
  import { createBranches, createRouteContext, createRouterContext, getRouteMatches, RouteContextObj, RouterContextObj, useHref, useLocation, useNavigate, useResolvedPath, useRoute, useRouter } from "./routing";
6
- import { joinPaths } from "./utils";
6
+ import { joinPaths, normalizePath } from "./utils";
7
7
  export const Router = (props) => {
8
8
  const { source, url, base, data, out } = props;
9
9
  const integration = source || (isServer ? staticIntegration({ value: url || "" }) : pathIntegration());
@@ -76,7 +76,7 @@ export const Outlet = () => {
76
76
  };
77
77
  export function A(props) {
78
78
  props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
79
- const [, rest] = splitProps(props, ["href", "state", "activeClass", "inactiveClass", "end"]);
79
+ const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]);
80
80
  const to = useResolvedPath(() => props.href);
81
81
  const href = useHref(to);
82
82
  const location = useLocation();
@@ -84,11 +84,12 @@ export function A(props) {
84
84
  const to_ = to();
85
85
  if (to_ === undefined)
86
86
  return false;
87
- const path = to_.split(/[?#]/, 1)[0].toLowerCase();
88
- const loc = location.pathname.toLowerCase();
87
+ const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
88
+ const loc = normalizePath(location.pathname).toLowerCase();
89
89
  return props.end ? path === loc : loc.startsWith(path);
90
90
  });
91
91
  return (<a link {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{
92
+ ...(props.class && { [props.class]: true }),
92
93
  [props.inactiveClass]: !isActive(),
93
94
  [props.activeClass]: isActive(),
94
95
  ...rest.classList
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./components";
2
2
  export * from "./integration";
3
- export { useRouteData, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams } from "./routing";
3
+ export * from "./lifecycle";
4
+ export { useRouteData, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing";
4
5
  export { mergeSearchString as _mergeSearchString } from "./utils";
5
- export type { Location, LocationChange, LocationChangeSignal, NavigateOptions, Navigator, OutputMatch, Params, RouteDataFunc, RouteDataFuncArgs, RouteDefinition, RouterIntegration, RouterOutput, RouterUtils, SetParams } from "./types";
6
+ export type { Location, LocationChange, LocationChangeSignal, NavigateOptions, Navigator, OutputMatch, Params, RouteDataFunc, RouteDataFuncArgs, RouteDefinition, RouterIntegration, RouterOutput, RouterUtils, SetParams, BeforeLeaveEventArgs } from "./types";
package/dist/index.js CHANGED
@@ -1,15 +1,13 @@
1
- import { isServer, delegateEvents, createComponent as createComponent$1, spread, effect, setAttribute, classList, template } from 'solid-js/web';
1
+ import { isServer, delegateEvents, createComponent as createComponent$1, spread, mergeProps as mergeProps$1, template } from 'solid-js/web';
2
2
  import { createSignal, onCleanup, getOwner, runWithOwner, createMemo, createContext, useContext, untrack, createRenderEffect, createComponent, on, startTransition, resetErrorBoundaries, children, createRoot, Show, mergeProps, splitProps } from 'solid-js';
3
3
 
4
4
  function bindEvent(target, type, handler) {
5
5
  target.addEventListener(type, handler);
6
6
  return () => target.removeEventListener(type, handler);
7
7
  }
8
-
9
8
  function intercept([value, setValue], get, set) {
10
9
  return [get ? () => get(value()) : value, set ? v => setValue(set(v)) : setValue];
11
10
  }
12
-
13
11
  function querySelector(selector) {
14
12
  // Guard against selector being an invalid CSS selector
15
13
  try {
@@ -18,24 +16,19 @@ function querySelector(selector) {
18
16
  return null;
19
17
  }
20
18
  }
21
-
22
19
  function scrollToHash(hash, fallbackTop) {
23
20
  const el = querySelector(`#${hash}`);
24
-
25
21
  if (el) {
26
22
  el.scrollIntoView();
27
23
  } else if (fallbackTop) {
28
24
  window.scrollTo(0, 0);
29
25
  }
30
26
  }
31
-
32
27
  function createIntegration(get, set, init, utils) {
33
28
  let ignore = false;
34
-
35
29
  const wrap = value => typeof value === "string" ? {
36
30
  value
37
31
  } : value;
38
-
39
32
  const signal = intercept(createSignal(wrap(get()), {
40
33
  equals: (a, b) => a.value === b.value
41
34
  }), undefined, next => {
@@ -64,7 +57,6 @@ function normalizeIntegration(integration) {
64
57
  signal: integration
65
58
  };
66
59
  }
67
-
68
60
  return integration;
69
61
  }
70
62
  function staticIntegration(obj) {
@@ -87,7 +79,6 @@ function pathIntegration() {
87
79
  } else {
88
80
  window.history.pushState(state, "", value);
89
81
  }
90
-
91
82
  scrollToHash(window.location.hash.slice(1), scroll);
92
83
  }, notify => bindEvent(window, "popstate", () => notify()), {
93
84
  go: delta => window.history.go(delta)
@@ -105,7 +96,6 @@ function hashIntegration() {
105
96
  } else {
106
97
  window.location.hash = value;
107
98
  }
108
-
109
99
  const hashIndex = value.indexOf("#");
110
100
  const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
111
101
  scrollToHash(hash, scroll);
@@ -113,37 +103,63 @@ function hashIntegration() {
113
103
  go: delta => window.history.go(delta),
114
104
  renderPath: path => `#${path}`,
115
105
  parsePath: str => {
116
- const to = str.replace(/^.*?#/, ""); // Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to
106
+ const to = str.replace(/^.*?#/, "");
107
+ // Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to
117
108
  // `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash
118
109
  // to the current path so we can handle these in-page anchors correctly.
119
-
120
110
  if (!to.startsWith("/")) {
121
111
  const [, path = "/"] = window.location.hash.split("#", 2);
122
112
  return `${path}#${to}`;
123
113
  }
124
-
125
114
  return to;
126
115
  }
127
116
  });
128
117
  }
129
118
 
119
+ function createBeforeLeave() {
120
+ let listeners = new Set();
121
+ function subscribe(listener) {
122
+ listeners.add(listener);
123
+ return () => listeners.delete(listener);
124
+ }
125
+ let ignore = false;
126
+ function confirm(to, options) {
127
+ if (ignore) return !(ignore = false);
128
+ const e = {
129
+ to,
130
+ options,
131
+ defaultPrevented: false,
132
+ preventDefault: () => e.defaultPrevented = true
133
+ };
134
+ for (const l of listeners) l.listener({
135
+ ...e,
136
+ from: l.location,
137
+ retry: force => {
138
+ force && (ignore = true);
139
+ l.navigate(to, options);
140
+ }
141
+ });
142
+ return !e.defaultPrevented;
143
+ }
144
+ return {
145
+ subscribe,
146
+ confirm
147
+ };
148
+ }
149
+
130
150
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
131
151
  const trimPathRegex = /^\/+|\/+$/g;
132
-
133
- function normalize(path, omitSlash = false) {
152
+ function normalizePath(path, omitSlash = false) {
134
153
  const s = path.replace(trimPathRegex, "");
135
154
  return s ? omitSlash || /^[?#]/.test(s) ? s : "/" + s : "";
136
155
  }
137
-
138
156
  function resolvePath(base, path, from) {
139
157
  if (hasSchemeRegex.test(path)) {
140
158
  return undefined;
141
159
  }
142
-
143
- const basePath = normalize(base);
144
- const fromPath = from && normalize(from);
160
+ const basePath = normalizePath(base);
161
+ const fromPath = from && normalizePath(from);
145
162
  let result = "";
146
-
147
163
  if (!fromPath || path.startsWith("/")) {
148
164
  result = basePath;
149
165
  } else if (fromPath.toLowerCase().indexOf(basePath.toLowerCase()) !== 0) {
@@ -151,18 +167,16 @@ function resolvePath(base, path, from) {
151
167
  } else {
152
168
  result = fromPath;
153
169
  }
154
-
155
- return (result || "/") + normalize(path, !result);
170
+ return (result || "/") + normalizePath(path, !result);
156
171
  }
157
172
  function invariant(value, message) {
158
173
  if (value == null) {
159
174
  throw new Error(message);
160
175
  }
161
-
162
176
  return value;
163
177
  }
164
178
  function joinPaths(from, to) {
165
- return normalize(from).replace(/\/*(\*.*)?$/g, "") + normalize(to);
179
+ return normalizePath(from).replace(/\/*(\*.*)?$/g, "") + normalizePath(to);
166
180
  }
167
181
  function extractSearchParams(url) {
168
182
  const params = {};
@@ -171,9 +185,6 @@ function extractSearchParams(url) {
171
185
  });
172
186
  return params;
173
187
  }
174
- function urlDecode(str, isQuery) {
175
- return decodeURIComponent(isQuery ? str.replace(/\+/g, " ") : str);
176
- }
177
188
  function createMatcher(path, partial) {
178
189
  const [pattern, splat] = path.split("/*", 2);
179
190
  const segments = pattern.split("/").filter(Boolean);
@@ -181,20 +192,16 @@ function createMatcher(path, partial) {
181
192
  return location => {
182
193
  const locSegments = location.split("/").filter(Boolean);
183
194
  const lenDiff = locSegments.length - len;
184
-
185
195
  if (lenDiff < 0 || lenDiff > 0 && splat === undefined && !partial) {
186
196
  return null;
187
197
  }
188
-
189
198
  const match = {
190
199
  path: len ? "" : "/",
191
200
  params: {}
192
201
  };
193
-
194
202
  for (let i = 0; i < len; i++) {
195
203
  const segment = segments[i];
196
204
  const locSegment = locSegments[i];
197
-
198
205
  if (segment[0] === ":") {
199
206
  match.params[segment.slice(1)] = locSegment;
200
207
  } else if (segment.localeCompare(locSegment, undefined, {
@@ -202,14 +209,11 @@ function createMatcher(path, partial) {
202
209
  }) !== 0) {
203
210
  return null;
204
211
  }
205
-
206
212
  match.path += `/${locSegment}`;
207
213
  }
208
-
209
214
  if (splat) {
210
215
  match.params[splat] = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
211
216
  }
212
-
213
217
  return match;
214
218
  };
215
219
  }
@@ -226,21 +230,17 @@ function createMemoObject(fn) {
226
230
  if (!map.has(property)) {
227
231
  runWithOwner(owner, () => map.set(property, createMemo(() => fn()[property])));
228
232
  }
229
-
230
233
  return map.get(property)();
231
234
  },
232
-
233
235
  getOwnPropertyDescriptor() {
234
236
  return {
235
237
  enumerable: true,
236
238
  configurable: true
237
239
  };
238
240
  },
239
-
240
241
  ownKeys() {
241
242
  return Reflect.ownKeys(fn());
242
243
  }
243
-
244
244
  });
245
245
  }
246
246
  function mergeSearchString(search, params) {
@@ -260,17 +260,17 @@ function expandOptionals(pattern) {
260
260
  if (!match) return [pattern];
261
261
  let prefix = pattern.slice(0, match.index);
262
262
  let suffix = pattern.slice(match.index + match[0].length);
263
- const prefixes = [prefix, prefix += match[1]]; // This section handles adjacent optional params. We don't actually want all permuations since
263
+ const prefixes = [prefix, prefix += match[1]];
264
+
265
+ // This section handles adjacent optional params. We don't actually want all permuations since
264
266
  // that will lead to equivalent routes which have the same number of params. For example
265
267
  // `/:a?/:b?/:c`? only has the unique expansion: `/`, `/:a`, `/:a/:b`, `/:a/:b/:c` and we can
266
268
  // discard `/:b`, `/:c`, `/:b/:c` by building them up in order and not recursing. This also helps
267
269
  // ensure predictability where earlier params have precidence.
268
-
269
270
  while (match = /^(\/\:[^\/]+)\?/.exec(suffix)) {
270
271
  prefixes.push(prefix += match[1]);
271
272
  suffix = suffix.slice(match[0].length);
272
273
  }
273
-
274
274
  return expandOptionals(suffix).reduce((results, expansion) => [...results, ...prefixes.map(p => p + expansion)], []);
275
275
  }
276
276
 
@@ -296,25 +296,37 @@ const useLocation = () => useRouter().location;
296
296
  const useIsRouting = () => useRouter().isRouting;
297
297
  const useMatch = path => {
298
298
  const location = useLocation();
299
- const matcher = createMemo(() => createMatcher(path()));
300
- return createMemo(() => matcher()(location.pathname));
299
+ const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path)));
300
+ return createMemo(() => {
301
+ for (const matcher of matchers()) {
302
+ const match = matcher(location.pathname);
303
+ if (match) return match;
304
+ }
305
+ });
301
306
  };
302
307
  const useParams = () => useRoute().params;
303
308
  const useRouteData = () => useRoute().data;
304
309
  const useSearchParams = () => {
305
310
  const location = useLocation();
306
311
  const navigate = useNavigate();
307
-
308
312
  const setSearchParams = (params, options) => {
309
313
  const searchString = untrack(() => mergeSearchString(location.search, params));
310
- navigate(location.pathname + searchString, {
314
+ navigate(location.pathname + searchString + location.hash, {
311
315
  scroll: false,
316
+ resolve: false,
312
317
  ...options
313
318
  });
314
319
  };
315
-
316
320
  return [location.query, setSearchParams];
317
321
  };
322
+ const useBeforeLeave = listener => {
323
+ const s = useRouter().beforeLeave.subscribe({
324
+ listener,
325
+ location: useLocation(),
326
+ navigate: useNavigate()
327
+ });
328
+ onCleanup(s);
329
+ };
318
330
  function createRoutes(routeDef, base = "", fallback) {
319
331
  const {
320
332
  component,
@@ -337,13 +349,13 @@ function createRoutes(routeDef, base = "", fallback) {
337
349
  for (const originalPath of expandOptionals(path)) {
338
350
  const path = joinPaths(base, originalPath);
339
351
  const pattern = isLeaf ? path : path.split("/*", 1)[0];
340
- acc.push({ ...shared,
352
+ acc.push({
353
+ ...shared,
341
354
  originalPath,
342
355
  pattern,
343
356
  matcher: createMatcher(pattern, !isLeaf)
344
357
  });
345
358
  }
346
-
347
359
  return acc;
348
360
  }, []);
349
361
  }
@@ -351,76 +363,62 @@ function createBranch(routes, index = 0) {
351
363
  return {
352
364
  routes,
353
365
  score: scoreRoute(routes[routes.length - 1]) * 10000 - index,
354
-
355
366
  matcher(location) {
356
367
  const matches = [];
357
-
358
368
  for (let i = routes.length - 1; i >= 0; i--) {
359
369
  const route = routes[i];
360
370
  const match = route.matcher(location);
361
-
362
371
  if (!match) {
363
372
  return null;
364
373
  }
365
-
366
- matches.unshift({ ...match,
374
+ matches.unshift({
375
+ ...match,
367
376
  route
368
377
  });
369
378
  }
370
-
371
379
  return matches;
372
380
  }
373
-
374
381
  };
375
382
  }
376
-
377
383
  function asArray(value) {
378
384
  return Array.isArray(value) ? value : [value];
379
385
  }
380
-
381
386
  function createBranches(routeDef, base = "", fallback, stack = [], branches = []) {
382
387
  const routeDefs = asArray(routeDef);
383
-
384
388
  for (let i = 0, len = routeDefs.length; i < len; i++) {
385
389
  const def = routeDefs[i];
386
-
387
390
  if (def && typeof def === "object" && def.hasOwnProperty("path")) {
388
391
  const routes = createRoutes(def, base, fallback);
389
-
390
392
  for (const route of routes) {
391
393
  stack.push(route);
392
-
393
- if (def.children) {
394
+ const isEmptyArray = Array.isArray(def.children) && def.children.length === 0;
395
+ if (def.children && !isEmptyArray) {
394
396
  createBranches(def.children, route.pattern, fallback, stack, branches);
395
397
  } else {
396
398
  const branch = createBranch([...stack], branches.length);
397
399
  branches.push(branch);
398
400
  }
399
-
400
401
  stack.pop();
401
402
  }
402
403
  }
403
- } // Stack will be empty on final return
404
-
404
+ }
405
405
 
406
+ // Stack will be empty on final return
406
407
  return stack.length ? branches : branches.sort((a, b) => b.score - a.score);
407
408
  }
408
409
  function getRouteMatches(branches, location) {
409
410
  for (let i = 0, len = branches.length; i < len; i++) {
410
411
  const match = branches[i].matcher(location);
411
-
412
412
  if (match) {
413
413
  return match;
414
414
  }
415
415
  }
416
-
417
416
  return [];
418
417
  }
419
418
  function createLocation(path, state) {
420
419
  const origin = new URL("http://sar");
421
420
  const url = createMemo(prev => {
422
421
  const path_ = path();
423
-
424
422
  try {
425
423
  return new URL(path_, origin);
426
424
  } catch (err) {
@@ -430,31 +428,26 @@ function createLocation(path, state) {
430
428
  }, origin, {
431
429
  equals: (a, b) => a.href === b.href
432
430
  });
433
- const pathname = createMemo(() => urlDecode(url().pathname));
434
- const search = createMemo(() => urlDecode(url().search, true));
435
- const hash = createMemo(() => urlDecode(url().hash));
431
+ const pathname = createMemo(() => url().pathname);
432
+ const search = createMemo(() => url().search, true);
433
+ const hash = createMemo(() => url().hash);
436
434
  const key = createMemo(() => "");
437
435
  return {
438
436
  get pathname() {
439
437
  return pathname();
440
438
  },
441
-
442
439
  get search() {
443
440
  return search();
444
441
  },
445
-
446
442
  get hash() {
447
443
  return hash();
448
444
  },
449
-
450
445
  get state() {
451
446
  return state();
452
447
  },
453
-
454
448
  get key() {
455
449
  return key();
456
450
  },
457
-
458
451
  query: createMemoObject(on(search, () => extractSearchParams(url())))
459
452
  };
460
453
  }
@@ -463,17 +456,14 @@ function createRouterContext(integration, base = "", data, out) {
463
456
  signal: [source, setSource],
464
457
  utils = {}
465
458
  } = normalizeIntegration(integration);
466
-
467
459
  const parsePath = utils.parsePath || (p => p);
468
-
469
460
  const renderPath = utils.renderPath || (p => p);
470
-
461
+ const beforeLeave = utils.beforeLeave || createBeforeLeave();
471
462
  const basePath = resolvePath("", base);
472
463
  const output = isServer && out ? Object.assign(out, {
473
464
  matches: [],
474
465
  url: undefined
475
466
  }) : undefined;
476
-
477
467
  if (basePath === undefined) {
478
468
  throw new Error(`${basePath} is not a valid base path`);
479
469
  } else if (basePath && !source().value) {
@@ -483,19 +473,15 @@ function createRouterContext(integration, base = "", data, out) {
483
473
  scroll: false
484
474
  });
485
475
  }
486
-
487
476
  const [isRouting, setIsRouting] = createSignal(false);
488
-
489
477
  const start = async callback => {
490
478
  setIsRouting(true);
491
-
492
479
  try {
493
480
  await startTransition(callback);
494
481
  } finally {
495
482
  setIsRouting(false);
496
483
  }
497
484
  };
498
-
499
485
  const [reference, setReference] = createSignal(source().value);
500
486
  const [state, setState] = createSignal(source().state);
501
487
  const location = createLocation(reference, state);
@@ -505,13 +491,10 @@ function createRouterContext(integration, base = "", data, out) {
505
491
  params: {},
506
492
  path: () => basePath,
507
493
  outlet: () => null,
508
-
509
494
  resolvePath(to) {
510
495
  return resolvePath(basePath, to);
511
496
  }
512
-
513
497
  };
514
-
515
498
  if (data) {
516
499
  try {
517
500
  TempRoute = baseRoute;
@@ -525,20 +508,17 @@ function createRouterContext(integration, base = "", data, out) {
525
508
  TempRoute = undefined;
526
509
  }
527
510
  }
528
-
529
511
  function navigateFromRoute(route, to, options) {
530
512
  // Untrack in case someone navigates in an effect - don't want to track `reference` or route paths
531
513
  untrack(() => {
532
514
  if (typeof to === "number") {
533
515
  if (!to) ; else if (utils.go) {
534
- utils.go(to);
516
+ beforeLeave.confirm(to, options) && utils.go(to);
535
517
  } else {
536
518
  console.warn("Router integration does not support relative routing");
537
519
  }
538
-
539
520
  return;
540
521
  }
541
-
542
522
  const {
543
523
  replace,
544
524
  resolve,
@@ -551,28 +531,24 @@ function createRouterContext(integration, base = "", data, out) {
551
531
  ...options
552
532
  };
553
533
  const resolvedTo = resolve ? route.resolvePath(to) : resolvePath("", to);
554
-
555
534
  if (resolvedTo === undefined) {
556
535
  throw new Error(`Path '${to}' is not a routable path`);
557
536
  } else if (referrers.length >= MAX_REDIRECTS) {
558
537
  throw new Error("Too many redirects");
559
538
  }
560
-
561
539
  const current = reference();
562
-
563
540
  if (resolvedTo !== current || nextState !== state()) {
564
541
  if (isServer) {
565
542
  if (output) {
566
543
  output.url = resolvedTo;
567
544
  }
568
-
569
545
  setSource({
570
546
  value: resolvedTo,
571
547
  replace,
572
548
  scroll,
573
549
  state: nextState
574
550
  });
575
- } else {
551
+ } else if (beforeLeave.confirm(resolvedTo, options)) {
576
552
  const len = referrers.push({
577
553
  value: current,
578
554
  replace,
@@ -595,34 +571,30 @@ function createRouterContext(integration, base = "", data, out) {
595
571
  }
596
572
  });
597
573
  }
598
-
599
574
  function navigatorFactory(route) {
600
575
  // Workaround for vite issue (https://github.com/vitejs/vite/issues/3803)
601
576
  route = route || useContext(RouteContextObj) || baseRoute;
602
577
  return (to, options) => navigateFromRoute(route, to, options);
603
578
  }
604
-
605
579
  function navigateEnd(next) {
606
580
  const first = referrers[0];
607
-
608
581
  if (first) {
609
582
  if (next.value !== first.value || next.state !== first.state) {
610
- setSource({ ...next,
583
+ setSource({
584
+ ...next,
611
585
  replace: first.replace,
612
586
  scroll: first.scroll
613
587
  });
614
588
  }
615
-
616
589
  referrers.length = 0;
617
590
  }
618
591
  }
619
-
620
592
  createRenderEffect(() => {
621
593
  const {
622
594
  value,
623
595
  state
624
- } = source(); // Untrack this whole block so `start` doesn't cause Solid's Listener to be preserved
625
-
596
+ } = source();
597
+ // Untrack this whole block so `start` doesn't cause Solid's Listener to be preserved
626
598
  untrack(() => {
627
599
  if (value !== reference()) {
628
600
  start(() => {
@@ -632,7 +604,6 @@ function createRouterContext(integration, base = "", data, out) {
632
604
  }
633
605
  });
634
606
  });
635
-
636
607
  if (!isServer) {
637
608
  function handleAnchorClick(evt) {
638
609
  if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
@@ -643,9 +614,8 @@ function createRouterContext(integration, base = "", data, out) {
643
614
  const rel = (a.getAttribute("rel") || "").split(/\s+/);
644
615
  if (a.hasAttribute("download") || rel && rel.includes("external")) return;
645
616
  const url = new URL(href);
646
- const pathname = urlDecode(url.pathname);
647
- if (url.origin !== window.location.origin || basePath && pathname && !pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
648
- const to = parsePath(pathname + urlDecode(url.search, true) + urlDecode(url.hash));
617
+ if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
618
+ const to = parsePath(url.pathname + url.search + url.hash);
649
619
  const state = a.getAttribute("state");
650
620
  evt.preventDefault();
651
621
  navigateFromRoute(baseRoute, to, {
@@ -654,14 +624,13 @@ function createRouterContext(integration, base = "", data, out) {
654
624
  scroll: !a.hasAttribute("noscroll"),
655
625
  state: state && JSON.parse(state)
656
626
  });
657
- } // ensure delegated events run first
658
-
627
+ }
659
628
 
629
+ // ensure delegated events run first
660
630
  delegateEvents(["click"]);
661
631
  document.addEventListener("click", handleAnchorClick);
662
632
  onCleanup(() => document.removeEventListener("click", handleAnchorClick));
663
633
  }
664
-
665
634
  return {
666
635
  base: baseRoute,
667
636
  out: output,
@@ -669,7 +638,8 @@ function createRouterContext(integration, base = "", data, out) {
669
638
  isRouting,
670
639
  renderPath,
671
640
  parsePath,
672
- navigatorFactory
641
+ navigatorFactory,
642
+ beforeLeave
673
643
  };
674
644
  }
675
645
  function createRouteContext(router, parent, child, match) {
@@ -690,22 +660,17 @@ function createRouteContext(router, parent, child, match) {
690
660
  const route = {
691
661
  parent,
692
662
  pattern,
693
-
694
663
  get child() {
695
664
  return child();
696
665
  },
697
-
698
666
  path,
699
667
  params,
700
668
  data: parent.data,
701
669
  outlet,
702
-
703
670
  resolvePath(to) {
704
671
  return resolvePath(base.path(), to, path());
705
672
  }
706
-
707
673
  };
708
-
709
674
  if (data) {
710
675
  try {
711
676
  TempRoute = route;
@@ -719,7 +684,6 @@ function createRouteContext(router, parent, child, match) {
719
684
  TempRoute = undefined;
720
685
  }
721
686
  }
722
-
723
687
  return route;
724
688
  }
725
689
 
@@ -738,11 +702,9 @@ const Router = props => {
738
702
  const routerState = createRouterContext(integration, base, data, out);
739
703
  return createComponent$1(RouterContextObj.Provider, {
740
704
  value: routerState,
741
-
742
705
  get children() {
743
706
  return props.children;
744
707
  }
745
-
746
708
  });
747
709
  };
748
710
  const Routes = props => {
@@ -751,7 +713,6 @@ const Routes = props => {
751
713
  const routeDefs = children(() => props.children);
752
714
  const branches = createMemo(() => createBranches(routeDefs(), joinPaths(parentRoute.pattern, props.base || ""), Outlet));
753
715
  const matches = createMemo(() => getRouteMatches(branches(), router.location.pathname));
754
-
755
716
  if (router.out) {
756
717
  router.out.matches.push(matches().map(({
757
718
  route,
@@ -764,39 +725,31 @@ const Routes = props => {
764
725
  params
765
726
  })));
766
727
  }
767
-
768
728
  const disposers = [];
769
729
  let root;
770
730
  const routeStates = createMemo(on(matches, (nextMatches, prevMatches, prev) => {
771
731
  let equal = prevMatches && nextMatches.length === prevMatches.length;
772
732
  const next = [];
773
-
774
733
  for (let i = 0, len = nextMatches.length; i < len; i++) {
775
734
  const prevMatch = prevMatches && prevMatches[i];
776
735
  const nextMatch = nextMatches[i];
777
-
778
736
  if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) {
779
737
  next[i] = prev[i];
780
738
  } else {
781
739
  equal = false;
782
-
783
740
  if (disposers[i]) {
784
741
  disposers[i]();
785
742
  }
786
-
787
743
  createRoot(dispose => {
788
744
  disposers[i] = dispose;
789
745
  next[i] = createRouteContext(router, next[i - 1] || parentRoute, () => routeStates()[i + 1], () => matches()[i]);
790
746
  });
791
747
  }
792
748
  }
793
-
794
749
  disposers.splice(nextMatches.length).forEach(dispose => dispose());
795
-
796
750
  if (prev && equal) {
797
751
  return prev;
798
752
  }
799
-
800
753
  root = next[0];
801
754
  return next;
802
755
  }));
@@ -804,14 +757,11 @@ const Routes = props => {
804
757
  get when() {
805
758
  return routeStates() && root;
806
759
  },
807
-
808
760
  children: route => createComponent$1(RouteContextObj.Provider, {
809
761
  value: route,
810
-
811
762
  get children() {
812
763
  return route.outlet();
813
764
  }
814
-
815
765
  })
816
766
  });
817
767
  };
@@ -827,7 +777,6 @@ const Route = props => {
827
777
  get children() {
828
778
  return childRoutes();
829
779
  }
830
-
831
780
  });
832
781
  };
833
782
  const Outlet = () => {
@@ -836,14 +785,11 @@ const Outlet = () => {
836
785
  get when() {
837
786
  return route.child;
838
787
  },
839
-
840
788
  children: child => createComponent$1(RouteContextObj.Provider, {
841
789
  value: child,
842
-
843
790
  get children() {
844
791
  return child.outlet();
845
792
  }
846
-
847
793
  })
848
794
  });
849
795
  };
@@ -852,47 +798,43 @@ function A(props) {
852
798
  inactiveClass: "inactive",
853
799
  activeClass: "active"
854
800
  }, props);
855
- const [, rest] = splitProps(props, ["href", "state", "activeClass", "inactiveClass", "end"]);
801
+ const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]);
856
802
  const to = useResolvedPath(() => props.href);
857
803
  const href = useHref(to);
858
804
  const location = useLocation();
859
805
  const isActive = createMemo(() => {
860
806
  const to_ = to();
861
807
  if (to_ === undefined) return false;
862
- const path = to_.split(/[?#]/, 1)[0].toLowerCase();
863
- const loc = location.pathname.toLowerCase();
808
+ const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
809
+ const loc = normalizePath(location.pathname).toLowerCase();
864
810
  return props.end ? path === loc : loc.startsWith(path);
865
811
  });
866
812
  return (() => {
867
813
  const _el$ = _tmpl$.cloneNode(true);
868
-
869
- spread(_el$, rest, false, false);
870
-
871
- effect(_p$ => {
872
- const _v$ = href() || props.href,
873
- _v$2 = JSON.stringify(props.state),
874
- _v$3 = {
875
- [props.inactiveClass]: !isActive(),
876
- [props.activeClass]: isActive(),
877
- ...rest.classList
814
+ spread(_el$, mergeProps$1(rest, {
815
+ get href() {
816
+ return href() || props.href;
878
817
  },
879
- _v$4 = isActive() ? "page" : undefined;
880
-
881
- _v$ !== _p$._v$ && setAttribute(_el$, "href", _p$._v$ = _v$);
882
- _v$2 !== _p$._v$2 && setAttribute(_el$, "state", _p$._v$2 = _v$2);
883
- _p$._v$3 = classList(_el$, _v$3, _p$._v$3);
884
- _v$4 !== _p$._v$4 && setAttribute(_el$, "aria-current", _p$._v$4 = _v$4);
885
- return _p$;
886
- }, {
887
- _v$: undefined,
888
- _v$2: undefined,
889
- _v$3: undefined,
890
- _v$4: undefined
891
- });
892
-
818
+ get state() {
819
+ return JSON.stringify(props.state);
820
+ },
821
+ get classList() {
822
+ return {
823
+ ...(props.class && {
824
+ [props.class]: true
825
+ }),
826
+ [props.inactiveClass]: !isActive(),
827
+ [props.activeClass]: isActive(),
828
+ ...rest.classList
829
+ };
830
+ },
831
+ get ["aria-current"]() {
832
+ return isActive() ? "page" : undefined;
833
+ }
834
+ }), false, false);
893
835
  return _el$;
894
836
  })();
895
- } // deprecated alias exports
837
+ }
896
838
  function Navigate(props) {
897
839
  const navigate = useNavigate();
898
840
  const location = useLocation();
@@ -911,4 +853,4 @@ function Navigate(props) {
911
853
  return null;
912
854
  }
913
855
 
914
- export { A, A as Link, A as NavLink, Navigate, Outlet, Route, Router, Routes, mergeSearchString as _mergeSearchString, createIntegration, hashIntegration, normalizeIntegration, pathIntegration, staticIntegration, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useRouteData, useRoutes, useSearchParams };
856
+ export { A, A as Link, A as NavLink, Navigate, Outlet, Route, Router, Routes, mergeSearchString as _mergeSearchString, createBeforeLeave, createIntegration, hashIntegration, normalizeIntegration, pathIntegration, staticIntegration, useBeforeLeave, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useRouteData, useRoutes, useSearchParams };
package/dist/index.jsx CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./components";
2
2
  export * from "./integration";
3
- export { useRouteData, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams } from "./routing";
3
+ export * from "./lifecycle";
4
+ export { useRouteData, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing";
4
5
  export { mergeSearchString as _mergeSearchString } from "./utils";
@@ -0,0 +1,2 @@
1
+ import { BeforeLeaveLifecycle } from "./types";
2
+ export declare function createBeforeLeave(): BeforeLeaveLifecycle;
@@ -0,0 +1,32 @@
1
+ export function createBeforeLeave() {
2
+ let listeners = new Set();
3
+ function subscribe(listener) {
4
+ listeners.add(listener);
5
+ return () => listeners.delete(listener);
6
+ }
7
+ let ignore = false;
8
+ function confirm(to, options) {
9
+ if (ignore)
10
+ return !(ignore = false);
11
+ const e = {
12
+ to,
13
+ options,
14
+ defaultPrevented: false,
15
+ preventDefault: () => (e.defaultPrevented = true)
16
+ };
17
+ for (const l of listeners)
18
+ l.listener({
19
+ ...e,
20
+ from: l.location,
21
+ retry: (force) => {
22
+ force && (ignore = true);
23
+ l.navigate(to, options);
24
+ }
25
+ });
26
+ return !e.defaultPrevented;
27
+ }
28
+ return {
29
+ subscribe,
30
+ confirm
31
+ };
32
+ }
package/dist/routing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Component, Accessor } from "solid-js";
2
- import type { Branch, Location, LocationChangeSignal, NavigateOptions, Navigator, Params, Route, RouteContext, RouteDataFunc, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SetParams } from "./types";
2
+ import type { BeforeLeaveEventArgs, Branch, Location, LocationChangeSignal, NavigateOptions, Navigator, Params, Route, RouteContext, RouteDataFunc, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SetParams } from "./types";
3
3
  export declare const RouterContextObj: import("solid-js").Context<RouterContext | undefined>;
4
4
  export declare const RouteContextObj: import("solid-js").Context<RouteContext | undefined>;
5
5
  export declare const useRouter: () => RouterContext;
@@ -9,11 +9,12 @@ export declare const useHref: (to: () => string | undefined) => Accessor<string
9
9
  export declare const useNavigate: () => Navigator;
10
10
  export declare const useLocation: <S = unknown>() => Location<S>;
11
11
  export declare const useIsRouting: () => () => boolean;
12
- export declare const useMatch: (path: () => string) => Accessor<import("./types").PathMatch | null>;
12
+ export declare const useMatch: (path: () => string) => Accessor<import("./types").PathMatch | undefined>;
13
13
  export declare const useParams: <T extends Params>() => T;
14
- declare type MaybeReturnType<T> = T extends (...args: any) => infer R ? R : T;
14
+ type MaybeReturnType<T> = T extends (...args: any) => infer R ? R : T;
15
15
  export declare const useRouteData: <T>() => MaybeReturnType<T>;
16
16
  export declare const useSearchParams: <T extends Params>() => [T, (params: SetParams, options?: Partial<NavigateOptions>) => void];
17
+ export declare const useBeforeLeave: (listener: (e: BeforeLeaveEventArgs) => void) => void;
17
18
  export declare function createRoutes(routeDef: RouteDefinition, base?: string, fallback?: Component): Route[];
18
19
  export declare function createBranch(routes: Route[], index?: number): Branch;
19
20
  export declare function createBranches(routeDef: RouteDefinition | RouteDefinition[], base?: string, fallback?: Component, stack?: Route[], branches?: Branch[]): Branch[];
package/dist/routing.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createComponent, createContext, createMemo, createRenderEffect, createSignal, on, onCleanup, untrack, useContext, startTransition, resetErrorBoundaries } from "solid-js";
2
2
  import { isServer, delegateEvents } from "solid-js/web";
3
3
  import { normalizeIntegration } from "./integration";
4
- import { createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, urlDecode, expandOptionals } from "./utils";
4
+ import { createBeforeLeave } from "./lifecycle";
5
+ import { createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils";
5
6
  const MAX_REDIRECTS = 100;
6
7
  export const RouterContextObj = createContext();
7
8
  export const RouteContextObj = createContext();
@@ -24,8 +25,14 @@ export const useLocation = () => useRouter().location;
24
25
  export const useIsRouting = () => useRouter().isRouting;
25
26
  export const useMatch = (path) => {
26
27
  const location = useLocation();
27
- const matcher = createMemo(() => createMatcher(path()));
28
- return createMemo(() => matcher()(location.pathname));
28
+ const matchers = createMemo(() => expandOptionals(path()).map((path) => createMatcher(path)));
29
+ return createMemo(() => {
30
+ for (const matcher of matchers()) {
31
+ const match = matcher(location.pathname);
32
+ if (match)
33
+ return match;
34
+ }
35
+ });
29
36
  };
30
37
  export const useParams = () => useRoute().params;
31
38
  export const useRouteData = () => useRoute().data;
@@ -34,10 +41,14 @@ export const useSearchParams = () => {
34
41
  const navigate = useNavigate();
35
42
  const setSearchParams = (params, options) => {
36
43
  const searchString = untrack(() => mergeSearchString(location.search, params));
37
- navigate(location.pathname + searchString, { scroll: false, ...options });
44
+ navigate(location.pathname + searchString + location.hash, { scroll: false, resolve: false, ...options });
38
45
  };
39
46
  return [location.query, setSearchParams];
40
47
  };
48
+ export const useBeforeLeave = (listener) => {
49
+ const s = useRouter().beforeLeave.subscribe({ listener, location: useLocation(), navigate: useNavigate() });
50
+ onCleanup(s);
51
+ };
41
52
  export function createRoutes(routeDef, base = "", fallback) {
42
53
  const { component, data, children } = routeDef;
43
54
  const isLeaf = !children || (Array.isArray(children) && !children.length);
@@ -102,7 +113,8 @@ export function createBranches(routeDef, base = "", fallback, stack = [], branch
102
113
  const routes = createRoutes(def, base, fallback);
103
114
  for (const route of routes) {
104
115
  stack.push(route);
105
- if (def.children) {
116
+ const isEmptyArray = Array.isArray(def.children) && def.children.length === 0;
117
+ if (def.children && !isEmptyArray) {
106
118
  createBranches(def.children, route.pattern, fallback, stack, branches);
107
119
  }
108
120
  else {
@@ -139,9 +151,9 @@ export function createLocation(path, state) {
139
151
  }, origin, {
140
152
  equals: (a, b) => a.href === b.href
141
153
  });
142
- const pathname = createMemo(() => urlDecode(url().pathname));
143
- const search = createMemo(() => urlDecode(url().search, true));
144
- const hash = createMemo(() => urlDecode(url().hash));
154
+ const pathname = createMemo(() => url().pathname);
155
+ const search = createMemo(() => url().search, true);
156
+ const hash = createMemo(() => url().hash);
145
157
  const key = createMemo(() => "");
146
158
  return {
147
159
  get pathname() {
@@ -166,6 +178,7 @@ export function createRouterContext(integration, base = "", data, out) {
166
178
  const { signal: [source, setSource], utils = {} } = normalizeIntegration(integration);
167
179
  const parsePath = utils.parsePath || (p => p);
168
180
  const renderPath = utils.renderPath || (p => p);
181
+ const beforeLeave = utils.beforeLeave || createBeforeLeave();
169
182
  const basePath = resolvePath("", base);
170
183
  const output = isServer && out
171
184
  ? Object.assign(out, {
@@ -224,7 +237,7 @@ export function createRouterContext(integration, base = "", data, out) {
224
237
  // A delta of 0 means stay at the current location, so it is ignored
225
238
  }
226
239
  else if (utils.go) {
227
- utils.go(to);
240
+ beforeLeave.confirm(to, options) && utils.go(to);
228
241
  }
229
242
  else {
230
243
  console.warn("Router integration does not support relative routing");
@@ -252,7 +265,7 @@ export function createRouterContext(integration, base = "", data, out) {
252
265
  }
253
266
  setSource({ value: resolvedTo, replace, scroll, state: nextState });
254
267
  }
255
- else {
268
+ else if (beforeLeave.confirm(resolvedTo, options)) {
256
269
  const len = referrers.push({ value: current, replace, scroll, state: state() });
257
270
  start(() => {
258
271
  setReference(resolvedTo);
@@ -321,11 +334,10 @@ export function createRouterContext(integration, base = "", data, out) {
321
334
  if (a.hasAttribute("download") || (rel && rel.includes("external")))
322
335
  return;
323
336
  const url = new URL(href);
324
- const pathname = urlDecode(url.pathname);
325
337
  if (url.origin !== window.location.origin ||
326
- (basePath && pathname && !pathname.toLowerCase().startsWith(basePath.toLowerCase())))
338
+ (basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())))
327
339
  return;
328
- const to = parsePath(pathname + urlDecode(url.search, true) + urlDecode(url.hash));
340
+ const to = parsePath(url.pathname + url.search + url.hash);
329
341
  const state = a.getAttribute("state");
330
342
  evt.preventDefault();
331
343
  navigateFromRoute(baseRoute, to, {
@@ -347,7 +359,8 @@ export function createRouterContext(integration, base = "", data, out) {
347
359
  isRouting,
348
360
  renderPath,
349
361
  parsePath,
350
- navigatorFactory
362
+ navigatorFactory,
363
+ beforeLeave
351
364
  };
352
365
  }
353
366
  export function createRouteContext(router, parent, child, match) {
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Component, JSX } from "solid-js";
2
- export declare type Params = Record<string, string>;
3
- export declare type SetParams = Record<string, string | number | boolean | null | undefined>;
2
+ export type Params = Record<string, string>;
3
+ export type SetParams = Record<string, string | number | boolean | null | undefined>;
4
4
  export interface Path {
5
5
  pathname: string;
6
6
  search: string;
@@ -21,14 +21,14 @@ export interface Navigator {
21
21
  (to: string, options?: Partial<NavigateOptions>): void;
22
22
  (delta: number): void;
23
23
  }
24
- export declare type NavigatorFactory = (route?: RouteContext) => Navigator;
24
+ export type NavigatorFactory = (route?: RouteContext) => Navigator;
25
25
  export interface LocationChange<S = unknown> {
26
26
  value: string;
27
27
  replace?: boolean;
28
28
  scroll?: boolean;
29
29
  state?: S;
30
30
  }
31
- export declare type LocationChangeSignal = [() => LocationChange, (next: LocationChange) => void];
31
+ export type LocationChangeSignal = [() => LocationChange, (next: LocationChange) => void];
32
32
  export interface RouterIntegration {
33
33
  signal: LocationChangeSignal;
34
34
  utils?: Partial<RouterUtils>;
@@ -39,8 +39,8 @@ export interface RouteDataFuncArgs<T = unknown> {
39
39
  location: Location;
40
40
  navigate: Navigator;
41
41
  }
42
- export declare type RouteDataFunc<T = unknown, R = unknown> = (args: RouteDataFuncArgs<T>) => R;
43
- export declare type RouteDefinition = {
42
+ export type RouteDataFunc<T = unknown, R = unknown> = (args: RouteDataFuncArgs<T>) => R;
43
+ export type RouteDefinition = {
44
44
  path: string | string[];
45
45
  data?: RouteDataFunc;
46
46
  children?: RouteDefinition | RouteDefinition[];
@@ -93,6 +93,7 @@ export interface RouterUtils {
93
93
  renderPath(path: string): string;
94
94
  parsePath(str: string): string;
95
95
  go(delta: number): void;
96
+ beforeLeave: BeforeLeaveLifecycle;
96
97
  }
97
98
  export interface OutputMatch {
98
99
  originalPath: string;
@@ -112,4 +113,22 @@ export interface RouterContext {
112
113
  isRouting: () => boolean;
113
114
  renderPath(path: string): string;
114
115
  parsePath(str: string): string;
116
+ beforeLeave: BeforeLeaveLifecycle;
117
+ }
118
+ export interface BeforeLeaveEventArgs {
119
+ from: Location;
120
+ to: string | number;
121
+ options?: Partial<NavigateOptions>;
122
+ readonly defaultPrevented: boolean;
123
+ preventDefault(): void;
124
+ retry(force?: boolean): void;
125
+ }
126
+ export interface BeforeLeaveListener {
127
+ listener(e: BeforeLeaveEventArgs): void;
128
+ location: Location;
129
+ navigate: Navigator;
130
+ }
131
+ export interface BeforeLeaveLifecycle {
132
+ subscribe(listener: BeforeLeaveListener): () => void;
133
+ confirm(to: string | number, options?: Partial<NavigateOptions>): boolean;
115
134
  }
package/dist/utils.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { Params, PathMatch, Route, SetParams } from "./types";
2
+ export declare function normalizePath(path: string, omitSlash?: boolean): string;
2
3
  export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
3
4
  export declare function invariant<T>(value: T | null | undefined, message: string): T;
4
5
  export declare function joinPaths(from: string, to: string): string;
5
6
  export declare function extractSearchParams(url: URL): Params;
6
- export declare function urlDecode(str: string, isQuery?: boolean): string;
7
7
  export declare function createMatcher(path: string, partial?: boolean): (location: string) => PathMatch | null;
8
8
  export declare function scoreRoute(route: Route): number;
9
9
  export declare function createMemoObject<T extends Record<string | symbol, unknown>>(fn: () => T): T;
package/dist/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createMemo, getOwner, runWithOwner } from "solid-js";
2
2
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
3
3
  const trimPathRegex = /^\/+|\/+$/g;
4
- function normalize(path, omitSlash = false) {
4
+ export function normalizePath(path, omitSlash = false) {
5
5
  const s = path.replace(trimPathRegex, "");
6
6
  return s ? (omitSlash || /^[?#]/.test(s) ? s : "/" + s) : "";
7
7
  }
@@ -9,8 +9,8 @@ export function resolvePath(base, path, from) {
9
9
  if (hasSchemeRegex.test(path)) {
10
10
  return undefined;
11
11
  }
12
- const basePath = normalize(base);
13
- const fromPath = from && normalize(from);
12
+ const basePath = normalizePath(base);
13
+ const fromPath = from && normalizePath(from);
14
14
  let result = "";
15
15
  if (!fromPath || path.startsWith("/")) {
16
16
  result = basePath;
@@ -21,7 +21,7 @@ export function resolvePath(base, path, from) {
21
21
  else {
22
22
  result = fromPath;
23
23
  }
24
- return (result || "/") + normalize(path, !result);
24
+ return (result || "/") + normalizePath(path, !result);
25
25
  }
26
26
  export function invariant(value, message) {
27
27
  if (value == null) {
@@ -30,7 +30,7 @@ export function invariant(value, message) {
30
30
  return value;
31
31
  }
32
32
  export function joinPaths(from, to) {
33
- return normalize(from).replace(/\/*(\*.*)?$/g, "") + normalize(to);
33
+ return normalizePath(from).replace(/\/*(\*.*)?$/g, "") + normalizePath(to);
34
34
  }
35
35
  export function extractSearchParams(url) {
36
36
  const params = {};
@@ -39,9 +39,6 @@ export function extractSearchParams(url) {
39
39
  });
40
40
  return params;
41
41
  }
42
- export function urlDecode(str, isQuery) {
43
- return decodeURIComponent(isQuery ? str.replace(/\+/g, " ") : str);
44
- }
45
42
  export function createMatcher(path, partial) {
46
43
  const [pattern, splat] = path.split("/*", 2);
47
44
  const segments = pattern.split("/").filter(Boolean);
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.5.0",
9
+ "version": "0.6.0",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -28,36 +28,36 @@
28
28
  "dist"
29
29
  ],
30
30
  "sideEffects": false,
31
- "scripts": {
32
- "build": "tsc && rollup -c",
33
- "prepublishOnly": "npm run build",
34
- "test": "jest && npm run test:types",
35
- "test:watch": "jest --watch",
36
- "test:coverage": "jest --coverage && npm run test:types",
37
- "test:types": "tsc --project tsconfig.test.json",
38
- "pretty": "prettier --write \"{src,test}/**/*.{ts,tsx}\""
39
- },
40
31
  "devDependencies": {
41
32
  "@babel/core": "^7.18.13",
42
33
  "@babel/preset-typescript": "^7.18.6",
43
- "@rollup/plugin-babel": "5.3.1",
44
- "@rollup/plugin-node-resolve": "13.3.0",
34
+ "@rollup/plugin-babel": "6.0.3",
35
+ "@rollup/plugin-node-resolve": "15.0.1",
36
+ "@rollup/plugin-terser": "0.2.0",
45
37
  "@types/jest": "^29.0.0",
46
38
  "@types/node": "^18.7.14",
47
- "babel-preset-solid": "^1.5.3",
39
+ "babel-jest": "^29.0.1",
40
+ "babel-preset-solid": "^1.6.6",
48
41
  "jest": "^29.0.1",
49
- "jest-environment-jsdom": "^29.1.2",
42
+ "jest-environment-jsdom": "^29.2.1",
50
43
  "prettier": "^2.7.1",
51
- "rollup": "^2.79.0",
52
- "rollup-plugin-terser": "^7.0.2",
44
+ "rollup": "^3.7.5",
53
45
  "solid-jest": "^0.2.0",
54
- "solid-js": "^1.5.3",
55
- "typescript": "^4.8.2"
46
+ "solid-js": "^1.6.6",
47
+ "typescript": "^4.9.4"
56
48
  },
57
49
  "peerDependencies": {
58
50
  "solid-js": "^1.5.3"
59
51
  },
60
52
  "jest": {
61
53
  "preset": "solid-jest/preset/browser"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc && rollup -c",
57
+ "test": "jest && npm run test:types",
58
+ "test:watch": "jest --watch",
59
+ "test:coverage": "jest --coverage && npm run test:types",
60
+ "test:types": "tsc --project tsconfig.test.json",
61
+ "pretty": "prettier --write \"{src,test}/**/*.{ts,tsx}\""
62
62
  }
63
- }
63
+ }