Skip to content

Commit a5c7133

Browse files
authored
docs(solid-router): navigation-blocking example (#5843)
* docs(solid-router): navigation-blocking example * link to example
1 parent 36b16c5 commit a5c7133

File tree

12 files changed

+374
-1
lines changed

12 files changed

+374
-1
lines changed

docs/router/config.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@
652652
},
653653
{
654654
"label": "Location Masking",
655-
"to": "framework/react/examples/location-masking"
655+
"to": "framework/solid/examples/location-masking"
656656
},
657657
{
658658
"label": "Authenticated Routes",
@@ -666,6 +666,10 @@
666666
"label": "Deferred Data",
667667
"to": "framework/solid/examples/deferred-data"
668668
},
669+
{
670+
"label": "Navigation Blocking",
671+
"to": "framework/solid/examples/navigation-blocking"
672+
},
669673
{
670674
"label": "View Transitions",
671675
"to": "framework/solid/examples/view-transitions"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.DS_Store
3+
dist
4+
dist-ssr
5+
*.local
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"files.watcherExclude": {
3+
"**/routeTree.gen.ts": true
4+
},
5+
"search.exclude": {
6+
"**/routeTree.gen.ts": true
7+
},
8+
"files.readonlyInclude": {
9+
"**/routeTree.gen.ts": true
10+
}
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install` or `yarn`
6+
- `npm start` or `yarn start`
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Vite App</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "tanstack-router-solid-example-navigation-blocking",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite --port 3000",
7+
"build": "vite build && tsc --noEmit",
8+
"serve": "vite preview",
9+
"start": "vite"
10+
},
11+
"dependencies": {
12+
"@tailwindcss/postcss": "^4.1.15",
13+
"@tanstack/solid-query": "^5.90.0",
14+
"@tanstack/solid-router": "^1.135.2",
15+
"@tanstack/solid-router-devtools": "^1.135.2",
16+
"postcss": "^8.5.1",
17+
"solid-js": "^1.9.10",
18+
"redaxios": "^0.5.1",
19+
"tailwindcss": "^4.1.15"
20+
},
21+
"devDependencies": {
22+
"vite-plugin-solid": "^2.11.10",
23+
"typescript": "^5.7.2",
24+
"vite": "^7.1.7"
25+
}
26+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
plugins: {
3+
'@tailwindcss/postcss': {},
4+
},
5+
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { createSignal } from 'solid-js'
2+
import { render } from 'solid-js/web'
3+
import {
4+
Link,
5+
Outlet,
6+
RouterProvider,
7+
createRootRoute,
8+
createRoute,
9+
createRouter,
10+
useBlocker,
11+
} from '@tanstack/solid-router'
12+
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
13+
import './styles.css'
14+
15+
const rootRoute = createRootRoute({
16+
component: RootComponent,
17+
})
18+
19+
function RootComponent() {
20+
// block going from editor-1 to /foo/123?hello=world
21+
const blocker = useBlocker({
22+
shouldBlockFn: ({ current, next }) => {
23+
if (
24+
current.routeId === '/editor-1' &&
25+
next.fullPath === '/foo/$id' &&
26+
next.params.id === '123' &&
27+
next.search.hello === 'world'
28+
) {
29+
return true
30+
}
31+
return false
32+
},
33+
enableBeforeUnload: false,
34+
withResolver: true,
35+
})
36+
37+
return (
38+
<>
39+
<div class="p-2 flex gap-2 text-lg">
40+
<Link
41+
to="/"
42+
activeProps={{
43+
class: 'font-bold',
44+
}}
45+
activeOptions={{ exact: true }}
46+
>
47+
Home
48+
</Link>{' '}
49+
<Link
50+
to="/editor-1"
51+
activeProps={{
52+
class: 'font-bold',
53+
}}
54+
>
55+
Editor 1
56+
</Link>{' '}
57+
<Link
58+
to={'/editor-1/editor-2'}
59+
activeProps={{
60+
class: 'font-bold',
61+
}}
62+
>
63+
Editor 2
64+
</Link>{' '}
65+
<Link
66+
to="/foo/$id"
67+
params={{ id: '123' }}
68+
search={{ hello: 'world' }}
69+
activeProps={{
70+
class: 'font-bold',
71+
}}
72+
activeOptions={{ exact: true, includeSearch: true }}
73+
>
74+
foo 123
75+
</Link>{' '}
76+
<Link
77+
to="/foo/$id"
78+
params={{ id: '456' }}
79+
search={{ hello: 'universe' }}
80+
activeProps={{
81+
class: 'font-bold',
82+
}}
83+
activeOptions={{ exact: true, includeSearch: true }}
84+
>
85+
foo 456
86+
</Link>{' '}
87+
</div>
88+
<hr />
89+
90+
{blocker().status === 'blocked' && (
91+
<div class="mt-2">
92+
<div>
93+
Are you sure you want to leave editor 1 for /foo/123?hello=world ?
94+
</div>
95+
<button
96+
class="bg-lime-500 text-white rounded-sm p-1 px-2 mr-2"
97+
onClick={blocker().proceed}
98+
>
99+
YES
100+
</button>
101+
<button
102+
class="bg-red-500 text-white rounded-sm p-1 px-2"
103+
onClick={blocker().reset}
104+
>
105+
NO
106+
</button>
107+
</div>
108+
)}
109+
<Outlet />
110+
<TanStackRouterDevtools position="bottom-right" />
111+
</>
112+
)
113+
}
114+
115+
const indexRoute = createRoute({
116+
getParentRoute: () => rootRoute,
117+
path: '/',
118+
component: IndexComponent,
119+
})
120+
121+
function IndexComponent() {
122+
return (
123+
<div class="p-2">
124+
<h3>Welcome Home!</h3>
125+
</div>
126+
)
127+
}
128+
129+
const fooRoute = createRoute({
130+
getParentRoute: () => rootRoute,
131+
path: 'foo/$id',
132+
validateSearch: (search) => ({ hello: search.hello }) as { hello: string },
133+
component: () => <>foo {fooRoute.useParams()().id}</>,
134+
})
135+
136+
const editor1Route = createRoute({
137+
getParentRoute: () => rootRoute,
138+
path: 'editor-1',
139+
component: Editor1Component,
140+
})
141+
142+
function Editor1Component() {
143+
const [value, setValue] = createSignal('')
144+
145+
// Block leaving editor-1 if there is text in the input
146+
const blocker = useBlocker({
147+
shouldBlockFn: () => value() !== '',
148+
enableBeforeUnload: () => value() !== '',
149+
withResolver: true,
150+
})
151+
152+
return (
153+
<div class="flex flex-col p-2">
154+
<h3>Editor 1</h3>
155+
<div>
156+
<input
157+
value={value()}
158+
onInput={(e) => setValue(e.currentTarget.value)}
159+
class="border"
160+
/>
161+
</div>
162+
<hr class="m-2" />
163+
<Link to="/editor-1/editor-2">Go to Editor 2</Link>
164+
<Outlet />
165+
166+
{blocker().status === 'blocked' && (
167+
<div class="mt-2">
168+
<div>Are you sure you want to leave editor 1?</div>
169+
<div>
170+
You are going from {blocker().current?.pathname} to{' '}
171+
{blocker().next?.pathname}
172+
</div>
173+
<button
174+
class="bg-lime-500 text-white rounded-sm p-1 px-2 mr-2"
175+
onClick={blocker().proceed}
176+
>
177+
YES
178+
</button>
179+
<button
180+
class="bg-red-500 text-white rounded-sm p-1 px-2"
181+
onClick={blocker().reset}
182+
>
183+
NO
184+
</button>
185+
</div>
186+
)}
187+
</div>
188+
)
189+
}
190+
191+
const editor2Route = createRoute({
192+
getParentRoute: () => editor1Route,
193+
path: 'editor-2',
194+
component: Editor2Component,
195+
})
196+
197+
function Editor2Component() {
198+
const [value, setValue] = createSignal('')
199+
200+
return (
201+
<div class="p-2">
202+
<h3>Editor 2</h3>
203+
<input
204+
value={value()}
205+
onInput={(e) => setValue(e.currentTarget.value)}
206+
class="border"
207+
/>
208+
</div>
209+
)
210+
}
211+
212+
const routeTree = rootRoute.addChildren([
213+
indexRoute,
214+
fooRoute,
215+
editor1Route.addChildren([editor2Route]),
216+
])
217+
218+
// Set up a Router instance
219+
const router = createRouter({
220+
routeTree,
221+
defaultPreload: 'intent',
222+
scrollRestoration: true,
223+
})
224+
225+
const rootElement = document.getElementById('app')!
226+
227+
if (!rootElement.innerHTML) {
228+
render(() => <RouterProvider router={router} />, rootElement)
229+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@import 'tailwindcss';
2+
3+
@layer base {
4+
*,
5+
::after,
6+
::before,
7+
::backdrop,
8+
::file-selector-button {
9+
border-color: var(--color-gray-200, currentcolor);
10+
}
11+
}
12+
13+
html {
14+
color-scheme: light dark;
15+
}
16+
* {
17+
@apply border-gray-200 dark:border-gray-800;
18+
}
19+
body {
20+
@apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
21+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"esModuleInterop": true,
5+
"jsx": "preserve",
6+
"jsxImportSource": "solid-js",
7+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
8+
"skipLibCheck": true
9+
}
10+
}

0 commit comments

Comments
 (0)