@solidjs/router 0.9.0 → 0.10.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.jsx CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./components";
2
2
  export * from "./integration";
3
3
  export * from "./lifecycle";
4
- export { useRouteData, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing";
4
+ export { useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing";
5
5
  export { mergeSearchString as _mergeSearchString } from "./utils";
6
+ export * from "./data";
package/dist/routing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Component, Accessor } from "solid-js";
2
- import type { BeforeLeaveEventArgs, Branch, Location, LocationChangeSignal, MatchFilters, NavigateOptions, Navigator, Params, Route, RouteContext, RouteDataFunc, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SetParams } from "./types";
1
+ import type { JSX, Accessor } from "solid-js";
2
+ import type { BeforeLeaveEventArgs, Branch, Location, LocationChangeSignal, MatchFilters, NavigateOptions, Navigator, Params, Route, RouteContext, 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;
@@ -11,15 +11,14 @@ export declare const useLocation: <S = unknown>() => Location<S>;
11
11
  export declare const useIsRouting: () => () => boolean;
12
12
  export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S> | undefined) => Accessor<import("./types").PathMatch | undefined>;
13
13
  export declare const useParams: <T extends Params>() => T;
14
- type MaybeReturnType<T> = T extends (...args: any) => infer R ? R : T;
15
- export declare const useRouteData: <T>() => MaybeReturnType<T>;
16
14
  export declare const useSearchParams: <T extends Params>() => [T, (params: SetParams, options?: Partial<NavigateOptions>) => void];
17
15
  export declare const useBeforeLeave: (listener: (e: BeforeLeaveEventArgs) => void) => void;
18
- export declare function createRoutes(routeDef: RouteDefinition, base?: string, fallback?: Component): Route[];
16
+ export declare function createRoutes(routeDef: RouteDefinition, base?: string): Route[];
19
17
  export declare function createBranch(routes: Route[], index?: number): Branch;
20
- export declare function createBranches(routeDef: RouteDefinition | RouteDefinition[], base?: string, fallback?: Component, stack?: Route[], branches?: Branch[]): Branch[];
18
+ export declare function createBranches(routeDef: RouteDefinition | RouteDefinition[], base?: string, stack?: Route[], branches?: Branch[]): Branch[];
21
19
  export declare function getRouteMatches(branches: Branch[], location: string): RouteMatch[];
22
20
  export declare function createLocation(path: Accessor<string>, state: Accessor<any>): Location;
23
- export declare function createRouterContext(integration?: RouterIntegration | LocationChangeSignal, base?: string, data?: RouteDataFunc, out?: object): RouterContext;
24
- export declare function createRouteContext(router: RouterContext, parent: RouteContext, child: () => RouteContext, match: () => RouteMatch, params: Params): RouteContext;
25
- export {};
21
+ export declare function registerAction(url: string, fn: Function): void;
22
+ export declare function getIntent(): "native" | "navigate" | "preload" | undefined;
23
+ export declare function createRouterContext(integration?: RouterIntegration | LocationChangeSignal, getBranches?: () => Branch[], base?: string): RouterContext;
24
+ export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch, params: Params): RouteContext;
package/dist/routing.js CHANGED
@@ -35,7 +35,6 @@ export const useMatch = (path, matchFilters) => {
35
35
  });
36
36
  };
37
37
  export const useParams = () => useRoute().params;
38
- export const useRouteData = () => useRoute().data;
39
38
  export const useSearchParams = () => {
40
39
  const location = useLocation();
41
40
  const navigate = useNavigate();
@@ -57,23 +56,13 @@ export const useBeforeLeave = (listener) => {
57
56
  });
58
57
  onCleanup(s);
59
58
  };
60
- export function createRoutes(routeDef, base = "", fallback) {
61
- const { component, data, children } = routeDef;
59
+ export function createRoutes(routeDef, base = "") {
60
+ const { component, load, children } = routeDef;
62
61
  const isLeaf = !children || (Array.isArray(children) && !children.length);
63
62
  const shared = {
64
63
  key: routeDef,
65
- element: component
66
- ? () => createComponent(component, {})
67
- : () => {
68
- const { element } = routeDef;
69
- return element === undefined && fallback
70
- ? createComponent(fallback, {})
71
- : element;
72
- },
73
- preload: routeDef.component
74
- ? component.preload
75
- : routeDef.preload,
76
- data
64
+ component,
65
+ load
77
66
  };
78
67
  return asArray(routeDef.path).reduce((acc, path) => {
79
68
  for (const originalPath of expandOptionals(path)) {
@@ -113,17 +102,19 @@ export function createBranch(routes, index = 0) {
113
102
  function asArray(value) {
114
103
  return Array.isArray(value) ? value : [value];
115
104
  }
116
- export function createBranches(routeDef, base = "", fallback, stack = [], branches = []) {
105
+ export function createBranches(routeDef, base = "", stack = [], branches = []) {
117
106
  const routeDefs = asArray(routeDef);
118
107
  for (let i = 0, len = routeDefs.length; i < len; i++) {
119
108
  const def = routeDefs[i];
120
- if (def && typeof def === "object" && def.hasOwnProperty("path")) {
121
- const routes = createRoutes(def, base, fallback);
109
+ if (def && typeof def === "object") {
110
+ if (!def.hasOwnProperty("path"))
111
+ def.path = "";
112
+ const routes = createRoutes(def, base);
122
113
  for (const route of routes) {
123
114
  stack.push(route);
124
115
  const isEmptyArray = Array.isArray(def.children) && def.children.length === 0;
125
116
  if (def.children && !isEmptyArray) {
126
- createBranches(def.children, route.pattern, fallback, stack, branches);
117
+ createBranches(def.children, route.pattern, stack, branches);
127
118
  }
128
119
  else {
129
120
  const branch = createBranch([...stack], branches.length);
@@ -182,18 +173,21 @@ export function createLocation(path, state) {
182
173
  query: createMemoObject(on(search, () => extractSearchParams(url())))
183
174
  };
184
175
  }
185
- export function createRouterContext(integration, base = "", data, out) {
176
+ const actions = new Map();
177
+ export function registerAction(url, fn) {
178
+ actions.set(url, fn);
179
+ }
180
+ let intent;
181
+ export function getIntent() {
182
+ return intent;
183
+ }
184
+ export function createRouterContext(integration, getBranches, base = "") {
186
185
  const { signal: [source, setSource], utils = {} } = normalizeIntegration(integration);
187
186
  const parsePath = utils.parsePath || (p => p);
188
187
  const renderPath = utils.renderPath || (p => p);
189
188
  const beforeLeave = utils.beforeLeave || createBeforeLeave();
189
+ let submissions = [];
190
190
  const basePath = resolvePath("", base);
191
- const output = isServer && out
192
- ? Object.assign(out, {
193
- matches: [],
194
- url: undefined
195
- })
196
- : undefined;
197
191
  if (basePath === undefined) {
198
192
  throw new Error(`${basePath} is not a valid base path`);
199
193
  }
@@ -223,20 +217,6 @@ export function createRouterContext(integration, base = "", data, out) {
223
217
  return resolvePath(basePath, to);
224
218
  }
225
219
  };
226
- if (data) {
227
- try {
228
- TempRoute = baseRoute;
229
- baseRoute.data = data({
230
- data: undefined,
231
- params: {},
232
- location,
233
- navigate: navigatorFactory(baseRoute)
234
- });
235
- }
236
- finally {
237
- TempRoute = undefined;
238
- }
239
- }
240
220
  function navigateFromRoute(route, to, options) {
241
221
  // Untrack in case someone navigates in an effect - don't want to track `reference` or route paths
242
222
  untrack(() => {
@@ -268,9 +248,6 @@ export function createRouterContext(integration, base = "", data, out) {
268
248
  const current = reference();
269
249
  if (resolvedTo !== current || nextState !== state()) {
270
250
  if (isServer) {
271
- if (output) {
272
- output.url = resolvedTo;
273
- }
274
251
  const e = getRequestEvent();
275
252
  e && (e.response = Response.redirect(resolvedTo, 302));
276
253
  setSource({ value: resolvedTo, replace, scroll, state: nextState });
@@ -278,11 +255,13 @@ export function createRouterContext(integration, base = "", data, out) {
278
255
  else if (beforeLeave.confirm(resolvedTo, options)) {
279
256
  const len = referrers.push({ value: current, replace, scroll, state: state() });
280
257
  start(() => {
258
+ intent = "navigate";
281
259
  setReference(resolvedTo);
282
260
  setState(nextState);
283
261
  resetErrorBoundaries();
284
262
  }).then(() => {
285
263
  if (referrers.length === len) {
264
+ intent = undefined;
286
265
  navigateEnd({
287
266
  value: resolvedTo,
288
267
  state: nextState
@@ -317,14 +296,21 @@ export function createRouterContext(integration, base = "", data, out) {
317
296
  untrack(() => {
318
297
  if (value !== reference()) {
319
298
  start(() => {
299
+ intent = "native";
320
300
  setReference(value);
321
301
  setState(state);
302
+ }).then(() => {
303
+ intent = undefined;
322
304
  });
323
305
  }
324
306
  });
325
307
  });
326
308
  if (!isServer) {
327
- function handleAnchorClick(evt) {
309
+ let preloadTimeout = {};
310
+ function isSvg(el) {
311
+ return el.namespaceURI === "http://www.w3.org/2000/svg";
312
+ }
313
+ function handleAnchor(evt) {
328
314
  if (evt.defaultPrevented ||
329
315
  evt.button !== 0 ||
330
316
  evt.metaKey ||
@@ -335,18 +321,27 @@ export function createRouterContext(integration, base = "", data, out) {
335
321
  const a = evt
336
322
  .composedPath()
337
323
  .find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
338
- if (!a || !a.hasAttribute("link"))
324
+ if (!a)
339
325
  return;
340
- const href = a.href;
341
- if (a.target || (!href && !a.hasAttribute("state")))
326
+ const svg = isSvg(a);
327
+ const href = svg ? a.href.baseVal : a.href;
328
+ const target = svg ? a.target.baseVal : a.target;
329
+ if (target || (!href && !a.hasAttribute("state")))
342
330
  return;
343
331
  const rel = (a.getAttribute("rel") || "").split(/\s+/);
344
332
  if (a.hasAttribute("download") || (rel && rel.includes("external")))
345
333
  return;
346
- const url = new URL(href);
334
+ const url = svg ? new URL(href, document.baseURI) : new URL(href);
347
335
  if (url.origin !== window.location.origin ||
348
336
  (basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())))
349
337
  return;
338
+ return [a, url];
339
+ }
340
+ function handleAnchorClick(evt) {
341
+ const res = handleAnchor(evt);
342
+ if (!res)
343
+ return;
344
+ const [a, url] = res;
350
345
  const to = parsePath(url.pathname + url.search + url.hash);
351
346
  const state = a.getAttribute("state");
352
347
  evt.preventDefault();
@@ -357,49 +352,127 @@ export function createRouterContext(integration, base = "", data, out) {
357
352
  state: state && JSON.parse(state)
358
353
  });
359
354
  }
360
- // ensure delegated events run first
361
- delegateEvents(["click"]);
355
+ function doPreload(a, path) {
356
+ const preload = a.getAttribute("preload") !== "false";
357
+ const matches = getRouteMatches(getBranches(), path);
358
+ const prevIntent = intent;
359
+ intent = "preload";
360
+ for (let match in matches) {
361
+ const { route, params } = matches[match];
362
+ route.component &&
363
+ route.component.preload &&
364
+ route.component.preload();
365
+ preload && route.load && route.load({ params, location });
366
+ }
367
+ intent = prevIntent;
368
+ }
369
+ function handleAnchorPreload(evt) {
370
+ const res = handleAnchor(evt);
371
+ if (!res)
372
+ return;
373
+ const [a, url] = res;
374
+ if (!preloadTimeout[url.pathname])
375
+ doPreload(a, url.pathname);
376
+ }
377
+ function handleAnchorIn(evt) {
378
+ const res = handleAnchor(evt);
379
+ if (!res)
380
+ return;
381
+ const [a, url] = res;
382
+ if (preloadTimeout[url.pathname])
383
+ return;
384
+ preloadTimeout[url.pathname] = setTimeout(() => {
385
+ doPreload(a, url.pathname);
386
+ delete preloadTimeout[url.pathname];
387
+ }, 50);
388
+ }
389
+ function handleAnchorOut(evt) {
390
+ const res = handleAnchor(evt);
391
+ if (!res)
392
+ return;
393
+ const [, url] = res;
394
+ if (preloadTimeout[url.pathname]) {
395
+ clearTimeout(preloadTimeout[url.pathname]);
396
+ delete preloadTimeout[url.pathname];
397
+ }
398
+ }
399
+ function handleFormSubmit(evt) {
400
+ let actionRef = evt.submitter && evt.submitter.getAttribute("formaction") || evt.target.action;
401
+ if (actionRef && actionRef.startsWith("action:")) {
402
+ const data = new FormData(evt.target);
403
+ actions.get(actionRef.slice(7))(data);
404
+ evt.preventDefault();
405
+ }
406
+ }
407
+ ;
408
+ // ensure delegated event run first
409
+ delegateEvents(["click", "submit"]);
362
410
  document.addEventListener("click", handleAnchorClick);
363
- onCleanup(() => document.removeEventListener("click", handleAnchorClick));
411
+ document.addEventListener("mouseover", handleAnchorIn);
412
+ document.addEventListener("mouseout", handleAnchorOut);
413
+ document.addEventListener("focusin", handleAnchorPreload);
414
+ document.addEventListener("touchstart", handleAnchorPreload);
415
+ document.addEventListener("submit", handleFormSubmit);
416
+ onCleanup(() => {
417
+ document.removeEventListener("click", handleAnchorClick);
418
+ document.removeEventListener("mouseover", handleAnchorIn);
419
+ document.removeEventListener("mouseout", handleAnchorOut);
420
+ document.removeEventListener("focusin", handleAnchorPreload);
421
+ document.removeEventListener("touchstart", handleAnchorPreload);
422
+ document.removeEventListener("submit", handleFormSubmit);
423
+ });
424
+ }
425
+ else {
426
+ function initFromFlash(params) {
427
+ let param = params.form ? JSON.parse(params.form) : null;
428
+ if (!param || !param.result) {
429
+ return [];
430
+ }
431
+ const input = new Map(param.entries);
432
+ return [{
433
+ url: param.url,
434
+ result: param.error ? new Error(param.result.message) : param.result,
435
+ input: input
436
+ }];
437
+ }
438
+ submissions = initFromFlash(location.query);
364
439
  }
365
440
  return {
366
441
  base: baseRoute,
367
- out: output,
368
442
  location,
369
443
  isRouting,
370
444
  renderPath,
371
445
  parsePath,
372
446
  navigatorFactory,
373
- beforeLeave
447
+ beforeLeave,
448
+ submissions: createSignal(submissions)
374
449
  };
375
450
  }
376
- export function createRouteContext(router, parent, child, match, params) {
377
- const { base, location, navigatorFactory } = router;
378
- const { pattern, element: outlet, preload, data } = match().route;
451
+ export function createRouteContext(router, parent, outlet, match, params) {
452
+ const { base, location } = router;
453
+ const { pattern, component, load } = match().route;
379
454
  const path = createMemo(() => match().path);
380
- preload && preload();
381
455
  const route = {
382
456
  parent,
383
457
  pattern,
384
- get child() {
385
- return child();
386
- },
387
458
  path,
388
459
  params,
389
- data: parent.data,
390
- outlet,
460
+ outlet: () => component
461
+ ? createComponent(component, {
462
+ params,
463
+ location,
464
+ get children() {
465
+ return outlet();
466
+ }
467
+ })
468
+ : outlet(),
391
469
  resolvePath(to) {
392
470
  return resolvePath(base.path(), to, path());
393
471
  }
394
472
  };
395
- if (data) {
396
- try {
397
- TempRoute = route;
398
- route.data = data({ data: parent.data, params, location, navigate: navigatorFactory(route) });
399
- }
400
- finally {
401
- TempRoute = undefined;
402
- }
403
- }
473
+ component &&
474
+ component.preload &&
475
+ component.preload();
476
+ load && load({ params, location });
404
477
  return route;
405
478
  }
package/dist/types.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { Component, JSX } from "solid-js";
1
+ import { Component, JSX, Signal } from "solid-js";
2
2
  declare module "solid-js/web" {
3
3
  interface RequestEvent {
4
4
  response?: Response;
5
+ routerCache?: Map<any, any>;
5
6
  }
6
7
  }
7
8
  export type Params = Record<string, string>;
@@ -38,26 +39,21 @@ export interface RouterIntegration {
38
39
  signal: LocationChangeSignal;
39
40
  utils?: Partial<RouterUtils>;
40
41
  }
41
- export interface RouteDataFuncArgs<T = unknown> {
42
- data: T extends RouteDataFunc<infer _, infer R> ? R : T;
42
+ export interface RouteLoadFuncArgs {
43
43
  params: Params;
44
44
  location: Location;
45
- navigate: Navigator;
46
45
  }
47
- export type RouteDataFunc<T = unknown, R = unknown> = (args: RouteDataFuncArgs<T>) => R;
46
+ export type RouteLoadFunc = (args: RouteLoadFuncArgs) => void;
47
+ export interface RouteSectionProps extends RouteLoadFuncArgs {
48
+ children?: JSX.Element;
49
+ }
48
50
  export type RouteDefinition<S extends string | string[] = any> = {
49
51
  path: S;
50
52
  matchFilters?: MatchFilters<S>;
51
- data?: RouteDataFunc;
53
+ load?: RouteLoadFunc;
52
54
  children?: RouteDefinition | RouteDefinition[];
53
- } & ({
54
- element?: never;
55
- component: Component;
56
- } | {
57
- component?: never;
58
- element?: JSX.Element;
59
- preload?: () => void;
60
- });
55
+ component?: Component<RouteSectionProps>;
56
+ };
61
57
  export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);
62
58
  export type PathParams<P extends string | readonly string[]> = P extends `${infer Head}/${infer Tail}` ? [...PathParams<Head>, ...PathParams<Tail>] : P extends `:${infer S}?` ? [S] : P extends `:${infer S}` ? [S] : P extends `*${infer S}` ? [S] : [];
63
59
  export type MatchFilters<P extends string | readonly string[] = any> = P extends string ? {
@@ -80,9 +76,8 @@ export interface Route {
80
76
  key: unknown;
81
77
  originalPath: string;
82
78
  pattern: string;
83
- element: () => JSX.Element;
84
- preload?: () => void;
85
- data?: RouteDataFunc;
79
+ component?: Component<RouteSectionProps>;
80
+ load?: RouteLoadFunc;
86
81
  matcher: (location: string) => PathMatch | null;
87
82
  matchFilters?: MatchFilters;
88
83
  }
@@ -94,7 +89,6 @@ export interface Branch {
94
89
  export interface RouteContext {
95
90
  parent?: RouteContext;
96
91
  child?: RouteContext;
97
- data?: unknown;
98
92
  pattern: string;
99
93
  params: Params;
100
94
  path: () => string;
@@ -119,13 +113,13 @@ export interface RouterOutput {
119
113
  }
120
114
  export interface RouterContext {
121
115
  base: RouteContext;
122
- out?: RouterOutput;
123
116
  location: Location;
124
117
  navigatorFactory: NavigatorFactory;
125
118
  isRouting: () => boolean;
126
119
  renderPath(path: string): string;
127
120
  parsePath(str: string): string;
128
121
  beforeLeave: BeforeLeaveLifecycle;
122
+ submissions: Signal<Submission<any, any>[]>;
129
123
  }
130
124
  export interface BeforeLeaveEventArgs {
131
125
  from: Location;
@@ -144,3 +138,11 @@ export interface BeforeLeaveLifecycle {
144
138
  subscribe(listener: BeforeLeaveListener): () => void;
145
139
  confirm(to: string | number, options?: Partial<NavigateOptions>): boolean;
146
140
  }
141
+ export type Submission<T, U> = {
142
+ readonly input: T;
143
+ readonly result?: U;
144
+ readonly pending: boolean;
145
+ readonly url: string;
146
+ clear: () => void;
147
+ retry: () => void;
148
+ };
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { MatchFilters, Params, PathMatch, Route, SetParams } from "./types";
2
+ export declare const redirectStatusCodes: Set<number>;
2
3
  export declare function normalizePath(path: string, omitSlash?: boolean): string;
3
4
  export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
4
5
  export declare function invariant<T>(value: T | null | undefined, message: string): T;
package/dist/utils.js CHANGED
@@ -1,6 +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
+ export const redirectStatusCodes = new Set([204, 301, 302, 303, 307, 308]);
4
5
  export function normalizePath(path, omitSlash = false) {
5
6
  const s = path.replace(trimPathRegex, "$1");
6
7
  return s ? (omitSlash || /^[?#]/.test(s) ? s : "/" + s) : "";
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.9.0",
9
+ "version": "0.10.0-beta.0",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -35,7 +35,7 @@
35
35
  "@rollup/plugin-node-resolve": "15.0.1",
36
36
  "@rollup/plugin-terser": "0.2.0",
37
37
  "@types/jest": "^29.0.0",
38
- "@types/node": "^18.7.14",
38
+ "@types/node": "^20.9.0",
39
39
  "babel-jest": "^29.0.1",
40
40
  "babel-preset-solid": "^1.6.6",
41
41
  "jest": "^29.0.1",
@@ -43,11 +43,11 @@
43
43
  "prettier": "^2.7.1",
44
44
  "rollup": "^3.7.5",
45
45
  "solid-jest": "^0.2.0",
46
- "solid-js": "^1.7.9",
47
- "typescript": "^4.9.4"
46
+ "solid-js": "^1.8.4",
47
+ "typescript": "^5.2.2"
48
48
  },
49
49
  "peerDependencies": {
50
- "solid-js": "^1.5.3"
50
+ "solid-js": "^1.8.4"
51
51
  },
52
52
  "jest": {
53
53
  "preset": "solid-jest/preset/browser"