Skip to content

Commit

Permalink
feat: critical DX/UX improvement to SPA navigation in Qwik City (#3244)
Browse files Browse the repository at this point in the history
  • Loading branch information
billykwok committed Jun 6, 2023
1 parent 06be4c8 commit 3daee3d
Show file tree
Hide file tree
Showing 20 changed files with 729 additions and 288 deletions.
32 changes: 30 additions & 2 deletions packages/docs/src/routes/api/qwik-city/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,20 @@
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/types.ts",
"mdFile": "qwik-city.menudata.md"
},
{
"name": "NavigationType",
"id": "navigationtype",
"hierarchy": [
{
"name": "NavigationType",
"id": "navigationtype"
}
],
"kind": "TypeAlias",
"content": "```typescript\nexport type NavigationType = 'initial' | 'form' | 'link' | 'popstate';\n```",
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/types.ts",
"mdFile": "qwik-city.navigationtype.md"
},
{
"name": "PageModule",
"id": "pagemodule",
Expand Down Expand Up @@ -394,6 +408,20 @@
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/types.ts",
"mdFile": "qwik-city.pathparams.md"
},
{
"name": "QwikCityMockProps",
"id": "qwikcitymockprops",
"hierarchy": [
{
"name": "QwikCityMockProps",
"id": "qwikcitymockprops"
}
],
"kind": "Interface",
"content": "```typescript\nexport interface QwikCityMockProps \n```\n\n\n| Property | Modifiers | Type | Description |\n| --- | --- | --- | --- |\n| [params?](#) | | Record<string, string> | _(Optional)_ |\n| [url?](#) | | string | _(Optional)_ |",
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/qwik-city-component.tsx",
"mdFile": "qwik-city.qwikcitymockprops.md"
},
{
"name": "QwikCityMockProvider",
"id": "qwikcitymockprovider",
Expand Down Expand Up @@ -432,7 +460,7 @@
}
],
"kind": "Interface",
"content": "```typescript\nexport interface QwikCityProps \n```\n\n\n| Property | Modifiers | Type | Description |\n| --- | --- | --- | --- |\n| [viewTransition?](#) | | boolean | <p>_(Optional)_ Enable the ViewTransition API</p><p>Default: <code>true</code></p> |",
"content": "```typescript\nexport interface QwikCityProps \n```\n\n\n| Property | Modifiers | Type | Description |\n| --- | --- | --- | --- |\n| [restoreScroll$?](#) | | PropFunction&lt;RestoreScroll&gt; | _(Optional)_ |\n| [viewTransition?](#) | | boolean | <p>_(Optional)_ Enable the ViewTransition API</p><p>Default: <code>true</code></p> |",
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/qwik-city-component.tsx",
"mdFile": "qwik-city.qwikcityprops.md"
},
Expand Down Expand Up @@ -558,7 +586,7 @@
}
],
"kind": "TypeAlias",
"content": "```typescript\nexport type RouteNavigate = QRL<(path?: string, forceReload?: boolean) => Promise<void>>;\n```",
"content": "```typescript\nexport type RouteNavigate = QRL<(path?: string, options?: {\n type?: Exclude<NavigationType, 'initial'>;\n forceReload?: boolean;\n} | boolean) => Promise<void>>;\n```\n**References:** [NavigationType](#navigationtype)",
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/types.ts",
"mdFile": "qwik-city.routenavigate.md"
},
Expand Down
40 changes: 36 additions & 4 deletions packages/docs/src/routes/api/qwik-city/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,14 @@ export type MenuData = [pathname: string, menuLoader: MenuModuleLoader];
[Edit this section](https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/types.ts)
## NavigationType
```typescript
export type NavigationType = "initial" | "form" | "link" | "popstate";
```
[Edit this section](https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/types.ts)
## PageModule
```typescript
Expand All @@ -387,6 +395,19 @@ export declare type PathParams = Record<string, string>;
[Edit this section](https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/types.ts)
## QwikCityMockProps
```typescript
export interface QwikCityMockProps
```
| Property | Modifiers | Type | Description |
| ------------ | --------- | ---------------------------- | ------------ |
| [params?](#) | | Record&lt;string, string&gt; | _(Optional)_ |
| [url?](#) | | string | _(Optional)_ |
[Edit this section](https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/qwik-city-component.tsx)
## QwikCityMockProvider
```typescript
Expand Down Expand Up @@ -418,9 +439,10 @@ export interface QwikCityPlan
export interface QwikCityProps
```
| Property | Modifiers | Type | Description |
| -------------------- | --------- | ------- | ---------------------------------------------------------------------------------- |
| [viewTransition?](#) | | boolean | <p>_(Optional)_ Enable the ViewTransition API</p><p>Default: <code>true</code></p> |
| Property | Modifiers | Type | Description |
| -------------------- | --------- | --------------------------------- | ---------------------------------------------------------------------------------- |
| [restoreScroll$?](#) | | PropFunction&lt;RestoreScroll&gt; | _(Optional)_ |
| [viewTransition?](#) | | boolean | <p>_(Optional)_ Enable the ViewTransition API</p><p>Default: <code>true</code></p> |
[Edit this section](https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/qwik-city-component.tsx)
Expand Down Expand Up @@ -510,10 +532,20 @@ export interface RouteLocation
```typescript
export type RouteNavigate = QRL<
(path?: string, forceReload?: boolean) => Promise<void>
(
path?: string,
options?:
| {
type?: Exclude<NavigationType, "initial">;
forceReload?: boolean;
}
| boolean
) => Promise<void>
>;
```
**References:** [NavigationType](#navigationtype)
[Edit this section](https://github.com/BuilderIO/qwik/tree/main/packages/qwik-city/runtime/src/types.ts)
## RouterOutlet
Expand Down
34 changes: 32 additions & 2 deletions packages/docs/src/routes/docs/(qwikcity)/api/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,39 @@ export default function Root() {
## `<RouterOutlet>`

This component is responsible for rendering the matched route at a given moment, it used internally the [`useContent()`](/docs/(qwikcity)/api/index.mdx#usecontent) and to render the current page, as well as all the nested layouts.
The `RouterOutlet` component is responsible for rendering the matched route at a given moment, it uses internally the [`useContent()`](/docs/(qwikcity)/api/index.mdx#usecontent) and to render the current page, as well as all the nested layouts.

This component is usually located as a child of `<body>`, in most of the starters you will find it in the `src/root.tsx` file:
This component is usually located as a child of `<body>`, in most of the starters you will find it in the `src/root.tsx` file (refer to the example in `QwikCityProvider`).

It accepts an optional `restoreScroll$` prop that can be used to customize the scrolling behavior of SPAs. The function supplied to `restoreScroll$` is called with the following arguments upon navigation events:

```ts
type RestoreScroll = (
type: 'initial' | 'form' | 'link' | 'popstate';
fromUrl: URL;
toUrlSettled: Promise<URL>;
) => void;
```

There are two built-in scroll restoration presets provided by QwikCity - `toTopAlways` and `toLastPositionOnPopState`.

- `toTopAlways` (default), as the name suggested, always scrolls the page to the top of the window when navigating to new or previous pages.
- `toLastPositionOnPopState` mimics how browsers handle MPA - scrolling the page to the top when navigating to a new page, but to the last-visited position when navigating with a `popstate` event (e.g. back/next button).

> Note that both presets treat hash navigation as an exception and always scroll to the hashed element when it is available and specified in the URL.
You can also implement your own scroll restoration logic by supplying a function with the above signature. The `toUrlSettled` promise allows you to wait for DOM updates to complete before setting scroll positions.

```ts
const restoreScroll: RestoreScroll = $(async (type, fromUrl, toUrlSettled) => {
// do something before navigation, e.g. saving current scroll position to sessionStorage
prepare(type, fromUrl);
// wait for final URL to be resolved and DOM to settle
const toUrl = await toUrlSettled;
// handle the actual scroll restoration
handleScroll(type, fromUrl, toUrl);
});
```

```tsx title="src/routes.tsx"
export default component$(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik-city/buildtime/build-layout.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { testAppSuite } from '../utils/test-suite';
const test = testAppSuite('Build Layout');

test('total layouts', ({ layouts }) => {
equal(layouts.length, 7, JSON.stringify(layouts, null, 2));
equal(layouts.length, 8, JSON.stringify(layouts, null, 2));
});

test('nested named layout', ({ assertLayout }) => {
Expand Down
37 changes: 34 additions & 3 deletions packages/qwik-city/runtime/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CookieOptions } from '@builder.io/qwik-city/middleware/request-handler'
import { CookieValue } from '@builder.io/qwik-city/middleware/request-handler';
import { DeferReturn } from '@builder.io/qwik-city/middleware/request-handler';
import { JSXNode } from '@builder.io/qwik';
import { PropFunction } from '@builder.io/qwik';
import { QRL } from '@builder.io/qwik';
import { QwikIntrinsicElements } from '@builder.io/qwik';
import { QwikJSX } from '@builder.io/qwik';
Expand Down Expand Up @@ -220,6 +221,9 @@ export interface FormSubmitSuccessDetail<T> {
value: T;
}

// @alpha (undocumented)
export const getHistoryId: () => string;

// @public (undocumented)
export const globalAction$: ActionConstructor;

Expand Down Expand Up @@ -266,6 +270,9 @@ export type LoaderSignal<T> = T extends () => ValueOrPromise<infer B> ? Readonly
// @public (undocumented)
export type MenuData = [pathname: string, menuLoader: MenuModuleLoader];

// @public (undocumented)
export type NavigationType = 'initial' | 'form' | 'link' | 'popstate';

// Warning: (ae-forgotten-export) The symbol "RouteModule" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
Expand All @@ -285,8 +292,14 @@ export interface PageModule extends RouteModule {
// @public (undocumented)
export type PathParams = Record<string, string>;

// Warning: (ae-forgotten-export) The symbol "QwikCityMockProps" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export interface QwikCityMockProps {
// (undocumented)
params?: Record<string, string>;
// (undocumented)
url?: string;
}

// @public (undocumented)
export const QwikCityMockProvider: Component<QwikCityMockProps>;

Expand All @@ -308,6 +321,10 @@ export interface QwikCityPlan {

// @public (undocumented)
export interface QwikCityProps {
// Warning: (ae-incompatible-release-tags) The symbol "restoreScroll$" is marked as @public, but its signature references "RestoreScroll" which is marked as @alpha
//
// (undocumented)
restoreScroll$?: PropFunction<RestoreScroll>;
viewTransition?: boolean;
}

Expand All @@ -329,6 +346,11 @@ export { RequestHandler }
// @public (undocumented)
export type ResolvedDocumentHead = Required<DocumentHeadValue>;

// Warning: (ae-forgotten-export) The symbol "ScrollRecord" needs to be exported by the entry point index.d.ts
//
// @alpha (undocumented)
export type RestoreScroll = (navigationType: NavigationType, fromUrl: URL, toUrl: URL, records: ScrollRecord) => void | Promise<void>;

// @public (undocumented)
export const routeAction$: ActionConstructor;

Expand Down Expand Up @@ -369,7 +391,10 @@ export interface RouteLocation {
}

// @public (undocumented)
export type RouteNavigate = QRL<(path?: string, forceReload?: boolean) => Promise<void>>;
export type RouteNavigate = QRL<(path?: string, options?: {
type?: Exclude<NavigationType, 'initial'>;
forceReload?: boolean;
} | boolean) => Promise<void>>;

// @public (undocumented)
export const RouterOutlet: Component< {}>;
Expand Down Expand Up @@ -398,6 +423,12 @@ export interface StaticGenerate {
// @public (undocumented)
export type StaticGenerateHandler = () => Promise<StaticGenerate> | StaticGenerate;

// @alpha (undocumented)
export const toLastPositionOnPopState: QRL<RestoreScroll>;

// @alpha (undocumented)
export const toTopAlways: QRL<RestoreScroll>;

// Warning: (ae-forgotten-export) The symbol "ContentState" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
Expand Down

0 comments on commit 3daee3d

Please sign in to comment.