@llui/vike 0.0.13 → 0.0.15
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 +48 -6
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/on-render-client.d.ts +93 -4
- package/dist/on-render-client.d.ts.map +1 -1
- package/dist/on-render-client.js +71 -7
- package/dist/on-render-client.js.map +1 -1
- package/dist/on-render-html.js.map +1 -1
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -57,6 +57,47 @@ export const onRenderClient = createOnRenderClient({
|
|
|
57
57
|
})
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
### Page Transitions
|
|
61
|
+
|
|
62
|
+
`createOnRenderClient` accepts `onLeave` and `onEnter` hooks that fire around the dispose-and-remount cycle on client navigation. `onLeave` is awaited — return a promise to defer the swap until a leave animation finishes:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createOnRenderClient, fromTransition } from '@llui/vike/client'
|
|
66
|
+
import { routeTransition } from '@llui/transitions'
|
|
67
|
+
|
|
68
|
+
export const onRenderClient = createOnRenderClient({
|
|
69
|
+
...fromTransition(routeTransition({ duration: 200 })),
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`fromTransition` adapts any `TransitionOptions` (the shape returned by `routeTransition`, `fade`, `slide`, etc.) into the hook pair. The transition operates on the container element — its opacity / transform fades out the outgoing page, then the new page fades in after mount.
|
|
74
|
+
|
|
75
|
+
For raw animations without `@llui/transitions`, write the hooks yourself:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
export const onRenderClient = createOnRenderClient({
|
|
79
|
+
onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,
|
|
80
|
+
onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Client Navigation Lifecycle
|
|
85
|
+
|
|
86
|
+
When Vike fires a client-side navigation, `@llui/vike` runs this sequence inside `onRenderClient`:
|
|
87
|
+
|
|
88
|
+
1. **`onLeave(el)`** — awaited. The outgoing page's DOM is still mounted; this is the only moment where a leave animation can read/write it.
|
|
89
|
+
2. **`currentHandle.dispose()`** — tears down the outgoing component's scope tree. All `onMount` cleanups run here, portals are removed from their targets, focus traps are popped, body scroll locks release, sibling `aria-hidden` is restored. The regression test in `@llui/components/test/components/dialog-dispose.test.ts` covers this path explicitly.
|
|
90
|
+
3. **`el.textContent = ''`** — the outgoing DOM is cleared from the container.
|
|
91
|
+
4. **`mountApp(el, Page, data)`** — the new page mounts.
|
|
92
|
+
5. **`onEnter(el)`** — synchronous; fire-and-forget. Promises are ignored here.
|
|
93
|
+
6. **`onMount()`** — legacy hook, fires last on every render (including the initial hydration).
|
|
94
|
+
|
|
95
|
+
On the initial hydration render, `onLeave` and `onEnter` are both skipped — there's no outgoing page to leave, and hydration doesn't insert new DOM that needs an enter animation.
|
|
96
|
+
|
|
97
|
+
**AbortSignal semantics for in-flight effects.** When a component is disposed, its `AbortController` fires and `inst.signal.aborted` becomes `true`. Effect handlers should guard their `send()` calls against `signal.aborted` — the base package already does this in `@llui/effects`. Network requests that have already been accepted by the server are NOT cancelled by navigation; cancellation only applies to future `send()` dispatches into the now-aborted instance. This is intentional: cancelling a successful signup POST just because the user clicked a nav link would lose data.
|
|
98
|
+
|
|
99
|
+
**Scroll position is the host's problem.** Vike controls scroll-to-top behavior via `scrollToTop` in `+config.ts`. `@llui/vike` doesn't touch scroll — if you need custom scroll handling, configure it on the Vike side.
|
|
100
|
+
|
|
60
101
|
## How It Works
|
|
61
102
|
|
|
62
103
|
### Server (`onRenderHtml`)
|
|
@@ -69,11 +110,12 @@ Hydrates the server-rendered HTML on the client. Attaches event listeners and re
|
|
|
69
110
|
|
|
70
111
|
## API
|
|
71
112
|
|
|
72
|
-
| Export | Sub-path | Description
|
|
73
|
-
| ---------------------- | ------------------- |
|
|
74
|
-
| `onRenderHtml` | `@llui/vike/server` | Default server hook — minimal HTML template
|
|
75
|
-
| `createOnRenderHtml` | `@llui/vike/server` | Factory for custom document templates
|
|
76
|
-
| `onRenderClient` | `@llui/vike/client` | Default client hook — hydrate or mount
|
|
77
|
-
| `createOnRenderClient` | `@llui/vike/client` | Factory for custom container/
|
|
113
|
+
| Export | Sub-path | Description |
|
|
114
|
+
| ---------------------- | ------------------- | ---------------------------------------------------------------- |
|
|
115
|
+
| `onRenderHtml` | `@llui/vike/server` | Default server hook — minimal HTML template |
|
|
116
|
+
| `createOnRenderHtml` | `@llui/vike/server` | Factory for custom document templates |
|
|
117
|
+
| `onRenderClient` | `@llui/vike/client` | Default client hook — hydrate or mount |
|
|
118
|
+
| `createOnRenderClient` | `@llui/vike/client` | Factory for custom container + `onLeave` / `onEnter` / `onMount` |
|
|
119
|
+
| `fromTransition` | `@llui/vike/client` | Adapter: `TransitionOptions` → `{ onLeave, onEnter }` hook pair |
|
|
78
120
|
|
|
79
121
|
The barrel export (`@llui/vike`) re-exports everything, but prefer sub-path imports to avoid bundling jsdom into the client.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { onRenderHtml, createOnRenderHtml } from './on-render-html';
|
|
2
|
-
export type { PageContext, DocumentContext, RenderHtmlResult } from './on-render-html';
|
|
3
|
-
export { onRenderClient, createOnRenderClient } from './on-render-client';
|
|
4
|
-
export type { ClientPageContext, RenderClientOptions } from './on-render-client';
|
|
1
|
+
export { onRenderHtml, createOnRenderHtml } from './on-render-html.js';
|
|
2
|
+
export type { PageContext, DocumentContext, RenderHtmlResult } from './on-render-html.js';
|
|
3
|
+
export { onRenderClient, createOnRenderClient, fromTransition } from './on-render-client.js';
|
|
4
|
+
export type { ClientPageContext, RenderClientOptions } from './on-render-client.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEzF,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC5F,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { onRenderHtml, createOnRenderHtml } from './on-render-html';
|
|
2
|
-
export { onRenderClient, createOnRenderClient } from './on-render-client';
|
|
1
|
+
export { onRenderHtml, createOnRenderHtml } from './on-render-html.js';
|
|
2
|
+
export { onRenderClient, createOnRenderClient, fromTransition } from './on-render-client.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAGtE,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA","sourcesContent":["export { onRenderHtml, createOnRenderHtml } from './on-render-html.js'\nexport type { PageContext, DocumentContext, RenderHtmlResult } from './on-render-html.js'\n\nexport { onRenderClient, createOnRenderClient, fromTransition } from './on-render-client.js'\nexport type { ClientPageContext, RenderClientOptions } from './on-render-client.js'\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentDef } from '@llui/dom';
|
|
1
|
+
import type { ComponentDef, TransitionOptions } from '@llui/dom';
|
|
2
2
|
declare global {
|
|
3
3
|
interface Window {
|
|
4
4
|
__LLUI_STATE__?: unknown;
|
|
@@ -9,15 +9,102 @@ export interface ClientPageContext {
|
|
|
9
9
|
data?: unknown;
|
|
10
10
|
isHydration?: boolean;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Page-lifecycle hooks that fire around the dispose → clear → mount
|
|
14
|
+
* sequence on client navigation. Use these to animate page transitions,
|
|
15
|
+
* save scroll state, emit analytics events, or defer the swap behind
|
|
16
|
+
* any async work that must complete before the next page appears.
|
|
17
|
+
*
|
|
18
|
+
* The sequence is:
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* client nav triggered
|
|
22
|
+
* │
|
|
23
|
+
* ▼
|
|
24
|
+
* onLeave(el) ← awaited if it returns a promise
|
|
25
|
+
* │ (the outgoing page's DOM is still mounted here)
|
|
26
|
+
* ▼
|
|
27
|
+
* currentHandle.dispose()
|
|
28
|
+
* │ (all scopes torn down — portals, focus traps,
|
|
29
|
+
* │ onMount cleanups all fire synchronously here)
|
|
30
|
+
* ▼
|
|
31
|
+
* el.textContent = ''
|
|
32
|
+
* │ (old DOM removed)
|
|
33
|
+
* ▼
|
|
34
|
+
* mountApp(el, Page, data)
|
|
35
|
+
* │ (new page mounted)
|
|
36
|
+
* ▼
|
|
37
|
+
* onEnter(el) ← not awaited; animate in-place
|
|
38
|
+
* │
|
|
39
|
+
* ▼
|
|
40
|
+
* onMount() ← legacy shim, still fires last
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* On the initial render (hydration), `onLeave` and `onEnter` are NOT
|
|
44
|
+
* called — there's no outgoing page to leave and no animation to enter.
|
|
45
|
+
* If you need to run code after hydration, use `onMount`.
|
|
46
|
+
*/
|
|
12
47
|
export interface RenderClientOptions {
|
|
13
48
|
/** CSS selector for the mount container. Default: '#app' */
|
|
14
49
|
container?: string;
|
|
15
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Called on the outgoing page's container BEFORE dispose + clear + mount.
|
|
52
|
+
* Return a promise to defer the swap until the leave animation finishes.
|
|
53
|
+
* The container element is passed as the argument — its children are
|
|
54
|
+
* still the previous page's DOM at this point.
|
|
55
|
+
*
|
|
56
|
+
* Not called on the initial hydration render.
|
|
57
|
+
*/
|
|
58
|
+
onLeave?: (el: HTMLElement) => void | Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Called after the new page is mounted into the container. Use this to
|
|
61
|
+
* kick off an enter animation on the freshly-rendered content. Not
|
|
62
|
+
* awaited — if you return a promise, the resolution is ignored.
|
|
63
|
+
*
|
|
64
|
+
* Not called on the initial hydration render.
|
|
65
|
+
*/
|
|
66
|
+
onEnter?: (el: HTMLElement) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Called after mount or hydration completes. Fires on every render
|
|
69
|
+
* including the initial hydration. Use this for per-render side
|
|
70
|
+
* effects that don't fit the animation hooks (analytics, focus
|
|
71
|
+
* management, etc.).
|
|
72
|
+
*/
|
|
16
73
|
onMount?: () => void;
|
|
17
74
|
}
|
|
18
75
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
76
|
+
* Adapt a `TransitionOptions` object (e.g. the output of
|
|
77
|
+
* `routeTransition()` from `@llui/transitions`, or any preset like
|
|
78
|
+
* `fade()` / `slide()`) into the `onLeave` / `onEnter` shape expected
|
|
79
|
+
* by `createOnRenderClient`.
|
|
80
|
+
*
|
|
81
|
+
* ```typescript
|
|
82
|
+
* import { createOnRenderClient, fromTransition } from '@llui/vike/client'
|
|
83
|
+
* import { routeTransition } from '@llui/transitions'
|
|
84
|
+
*
|
|
85
|
+
* export const onRenderClient = createOnRenderClient({
|
|
86
|
+
* ...fromTransition(routeTransition({ duration: 200 })),
|
|
87
|
+
* })
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* The transition operates on the container element itself — its
|
|
91
|
+
* opacity / transform fades out the outgoing page, then the new page
|
|
92
|
+
* fades in when it mounts. If the preset doesn't restore its starting
|
|
93
|
+
* style on `leave`, the container may still carry leftover properties
|
|
94
|
+
* when the new page mounts; use `enter` to reset them explicitly or
|
|
95
|
+
* pick presets that self-clean.
|
|
96
|
+
*/
|
|
97
|
+
export declare function fromTransition(t: TransitionOptions): Pick<RenderClientOptions, 'onLeave' | 'onEnter'>;
|
|
98
|
+
/**
|
|
99
|
+
* @internal — test helper. Disposes the current handle (if any) and clears
|
|
100
|
+
* the module-level state so subsequent calls behave as a first mount.
|
|
101
|
+
* Not part of the public API; subject to change without notice.
|
|
102
|
+
*/
|
|
103
|
+
export declare function _resetCurrentHandleForTest(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Default onRenderClient hook — no animation hooks. Hydrates if
|
|
106
|
+
* `isHydration` is true, otherwise mounts fresh. Use `createOnRenderClient`
|
|
107
|
+
* for the customizable factory form.
|
|
21
108
|
*/
|
|
22
109
|
export declare function onRenderClient(pageContext: ClientPageContext): Promise<void>;
|
|
23
110
|
/**
|
|
@@ -29,6 +116,8 @@ export declare function onRenderClient(pageContext: ClientPageContext): Promise<
|
|
|
29
116
|
*
|
|
30
117
|
* export const onRenderClient = createOnRenderClient({
|
|
31
118
|
* container: '#root',
|
|
119
|
+
* onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,
|
|
120
|
+
* onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),
|
|
32
121
|
* onMount: () => console.log('Page ready'),
|
|
33
122
|
* })
|
|
34
123
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-render-client.d.ts","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"on-render-client.d.ts","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAa,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAE3E,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,cAAc,CAAC,EAAE,OAAO,CAAA;KACzB;CACF;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACtD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAA;IAEnC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,iBAAiB,GACnB,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,SAAS,CAAC,CAgBlD;AAMD;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAKjD;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,mBAAmB,GAC3B,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAEnD"}
|
package/dist/on-render-client.js
CHANGED
|
@@ -1,12 +1,64 @@
|
|
|
1
1
|
import { hydrateApp, mountApp } from '@llui/dom';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Adapt a `TransitionOptions` object (e.g. the output of
|
|
4
|
+
* `routeTransition()` from `@llui/transitions`, or any preset like
|
|
5
|
+
* `fade()` / `slide()`) into the `onLeave` / `onEnter` shape expected
|
|
6
|
+
* by `createOnRenderClient`.
|
|
7
|
+
*
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createOnRenderClient, fromTransition } from '@llui/vike/client'
|
|
10
|
+
* import { routeTransition } from '@llui/transitions'
|
|
11
|
+
*
|
|
12
|
+
* export const onRenderClient = createOnRenderClient({
|
|
13
|
+
* ...fromTransition(routeTransition({ duration: 200 })),
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The transition operates on the container element itself — its
|
|
18
|
+
* opacity / transform fades out the outgoing page, then the new page
|
|
19
|
+
* fades in when it mounts. If the preset doesn't restore its starting
|
|
20
|
+
* style on `leave`, the container may still carry leftover properties
|
|
21
|
+
* when the new page mounts; use `enter` to reset them explicitly or
|
|
22
|
+
* pick presets that self-clean.
|
|
23
|
+
*/
|
|
24
|
+
export function fromTransition(t) {
|
|
25
|
+
return {
|
|
26
|
+
onLeave: t.leave
|
|
27
|
+
? (el) => {
|
|
28
|
+
const result = t.leave([el]);
|
|
29
|
+
return result && typeof result.then === 'function'
|
|
30
|
+
? result
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
: undefined,
|
|
34
|
+
onEnter: t.enter
|
|
35
|
+
? (el) => {
|
|
36
|
+
t.enter([el]);
|
|
37
|
+
}
|
|
38
|
+
: undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Track the current app handle so we can dispose it on client navigation.
|
|
42
|
+
// Module-level state: there's exactly one Vike-managed app per page load.
|
|
3
43
|
let currentHandle = null;
|
|
4
44
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
45
|
+
* @internal — test helper. Disposes the current handle (if any) and clears
|
|
46
|
+
* the module-level state so subsequent calls behave as a first mount.
|
|
47
|
+
* Not part of the public API; subject to change without notice.
|
|
48
|
+
*/
|
|
49
|
+
export function _resetCurrentHandleForTest() {
|
|
50
|
+
if (currentHandle) {
|
|
51
|
+
currentHandle.dispose();
|
|
52
|
+
currentHandle = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Default onRenderClient hook — no animation hooks. Hydrates if
|
|
57
|
+
* `isHydration` is true, otherwise mounts fresh. Use `createOnRenderClient`
|
|
58
|
+
* for the customizable factory form.
|
|
7
59
|
*/
|
|
8
60
|
export async function onRenderClient(pageContext) {
|
|
9
|
-
renderClient(pageContext, {});
|
|
61
|
+
await renderClient(pageContext, {});
|
|
10
62
|
}
|
|
11
63
|
/**
|
|
12
64
|
* Factory to create a customized onRenderClient hook.
|
|
@@ -17,6 +69,8 @@ export async function onRenderClient(pageContext) {
|
|
|
17
69
|
*
|
|
18
70
|
* export const onRenderClient = createOnRenderClient({
|
|
19
71
|
* container: '#root',
|
|
72
|
+
* onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,
|
|
73
|
+
* onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),
|
|
20
74
|
* onMount: () => console.log('Page ready'),
|
|
21
75
|
* })
|
|
22
76
|
* ```
|
|
@@ -31,20 +85,30 @@ async function renderClient(pageContext, options) {
|
|
|
31
85
|
if (!container) {
|
|
32
86
|
throw new Error(`@llui/vike: container "${selector}" not found in DOM`);
|
|
33
87
|
}
|
|
34
|
-
|
|
88
|
+
const el = container;
|
|
89
|
+
// Dispose the previous page's component on client navigation. If the
|
|
90
|
+
// caller supplied an onLeave hook and this isn't the initial hydration,
|
|
91
|
+
// await it BEFORE tearing down — that's the only moment where the
|
|
92
|
+
// outgoing page's DOM still exists for an animation to read/write.
|
|
35
93
|
if (currentHandle) {
|
|
94
|
+
if (!pageContext.isHydration && options.onLeave) {
|
|
95
|
+
await options.onLeave(el);
|
|
96
|
+
}
|
|
36
97
|
currentHandle.dispose();
|
|
37
98
|
currentHandle = null;
|
|
38
99
|
}
|
|
39
|
-
const el = container;
|
|
40
100
|
if (pageContext.isHydration) {
|
|
41
101
|
const serverState = window.__LLUI_STATE__;
|
|
42
102
|
currentHandle = hydrateApp(el, Page, serverState);
|
|
43
103
|
}
|
|
44
104
|
else {
|
|
45
|
-
// Clear old DOM before mounting new page
|
|
105
|
+
// Clear old DOM before mounting the new page
|
|
46
106
|
el.textContent = '';
|
|
47
107
|
currentHandle = mountApp(el, Page, pageContext.data);
|
|
108
|
+
// onEnter fires AFTER mount so the hook can animate the freshly
|
|
109
|
+
// rendered children. It's intentionally sync — a promise return is
|
|
110
|
+
// ignored, matching typical enter-animation ergonomics (fire-and-forget).
|
|
111
|
+
options.onEnter?.(el);
|
|
48
112
|
}
|
|
49
113
|
options.onMount?.();
|
|
50
114
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-render-client.js","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"on-render-client.js","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { hydrateApp, mountApp } from '@llui/dom'\nimport type { ComponentDef, AppHandle, TransitionOptions } from '@llui/dom'\n\ndeclare global {\n interface Window {\n __LLUI_STATE__?: unknown\n }\n}\n\nexport interface ClientPageContext {\n Page: ComponentDef<unknown, unknown, unknown, unknown>\n data?: unknown\n isHydration?: boolean\n}\n\n/**\n * Page-lifecycle hooks that fire around the dispose → clear → mount\n * sequence on client navigation. Use these to animate page transitions,\n * save scroll state, emit analytics events, or defer the swap behind\n * any async work that must complete before the next page appears.\n *\n * The sequence is:\n *\n * ```\n * client nav triggered\n * │\n * ▼\n * onLeave(el) ← awaited if it returns a promise\n * │ (the outgoing page's DOM is still mounted here)\n * ▼\n * currentHandle.dispose()\n * │ (all scopes torn down — portals, focus traps,\n * │ onMount cleanups all fire synchronously here)\n * ▼\n * el.textContent = ''\n * │ (old DOM removed)\n * ▼\n * mountApp(el, Page, data)\n * │ (new page mounted)\n * ▼\n * onEnter(el) ← not awaited; animate in-place\n * │\n * ▼\n * onMount() ← legacy shim, still fires last\n * ```\n *\n * On the initial render (hydration), `onLeave` and `onEnter` are NOT\n * called — there's no outgoing page to leave and no animation to enter.\n * If you need to run code after hydration, use `onMount`.\n */\nexport interface RenderClientOptions {\n /** CSS selector for the mount container. Default: '#app' */\n container?: string\n\n /**\n * Called on the outgoing page's container BEFORE dispose + clear + mount.\n * Return a promise to defer the swap until the leave animation finishes.\n * The container element is passed as the argument — its children are\n * still the previous page's DOM at this point.\n *\n * Not called on the initial hydration render.\n */\n onLeave?: (el: HTMLElement) => void | Promise<void>\n\n /**\n * Called after the new page is mounted into the container. Use this to\n * kick off an enter animation on the freshly-rendered content. Not\n * awaited — if you return a promise, the resolution is ignored.\n *\n * Not called on the initial hydration render.\n */\n onEnter?: (el: HTMLElement) => void\n\n /**\n * Called after mount or hydration completes. Fires on every render\n * including the initial hydration. Use this for per-render side\n * effects that don't fit the animation hooks (analytics, focus\n * management, etc.).\n */\n onMount?: () => void\n}\n\n/**\n * Adapt a `TransitionOptions` object (e.g. the output of\n * `routeTransition()` from `@llui/transitions`, or any preset like\n * `fade()` / `slide()`) into the `onLeave` / `onEnter` shape expected\n * by `createOnRenderClient`.\n *\n * ```typescript\n * import { createOnRenderClient, fromTransition } from '@llui/vike/client'\n * import { routeTransition } from '@llui/transitions'\n *\n * export const onRenderClient = createOnRenderClient({\n * ...fromTransition(routeTransition({ duration: 200 })),\n * })\n * ```\n *\n * The transition operates on the container element itself — its\n * opacity / transform fades out the outgoing page, then the new page\n * fades in when it mounts. If the preset doesn't restore its starting\n * style on `leave`, the container may still carry leftover properties\n * when the new page mounts; use `enter` to reset them explicitly or\n * pick presets that self-clean.\n */\nexport function fromTransition(\n t: TransitionOptions,\n): Pick<RenderClientOptions, 'onLeave' | 'onEnter'> {\n return {\n onLeave: t.leave\n ? (el): void | Promise<void> => {\n const result = t.leave!([el])\n return result && typeof (result as Promise<void>).then === 'function'\n ? (result as Promise<void>)\n : undefined\n }\n : undefined,\n onEnter: t.enter\n ? (el): void => {\n t.enter!([el])\n }\n : undefined,\n }\n}\n\n// Track the current app handle so we can dispose it on client navigation.\n// Module-level state: there's exactly one Vike-managed app per page load.\nlet currentHandle: AppHandle | null = null\n\n/**\n * @internal — test helper. Disposes the current handle (if any) and clears\n * the module-level state so subsequent calls behave as a first mount.\n * Not part of the public API; subject to change without notice.\n */\nexport function _resetCurrentHandleForTest(): void {\n if (currentHandle) {\n currentHandle.dispose()\n currentHandle = null\n }\n}\n\n/**\n * Default onRenderClient hook — no animation hooks. Hydrates if\n * `isHydration` is true, otherwise mounts fresh. Use `createOnRenderClient`\n * for the customizable factory form.\n */\nexport async function onRenderClient(pageContext: ClientPageContext): Promise<void> {\n await renderClient(pageContext, {})\n}\n\n/**\n * Factory to create a customized onRenderClient hook.\n *\n * ```typescript\n * // pages/+onRenderClient.ts\n * import { createOnRenderClient } from '@llui/vike/client'\n *\n * export const onRenderClient = createOnRenderClient({\n * container: '#root',\n * onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,\n * onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),\n * onMount: () => console.log('Page ready'),\n * })\n * ```\n */\nexport function createOnRenderClient(\n options: RenderClientOptions,\n): (pageContext: ClientPageContext) => Promise<void> {\n return (pageContext) => renderClient(pageContext, options)\n}\n\nasync function renderClient(\n pageContext: ClientPageContext,\n options: RenderClientOptions,\n): Promise<void> {\n const { Page } = pageContext\n const selector = options.container ?? '#app'\n const container = document.querySelector(selector)\n\n if (!container) {\n throw new Error(`@llui/vike: container \"${selector}\" not found in DOM`)\n }\n\n const el = container as HTMLElement\n\n // Dispose the previous page's component on client navigation. If the\n // caller supplied an onLeave hook and this isn't the initial hydration,\n // await it BEFORE tearing down — that's the only moment where the\n // outgoing page's DOM still exists for an animation to read/write.\n if (currentHandle) {\n if (!pageContext.isHydration && options.onLeave) {\n await options.onLeave(el)\n }\n currentHandle.dispose()\n currentHandle = null\n }\n\n if (pageContext.isHydration) {\n const serverState = window.__LLUI_STATE__\n currentHandle = hydrateApp(el, Page, serverState)\n } else {\n // Clear old DOM before mounting the new page\n el.textContent = ''\n currentHandle = mountApp(el, Page, pageContext.data)\n // onEnter fires AFTER mount so the hook can animate the freshly\n // rendered children. It's intentionally sync — a promise return is\n // ignored, matching typical enter-animation ergonomics (fire-and-forget).\n options.onEnter?.(el)\n }\n\n options.onMount?.()\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-render-html.js","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAyB1C,MAAM,gBAAgB,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAmB,EAAU,EAAE,CAAC;;;;MAIvE,IAAI;;;oBAGU,IAAI;sCACc,KAAK;;QAEnC,CAAA;AAER;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,WAAwB;IACzD,OAAO,UAAU,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAElC;IACC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;AACnE,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,WAAwB,EACxB,QAA0C;IAE1C,wEAAwE;IACxE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;IACpD,MAAM,UAAU,EAAE,CAAA;IAElB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAW,CAAA;IAClC,MAAM,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAA;IAC1C,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,IAAI,EAAE,CAAA;IAEnC,MAAM,YAAY,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;IAEjE,OAAO;QACL,kEAAkE;QAClE,yDAAyD;QACzD,YAAY,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;QACxC,WAAW,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;KACzC,CAAA;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"on-render-html.js","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAyB1C,MAAM,gBAAgB,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAmB,EAAU,EAAE,CAAC;;;;MAIvE,IAAI;;;oBAGU,IAAI;sCACc,KAAK;;QAEnC,CAAA;AAER;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,WAAwB;IACzD,OAAO,UAAU,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAElC;IACC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;AACnE,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,WAAwB,EACxB,QAA0C;IAE1C,wEAAwE;IACxE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;IACpD,MAAM,UAAU,EAAE,CAAA;IAElB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAW,CAAA;IAClC,MAAM,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAA;IAC1C,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,IAAI,EAAE,CAAA;IAEnC,MAAM,YAAY,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;IAEjE,OAAO;QACL,kEAAkE;QAClE,yDAAyD;QACzD,YAAY,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;QACxC,WAAW,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;KACzC,CAAA;AACH,CAAC","sourcesContent":["import { renderToString } from '@llui/dom'\nimport type { ComponentDef } from '@llui/dom'\n\nexport interface PageContext {\n Page: ComponentDef<unknown, unknown, unknown, unknown>\n data?: unknown\n head?: string\n}\n\nexport interface DocumentContext {\n /** Rendered component HTML */\n html: string\n /** JSON-serialized initial state */\n state: string\n /** Head content from pageContext.head (e.g. from +Head.ts) */\n head: string\n /** Full page context for custom logic */\n pageContext: PageContext\n}\n\nexport interface RenderHtmlResult {\n documentHtml: string | { _escaped: string }\n pageContext: { lluiState: unknown }\n}\n\nconst DEFAULT_DOCUMENT = ({ html, state, head }: DocumentContext): string => `<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n ${head}\n </head>\n <body>\n <div id=\"app\">${html}</div>\n <script>window.__LLUI_STATE__ = ${state}</script>\n </body>\n</html>`\n\n/**\n * Default onRenderHtml hook for simple cases.\n * Uses a minimal HTML document template.\n */\nexport async function onRenderHtml(pageContext: PageContext): Promise<RenderHtmlResult> {\n return renderPage(pageContext, DEFAULT_DOCUMENT)\n}\n\n/**\n * Factory to create a customized onRenderHtml hook.\n *\n * ```typescript\n * // pages/+onRenderHtml.ts\n * import { createOnRenderHtml } from '@llui/vike'\n *\n * export const onRenderHtml = createOnRenderHtml({\n * document: ({ html, state, head }) => `<!DOCTYPE html>\n * <html>\n * <head>${head}<link rel=\"stylesheet\" href=\"/styles.css\" /></head>\n * <body><div id=\"app\">${html}</div>\n * <script>window.__LLUI_STATE__ = ${state}</script></body>\n * </html>`,\n * })\n * ```\n */\nexport function createOnRenderHtml(options: {\n document: (ctx: DocumentContext) => string\n}): (pageContext: PageContext) => Promise<RenderHtmlResult> {\n return (pageContext) => renderPage(pageContext, options.document)\n}\n\nasync function renderPage(\n pageContext: PageContext,\n document: (ctx: DocumentContext) => string,\n): Promise<RenderHtmlResult> {\n // Lazy-import to keep jsdom out of the client bundle's dependency graph\n const { initSsrDom } = await import('@llui/dom/ssr')\n await initSsrDom()\n\n const { Page, data } = pageContext\n const [initialState] = Page.init(data)\n const html = renderToString(Page, initialState)\n const state = JSON.stringify(initialState)\n const head = pageContext.head ?? ''\n\n const documentHtml = document({ html, state, head, pageContext })\n\n return {\n // Use Vike's dangerouslySkipEscape format — the document template\n // is trusted (authored by the developer, not user input)\n documentHtml: { _escaped: documentHtml },\n pageContext: { lluiState: initialState },\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llui/vike",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -20,16 +20,9 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"build": "tsc -p tsconfig.build.json",
|
|
25
|
-
"check": "tsc --noEmit",
|
|
26
|
-
"lint": "eslint src",
|
|
27
|
-
"test": "vitest run",
|
|
28
|
-
"test:coverage": "vitest run --coverage"
|
|
29
|
-
},
|
|
30
23
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
24
|
+
"jsdom": "^26.1.0",
|
|
25
|
+
"@llui/dom": "0.0.15"
|
|
33
26
|
},
|
|
34
27
|
"description": "LLui Vike SSR adapter — onRenderHtml, onRenderClient hooks",
|
|
35
28
|
"keywords": [
|
|
@@ -48,5 +41,12 @@
|
|
|
48
41
|
"bugs": {
|
|
49
42
|
"url": "https://github.com/fponticelli/llui/issues"
|
|
50
43
|
},
|
|
51
|
-
"homepage": "https://github.com/fponticelli/llui/tree/main/packages/vike#readme"
|
|
52
|
-
|
|
44
|
+
"homepage": "https://github.com/fponticelli/llui/tree/main/packages/vike#readme",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"check": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint src",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:coverage": "vitest run --coverage"
|
|
51
|
+
}
|
|
52
|
+
}
|