@nifrajs/web-solid 5.5.1 → 5.7.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 (59) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/client.js +2 -2
  3. package/dist/client.js.map +1 -1
  4. package/dist/content.d.ts +1 -1
  5. package/dist/content.js +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +42 -3
  8. package/dist/index.js.map +1 -1
  9. package/dist/keep-alive-view.d.ts +35 -0
  10. package/dist/keep-alive-view.d.ts.map +1 -0
  11. package/dist/keep-alive-view.js +135 -0
  12. package/dist/keep-alive-view.js.map +1 -0
  13. package/dist/keep-alive.d.ts +25 -0
  14. package/dist/keep-alive.d.ts.map +1 -0
  15. package/dist/keep-alive.js +51 -0
  16. package/dist/keep-alive.js.map +1 -0
  17. package/dist/mdx.d.ts.map +1 -1
  18. package/dist/mdx.js +5 -16
  19. package/dist/mdx.js.map +1 -1
  20. package/dist/page-visibility.d.ts +28 -0
  21. package/dist/page-visibility.d.ts.map +1 -0
  22. package/dist/page-visibility.js +42 -0
  23. package/dist/page-visibility.js.map +1 -0
  24. package/dist/ppr-client.d.ts +9 -0
  25. package/dist/ppr-client.d.ts.map +1 -0
  26. package/dist/ppr-client.js +63 -0
  27. package/dist/ppr-client.js.map +1 -0
  28. package/dist/ppr.d.ts +37 -0
  29. package/dist/ppr.d.ts.map +1 -0
  30. package/dist/ppr.js +61 -0
  31. package/dist/ppr.js.map +1 -0
  32. package/dist/query.d.ts +4 -3
  33. package/dist/query.d.ts.map +1 -1
  34. package/dist/query.js +7 -8
  35. package/dist/query.js.map +1 -1
  36. package/dist/route-props.d.ts +5 -2
  37. package/dist/route-props.d.ts.map +1 -1
  38. package/dist/route-props.js +16 -7
  39. package/dist/route-props.js.map +1 -1
  40. package/dist/router.d.ts +3 -2
  41. package/dist/router.d.ts.map +1 -1
  42. package/dist/router.js +7 -3
  43. package/dist/router.js.map +1 -1
  44. package/dist/server-components/Outlet.jsx +1 -1
  45. package/dist/server-components/Outlet.jsx.map +1 -1
  46. package/package.json +25 -7
  47. package/src/client.ts +2 -2
  48. package/src/content.ts +1 -1
  49. package/src/index.ts +50 -5
  50. package/src/keep-alive-view.ts +202 -0
  51. package/src/keep-alive.ts +76 -0
  52. package/src/mdx.ts +5 -9
  53. package/src/page-visibility.ts +53 -0
  54. package/src/ppr-client.ts +77 -0
  55. package/src/ppr.ts +86 -0
  56. package/src/query.ts +13 -8
  57. package/src/route-props.ts +16 -6
  58. package/src/router.ts +8 -3
  59. package/src/server-components/Outlet.tsx +1 -1
package/src/query.ts CHANGED
@@ -1,8 +1,10 @@
1
- import type { QueryClient, QueryState } from "@nifrajs/web"
1
+ import type { QueryClient, QueryOptions, QueryState } from "@nifrajs/web"
2
2
  import {
3
3
  createNoClientRefetch,
4
+ fetchOnMount,
4
5
  getQueryClientSingleton,
5
6
  IDLE_QUERY_STATE,
7
+ noop,
6
8
  } from "@nifrajs/web/internal/query-runtime"
7
9
  /**
8
10
  * `@nifrajs/web-solid/query` - Solid bindings for the keyed query-cache. `createQuery(key, fn)` bridges a
@@ -18,7 +20,7 @@ import { type Accessor, createSignal, onCleanup, onMount } from "solid-js"
18
20
 
19
21
  /** Access the query client to imperatively `invalidateQueries(keyOrPrefix)` (e.g. after a mutation). */
20
22
  export function useQueryClient(): Pick<QueryClient, "invalidateQueries"> {
21
- return getQueryClientSingleton() ?? { invalidateQueries: () => {} }
23
+ return getQueryClientSingleton() ?? { invalidateQueries: noop }
22
24
  }
23
25
 
24
26
  /** A query's reactive state accessor plus `refetch`. */
@@ -32,18 +34,21 @@ export interface CreateQueryResult<T> {
32
34
  /**
33
35
  * Bind the keyed query for `key`, fetched via `fn`. Returns a reactive `state()` accessor + `refetch`.
34
36
  * Concurrent `createQuery`s with the same key share one cache entry + one in-flight fetch (dedup).
35
- * Fetches on mount; SSR-idle. Call inside a component (owns the subscription).
37
+ * Fetches on mount; SSR-idle. Call inside a component (owns the subscription). `options.live` names
38
+ * the live keys that refetch it.
36
39
  */
37
- export function createQuery<T>(key: unknown, fn: () => Promise<T>): CreateQueryResult<T> {
38
- const handle = getQueryClientSingleton()?.query<T>(key, fn)
40
+ export function createQuery<T>(
41
+ key: unknown,
42
+ fn: () => Promise<T>,
43
+ options?: QueryOptions,
44
+ ): CreateQueryResult<T> {
45
+ const handle = getQueryClientSingleton()?.query<T>(key, fn, options)
39
46
  const [state, setState] = createSignal<QueryState<T>>(
40
47
  handle ? handle.snapshot() : IDLE_QUERY_STATE,
41
48
  )
42
49
  if (handle !== undefined) {
43
50
  onCleanup(handle.subscribe(() => setState(() => handle.snapshot())))
44
- onMount(() => {
45
- handle.fetch().catch(() => {})
46
- })
51
+ onMount(fetchOnMount(handle))
47
52
  return { state, refetch: handle.refetch }
48
53
  }
49
54
  return {
@@ -5,16 +5,22 @@ import { searchOfChain } from "@nifrajs/web/client"
5
5
 
6
6
  /**
7
7
  * The client half of the server's `RenderProps`, as getters so a settle re-renders only what reads the
8
- * changed field. Cast: a getter always exists where `exactOptionalPropertyTypes` expects absence.
8
+ * changed field. `derive` is `createMemo` on the client mount: `data` and `layoutData` then notify only
9
+ * when they change identity, so a reload that kept them (the router shares what it did not change)
10
+ * re-runs nothing that reads them - a `<For>` keeps its rows. Cast: a getter always exists where
11
+ * `exactOptionalPropertyTypes` expects absence.
9
12
  */
10
13
  export function routeProps(
11
14
  snapshot: () => RouterState,
12
15
  searchSchemas: MountRouterOptions["searchSchemas"],
13
16
  matchChains?: MountRouterOptions["matchChains"],
17
+ derive: <T>(read: () => T) => () => T = (read) => read,
14
18
  ): RenderProps {
19
+ const data = derive(() => snapshot().data)
20
+ const layoutData = derive(() => snapshot().layoutData)
15
21
  return {
16
22
  get data() {
17
- return snapshot().data
23
+ return data()
18
24
  },
19
25
  // Each layout's own loader data. Without it a layout renders `data: null` on the client while the
20
26
  // server rendered it with its data.
@@ -22,7 +28,7 @@ export function routeProps(
22
28
  return snapshot().serverComponents
23
29
  },
24
30
  get layoutData() {
25
- return snapshot().layoutData
31
+ return layoutData()
26
32
  },
27
33
  get actionData() {
28
34
  return snapshot().actionData
@@ -34,15 +40,19 @@ export function routeProps(
34
40
  get params() {
35
41
  return snapshot().params
36
42
  },
43
+ // While an intercept boundary holds a target the URL is the target's; the route's props describe
44
+ // the page underneath, so they read the page's own path.
37
45
  get path() {
38
- return snapshot().path
46
+ const s = snapshot()
47
+ return s.intercepted?.pagePath ?? s.path
39
48
  },
40
49
  get search() {
41
50
  // The SAME searchOfChain the server ran, over this snapshot's URL - a same-route search change
42
51
  // updates in place (fine-grained), matching the SSR value on hydration.
43
52
  const s = snapshot()
44
- const idx = s.path.indexOf("?")
45
- return searchOfChain(searchSchemas?.[s.routeId] ?? [], idx === -1 ? "" : s.path.slice(idx))
53
+ const path = s.intercepted?.pagePath ?? s.path
54
+ const idx = path.indexOf("?")
55
+ return searchOfChain(searchSchemas?.[s.routeId] ?? [], idx === -1 ? "" : path.slice(idx))
46
56
  },
47
57
  get submission() {
48
58
  return snapshot().submission
package/src/router.ts CHANGED
@@ -38,7 +38,9 @@ import {
38
38
  onCleanup,
39
39
  useContext,
40
40
  } from "solid-js"
41
+ import { PageContext } from "./page-visibility.ts"
41
42
 
43
+ export { onPageHide, onPageShow, usePageVisibility } from "./page-visibility.ts"
42
44
  export type { Blocker, BlockerFunction, BlockerState, NavigateFunction, UIMatch }
43
45
 
44
46
  // Frozen empty search + a stable accessor for a `useSearch` used outside a nifra route tree.
@@ -126,17 +128,20 @@ export function useNavigate(): NavigateFunction {
126
128
  * predicate, and get back a reactive {@link Blocker} accessor. When a navigation (an anchor click,
127
129
  * `useNavigate`, or a browser back/forward) is intercepted, `blocker().state` becomes `"blocked"` and
128
130
  * `proceed`/`reset` go live - show a dialog and call `proceed()` to continue or `reset()` to stay put.
129
- * It also arms the browser's native "Leave site?" prompt on tab close / reload. Idle on the server and
130
- * before hydration.
131
+ * It also arms the browser's native "Leave site?" prompt on tab close / reload. Idle on the server,
132
+ * before hydration, and while a `keepAlive` page is hidden.
131
133
  *
132
134
  * The component body runs once, so to track a CHANGING flag pass a function -
133
135
  * `useBlocker(() => isDirty())` - not a bare signal accessor's current value. A constant boolean is fine.
134
136
  */
135
137
  export function useBlocker(shouldBlock: boolean | BlockerFunction): Accessor<Blocker> {
136
138
  const [blocker, setBlocker] = createSignal<Blocker>(IDLE_BLOCKER)
139
+ const slot = useContext(PageContext)
137
140
  onCleanup(
138
141
  registerBlocker(
139
- (args) => (typeof shouldBlock === "function" ? shouldBlock(args) : shouldBlock),
142
+ (args) =>
143
+ slot?.hidden() !== true &&
144
+ (typeof shouldBlock === "function" ? shouldBlock(args) : shouldBlock),
140
145
  setBlocker,
141
146
  ),
142
147
  )
@@ -61,7 +61,7 @@ function Range(props: {
61
61
  if (controller !== undefined)
62
62
  void (props.controller === undefined
63
63
  ? controller.dispose()
64
- : controller.unmount(record.instance))
64
+ : controller.unmount(record.instance, host))
65
65
  })
66
66
  createEffect(() => {
67
67
  const state = props.state