@solidjs/router 0.16.0 → 0.17.0-next.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.
Files changed (78) hide show
  1. package/README.md +85 -79
  2. package/dist/components.jsx +22 -16
  3. package/dist/data/action.d.ts +12 -10
  4. package/dist/data/action.js +98 -78
  5. package/dist/data/events.d.ts +8 -1
  6. package/dist/data/events.js +3 -3
  7. package/dist/data/index.d.ts +2 -3
  8. package/dist/data/index.js +2 -3
  9. package/dist/data/query.d.ts +1 -3
  10. package/dist/data/query.js +10 -16
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +212 -261
  13. package/dist/index.jsx +1 -1
  14. package/dist/lifecycle.js +1 -1
  15. package/dist/routers/HashRouter.js +1 -1
  16. package/dist/routers/MemoryRouter.js +1 -1
  17. package/dist/routers/Router.js +2 -2
  18. package/dist/routers/StaticRouter.js +1 -1
  19. package/dist/routers/components.d.ts +0 -4
  20. package/dist/routers/components.jsx +30 -19
  21. package/dist/routing.d.ts +4 -3
  22. package/dist/routing.js +71 -49
  23. package/dist/types.d.ts +3 -18
  24. package/dist/utils.d.ts +1 -0
  25. package/dist/utils.js +8 -0
  26. package/package.json +8 -6
  27. package/dist/data/createAsync.d.ts +0 -32
  28. package/dist/data/createAsync.js +0 -93
  29. package/dist/src/components.d.ts +0 -31
  30. package/dist/src/components.jsx +0 -39
  31. package/dist/src/data/action.d.ts +0 -17
  32. package/dist/src/data/action.js +0 -163
  33. package/dist/src/data/action.spec.d.ts +0 -1
  34. package/dist/src/data/action.spec.js +0 -297
  35. package/dist/src/data/createAsync.d.ts +0 -32
  36. package/dist/src/data/createAsync.js +0 -96
  37. package/dist/src/data/createAsync.spec.d.ts +0 -1
  38. package/dist/src/data/createAsync.spec.js +0 -196
  39. package/dist/src/data/events.d.ts +0 -9
  40. package/dist/src/data/events.js +0 -123
  41. package/dist/src/data/events.spec.d.ts +0 -1
  42. package/dist/src/data/events.spec.js +0 -567
  43. package/dist/src/data/index.d.ts +0 -4
  44. package/dist/src/data/index.js +0 -4
  45. package/dist/src/data/query.d.ts +0 -23
  46. package/dist/src/data/query.js +0 -232
  47. package/dist/src/data/query.spec.d.ts +0 -1
  48. package/dist/src/data/query.spec.js +0 -354
  49. package/dist/src/data/response.d.ts +0 -4
  50. package/dist/src/data/response.js +0 -42
  51. package/dist/src/data/response.spec.d.ts +0 -1
  52. package/dist/src/data/response.spec.js +0 -165
  53. package/dist/src/index.d.ts +0 -7
  54. package/dist/src/index.jsx +0 -6
  55. package/dist/src/lifecycle.d.ts +0 -5
  56. package/dist/src/lifecycle.js +0 -69
  57. package/dist/src/routers/HashRouter.d.ts +0 -9
  58. package/dist/src/routers/HashRouter.js +0 -41
  59. package/dist/src/routers/MemoryRouter.d.ts +0 -24
  60. package/dist/src/routers/MemoryRouter.js +0 -57
  61. package/dist/src/routers/Router.d.ts +0 -9
  62. package/dist/src/routers/Router.js +0 -45
  63. package/dist/src/routers/StaticRouter.d.ts +0 -6
  64. package/dist/src/routers/StaticRouter.js +0 -15
  65. package/dist/src/routers/components.d.ts +0 -27
  66. package/dist/src/routers/components.jsx +0 -118
  67. package/dist/src/routers/createRouter.d.ts +0 -10
  68. package/dist/src/routers/createRouter.js +0 -41
  69. package/dist/src/routers/index.d.ts +0 -11
  70. package/dist/src/routers/index.js +0 -6
  71. package/dist/src/routing.d.ts +0 -175
  72. package/dist/src/routing.js +0 -560
  73. package/dist/src/types.d.ts +0 -200
  74. package/dist/src/types.js +0 -1
  75. package/dist/src/utils.d.ts +0 -13
  76. package/dist/src/utils.js +0 -185
  77. package/dist/test/helpers.d.ts +0 -6
  78. package/dist/test/helpers.js +0 -50
package/README.md CHANGED
@@ -64,7 +64,7 @@ npm add @solidjs/router
64
64
  Install `@solidjs/router`, then start your application by rendering the router component
65
65
 
66
66
  ```jsx
67
- import { render } from "solid-js/web";
67
+ import { render } from "@solidjs/web";
68
68
  import { Router } from "@solidjs/router";
69
69
 
70
70
  render(() => <Router />, document.getElementById("app"));
@@ -79,7 +79,7 @@ Solid Router allows you to configure your routes using JSX:
79
79
  1. Add each route to a `<Router>` using the `Route` component, specifying a path and a component to render when the user navigates to that path.
80
80
 
81
81
  ```jsx
82
- import { render } from "solid-js/web";
82
+ import { render } from "@solidjs/web";
83
83
  import { Router, Route } from "@solidjs/router";
84
84
 
85
85
  import Home from "./pages/Home";
@@ -101,7 +101,7 @@ render(
101
101
  This will always be there and won't update on page change. It is the ideal place to put top level navigation and Context Providers
102
102
 
103
103
  ```jsx
104
- import { render } from "solid-js/web";
104
+ import { render } from "@solidjs/web";
105
105
  import { Router, Route } from "@solidjs/router";
106
106
 
107
107
  import Home from "./pages/Home";
@@ -130,7 +130,7 @@ render(
130
130
  We can create catch-all routes for pages not found at any nested level of the router. We use `*` and optionally the name of a parameter to retrieve the rest of the path.
131
131
 
132
132
  ```jsx
133
- import { render } from "solid-js/web";
133
+ import { render } from "@solidjs/web";
134
134
  import { Router, Route } from "@solidjs/router";
135
135
 
136
136
  import Home from "./pages/Home";
@@ -162,7 +162,7 @@ This way, the `Users` and `Home` components will only be loaded if you're naviga
162
162
 
163
163
  ```jsx
164
164
  import { lazy } from "solid-js";
165
- import { render } from "solid-js/web";
165
+ import { render } from "@solidjs/web";
166
166
  import { Router, Route } from "@solidjs/router";
167
167
 
168
168
  const Users = lazy(() => import("./pages/Users"));
@@ -192,7 +192,7 @@ Use an anchor tag that takes you to a route:
192
192
 
193
193
  ```jsx
194
194
  import { lazy } from "solid-js";
195
- import { render } from "solid-js/web";
195
+ import { render } from "@solidjs/web";
196
196
  import { Router, Route } from "@solidjs/router";
197
197
 
198
198
  const Users = lazy(() => import("./pages/Users"));
@@ -226,7 +226,7 @@ If you don't know the path ahead of time, you might want to treat part of the pa
226
226
 
227
227
  ```jsx
228
228
  import { lazy } from "solid-js";
229
- import { render } from "solid-js/web";
229
+ import { render } from "@solidjs/web";
230
230
  import { Router, Route } from "@solidjs/router";
231
231
 
232
232
  const Users = lazy(() => import("./pages/Users"));
@@ -265,7 +265,7 @@ This allows for more complex routing descriptions than just checking the presenc
265
265
 
266
266
  ```jsx
267
267
  import { lazy } from "solid-js";
268
- import { render } from "solid-js/web";
268
+ import { render } from "@solidjs/web";
269
269
  import { Router, Route } from "@solidjs/router";
270
270
  import type { MatchFilters } from "@solidjs/router";
271
271
 
@@ -498,9 +498,10 @@ Inside your page component you:
498
498
  ```jsx
499
499
  // pages/users/[id].js
500
500
  import { getUser } from ... // the query function
501
+ import { createMemo } from "solid-js";
501
502
 
502
503
  export default function User(props) {
503
- const user = createAsync(() => getUser(props.params.id));
504
+ const user = createMemo(() => getUser(props.params.id));
504
505
  return <h1>{user().name}</h1>;
505
506
  }
506
507
  ```
@@ -518,35 +519,24 @@ You can revalidate the query using the `revalidate` method or you can set `reval
518
519
 
519
520
  `query` can be defined anywhere and then used inside your components with:
520
521
 
521
- ### `createAsync`
522
+ ### Async reads in Solid 2
522
523
 
523
- This is light wrapper over `createResource` that aims to serve as stand-in for a future primitive we intend to bring to Solid core in 2.0. It is a simpler async primitive where the function tracks like `createMemo` and it expects a promise back that it turns into a Signal. Reading it before it is ready causes Suspense/Transitions to trigger.
524
+ On this Solid 2 branch, `query()` results are meant to be consumed directly with Solid primitives like `createMemo` and `createProjection`.
524
525
 
525
526
  ```jsx
526
- const user = createAsync((currentValue) => getUser(params.id));
527
+ const user = createMemo(() => getUser(params.id));
528
+ return <h1>{user().name}</h1>;
527
529
  ```
528
530
 
529
- It also preserves `latest` field from `createResource`. Note that it will be removed in the future.
531
+ For object-shaped data where you want a deeply reactive result, use `createProjection`.
530
532
 
531
533
  ```jsx
532
- const user = createAsync((currentValue) => getUser(params.id));
533
- return <h1>{user.latest.name}</h1>;
534
- ```
535
-
536
- Using `query` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
537
-
538
- ### `createAsyncStore`
539
-
540
- Similar to `createAsync` except it uses a deeply reactive store. Perfect for applying fine-grained changes to large model data that updates.
541
- It also supports `latest` field which will be removed in the future.
542
-
543
- ```jsx
544
- const todos = createAsyncStore(() => getTodos());
534
+ const todos = createProjection(() => getTodos(), []);
545
535
  ```
546
536
 
547
537
  ### `action`
548
538
 
549
- Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response helpers can be found below.
539
+ Router `action()` is the router-aware mutation wrapper for Solid 2. It keeps form submission, redirects, and invalidation wired into the router while letting you compose optimistic UI with Solid's built-in primitives.
550
540
 
551
541
  ```jsx
552
542
  import { action, revalidate, redirect } from "@solidjs/router"
@@ -566,6 +556,29 @@ const myAction = action(async (data) => {
566
556
 
567
557
  Actions only work with post requests, so make sure to put `method="post"` on your form.
568
558
 
559
+ For optimistic updates, use Solid's optimistic primitives for the rendered state and attach owner-scoped submit hooks to the router action:
560
+
561
+ ```jsx
562
+ import { createOptimisticStore } from "solid-js";
563
+ import { action, query } from "@solidjs/router";
564
+
565
+ const getTodos = query(async () => fetchTodos(), "todos");
566
+ const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
567
+
568
+ const addTodo = action(async (todo) => {
569
+ await saveTodo(todo);
570
+ return { ok: true, todo };
571
+ }, "add-todo").onSubmit(todo => {
572
+ setTodos(items => {
573
+ items.push({ ...todo, pending: true });
574
+ });
575
+ });
576
+ ```
577
+
578
+ `myAction.onSubmit(...)` registers a listener for that action in the current reactive owner. Multiple components can register hooks against the same action, and those hooks are automatically removed when their owner is disposed. `myAction.onSettled(...)` works the same way for observing completed submissions.
579
+
580
+ The preferred pattern is for actions to return values and let the client interpret the result. Throwing errors is still supported, but `Submission.error` is mainly an escape hatch for that legacy style.
581
+
569
582
  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.
570
583
 
571
584
  Picture an action that deletes Todo Item:
@@ -592,7 +605,7 @@ const deleteTodo = action(api.deleteTodo)
592
605
  </form>
593
606
  ```
594
607
 
595
- Actions also take a second argument which can be the name or an option object with `name` and `onComplete`. `name` is used to identify SSR actions that aren't server functions (see note below). `onComplete` allows you to configure behavior when `action`s complete. Keep in mind `onComplete` does not work when JavaScript is disabled.
608
+ Actions also take a second argument which can be the name or an option object with `name`. `name` is used to identify SSR actions that aren't server functions (see note below).
596
609
 
597
610
  #### Notes on `<form>` implementation and SSR
598
611
 
@@ -614,24 +627,26 @@ submit(...args);
614
627
 
615
628
  The outside of a form context you can use custom data instead of formData, and these helpers preserve types. However, even when used with server functions (in projects like SolidStart) this requires client side javascript and is not Progressive Enhanceable like forms are.
616
629
 
617
- ### `useSubmission`/`useSubmissions`
630
+ ### `useSubmissions`
618
631
 
619
- Are used to injecting the optimistic updates while actions are in flight. They either return a single Submission(latest) or all that match with an optional filter function.
632
+ This returns settled submission records for an action. It is useful for reading completed results, clearing old submissions, retrying a prior submission, or replaying settled errors. It is not the optimistic state layer.
620
633
 
621
634
  ```jsx
622
635
  type Submission<T, U> = {
623
636
  readonly input: T;
624
637
  readonly result?: U;
625
- readonly pending: boolean;
638
+ readonly error: any;
626
639
  readonly url: string;
627
640
  clear: () => void;
628
641
  retry: () => void;
629
642
  };
630
643
 
631
644
  const submissions = useSubmissions(action, (input) => filter(input));
632
- const submission = useSubmission(action, (input) => filter(input));
645
+ const latestSubmission = submissions.at(-1);
633
646
  ```
634
647
 
648
+ Use Solid's `createOptimistic` or `createOptimisticStore` for in-flight UI, and use submissions as the durable settled record layer.
649
+
635
650
  ### Response Helpers
636
651
 
637
652
  These are used to communicate router navigations from query/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.
@@ -670,7 +685,7 @@ You don't have to use JSX to set up your routes; you can pass an array of route
670
685
 
671
686
  ```jsx
672
687
  import { lazy } from "solid-js";
673
- import { render } from "solid-js/web";
688
+ import { render } from "@solidjs/web";
674
689
  import { Router } from "@solidjs/router";
675
690
 
676
691
  const routes = [
@@ -713,7 +728,7 @@ Also you can pass a single route definition object for a single route:
713
728
 
714
729
  ```jsx
715
730
  import { lazy } from "solid-js";
716
- import { render } from "solid-js/web";
731
+ import { render } from "@solidjs/web";
717
732
  import { Router } from "@solidjs/router";
718
733
 
719
734
  const route = {
@@ -751,7 +766,7 @@ import { MemoryRouter } from "@solidjs/router";
751
766
  For SSR you can use the static router directly or the browser Router defaults to it on the server, just pass in the url.
752
767
 
753
768
  ```jsx
754
- import { isServer } from "solid-js/web";
769
+ import { isServer } from "@solidjs/web";
755
770
  import { Router } from "@solidjs/router";
756
771
 
757
772
  <Router url={isServer ? req.url : ""} />;
@@ -884,13 +899,13 @@ return (
884
899
 
885
900
  ### useIsRouting
886
901
 
887
- Retrieves signal that indicates whether the route is currently in a Transition. Useful for showing stale/pending state when the route resolution is Suspended during concurrent rendering.
902
+ Retrieves a signal that indicates whether the router is currently processing a navigation. Useful for showing pending navigation state while the next route and its data settle.
888
903
 
889
904
  ```js
890
905
  const isRouting = useIsRouting();
891
906
 
892
907
  return (
893
- <div classList={{ "grey-out": isRouting() }}>
908
+ <div class={{ "grey-out": isRouting() }}>
894
909
  <MyAwesomeContent />
895
910
  </div>
896
911
  );
@@ -903,7 +918,7 @@ return (
903
918
  ```js
904
919
  const match = useMatch(() => props.href);
905
920
 
906
- return <div classList={{ active: Boolean(match()) }} />;
921
+ return <div class={{ active: Boolean(match()) }} />;
907
922
  ```
908
923
 
909
924
  ### useCurrentMatches
@@ -958,60 +973,51 @@ useBeforeLeave((e: BeforeLeaveEventArgs) => {
958
973
  });
959
974
  ```
960
975
 
961
- ## Migrations from 0.9.x
962
-
963
- 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.
964
-
965
- The biggest changes are around removed APIs that need to be replaced.
966
-
967
- ### `<Outlet>`, `<Routes>`, `useRoutes`
968
-
969
- This is no longer used and instead will use `props.children` passed from into the page components for outlets. This keeps the outlet directly passed from its page and avoids oddness of trying to use context across Islands boundaries. Nested `<Routes>` components inherently cause waterfalls and are `<Outlets>` themselves so they have the same concerns.
970
-
971
- Keep in mind no `<Routes>` means the `<Router>` API is different. The `<Router>` acts as the `<Routes>` component and its children can only be `<Route>` components. Your top-level layout should go in the root prop of the router [as shown above](#configure-your-routes)
976
+ ## Migration from 0.16.x
972
977
 
973
- ## `element` prop removed from `Route`
978
+ This branch is the Solid 2 migration. Most route configuration stays the same, but the data APIs and recommended async patterns have changed.
974
979
 
975
- Related without Outlet component it has to be passed in manually. At which point the `element` prop has less value. Removing the second way to define route components to reduce confusion and edge cases.
980
+ ### Async reads move to Solid 2 primitives
976
981
 
977
- ### `data` functions & `useRouteData`
982
+ `createAsync` and `createAsyncStore` are gone. Read query results with Solid 2 primitives like `createMemo`, `createProjection`, `createOptimistic`, and `createOptimisticStore`.
978
983
 
979
- These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/query 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.
984
+ ```jsx
985
+ const user = createMemo(() => getUser(params.id));
986
+ const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
987
+ ```
980
988
 
981
- That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context:
989
+ ### `query()` stays the source of truth
982
990
 
983
- ```js
984
- import { lazy } from "solid-js";
985
- import { Route } from "@solidjs/router";
991
+ Continue using `query()` for cached reads and invalidation, but consume the results directly through Solid 2's async primitives instead of router-specific wrappers.
986
992
 
987
- const User = lazy(() => import("./pages/users/[id].js"));
993
+ ### `action()` lifecycle hooks changed
988
994
 
989
- // preload function
990
- function preloadUser({ params, location }) {
991
- const [user] = createResource(() => params.id, fetchUser);
992
- return user;
993
- }
995
+ The action API is now centered around instance methods:
994
996
 
995
- // Pass it in the route definition
996
- <Router preload={false}>
997
- <Route path="/users/:id" component={User} preload={preloadUser} />
998
- </Router>;
997
+ ```jsx
998
+ const saveTodo = action(async (todo) => {
999
+ await api.saveTodo(todo);
1000
+ return { ok: true, todo };
1001
+ }, "save-todo")
1002
+ .onSubmit(todo => {
1003
+ // optimistic write
1004
+ })
1005
+ .onSettled(submission => {
1006
+ // observe settled result or retry state
1007
+ });
999
1008
  ```
1000
1009
 
1001
- And then in your component taking the page props and putting them in a Context.
1010
+ - Use `onSubmit(...)` for owner-scoped optimistic/pre-submit work.
1011
+ - Use `onSettled(...)` for owner-scoped observation of completed submissions.
1012
+ - Use returned values for expected application-level results. Thrown errors are still captured on `Submission.error` when something fails unexpectedly.
1002
1013
 
1003
- ```js
1004
- function User(props) {
1005
- <UserContext.Provider value={props.data}>
1006
- {/* my component content */}
1007
- </UserContext.Provider>;
1008
- }
1014
+ ### `useSubmissions()` is the submission API
1009
1015
 
1010
- // Somewhere else
1011
- function UserDetails() {
1012
- const user = useContext(UserContext);
1013
- // render stuff
1014
- }
1016
+ Submissions are now settled history, not in-flight mutation state. Read them through `useSubmissions()` and select the latest entry with `at(-1)` when needed.
1017
+
1018
+ ```jsx
1019
+ const submissions = useSubmissions(saveTodo);
1020
+ const latestSubmission = submissions.at(-1);
1015
1021
  ```
1016
1022
 
1017
1023
  ## SPAs in Deployed Environments
@@ -1,16 +1,24 @@
1
- import { createMemo, mergeProps, splitProps } from "solid-js";
1
+ import { createMemo, merge, omit } from "solid-js";
2
2
  import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing.js";
3
3
  import { normalizePath } from "./utils.js";
4
+ function toClassName(value) {
5
+ if (!value)
6
+ return "";
7
+ if (typeof value === "string" || typeof value === "number")
8
+ return String(value);
9
+ if (Array.isArray(value))
10
+ return value.map(toClassName).filter(Boolean).join(" ");
11
+ if (typeof value === "object") {
12
+ return Object.entries(value)
13
+ .filter(([, enabled]) => enabled)
14
+ .map(([name]) => name)
15
+ .join(" ");
16
+ }
17
+ return "";
18
+ }
4
19
  export function A(props) {
5
- props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
6
- const [, rest] = splitProps(props, [
7
- "href",
8
- "state",
9
- "class",
10
- "activeClass",
11
- "inactiveClass",
12
- "end"
13
- ]);
20
+ props = merge({ inactiveClass: "inactive", activeClass: "active" }, props);
21
+ const rest = omit(props, "href", "state", "class", "activeClass", "inactiveClass", "end");
14
22
  const to = useResolvedPath(() => props.href);
15
23
  const href = useHref(to);
16
24
  const location = useLocation();
@@ -22,12 +30,10 @@ export function A(props) {
22
30
  const loc = decodeURI(normalizePath(location.pathname).toLowerCase());
23
31
  return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
24
32
  });
25
- return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{
26
- ...(props.class && { [props.class]: true }),
27
- [props.inactiveClass]: !isActive()[0],
28
- [props.activeClass]: isActive()[0],
29
- ...rest.classList
30
- }} link aria-current={isActive()[1] ? "page" : undefined}/>);
33
+ const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass]
34
+ .filter(Boolean)
35
+ .join(" "));
36
+ return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} class={className()} link aria-current={isActive()[1] ? "page" : undefined}/>);
31
37
  }
32
38
  export function Navigate(props) {
33
39
  const navigate = useNavigate();
@@ -1,17 +1,19 @@
1
1
  import { JSX } from "solid-js";
2
- import type { Submission, SubmissionStub, NarrowResponse } from "../types.js";
2
+ import type { Submission, NarrowResponse } from "../types.js";
3
3
  export type Action<T extends Array<any>, U, V = T> = (T extends [FormData | URLSearchParams] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
4
4
  url: string;
5
5
  with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U, V>;
6
+ onSubmit(hook: (...args: V extends Array<any> ? V : T) => void): Action<T, U, V>;
7
+ onSettled(hook: (submission: Submission<V extends Array<any> ? V : T, NarrowResponse<U>>) => void): Action<T, U, V>;
6
8
  };
7
- export declare const actions: Map<string, Action<any, any, any>>;
8
- export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>>[] & {
9
- pending: boolean;
9
+ type ActionFactory = {
10
+ <T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
11
+ <T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: {
12
+ name?: string;
13
+ }): Action<T, U>;
10
14
  };
11
- export declare function useSubmission<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>> | SubmissionStub;
15
+ export declare const actions: Map<string, Action<any, any, any>>;
16
+ export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<V, NarrowResponse<U>>[];
12
17
  export declare function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>): (...args: Parameters<Action<T, U, V>>) => Promise<NarrowResponse<U>>;
13
- export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
14
- export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: {
15
- name?: string;
16
- onComplete?: (s: Submission<T, U>) => void;
17
- }): Action<T, U>;
18
+ export declare const action: ActionFactory;
19
+ export {};
@@ -1,8 +1,11 @@
1
- import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
2
- import { isServer } from "solid-js/web";
1
+ import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
2
+ import { isServer } from "@solidjs/web";
3
3
  import { useRouter } from "../routing.js";
4
- import { mockBase } from "../utils.js";
4
+ import { mockBase, setFunctionName } from "../utils.js";
5
5
  import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
6
+ const submitHooksSymbol = Symbol("routerActionSubmitHooks");
7
+ const settledHooksSymbol = Symbol("routerActionSettledHooks");
8
+ const invokeSymbol = Symbol("routerActionInvoke");
6
9
  export const actions = /* #__PURE__ */ new Map();
7
10
  export function useSubmissions(fn, filter) {
8
11
  const router = useRouter();
@@ -11,8 +14,6 @@ export function useSubmissions(fn, filter) {
11
14
  get(_, property) {
12
15
  if (property === $TRACK)
13
16
  return subs();
14
- if (property === "pending")
15
- return subs().some(sub => !sub.result);
16
17
  return subs()[property];
17
18
  },
18
19
  has(_, property) {
@@ -20,101 +21,105 @@ export function useSubmissions(fn, filter) {
20
21
  }
21
22
  });
22
23
  }
23
- export function useSubmission(fn, filter) {
24
- const submissions = useSubmissions(fn, filter);
25
- return new Proxy({}, {
26
- get(_, property) {
27
- if ((submissions.length === 0 && property === "clear") || property === "retry")
28
- return () => { };
29
- return submissions[submissions.length - 1]?.[property];
30
- }
31
- });
32
- }
33
24
  export function useAction(action) {
34
25
  const r = useRouter();
35
26
  return (...args) => action.apply({ r }, args);
36
27
  }
37
- export function action(fn, options = {}) {
38
- function mutate(...variables) {
28
+ function actionImpl(fn, options = {}) {
29
+ async function invoke(variables, current) {
39
30
  const router = this.r;
40
31
  const form = this.f;
41
- const p = (router.singleFlight && fn.withOptions
32
+ const submitHooks = current[submitHooksSymbol];
33
+ const settledHooks = current[settledHooksSymbol];
34
+ const runMutation = () => (router.singleFlight && fn.withOptions
42
35
  ? fn.withOptions({ headers: { "X-Single-Flight": "true" } })
43
36
  : fn)(...variables);
44
- const [result, setResult] = createSignal();
45
- let submission;
46
- function handler(error) {
47
- return async (res) => {
48
- const result = await handleResponse(res, error, router.navigatorFactory());
49
- let retry = null;
50
- o.onComplete?.({
51
- ...submission,
52
- result: result?.data,
53
- error: result?.error,
54
- pending: false,
55
- retry() {
56
- return retry = submission.retry();
57
- }
58
- });
59
- if (retry)
60
- return retry;
61
- if (!result)
62
- return submission.clear();
63
- setResult(result);
64
- if (result.error && !form)
65
- throw result.error;
66
- return result.data;
67
- };
68
- }
69
- router.submissions[1](s => [
70
- ...s,
71
- (submission = {
72
- input: variables,
73
- url,
74
- get result() {
75
- return result()?.data;
76
- },
77
- get error() {
78
- return result()?.error;
79
- },
80
- get pending() {
81
- return !result();
82
- },
83
- clear() {
84
- router.submissions[1](v => v.filter(i => i !== submission));
85
- },
86
- retry() {
87
- setResult(undefined);
88
- const p = fn(...variables);
89
- return p.then(handler(), handler(true));
37
+ const run = createSolidAction(async function* (context) {
38
+ context.optimistic?.();
39
+ try {
40
+ const value = await context.call();
41
+ yield;
42
+ return { error: false, value };
43
+ }
44
+ catch (error) {
45
+ yield;
46
+ return { error: true, value: error };
47
+ }
48
+ });
49
+ const settled = await settleActionResult(run({
50
+ call: runMutation,
51
+ optimistic: submitHooks.size
52
+ ? () => {
53
+ for (const hook of submitHooks.values())
54
+ hook(...variables);
90
55
  }
91
- })
92
- ]);
93
- return p.then(handler(), handler(true));
56
+ : undefined
57
+ }));
58
+ const response = await handleResponse(settled.value, settled.error, router.navigatorFactory());
59
+ if (!response)
60
+ return undefined;
61
+ let submission;
62
+ submission = {
63
+ input: variables,
64
+ url,
65
+ result: response.data,
66
+ error: response.error,
67
+ clear() {
68
+ router.submissions[1](entries => entries.filter(entry => entry !== submission));
69
+ },
70
+ retry() {
71
+ submission.clear();
72
+ return current[invokeSymbol].call({ r: router, f: form }, variables, current);
73
+ }
74
+ };
75
+ router.submissions[1](entries => [...entries, submission]);
76
+ for (const hook of settledHooks.values())
77
+ hook(submission);
78
+ if (response.error && !form)
79
+ throw response.error;
80
+ return response.data;
94
81
  }
95
82
  const o = typeof options === "string" ? { name: options } : options;
96
- const url = fn.url ||
97
- (o.name && `https://action/${o.name}`) ||
98
- (!isServer ? `https://action/${hashString(fn.toString())}` : "");
99
- mutate.base = url;
100
- return toAction(mutate, url);
83
+ const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
84
+ const url = fn.url || (name && `https://action/${name}`) || "";
85
+ const wrapped = toAction(invoke, url);
86
+ if (name)
87
+ setFunctionName(wrapped, name);
88
+ return wrapped;
101
89
  }
102
- function toAction(fn, url) {
90
+ export const action = actionImpl;
91
+ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) {
92
+ const fn = function (...args) {
93
+ return invoke.call(this, [...boundArgs, ...args], fn);
94
+ };
103
95
  fn.toString = () => {
104
96
  if (!url)
105
97
  throw new Error("Client Actions need explicit names if server rendered");
106
98
  return url;
107
99
  };
108
100
  fn.with = function (...args) {
109
- const newFn = function (...passedArgs) {
110
- return fn.call(this, ...args, ...passedArgs);
111
- };
112
- newFn.base = fn.base;
113
101
  const uri = new URL(url, mockBase);
114
102
  uri.searchParams.set("args", hashKey(args));
115
- return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
103
+ const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
104
+ return next;
105
+ };
106
+ fn.onSubmit = function (hook) {
107
+ const id = Symbol("actionOnSubmitHook");
108
+ submitHooks.set(id, hook);
109
+ getOwner() && onCleanup(() => submitHooks.delete(id));
110
+ return this;
111
+ };
112
+ fn.onSettled = function (hook) {
113
+ const id = Symbol("actionOnSettledHook");
114
+ settledHooks.set(id, hook);
115
+ getOwner() && onCleanup(() => settledHooks.delete(id));
116
+ return this;
116
117
  };
117
118
  fn.url = url;
119
+ fn.base = base;
120
+ fn[submitHooksSymbol] = submitHooks;
121
+ fn[settledHooksSymbol] = settledHooks;
122
+ fn[invokeSymbol] = invoke;
118
123
  if (!isServer) {
119
124
  actions.set(url, fn);
120
125
  getOwner() && onCleanup(() => actions.delete(url));
@@ -122,6 +127,21 @@ function toAction(fn, url) {
122
127
  return fn;
123
128
  }
124
129
  const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
130
+ async function settleActionResult(result) {
131
+ const value = result;
132
+ if (value && typeof value.then === "function") {
133
+ return result.then(value => value);
134
+ }
135
+ if (value && typeof value.next === "function") {
136
+ const iterator = value;
137
+ let next = await iterator.next();
138
+ while (!next.done) {
139
+ next = await iterator.next();
140
+ }
141
+ return next.value;
142
+ }
143
+ return result;
144
+ }
125
145
  async function handleResponse(response, error, navigate) {
126
146
  let data;
127
147
  let custom;
@@ -1,2 +1,9 @@
1
1
  import type { RouterContext } from "../types.js";
2
- export declare function setupNativeEvents(preload?: boolean, explicitLinks?: boolean, actionBase?: string, transformUrl?: (url: string) => string): (router: RouterContext) => void;
2
+ type NativeEventConfig = {
3
+ preload?: boolean;
4
+ explicitLinks?: boolean;
5
+ actionBase?: string;
6
+ transformUrl?: (url: string) => string;
7
+ };
8
+ export declare function setupNativeEvents({ preload, explicitLinks, actionBase, transformUrl }?: NativeEventConfig): (router: RouterContext) => void;
9
+ export {};