@solidjs/router 0.10.0-beta.7 → 0.10.0-beta.9

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
@@ -368,6 +368,14 @@ This cache accomplishes the following:
368
368
  3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
369
369
  4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses it. Revalidation or new fetch updates the cache.
370
370
 
371
+ Cached function has a few useful methods for getting the key that are useful for invalidation.
372
+ ```ts
373
+ let id = 5;
374
+
375
+ getUser.key // returns "users"
376
+ getUser.keyFor(id) // returns "users[5]"
377
+ ```
378
+
371
379
  This cache can be defined anywhere and then used inside your components with:
372
380
 
373
381
  ### `createAsync`
@@ -378,6 +386,8 @@ This is light wrapper over `createResource` that aims to serve as stand-in for a
378
386
  const user = createAsync(() => getUser(params.id))
379
387
  ```
380
388
 
389
+ Using `cache` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
390
+
381
391
  ### `action`
382
392
 
383
393
  Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response builders can be found below(TODO).
@@ -391,12 +401,38 @@ const myAction = action(async (data) => {
391
401
  });
392
402
 
393
403
  // in component
394
- <form action={myAction} />
404
+ <form action={myAction} method="post" />
395
405
 
396
406
  //or
397
407
  <button type="submit" formaction={myAction}></button>
398
408
  ```
399
409
 
410
+ Actions only work with post requests, so make sure to put `method="post"` on your form.
411
+
412
+ Sometimes it might be easier to deal with typed data instead of `FormData` and adding additional hidden fields. For that reason Actions have a with method. That works similar to `bind` which applies the arguments in order.
413
+
414
+ Picture an action that deletes Todo Item:
415
+
416
+ ```js
417
+ const deleteTodo = action(async (formData: FormData) => {
418
+ const id = Number(formData.get("id"))
419
+ await api.deleteTodo(id)
420
+ })
421
+
422
+ <form action={deleteUser} method="post">
423
+ <input type="hidden" name="id" value={todo.id} />
424
+ <button type="submit">Delete</button>
425
+ </form>
426
+ ```
427
+ Instead with `with` you can write this:
428
+ ```js
429
+ const deleteUser = action(api.deleteUser)
430
+
431
+ <form action={deleteUser.with(todo.id)} method="post">
432
+ <button type="submit">Delete</button>
433
+ </form>
434
+ ```
435
+
400
436
  #### Notes of `<form>` implementation and SSR
401
437
  This requires stable references as you can only serialize a string as an attribute, and across SSR they'd need to match. The solution is providing a unique name.
402
438
 
@@ -571,9 +607,22 @@ import { Router } from "@solidjs/router";
571
607
 
572
608
  ## Components
573
609
 
610
+ ### `<Router>`
611
+
612
+ This is the main Router component for the browser.
613
+
614
+ | prop | type | description |
615
+ |-----|----|----|
616
+ | children | `JSX.Element` or `RouteDefinition[]` | The route definitions |
617
+ | root | Component | Top level layout comoponent |
618
+ | base | string | Base url to use for matching routes |
619
+ | actionBase | string | Root url for server actions, default: `/_server` |
620
+ | preload | boolean | Enables/disables preloads globally, default: `true` |
621
+ | explicitLinks | boolean | Disables all anchors being intercepted and instead requires `<A>`. default: `false` |
622
+
574
623
  ### `<A>`
575
624
 
576
- Like the `<a>` tag but supports relative paths and active class styling.
625
+ Like the `<a>` tag but supports relative paths and active class styling (requires client side JavaScript).
577
626
 
578
627
  The `<A>` tag has an `active` class if its href matches the current location, and `inactive` otherwise. **Note:** By default matching includes locations that are descendents (eg. href `/users` matches locations `/users` and `/users/123`), use the boolean `end` prop to prevent matching these. This is particularly useful for links to the root route `/` which would match everything.
579
628
 
@@ -729,9 +778,9 @@ useBeforeLeave((e: BeforeLeaveEventArgs) => {
729
778
  });
730
779
  ```
731
780
 
732
- ## Migrations from 0.8.x
781
+ ## Migrations from 0.9.x
733
782
 
734
- v0.9.0 brings some big changes to support the future of routing including Islands/Partial Hydration hybrid solutions. Most notably there is no Context API available in non-hydrating parts of the application.
783
+ v0.10.0 brings some big changes to support the future of routing including Islands/Partial Hydration hybrid solutions. Most notably there is no Context API available in non-hydrating parts of the application.
735
784
 
736
785
  The biggest changes are around removed APIs that need to be replaced.
737
786
 
@@ -745,7 +794,7 @@ Related without Outlet component it has to be passed in manually. At which point
745
794
 
746
795
  ### `data` functions & `useRouteData`
747
796
 
748
- These have been replaced by a load mechanism. This allows link hover preloads (as the load function can be run as much as wanted without worry about reactivity). It support deduping/cache APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.
797
+ These have been replaced by a load mechanism. This allows link hover preloads (as the load function can be run as much as wanted without worry about reactivity). It support deduping/cache APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.
749
798
 
750
799
  ## SPAs in Deployed Environments
751
800
 
@@ -7,6 +7,7 @@ declare module "solid-js" {
7
7
  noScroll?: boolean;
8
8
  replace?: boolean;
9
9
  preload?: boolean;
10
+ link?: boolean;
10
11
  }
11
12
  }
12
13
  }
@@ -27,7 +27,7 @@ export function A(props) {
27
27
  [props.inactiveClass]: !isActive(),
28
28
  [props.activeClass]: isActive(),
29
29
  ...rest.classList
30
- }} aria-current={isActive() ? "page" : undefined}/>);
30
+ }} link aria-current={isActive() ? "page" : undefined}/>);
31
31
  }
32
32
  export function Navigate(props) {
33
33
  const navigate = useNavigate();
@@ -1,6 +1,6 @@
1
1
  import { JSX } from "solid-js";
2
2
  import { Submission } from "../types";
3
- export type Action<T extends Array<any>, U> = ((...vars: T) => Promise<U>) & JSX.SerializableAttributeValue & {
3
+ export type Action<T extends Array<any>, U> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<U>) & {
4
4
  url: string;
5
5
  with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => U, ...args: A): Action<B, U>;
6
6
  };
@@ -80,7 +80,7 @@ function toAction(fn, url) {
80
80
  };
81
81
  const uri = new URL(url, "http://sar");
82
82
  uri.searchParams.set("args", hashKey(args));
83
- return toAction(newFn, uri.toString());
83
+ return toAction(newFn, uri.pathname + uri.search);
84
84
  };
85
85
  fn.url = url;
86
86
  if (!isServer) {
@@ -5,4 +5,7 @@ export type CachedFunction<T extends (...args: any) => U | Response, U> = T & {
5
5
  key: string;
6
6
  };
7
7
  export declare function cache<T extends (...args: any) => U | Response, U>(fn: T, name: string, options?: ReconcileOptions): CachedFunction<T, U>;
8
+ export declare namespace cache {
9
+ var set: (key: string, value: any) => void;
10
+ }
8
11
  export declare function hashKey<T extends Array<any>>(args: T): string;
@@ -22,6 +22,8 @@ function getCache() {
22
22
  if (!isServer)
23
23
  return cacheMap;
24
24
  const req = getRequestEvent() || sharedConfig.context;
25
+ if (!req)
26
+ throw new Error("Cannot find cache context");
25
27
  return req.routerCache || (req.routerCache = new Map());
26
28
  }
27
29
  export function revalidate(key) {
@@ -78,8 +80,9 @@ export function cache(fn, name, options) {
78
80
  ? sharedConfig.load(key) // hydrating
79
81
  : fn(...args);
80
82
  // serialize on server
81
- if (isServer && sharedConfig.context && !sharedConfig.context.noHydrate) {
82
- sharedConfig.context && sharedConfig.context.serialize(key, res);
83
+ if (isServer && (sharedConfig.context && !sharedConfig.context.noHydrate)) {
84
+ const e = getRequestEvent();
85
+ (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
83
86
  }
84
87
  if (cached) {
85
88
  cached[0] = now;
@@ -130,6 +133,27 @@ export function cache(fn, name, options) {
130
133
  cachedFn.key = name;
131
134
  return cachedFn;
132
135
  }
136
+ ;
137
+ cache.set = (key, value) => {
138
+ const cache = getCache();
139
+ const now = Date.now();
140
+ let cached = cache.get(key);
141
+ let version;
142
+ if (getOwner()) {
143
+ version = createSignal(now, {
144
+ equals: (p, v) => v - p < 50 // margin of error
145
+ });
146
+ onCleanup(() => cached[3].delete(version));
147
+ }
148
+ if (cached) {
149
+ cached[0] = now;
150
+ cached[1] = value;
151
+ cached[2] = "preload";
152
+ version && cached[3].add(version);
153
+ }
154
+ else
155
+ cache.set(key, (cached = [now, value, , new Set(version ? [version] : [])]));
156
+ };
133
157
  function matchKey(key, keys) {
134
158
  for (let k of keys) {
135
159
  if (key.startsWith(k))
@@ -1,2 +1,2 @@
1
1
  import type { RouterContext } from "../types";
2
- export declare function setupNativeEvents(router: RouterContext): void;
2
+ export declare function setupNativeEvents(preload?: boolean, explicitLinks?: boolean, actionBase?: string): (router: RouterContext) => void;
@@ -1,118 +1,126 @@
1
1
  import { delegateEvents } from "solid-js/web";
2
2
  import { onCleanup } from "solid-js";
3
3
  import { actions } from "./action";
4
- export function setupNativeEvents(router) {
5
- const basePath = router.base.path();
6
- const navigateFromRoute = router.navigatorFactory(router.base);
7
- let preloadTimeout = {};
8
- function isSvg(el) {
9
- return el.namespaceURI === "http://www.w3.org/2000/svg";
10
- }
11
- function handleAnchor(evt) {
12
- if (evt.defaultPrevented ||
13
- evt.button !== 0 ||
14
- evt.metaKey ||
15
- evt.altKey ||
16
- evt.ctrlKey ||
17
- evt.shiftKey)
18
- return;
19
- const a = evt
20
- .composedPath()
21
- .find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
22
- if (!a)
23
- return;
24
- const svg = isSvg(a);
25
- const href = svg ? a.href.baseVal : a.href;
26
- const target = svg ? a.target.baseVal : a.target;
27
- if (target || (!href && !a.hasAttribute("state")))
28
- return;
29
- const rel = (a.getAttribute("rel") || "").split(/\s+/);
30
- if (a.hasAttribute("download") || (rel && rel.includes("external")))
31
- return;
32
- const url = svg ? new URL(href, document.baseURI) : new URL(href);
33
- if (url.origin !== window.location.origin ||
34
- (basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())))
35
- return;
36
- return [a, url];
37
- }
38
- function handleAnchorClick(evt) {
39
- const res = handleAnchor(evt);
40
- if (!res)
41
- return;
42
- const [a, url] = res;
43
- const to = router.parsePath(url.pathname + url.search + url.hash);
44
- const state = a.getAttribute("state");
45
- evt.preventDefault();
46
- navigateFromRoute(to, {
47
- resolve: false,
48
- replace: a.hasAttribute("replace"),
49
- scroll: !a.hasAttribute("noscroll"),
50
- state: state && JSON.parse(state)
51
- });
52
- }
53
- function handleAnchorPreload(evt) {
54
- const res = handleAnchor(evt);
55
- if (!res)
56
- return;
57
- const [a, url] = res;
58
- if (!preloadTimeout[url.pathname])
59
- router.preloadRoute(url, a.getAttribute("preload") !== "false");
60
- }
61
- function handleAnchorIn(evt) {
62
- const res = handleAnchor(evt);
63
- if (!res)
64
- return;
65
- const [a, url] = res;
66
- if (preloadTimeout[url.pathname])
67
- return;
68
- preloadTimeout[url.pathname] = setTimeout(() => {
69
- router.preloadRoute(url, a.getAttribute("preload") !== "false");
70
- delete preloadTimeout[url.pathname];
71
- }, 200);
72
- }
73
- function handleAnchorOut(evt) {
74
- const res = handleAnchor(evt);
75
- if (!res)
76
- return;
77
- const [, url] = res;
78
- if (preloadTimeout[url.pathname]) {
79
- clearTimeout(preloadTimeout[url.pathname]);
80
- delete preloadTimeout[url.pathname];
4
+ export function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "/_server") {
5
+ return (router) => {
6
+ const basePath = router.base.path();
7
+ const navigateFromRoute = router.navigatorFactory(router.base);
8
+ let preloadTimeout = {};
9
+ function isSvg(el) {
10
+ return el.namespaceURI === "http://www.w3.org/2000/svg";
81
11
  }
82
- }
83
- function handleFormSubmit(evt) {
84
- let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
85
- ? evt.submitter.formAction
86
- : evt.target.action;
87
- if (!actionRef)
88
- return;
89
- if (!actionRef.startsWith("action:")) {
90
- const url = new URL(actionRef);
91
- actionRef = router.parsePath(url.pathname + url.search);
92
- if (!actionRef.startsWith(router.actionBase))
12
+ function handleAnchor(evt) {
13
+ if (evt.defaultPrevented ||
14
+ evt.button !== 0 ||
15
+ evt.metaKey ||
16
+ evt.altKey ||
17
+ evt.ctrlKey ||
18
+ evt.shiftKey)
19
+ return;
20
+ const a = evt
21
+ .composedPath()
22
+ .find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
23
+ if (!a || (explicitLinks && !a.getAttribute("link")))
24
+ return;
25
+ const svg = isSvg(a);
26
+ const href = svg ? a.href.baseVal : a.href;
27
+ const target = svg ? a.target.baseVal : a.target;
28
+ if (target || (!href && !a.hasAttribute("state")))
29
+ return;
30
+ const rel = (a.getAttribute("rel") || "").split(/\s+/);
31
+ if (a.hasAttribute("download") || (rel && rel.includes("external")))
32
+ return;
33
+ const url = svg ? new URL(href, document.baseURI) : new URL(href);
34
+ if (url.origin !== window.location.origin ||
35
+ (basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())))
93
36
  return;
37
+ return [a, url];
94
38
  }
95
- const handler = actions.get(actionRef);
96
- if (handler) {
39
+ function handleAnchorClick(evt) {
40
+ const res = handleAnchor(evt);
41
+ if (!res)
42
+ return;
43
+ const [a, url] = res;
44
+ const to = router.parsePath(url.pathname + url.search + url.hash);
45
+ const state = a.getAttribute("state");
97
46
  evt.preventDefault();
98
- const data = new FormData(evt.target);
99
- handler.call(router, data);
47
+ navigateFromRoute(to, {
48
+ resolve: false,
49
+ replace: a.hasAttribute("replace"),
50
+ scroll: !a.hasAttribute("noscroll"),
51
+ state: state && JSON.parse(state)
52
+ });
53
+ }
54
+ function handleAnchorPreload(evt) {
55
+ const res = handleAnchor(evt);
56
+ if (!res)
57
+ return;
58
+ const [a, url] = res;
59
+ if (!preloadTimeout[url.pathname])
60
+ router.preloadRoute(url, a.getAttribute("preload") !== "false");
100
61
  }
101
- }
102
- // ensure delegated event run first
103
- delegateEvents(["click", "submit"]);
104
- document.addEventListener("click", handleAnchorClick);
105
- document.addEventListener("mouseover", handleAnchorIn);
106
- document.addEventListener("mouseout", handleAnchorOut);
107
- document.addEventListener("focusin", handleAnchorPreload);
108
- document.addEventListener("touchstart", handleAnchorPreload);
109
- document.addEventListener("submit", handleFormSubmit);
110
- onCleanup(() => {
111
- document.removeEventListener("click", handleAnchorClick);
112
- document.removeEventListener("mouseover", handleAnchorIn);
113
- document.removeEventListener("mouseout", handleAnchorOut);
114
- document.removeEventListener("focusin", handleAnchorPreload);
115
- document.removeEventListener("touchstart", handleAnchorPreload);
116
- document.removeEventListener("submit", handleFormSubmit);
117
- });
62
+ function handleAnchorIn(evt) {
63
+ const res = handleAnchor(evt);
64
+ if (!res)
65
+ return;
66
+ const [a, url] = res;
67
+ if (preloadTimeout[url.pathname])
68
+ return;
69
+ preloadTimeout[url.pathname] = setTimeout(() => {
70
+ router.preloadRoute(url, a.getAttribute("preload") !== "false");
71
+ delete preloadTimeout[url.pathname];
72
+ }, 200);
73
+ }
74
+ function handleAnchorOut(evt) {
75
+ const res = handleAnchor(evt);
76
+ if (!res)
77
+ return;
78
+ const [, url] = res;
79
+ if (preloadTimeout[url.pathname]) {
80
+ clearTimeout(preloadTimeout[url.pathname]);
81
+ delete preloadTimeout[url.pathname];
82
+ }
83
+ }
84
+ function handleFormSubmit(evt) {
85
+ let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
86
+ ? evt.submitter.formAction
87
+ : evt.target.action;
88
+ if (!actionRef)
89
+ return;
90
+ if (!actionRef.startsWith("action:")) {
91
+ const url = new URL(actionRef);
92
+ actionRef = router.parsePath(url.pathname + url.search);
93
+ if (!actionRef.startsWith(actionBase))
94
+ return;
95
+ }
96
+ if (evt.target.method.toUpperCase() !== "POST")
97
+ throw new Error("Only POST forms are supported for Actions");
98
+ const handler = actions.get(actionRef);
99
+ if (handler) {
100
+ evt.preventDefault();
101
+ const data = new FormData(evt.target);
102
+ handler.call(router, data);
103
+ }
104
+ }
105
+ // ensure delegated event run first
106
+ delegateEvents(["click", "submit"]);
107
+ document.addEventListener("click", handleAnchorClick);
108
+ if (preload) {
109
+ document.addEventListener("mouseover", handleAnchorIn);
110
+ document.addEventListener("mouseout", handleAnchorOut);
111
+ document.addEventListener("focusin", handleAnchorPreload);
112
+ document.addEventListener("touchstart", handleAnchorPreload);
113
+ }
114
+ document.addEventListener("submit", handleFormSubmit);
115
+ onCleanup(() => {
116
+ document.removeEventListener("click", handleAnchorClick);
117
+ if (preload) {
118
+ document.removeEventListener("mouseover", handleAnchorIn);
119
+ document.removeEventListener("mouseout", handleAnchorOut);
120
+ document.removeEventListener("focusin", handleAnchorPreload);
121
+ document.removeEventListener("touchstart", handleAnchorPreload);
122
+ }
123
+ document.removeEventListener("submit", handleFormSubmit);
124
+ });
125
+ };
118
126
  }
package/dist/index.js CHANGED
@@ -421,7 +421,6 @@ function createRouterContext(integration, getBranches, options = {}) {
421
421
  });
422
422
  return {
423
423
  base: baseRoute,
424
- actionBase: options.actionBase || "/_server",
425
424
  location,
426
425
  isRouting,
427
426
  renderPath,
@@ -587,8 +586,7 @@ function createRouteContext(router, parent, outlet, match, params) {
587
586
 
588
587
  const createRouterComponent = router => props => {
589
588
  const {
590
- base,
591
- actionBase
589
+ base
592
590
  } = props;
593
591
  const routeDefs = children(() => props.children);
594
592
  const branches = createMemo(() => createBranches(props.root ? {
@@ -596,8 +594,7 @@ const createRouterComponent = router => props => {
596
594
  children: routeDefs()
597
595
  } : routeDefs(), props.base || ""));
598
596
  const routerState = createRouterContext(router, branches, {
599
- base,
600
- actionBase
597
+ base
601
598
  });
602
599
  router.create && router.create(routerState);
603
600
  return createComponent$1(RouterContextObj.Provider, {
@@ -768,6 +765,7 @@ if (!isServer) {
768
765
  function getCache() {
769
766
  if (!isServer) return cacheMap;
770
767
  const req = getRequestEvent() || sharedConfig.context;
768
+ if (!req) throw new Error("Cannot find cache context");
771
769
  return req.routerCache || (req.routerCache = new Map());
772
770
  }
773
771
  function revalidate(key) {
@@ -825,7 +823,8 @@ function cache(fn, name, options) {
825
823
 
826
824
  // serialize on server
827
825
  if (isServer && sharedConfig.context && !sharedConfig.context.noHydrate) {
828
- sharedConfig.context && sharedConfig.context.serialize(key, res);
826
+ const e = getRequestEvent();
827
+ (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
829
828
  }
830
829
  if (cached) {
831
830
  cached[0] = now;
@@ -868,6 +867,25 @@ function cache(fn, name, options) {
868
867
  cachedFn.key = name;
869
868
  return cachedFn;
870
869
  }
870
+ cache.set = (key, value) => {
871
+ const cache = getCache();
872
+ const now = Date.now();
873
+ let cached = cache.get(key);
874
+ let version;
875
+ if (getOwner()) {
876
+ version = createSignal(now, {
877
+ equals: (p, v) => v - p < 50 // margin of error
878
+ });
879
+
880
+ onCleanup(() => cached[3].delete(version));
881
+ }
882
+ if (cached) {
883
+ cached[0] = now;
884
+ cached[1] = value;
885
+ cached[2] = "preload";
886
+ version && cached[3].add(version);
887
+ } else cache.set(key, cached = [now, value,, new Set(version ? [version] : [])]);
888
+ };
871
889
  function matchKey(key, keys) {
872
890
  for (let k of keys) {
873
891
  if (key.startsWith(k)) return true;
@@ -961,7 +979,7 @@ function toAction(fn, url) {
961
979
  };
962
980
  const uri = new URL(url, "http://sar");
963
981
  uri.searchParams.set("args", hashKey(args));
964
- return toAction(newFn, uri.toString());
982
+ return toAction(newFn, uri.pathname + uri.search);
965
983
  };
966
984
  fn.url = url;
967
985
  if (!isServer) {
@@ -991,98 +1009,105 @@ async function handleResponse(response, navigate) {
991
1009
  return data;
992
1010
  }
993
1011
 
994
- function setupNativeEvents(router) {
995
- const basePath = router.base.path();
996
- const navigateFromRoute = router.navigatorFactory(router.base);
997
- let preloadTimeout = {};
998
- function isSvg(el) {
999
- return el.namespaceURI === "http://www.w3.org/2000/svg";
1000
- }
1001
- function handleAnchor(evt) {
1002
- if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
1003
- const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
1004
- if (!a) return;
1005
- const svg = isSvg(a);
1006
- const href = svg ? a.href.baseVal : a.href;
1007
- const target = svg ? a.target.baseVal : a.target;
1008
- if (target || !href && !a.hasAttribute("state")) return;
1009
- const rel = (a.getAttribute("rel") || "").split(/\s+/);
1010
- if (a.hasAttribute("download") || rel && rel.includes("external")) return;
1011
- const url = svg ? new URL(href, document.baseURI) : new URL(href);
1012
- if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
1013
- return [a, url];
1014
- }
1015
- function handleAnchorClick(evt) {
1016
- const res = handleAnchor(evt);
1017
- if (!res) return;
1018
- const [a, url] = res;
1019
- const to = router.parsePath(url.pathname + url.search + url.hash);
1020
- const state = a.getAttribute("state");
1021
- evt.preventDefault();
1022
- navigateFromRoute(to, {
1023
- resolve: false,
1024
- replace: a.hasAttribute("replace"),
1025
- scroll: !a.hasAttribute("noscroll"),
1026
- state: state && JSON.parse(state)
1027
- });
1028
- }
1029
- function handleAnchorPreload(evt) {
1030
- const res = handleAnchor(evt);
1031
- if (!res) return;
1032
- const [a, url] = res;
1033
- if (!preloadTimeout[url.pathname]) router.preloadRoute(url, a.getAttribute("preload") !== "false");
1034
- }
1035
- function handleAnchorIn(evt) {
1036
- const res = handleAnchor(evt);
1037
- if (!res) return;
1038
- const [a, url] = res;
1039
- if (preloadTimeout[url.pathname]) return;
1040
- preloadTimeout[url.pathname] = setTimeout(() => {
1041
- router.preloadRoute(url, a.getAttribute("preload") !== "false");
1042
- delete preloadTimeout[url.pathname];
1043
- }, 200);
1044
- }
1045
- function handleAnchorOut(evt) {
1046
- const res = handleAnchor(evt);
1047
- if (!res) return;
1048
- const [, url] = res;
1049
- if (preloadTimeout[url.pathname]) {
1050
- clearTimeout(preloadTimeout[url.pathname]);
1051
- delete preloadTimeout[url.pathname];
1012
+ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "/_server") {
1013
+ return router => {
1014
+ const basePath = router.base.path();
1015
+ const navigateFromRoute = router.navigatorFactory(router.base);
1016
+ let preloadTimeout = {};
1017
+ function isSvg(el) {
1018
+ return el.namespaceURI === "http://www.w3.org/2000/svg";
1052
1019
  }
1053
- }
1054
- function handleFormSubmit(evt) {
1055
- let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.formAction : evt.target.action;
1056
- if (!actionRef) return;
1057
- if (!actionRef.startsWith("action:")) {
1058
- const url = new URL(actionRef);
1059
- actionRef = router.parsePath(url.pathname + url.search);
1060
- if (!actionRef.startsWith(router.actionBase)) return;
1020
+ function handleAnchor(evt) {
1021
+ if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
1022
+ const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
1023
+ if (!a || explicitLinks && !a.getAttribute("link")) return;
1024
+ const svg = isSvg(a);
1025
+ const href = svg ? a.href.baseVal : a.href;
1026
+ const target = svg ? a.target.baseVal : a.target;
1027
+ if (target || !href && !a.hasAttribute("state")) return;
1028
+ const rel = (a.getAttribute("rel") || "").split(/\s+/);
1029
+ if (a.hasAttribute("download") || rel && rel.includes("external")) return;
1030
+ const url = svg ? new URL(href, document.baseURI) : new URL(href);
1031
+ if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
1032
+ return [a, url];
1061
1033
  }
1062
- const handler = actions.get(actionRef);
1063
- if (handler) {
1034
+ function handleAnchorClick(evt) {
1035
+ const res = handleAnchor(evt);
1036
+ if (!res) return;
1037
+ const [a, url] = res;
1038
+ const to = router.parsePath(url.pathname + url.search + url.hash);
1039
+ const state = a.getAttribute("state");
1064
1040
  evt.preventDefault();
1065
- const data = new FormData(evt.target);
1066
- handler.call(router, data);
1041
+ navigateFromRoute(to, {
1042
+ resolve: false,
1043
+ replace: a.hasAttribute("replace"),
1044
+ scroll: !a.hasAttribute("noscroll"),
1045
+ state: state && JSON.parse(state)
1046
+ });
1047
+ }
1048
+ function handleAnchorPreload(evt) {
1049
+ const res = handleAnchor(evt);
1050
+ if (!res) return;
1051
+ const [a, url] = res;
1052
+ if (!preloadTimeout[url.pathname]) router.preloadRoute(url, a.getAttribute("preload") !== "false");
1053
+ }
1054
+ function handleAnchorIn(evt) {
1055
+ const res = handleAnchor(evt);
1056
+ if (!res) return;
1057
+ const [a, url] = res;
1058
+ if (preloadTimeout[url.pathname]) return;
1059
+ preloadTimeout[url.pathname] = setTimeout(() => {
1060
+ router.preloadRoute(url, a.getAttribute("preload") !== "false");
1061
+ delete preloadTimeout[url.pathname];
1062
+ }, 200);
1063
+ }
1064
+ function handleAnchorOut(evt) {
1065
+ const res = handleAnchor(evt);
1066
+ if (!res) return;
1067
+ const [, url] = res;
1068
+ if (preloadTimeout[url.pathname]) {
1069
+ clearTimeout(preloadTimeout[url.pathname]);
1070
+ delete preloadTimeout[url.pathname];
1071
+ }
1072
+ }
1073
+ function handleFormSubmit(evt) {
1074
+ let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.formAction : evt.target.action;
1075
+ if (!actionRef) return;
1076
+ if (!actionRef.startsWith("action:")) {
1077
+ const url = new URL(actionRef);
1078
+ actionRef = router.parsePath(url.pathname + url.search);
1079
+ if (!actionRef.startsWith(actionBase)) return;
1080
+ }
1081
+ if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
1082
+ const handler = actions.get(actionRef);
1083
+ if (handler) {
1084
+ evt.preventDefault();
1085
+ const data = new FormData(evt.target);
1086
+ handler.call(router, data);
1087
+ }
1067
1088
  }
1068
- }
1069
1089
 
1070
- // ensure delegated event run first
1071
- delegateEvents(["click", "submit"]);
1072
- document.addEventListener("click", handleAnchorClick);
1073
- document.addEventListener("mouseover", handleAnchorIn);
1074
- document.addEventListener("mouseout", handleAnchorOut);
1075
- document.addEventListener("focusin", handleAnchorPreload);
1076
- document.addEventListener("touchstart", handleAnchorPreload);
1077
- document.addEventListener("submit", handleFormSubmit);
1078
- onCleanup(() => {
1079
- document.removeEventListener("click", handleAnchorClick);
1080
- document.removeEventListener("mouseover", handleAnchorIn);
1081
- document.removeEventListener("mouseout", handleAnchorOut);
1082
- document.removeEventListener("focusin", handleAnchorPreload);
1083
- document.removeEventListener("touchstart", handleAnchorPreload);
1084
- document.removeEventListener("submit", handleFormSubmit);
1085
- });
1090
+ // ensure delegated event run first
1091
+ delegateEvents(["click", "submit"]);
1092
+ document.addEventListener("click", handleAnchorClick);
1093
+ if (preload) {
1094
+ document.addEventListener("mouseover", handleAnchorIn);
1095
+ document.addEventListener("mouseout", handleAnchorOut);
1096
+ document.addEventListener("focusin", handleAnchorPreload);
1097
+ document.addEventListener("touchstart", handleAnchorPreload);
1098
+ }
1099
+ document.addEventListener("submit", handleFormSubmit);
1100
+ onCleanup(() => {
1101
+ document.removeEventListener("click", handleAnchorClick);
1102
+ if (preload) {
1103
+ document.removeEventListener("mouseover", handleAnchorIn);
1104
+ document.removeEventListener("mouseout", handleAnchorOut);
1105
+ document.removeEventListener("focusin", handleAnchorPreload);
1106
+ document.removeEventListener("touchstart", handleAnchorPreload);
1107
+ }
1108
+ document.removeEventListener("submit", handleFormSubmit);
1109
+ });
1110
+ };
1086
1111
  }
1087
1112
 
1088
1113
  function Router(props) {
@@ -1106,7 +1131,7 @@ function Router(props) {
1106
1131
  scrollToHash(window.location.hash.slice(1), scroll);
1107
1132
  },
1108
1133
  init: notify => bindEvent(window, "popstate", () => notify()),
1109
- create: setupNativeEvents,
1134
+ create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
1110
1135
  utils: {
1111
1136
  go: delta => window.history.go(delta)
1112
1137
  }
@@ -1143,7 +1168,7 @@ function HashRouter(props) {
1143
1168
  scrollToHash(hash, scroll);
1144
1169
  },
1145
1170
  init: notify => bindEvent(window, "hashchange", () => notify()),
1146
- create: setupNativeEvents,
1171
+ create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
1147
1172
  utils: {
1148
1173
  go: delta => window.history.go(delta),
1149
1174
  renderPath: path => `#${path}`,
@@ -1243,6 +1268,7 @@ function A(props) {
1243
1268
  ...rest.classList
1244
1269
  };
1245
1270
  },
1271
+ "link": "",
1246
1272
  get ["aria-current"]() {
1247
1273
  return isActive() ? "page" : undefined;
1248
1274
  }
@@ -1,5 +1,9 @@
1
1
  import type { JSX } from "solid-js";
2
2
  import type { BaseRouterProps } from "./components";
3
3
  export declare function hashParser(str: string): string;
4
- export type HashRouterProps = BaseRouterProps;
4
+ export type HashRouterProps = BaseRouterProps & {
5
+ actionBase?: string;
6
+ explicitLinks?: boolean;
7
+ preload?: boolean;
8
+ };
5
9
  export declare function HashRouter(props: HashRouterProps): JSX.Element;
@@ -26,7 +26,7 @@ export function HashRouter(props) {
26
26
  scrollToHash(hash, scroll);
27
27
  },
28
28
  init: notify => bindEvent(window, "hashchange", () => notify()),
29
- create: setupNativeEvents,
29
+ create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
30
30
  utils: {
31
31
  go: delta => window.history.go(delta),
32
32
  renderPath: path => `#${path}`,
@@ -2,5 +2,8 @@ import type { BaseRouterProps } from "./components";
2
2
  import type { JSX } from "solid-js";
3
3
  export type RouterProps = BaseRouterProps & {
4
4
  url?: string;
5
+ actionBase?: string;
6
+ explicitLinks?: boolean;
7
+ preload?: boolean;
5
8
  };
6
9
  export declare function Router(props: RouterProps): JSX.Element;
@@ -20,7 +20,7 @@ export function Router(props) {
20
20
  scrollToHash(window.location.hash.slice(1), scroll);
21
21
  },
22
22
  init: notify => bindEvent(window, "popstate", () => notify()),
23
- create: setupNativeEvents,
23
+ create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
24
24
  utils: {
25
25
  go: delta => window.history.go(delta)
26
26
  }
@@ -2,7 +2,6 @@ import type { Component, JSX } from "solid-js";
2
2
  import type { MatchFilters, RouteLoadFunc, RouterIntegration, RouteSectionProps } from "../types";
3
3
  export type BaseRouterProps = {
4
4
  base?: string;
5
- actionBase?: string;
6
5
  root?: Component<RouteSectionProps>;
7
6
  children?: JSX.Element;
8
7
  };
@@ -3,10 +3,10 @@ import { children, createMemo, createRoot, mergeProps, on, Show } from "solid-js
3
3
  import { createBranches, createRouteContext, createRouterContext, getRouteMatches, RouteContextObj, RouterContextObj } from "../routing";
4
4
  import { createMemoObject } from "../utils";
5
5
  export const createRouterComponent = (router) => (props) => {
6
- const { base, actionBase } = props;
6
+ const { base } = props;
7
7
  const routeDefs = children(() => props.children);
8
8
  const branches = createMemo(() => createBranches(props.root ? { component: props.root, children: routeDefs() } : routeDefs(), props.base || ""));
9
- const routerState = createRouterContext(router, branches, { base, actionBase });
9
+ const routerState = createRouterContext(router, branches, { base });
10
10
  router.create && router.create(routerState);
11
11
  return (<RouterContextObj.Provider value={routerState}>
12
12
  <Routes routerState={routerState} branches={branches()}/>
package/dist/routing.d.ts CHANGED
@@ -21,6 +21,5 @@ export declare function createLocation(path: Accessor<string>, state: Accessor<a
21
21
  export declare function getIntent(): Intent | undefined;
22
22
  export declare function createRouterContext(integration: RouterIntegration, getBranches?: () => Branch[], options?: {
23
23
  base?: string;
24
- actionBase?: string;
25
24
  }): RouterContext;
26
25
  export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch, params: Params): RouteContext;
package/dist/routing.js CHANGED
@@ -231,7 +231,6 @@ export function createRouterContext(integration, getBranches, options = {}) {
231
231
  });
232
232
  return {
233
233
  base: baseRoute,
234
- actionBase: options.actionBase || "/_server",
235
234
  location,
236
235
  isRouting,
237
236
  renderPath,
package/dist/types.d.ts CHANGED
@@ -4,6 +4,7 @@ declare module "solid-js/web" {
4
4
  response?: Response;
5
5
  routerCache?: Map<any, any>;
6
6
  initialSubmission?: Submission<any, any>;
7
+ serverOnly?: boolean;
7
8
  }
8
9
  }
9
10
  export type Params = Record<string, string>;
@@ -118,7 +119,6 @@ export interface RouterOutput {
118
119
  }
119
120
  export interface RouterContext {
120
121
  base: RouteContext;
121
- actionBase: string;
122
122
  location: Location;
123
123
  navigatorFactory: NavigatorFactory;
124
124
  isRouting: () => boolean;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.10.0-beta.7",
9
+ "version": "0.10.0-beta.9",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",