Skip to content

Commit

Permalink
feat(qwik-city): usePreventNavigate()
Browse files Browse the repository at this point in the history
  • Loading branch information
wmertens committed Sep 17, 2024
1 parent 46e6dd4 commit 3125156
Show file tree
Hide file tree
Showing 22 changed files with 630 additions and 72 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-olives-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@builder.io/qwik-city': minor
---

FEAT: **(EXPERIMENTAL)** `usePreventNavigate` lets you prevent navigation while your app's state is unsaved. It works asynchronously for SPA navigation and falls back to the browser's default dialogs for other navigations. To use it, add `experimental: ['preventNavigate']` to your `qwikVite` options.
30 changes: 29 additions & 1 deletion packages/docs/src/routes/api/qwik-city/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/types.ts",
"mdFile": "qwik-city.pathparams.md"
},
{
"name": "PreventNavigateCallback",
"id": "preventnavigatecallback",
"hierarchy": [
{
"name": "PreventNavigateCallback",
"id": "preventnavigatecallback"
}
],
"kind": "TypeAlias",
"content": "```typescript\nexport type PreventNavigateCallback = (url?: number | URL) => ValueOrPromise<boolean>;\n```",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/types.ts",
"mdFile": "qwik-city.preventnavigatecallback.md"
},
{
"name": "QWIK_CITY_SCROLLER",
"id": "qwik_city_scroller",
Expand Down Expand Up @@ -670,7 +684,7 @@
}
],
"kind": "TypeAlias",
"content": "```typescript\nexport type RouteNavigate = QRL<(path?: string | number, options?: {\n type?: Exclude<NavigationType, 'initial'>;\n forceReload?: boolean;\n replaceState?: boolean;\n scroll?: boolean;\n} | boolean) => Promise<void>>;\n```\n**References:** [NavigationType](#navigationtype)",
"content": "```typescript\nexport type RouteNavigate = QRL<(path?: string | number | URL, options?: {\n type?: Exclude<NavigationType, 'initial'>;\n forceReload?: boolean;\n replaceState?: boolean;\n scroll?: boolean;\n} | boolean) => Promise<void>>;\n```\n**References:** [NavigationType](#navigationtype)",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/types.ts",
"mdFile": "qwik-city.routenavigate.md"
},
Expand Down Expand Up @@ -870,6 +884,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/use-functions.ts",
"mdFile": "qwik-city.usenavigate.md"
},
{
"name": "usePreventNavigate$",
"id": "usepreventnavigate_",
"hierarchy": [
{
"name": "usePreventNavigate$",
"id": "usepreventnavigate_"
}
],
"kind": "Function",
"content": "Prevent navigation attempts. This hook registers a callback that will be called before SPA or browser navigation.\n\nReturn `true` to prevent navigation.\n\n\\#\\#\\#\\# SPA Navigation\n\nFor Single-Page-App (SPA) navigation (via `<Link />`<!-- -->, `const nav = useNavigate()`<!-- -->, and browser backwards/forwards inside SPA history), the callback will be provided with the target, either a URL or a number. It will only be a number if `nav(number)` was called to navigate forwards or backwards in SPA history.\n\nIf you return a Promise, the navigation will be blocked until the promise resolves.\n\nThis can be used to show a nice dialog to the user, and wait for the user to confirm, or to record the url, prevent the navigation, and navigate there later via `nav(url)`<!-- -->.\n\n\\#\\#\\#\\# Browser Navigation\n\nHowever, when the user navigates away by clicking on a regular `<a />`<!-- -->, reloading, or moving backwards/forwards outside SPA history, this callback will not be awaited. This is because the browser does not provide a way to asynchronously prevent these navigations.\n\nIn this case, returning returning `true` will tell the browser to show a confirmation dialog, which cannot be customized. You are also not able to show your own `window.confirm()` dialog during the callback, the browser won't allow it. If you return a Promise, it will be considered as `true`<!-- -->.\n\nWhen the callback is called from the browser, no url will be provided. Use this to know whether you can show a dialog or just return `true` to prevent the navigation.\n\n\n```typescript\nusePreventNavigate$: (qrl: PreventNavigateCallback) => void\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nqrl\n\n\n</td><td>\n\n[PreventNavigateCallback](#preventnavigatecallback)\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nvoid",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/use-functions.ts",
"mdFile": "qwik-city.usepreventnavigate_.md"
},
{
"name": "valibot$",
"id": "valibot_",
Expand Down
69 changes: 68 additions & 1 deletion packages/docs/src/routes/api/qwik-city/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1712,6 +1712,16 @@ export declare type PathParams = Record<string, string>;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/types.ts)
## PreventNavigateCallback
```typescript
export type PreventNavigateCallback = (
url?: number | URL,
) => ValueOrPromise<boolean>;
```
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/types.ts)
## QWIK_CITY_SCROLLER
```typescript
Expand Down Expand Up @@ -2130,7 +2140,7 @@ URL
```typescript
export type RouteNavigate = QRL<
(
path?: string | number,
path?: string | number | URL,
options?:
| {
type?: Exclude<NavigationType, "initial">;
Expand Down Expand Up @@ -2411,6 +2421,63 @@ useNavigate: () => RouteNavigate;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/use-functions.ts)
## usePreventNavigate$
Prevent navigation attempts. This hook registers a callback that will be called before SPA or browser navigation.
Return `true` to prevent navigation.
\#### SPA Navigation
For Single-Page-App (SPA) navigation (via `<Link />`, `const nav = useNavigate()`, and browser backwards/forwards inside SPA history), the callback will be provided with the target, either a URL or a number. It will only be a number if `nav(number)` was called to navigate forwards or backwards in SPA history.
If you return a Promise, the navigation will be blocked until the promise resolves.
This can be used to show a nice dialog to the user, and wait for the user to confirm, or to record the url, prevent the navigation, and navigate there later via `nav(url)`.
\#### Browser Navigation
However, when the user navigates away by clicking on a regular `<a />`, reloading, or moving backwards/forwards outside SPA history, this callback will not be awaited. This is because the browser does not provide a way to asynchronously prevent these navigations.
In this case, returning returning `true` will tell the browser to show a confirmation dialog, which cannot be customized. You are also not able to show your own `window.confirm()` dialog during the callback, the browser won't allow it. If you return a Promise, it will be considered as `true`.
When the callback is called from the browser, no url will be provided. Use this to know whether you can show a dialog or just return `true` to prevent the navigation.
```typescript
usePreventNavigate$: (qrl: PreventNavigateCallback) => void
```
<table><thead><tr><th>
Parameter
</th><th>
Type
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
qrl
</td><td>
[PreventNavigateCallback](#preventnavigatecallback)
</td><td>
</td></tr>
</tbody></table>
**Returns:**
void
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/use-functions.ts)
## valibot$
> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.
Expand Down
7 changes: 6 additions & 1 deletion packages/docs/src/routes/docs/(qwikcity)/routing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ contributors:
- mrhoodz
- chsanch
- RumNCodeDev
updated_at: '2023-10-02T22:44:45Z'
- wmertens
updated_at: '2024-09-05T10:32:00Z'
created_at: '2023-03-20T23:45:13Z'
---

Expand Down Expand Up @@ -238,6 +239,10 @@ export default component$(() => {

> The `Link` component uses the `useNavigate()` hook [internally](https://github.com/QwikDev/qwik/blob/e452582f4728cbcb7bf85d03293e757302286683/packages/qwik-city/runtime/src/link-component.tsx#L33).
### Preventing navigation

We have an experimental API to prevent navigation while your app's state is unsaved. See the [usePreventNavigate](/docs/labs/usePreventNavigate) documentation for more information.

### `<Link reload>`

The `Link` component with the `reload` prop can be used together to refresh the current page.
Expand Down
79 changes: 43 additions & 36 deletions packages/docs/src/routes/docs/labs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ contributors:
- Craiqser
- mrhoodz
- thejackshelton
updated_at: '2023-07-05T08:00:42Z'
- wmertens
updated_at: '2024-09-17T00:00:00Z'
created_at: '2023-06-22T22:13:24Z'
---

Expand Down Expand Up @@ -37,40 +38,46 @@ Each Qwik Labs feature can roughly be thought of as going through these stages:

## Installation

Qwik labs are distributed as a separate node package. Because Qwik Labs is "work in progress" the node package is not published to NPM but instead as a github URL. The package is continually updated and so it will always contains the latest build. (You may read up on installing node packages [here](https://docs.npmjs.com/cli/v8/commands/npm-install).)


<PackageManagerTabs>
<span q:slot="pnpm">
```shell
pnpm install github:QwikDev/qwik-labs-build#main
```
</span>
<span q:slot="npm">
```shell
npm install github:QwikDev/qwik-labs-build#main
```
</span>
<span q:slot="yarn">
```shell
yarn add github:QwikDev/qwik-labs-build#main
```
</span>
<span q:slot="bun">
```shell
bun install github:QwikDev/qwik-labs-build#main
```
</span>
</PackageManagerTabs>

Or just add this to your `package.json`

```javascript
{
...
"dependencies": {
There are two ways experimental features are made available to the community:

1. As an `experimental` flag.
Some features are distributed under an experimental flag. This means that the feature is already part of the main package but is not enabled by default. To enable the feature you need to set the corresponding flag in the `qwikVite` `experimental[]` array.

2. As a separate node package.
Qwik labs are distributed as a separate node package. Because Qwik Labs is "work in progress" the node package is not published to NPM but instead as a github URL. The package is continually updated and so it will always contains the latest build. (You may read up on installing node packages [here](https://docs.npmjs.com/cli/v8/commands/npm-install).)

<PackageManagerTabs>
<span q:slot="pnpm">
```shell
pnpm install github:QwikDev/qwik-labs-build#main
```
</span>
<span q:slot="npm">
```shell
npm install github:QwikDev/qwik-labs-build#main
```
</span>
<span q:slot="yarn">
```shell
yarn add github:QwikDev/qwik-labs-build#main
```
</span>
<span q:slot="bun">
```shell
bun install github:QwikDev/qwik-labs-build#main
```
</span>
</PackageManagerTabs>

Or just add this to your `package.json`

```javascript
{
...
"@builder.io/qwik-labs": "github:QwikDev/qwik-labs-build#main",
"dependencies": {
...
"@builder.io/qwik-labs": "github:QwikDev/qwik-labs-build#main",
}
}
}
```
```

103 changes: 103 additions & 0 deletions packages/docs/src/routes/docs/labs/usePreventNavigate/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: "\U0001F9EA usePreventNavigate | Qwik Labs"
description: Prevent navigation when your application has unsaved data.
contributors:
- wmertens
updated_at: '2024-09-17T00:00:00Z'
created_at: '2024-09-17T00:00:00Z'
---

This feature is **EXPERIMENTAL**. We invite you to try it out and provide feedback via the [RFC issue](https://github.com/QwikDev/qwik-evolution/issues/15).

To use it, you must add `experimental: ['preventNavigate']` to your `qwikVite` plugin options.

# Preventing navigation

If the user can lose state by navigating away from the page, you can use `usePreventNavigate(callback)` to conditionally prevent the navigation.

The callback will be called with the URL that the user is trying to navigate to. If the callback returns `true`, the navigation will be prevented.

You can return a Promise, and qwik-city will wait until the promise resolves before navigating.

However, in some cases the browser will navigate without calling qwik-city, such as when the user reloads the tab or navigates using `<a/>` instead of `<Link />`. When this happens, the answer must be synchronous, and user interaction is not allowed.

You can tell the difference between qwik-city and browser navigation by looking at the provided URL. If the URL is `undefined`, the browser is navigating away, and you must respond synchronously.

Examples:

- using a modal library:

```tsx
export default component$(() => {
const okToNavigate = useSignal(true);
usePreventNavigate$((url) => {
if (!okToNavigate.value) {
// we we didn't get a url, the browser is navigating away
// and we must respond synchronously without dialogs
if (!url) return true;

// Here we assume that the confirmDialog function shows a modal and returns a promise for the result
return confirmDialog(
`Do you want to lose changes and go to ${url}?`
).then(answer => !answer);
// or simply using the browser confirm dialog:
// return !confirm(`Do you want to lose changes and go to ${url}?`);
}
});

return (
<div>
<button onClick$={() => (okToNavigate.value = !okToNavigate.value)}>
toggle user state
</button>
application content
</div>
);
});
```

- Using a separate modal:

```tsx
export default component$(() => {
const okToNavigate = useSignal(true);
const navSig = useSignal<URL | number>();
const showConfirm = useSignal(false);
const nav = useNavigate();
usePreventNavigate$((url) => {
if (!okToNavigate.value) {
if (url) {
navSig.value = url;
showConfirm.value = true;
}
return true;
}
});

return (
<div>
<button onClick$={() => (okToNavigate.value = !okToNavigate.value)}>
toggle user state
</button>
application content
{showConfirm.value && (
<div>
<div>
Do you want to lose changes and go to {String(navSig.value)}?
</div>
<button
onClick$={() => {
showConfirm.value = false;
okToNavigate.value = true;
nav(navSig.value!);
}}
>
Yes
</button>
<button onClick$={() => (showConfirm.value = false)}>No</button>
</div>
)}
</div>
);
});
```
1 change: 1 addition & 0 deletions packages/docs/src/routes/docs/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
- [Insights](/docs/labs/insights/index.mdx)
- [Typed Routes](/docs/labs/typed-routes/index.mdx)
- [Devtools](/docs/labs/devtools/index.mdx)
- [usePreventNavigate](/docs/labs/usePreventNavigate/index.mdx)

## Community

Expand Down
2 changes: 1 addition & 1 deletion packages/qwik-city/src/buildtime/build-layout.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { assert, testAppSuite } from '../utils/test-suite';
const test = testAppSuite('Build Layout');

test('total layouts', ({ ctx: { layouts } }) => {
assert.equal(layouts.length, 10, JSON.stringify(layouts, null, 2));
assert.equal(layouts.length, 11, JSON.stringify(layouts, null, 2));
});

test('nested named layout', ({ assertLayout }) => {
Expand Down
Loading

0 comments on commit 3125156

Please sign in to comment.