@dotvoid/stacked-router 1.0.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.
- package/README.md +594 -0
- package/dist/components/DefaultLayout.d.ts +4 -0
- package/dist/components/DefaultLayout.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +22 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorResolver.d.ts +9 -0
- package/dist/components/ErrorResolver.d.ts.map +1 -0
- package/dist/components/Link.d.ts +29 -0
- package/dist/components/Link.d.ts.map +1 -0
- package/dist/components/Slots.d.ts +14 -0
- package/dist/components/Slots.d.ts.map +1 -0
- package/dist/components/StackedView.d.ts +6 -0
- package/dist/components/StackedView.d.ts.map +1 -0
- package/dist/components/StackedViewGroup.d.ts +6 -0
- package/dist/components/StackedViewGroup.d.ts.map +1 -0
- package/dist/components/VoidViews.d.ts +6 -0
- package/dist/components/VoidViews.d.ts.map +1 -0
- package/dist/contexts/RouterContext.d.ts +27 -0
- package/dist/contexts/RouterContext.d.ts.map +1 -0
- package/dist/contexts/RouterProvider.d.ts +11 -0
- package/dist/contexts/RouterProvider.d.ts.map +1 -0
- package/dist/contexts/SlotContext.d.ts +6 -0
- package/dist/contexts/SlotContext.d.ts.map +1 -0
- package/dist/contexts/SlotProvider.d.ts +4 -0
- package/dist/contexts/SlotProvider.d.ts.map +1 -0
- package/dist/contexts/ViewContext.d.ts +17 -0
- package/dist/contexts/ViewContext.d.ts.map +1 -0
- package/dist/contexts/ViewProvider.d.ts +10 -0
- package/dist/contexts/ViewProvider.d.ts.map +1 -0
- package/dist/hooks/useHref.d.ts +14 -0
- package/dist/hooks/useHref.d.ts.map +1 -0
- package/dist/hooks/useNavigate.d.ts +19 -0
- package/dist/hooks/useNavigate.d.ts.map +1 -0
- package/dist/hooks/useOpenViews.d.ts +25 -0
- package/dist/hooks/useOpenViews.d.ts.map +1 -0
- package/dist/hooks/usePrevious.d.ts +2 -0
- package/dist/hooks/usePrevious.d.ts.map +1 -0
- package/dist/hooks/useRouter.d.ts +2 -0
- package/dist/hooks/useRouter.d.ts.map +1 -0
- package/dist/hooks/useView.d.ts +2 -0
- package/dist/hooks/useView.d.ts.map +1 -0
- package/dist/hooks/useViewStack.d.ts +18 -0
- package/dist/hooks/useViewStack.d.ts.map +1 -0
- package/dist/index.es.js +1112 -0
- package/dist/index.umd.js +22 -0
- package/dist/lib/RouterRegistry.d.ts +57 -0
- package/dist/lib/RouterRegistry.d.ts.map +1 -0
- package/dist/lib/allocation.d.ts +23 -0
- package/dist/lib/allocation.d.ts.map +1 -0
- package/dist/lib/events.d.ts +4 -0
- package/dist/lib/events.d.ts.map +1 -0
- package/dist/lib/history.d.ts +34 -0
- package/dist/lib/history.d.ts.map +1 -0
- package/dist/lib/href.d.ts +23 -0
- package/dist/lib/href.d.ts.map +1 -0
- package/dist/lib/mapRoutes.d.ts +6 -0
- package/dist/lib/mapRoutes.d.ts.map +1 -0
- package/dist/lib/viewsAreEqual.d.ts +8 -0
- package/dist/lib/viewsAreEqual.d.ts.map +1 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.d.ts.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
# stacked-router
|
|
2
|
+
|
|
3
|
+
A client side only, file based, router with path mapping to _"view component_ props. Treats opened views as a stack of cards. When a new view (route) is opened this view is added to the top of the stack. As long as enough screen estate is available for all open views they can be displayed side by side. If not the leftmost is hidden until on smaller screens only one view is visible.
|
|
4
|
+
|
|
5
|
+
The router maintains the browser history and navigation and include utilities to integrate with modern UI libraries navigation. The browser location always matches the focused view which allows links to individual views to be copied and shared.
|
|
6
|
+
|
|
7
|
+
## Basic concepts
|
|
8
|
+
|
|
9
|
+
### Routing
|
|
10
|
+
Routing is the process of mapping a URL to a view component (file) and its props. The router provides a `RouterProvider` component that takes a `config` prop which is an array of route definitions, preferrably mapped from the file structure. Each route definition is an object with a `path` and `component` property. The `path` property is a string that defines the URL path for the route. The `component` property is a React component that will be rendered when the route is matched.
|
|
11
|
+
|
|
12
|
+
Supports a `basePath` property that can be used to automatically prefix all paths in the router navigation.
|
|
13
|
+
|
|
14
|
+
Supports external routes that are loaded from remote urls. These routes are loaded asynchronously and can be used to load content from external sources. External routes can be defined using the `external` property in the route definition. The `external` property is a string that defines the URL for the external route.
|
|
15
|
+
|
|
16
|
+
### Stacked views
|
|
17
|
+
Allow placing multiple views side by side on large screens but still degrade gracefully on smaller screens (mobile). Mobile friendly should also be large desktop screen friendly.
|
|
18
|
+
|
|
19
|
+
Smaller screens/viewports stacks views on top of each other, hiding views that don't receive as much space as they want..
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
..........
|
|
23
|
+
. ..........
|
|
24
|
+
. . ..........
|
|
25
|
+
. . . __________
|
|
26
|
+
. . . | |
|
|
27
|
+
... . | |
|
|
28
|
+
... | |
|
|
29
|
+
..| |
|
|
30
|
+
----------
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Based on the minimum screen estate each view require, a larger screen/viewport could display more views if possible. Open views that do not receive enough screen estate will be hidden.
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
..........
|
|
37
|
+
. ---------- ---------- ----------
|
|
38
|
+
. | | | | | |
|
|
39
|
+
. | | | | | |
|
|
40
|
+
. | | | | | |
|
|
41
|
+
..| | | | | |
|
|
42
|
+
---------- ---------- ----------
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Void views
|
|
46
|
+
|
|
47
|
+
Void views are rendered outside the stack. These views do not take up any space in the stack. This is useful for displaying a view in a modal, sheet or popup.
|
|
48
|
+
|
|
49
|
+
### Layouts
|
|
50
|
+
|
|
51
|
+
Layouts are used to define the layout of the views in the stack. Layouts can be nested to create more complex layouts. This means that a layout further down the tree is rendered inside a layout further up in the tree.
|
|
52
|
+
|
|
53
|
+
Default layouts are named `_layout.tsx`.
|
|
54
|
+
|
|
55
|
+
It is also possible to define named, or keyed, layouts. When navigating to a view, the layouts with the matching name will be used. If no layout with the matching name is found, any default layouts will be used.
|
|
56
|
+
|
|
57
|
+
Named layouts are named `_layout.name.tsx`.
|
|
58
|
+
|
|
59
|
+
### Slots
|
|
60
|
+
|
|
61
|
+
Layouts support defining named slots. This allows view components to render content which is automatically rendered inside the layout instead. This allow for styling of for example headers and footers in the layout while still allowing the views to have the logic and decide what should be rendered these slots.
|
|
62
|
+
|
|
63
|
+
### Error Boundaries
|
|
64
|
+
|
|
65
|
+
Stacked router includes automatic error boundaries around each view that catch rendering errors and display contextual error components.
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### Basic setup
|
|
70
|
+
|
|
71
|
+
**main.tsx**
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { mapRoutes } from 'stacked-router'
|
|
75
|
+
import { RouterProvider } from 'stacked-router'
|
|
76
|
+
|
|
77
|
+
const modules = import.meta.glob('./views/**/*.tsx', { eager: true })
|
|
78
|
+
const config = mapRoutes(modules, './views')
|
|
79
|
+
|
|
80
|
+
createRoot(document.getElementById('root')!).render(
|
|
81
|
+
<StrictMode>
|
|
82
|
+
<RouterProvider config={ config }>
|
|
83
|
+
<App />
|
|
84
|
+
</RouterProvider>
|
|
85
|
+
</StrictMode>,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**App.tsx**
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { StackedViewGroup } from 'stacked-router'
|
|
94
|
+
|
|
95
|
+
export function App() {
|
|
96
|
+
return (
|
|
97
|
+
<div ref={grid} className='w-screen h-screen relative'>
|
|
98
|
+
{/* Stacked views rendered here */}
|
|
99
|
+
<StackedViewGroup className={`flex content-stretch h-screen overflow-hidden`} />
|
|
100
|
+
|
|
101
|
+
{/* Standalone views rendered here */}
|
|
102
|
+
<VoidViews />
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**_layout.tsx**
|
|
109
|
+
|
|
110
|
+
Layouts are optional and are automatically wrapped around all views at the same level or below it in the file tree. This layout handles active state and width css using tailwind.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { type PropsWithChildren } from 'react'
|
|
114
|
+
import { StackedView } from 'stacked-router'
|
|
115
|
+
import { useView } from 'stacked-router'
|
|
116
|
+
import { cva } from 'class-variance-authority'
|
|
117
|
+
import { cn } from '@/lib/cn'
|
|
118
|
+
import View from '@/components/View'
|
|
119
|
+
|
|
120
|
+
export default function Layout({ children }: PropsWithChildren) {
|
|
121
|
+
const { width, isActive } = useView()
|
|
122
|
+
const stackedView = cva('h-full grow transition-all group/view', {
|
|
123
|
+
variants: {
|
|
124
|
+
isActive: {
|
|
125
|
+
true: 'is-active',
|
|
126
|
+
false: 'border-s-1 border-s-foreground-300 [.is-active+&]:border-s-background'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<StackedView className={cn(stackedView({ isActive }))} style={{ flexBasis: `${width}vw` }}>
|
|
133
|
+
<View.Root>
|
|
134
|
+
{children}
|
|
135
|
+
</View.Root>
|
|
136
|
+
</StackedView>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**users/[id].tsx**
|
|
142
|
+
|
|
143
|
+
File names with the structure `[param].tsx` automatically receives `param` in the url as props. As so the url `/users/10104` can be mapped to the below view component.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
function PlanningItem({ id }: { id: string }) {
|
|
147
|
+
const user = useUser(id)
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div>
|
|
151
|
+
{user?.name ?? ''}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Mapping file structure
|
|
158
|
+
|
|
159
|
+
Use RouterProvider to store all client side routes. Either through a config or by mapping a directory structure.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { mapRoutes } from 'stacked-router'
|
|
163
|
+
import { RouterProvider } from 'stacked-router'
|
|
164
|
+
|
|
165
|
+
const modules = import.meta.glob('./views/**/*.tsx', { eager: true })
|
|
166
|
+
const config = mapRoutes(modules, './views')
|
|
167
|
+
|
|
168
|
+
createRoot(document.getElementById('root')!).render(
|
|
169
|
+
<StrictMode>
|
|
170
|
+
<RouterProvider config={ config }>
|
|
171
|
+
<App />
|
|
172
|
+
</RouterProvider>
|
|
173
|
+
</StrictMode>,
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Example view file structure**
|
|
178
|
+
|
|
179
|
+
A simple file structure with one default view (`views/index.tsx`) and one global layout (`_layout.tsx`) file used for all views. The `plannings/` have one default view that lists plannings and one specific view that opens one planning.
|
|
180
|
+
|
|
181
|
+
All directories prefixed with underscore (`_`) are ignored. So view specific components are placed in `_component/` directories.
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
views/
|
|
185
|
+
plannings/
|
|
186
|
+
_components/
|
|
187
|
+
Assignment.tsx
|
|
188
|
+
AssigmentAction.tsx
|
|
189
|
+
[id].tsx
|
|
190
|
+
index.tsx
|
|
191
|
+
_layout.tsx
|
|
192
|
+
_layout.dialog.tsx
|
|
193
|
+
index.tsx
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Example view - `views/plannings/[id].tsx`**
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
const meta = {
|
|
200
|
+
breakpoints: [
|
|
201
|
+
{
|
|
202
|
+
breakpoint: 720,
|
|
203
|
+
minVw: 50
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
breakpoint: 1024,
|
|
207
|
+
minVw: 30
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
breakpoint: 1280,
|
|
211
|
+
minVw: 20
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function PlanningItem({ id }: { id: string }) {
|
|
217
|
+
return (
|
|
218
|
+
<div>
|
|
219
|
+
Planning item
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
PlanningItem.meta = meta
|
|
225
|
+
export default PlanningItem
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The `mapRoutes()` creates a route configuration including minimum view width requirements for each breakpoint based on the meta data in each view file.
|
|
229
|
+
|
|
230
|
+
_The configuration includes the actual components which is not visible below, which is why the layout seems empty._
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"routes": [
|
|
235
|
+
{
|
|
236
|
+
"path": "/",
|
|
237
|
+
"meta": {
|
|
238
|
+
"breakpoints": [
|
|
239
|
+
{
|
|
240
|
+
"breakpoint": 1024,
|
|
241
|
+
"minVw": 50
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"breakpoint": 1280,
|
|
245
|
+
"minVw": 33
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"path": "/plannings/[id]",
|
|
252
|
+
"meta": {
|
|
253
|
+
"breakpoints": [
|
|
254
|
+
{
|
|
255
|
+
"breakpoint": 720,
|
|
256
|
+
"minVw": 50
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"breakpoint": 1024,
|
|
260
|
+
"minVw": 30
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"breakpoint": 1280,
|
|
264
|
+
"minVw": 20
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"path": "/plannings",
|
|
271
|
+
"meta": {
|
|
272
|
+
"breakpoints": [
|
|
273
|
+
{
|
|
274
|
+
"breakpoint": 1024,
|
|
275
|
+
"minVw": 50
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"breakpoint": 1280,
|
|
279
|
+
"minVw": 33
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
"layouts": {}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Integrating with UI libraries
|
|
290
|
+
|
|
291
|
+
Stacked router, as most router libraries, expose hooks to allow better integration with (some) UI libraries that can be configured to use the router mechanism inside its UI components like tabs, listboxes, buttons etc. The hook `useNavigate()` handles client-side navigation and `useHref()` can translate router hrefs to native HTML hrefs. Example below is based on HeroUI.
|
|
292
|
+
|
|
293
|
+
```jsx
|
|
294
|
+
import { StackedViewGroup } from 'stacked-router'
|
|
295
|
+
import { useHref, useNavigate } from 'stacked-router/hooks'
|
|
296
|
+
import { HeroUIProvider } from '@heroui/react'
|
|
297
|
+
|
|
298
|
+
export function App() {
|
|
299
|
+
const navigate = useNavigate()
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
|
303
|
+
<div ref={grid} className='w-screen h-screen relative'>
|
|
304
|
+
<StackedViewGroup className={`flex content-stretch h-screen overflow-hidden`} />
|
|
305
|
+
</div>
|
|
306
|
+
</HeroUIProvider>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Then it is a simple matter of using the UI libraries components for navigation.
|
|
312
|
+
|
|
313
|
+
**Link component example**
|
|
314
|
+
|
|
315
|
+
```jsx
|
|
316
|
+
import { Link } from '@heroui/react'
|
|
317
|
+
|
|
318
|
+
<Link href='/plannings/234'>Planning item nr 234</Link>
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Dropdown menu example**
|
|
322
|
+
|
|
323
|
+
```jsx
|
|
324
|
+
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger } from "@heroui/react";
|
|
325
|
+
import { Ellipsis } from 'lucide-react'
|
|
326
|
+
|
|
327
|
+
<Dropdown>
|
|
328
|
+
<DropdownTrigger>
|
|
329
|
+
<Button isIconOnly size="sm" variant="light">
|
|
330
|
+
<Ellipsis size={18} />
|
|
331
|
+
</Button>
|
|
332
|
+
</DropdownTrigger>
|
|
333
|
+
<DropdownMenu>
|
|
334
|
+
<DropdownSection showDivider>
|
|
335
|
+
<DropdownItem key='open' href={`/plannings/234`}>
|
|
336
|
+
Planning item nr 234
|
|
337
|
+
</DropdownItem>
|
|
338
|
+
</DropdownSection>
|
|
339
|
+
</DropdownMenu>
|
|
340
|
+
</Dropdown>
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**shadcn example**
|
|
345
|
+
|
|
346
|
+
Integration with shadcn is different as it does not provide the same convenience context. Most shadcn components that need navigation (like Button, NavigationMenu) accept an `asChild` prop which makes it easy to wrap the stacked router `Link` component.
|
|
347
|
+
|
|
348
|
+
```jsx
|
|
349
|
+
import { Button } from '@/components/ui/button'
|
|
350
|
+
import { Link } from 'stacked-router'
|
|
351
|
+
|
|
352
|
+
<Button asChild>
|
|
353
|
+
<Link to="/planning/234">Planning item nr 234</Link>
|
|
354
|
+
</Button>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Custom navigation
|
|
358
|
+
|
|
359
|
+
For more custom ways of navigating to another view, the hook `useNavigate()`, can be used. It allows sending _invisible_ props (params not visible in URL), specifying a specific layout or that the view should be rendered as a void view (outside of the stack).
|
|
360
|
+
|
|
361
|
+
The same can be achieved by using the `<Link />` component included in the `stacked-router` package.
|
|
362
|
+
|
|
363
|
+
```jsx
|
|
364
|
+
import { useNavigate } from 'stacked-router'
|
|
365
|
+
|
|
366
|
+
const navigate = useNavigate()
|
|
367
|
+
|
|
368
|
+
<button onPress={() => {
|
|
369
|
+
navigate('/planning/234', {
|
|
370
|
+
options: {
|
|
371
|
+
fromEvent: '3433'
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
}}>
|
|
375
|
+
Navigate to planning item nr 234
|
|
376
|
+
</button>
|
|
377
|
+
|
|
378
|
+
<button onPress={() => {
|
|
379
|
+
navigate('/planning/' + crypto.randomUUID(), {
|
|
380
|
+
options: {
|
|
381
|
+
fromEvent: '3433',
|
|
382
|
+
layout: 'dialog',
|
|
383
|
+
target: '_void'
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
}}>
|
|
387
|
+
Create new planning item
|
|
388
|
+
</button>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### useView()
|
|
392
|
+
|
|
393
|
+
Used to get query parameter, props, layout or update query parameters or props or if a named layout is used.
|
|
394
|
+
|
|
395
|
+
```jsx
|
|
396
|
+
import { useView } from 'stacked-router'
|
|
397
|
+
|
|
398
|
+
const { props, setProps, queryParams, setQueryParams, layout } = useView()
|
|
399
|
+
|
|
400
|
+
<p>
|
|
401
|
+
{layout
|
|
402
|
+
? layout
|
|
403
|
+
: 'No layout or default layout'
|
|
404
|
+
}
|
|
405
|
+
</p>
|
|
406
|
+
|
|
407
|
+
<button onPress={() => {
|
|
408
|
+
// Add one prop
|
|
409
|
+
setProps({ created: true})
|
|
410
|
+
}}>
|
|
411
|
+
Is created
|
|
412
|
+
</button>
|
|
413
|
+
|
|
414
|
+
<button onPress={() => {
|
|
415
|
+
// Set all (clear all) query parameters
|
|
416
|
+
setQueryParams({}, true)
|
|
417
|
+
}}>
|
|
418
|
+
Is created
|
|
419
|
+
</button>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### useOpenViews()
|
|
423
|
+
|
|
424
|
+
Can be used to get information on a subset of open views that match a given criteria. It is only possible to find views that have set the type in the view meta information. Useful to be able to mark rows that match an open view in a table or listing.
|
|
425
|
+
|
|
426
|
+
Example view:
|
|
427
|
+
|
|
428
|
+
```jsx
|
|
429
|
+
const meta: ViewMetadata = {
|
|
430
|
+
type: 'customer'
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function Customer({ id }: { id: string }) {
|
|
434
|
+
return <div>Example component</div>
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
Customer.meta = meta
|
|
438
|
+
export default Customer
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Usage in other views:
|
|
442
|
+
|
|
443
|
+
```jsx
|
|
444
|
+
import { useOpenViews } from 'stacked-router'
|
|
445
|
+
|
|
446
|
+
export function CustomerList() {
|
|
447
|
+
// Get all customers
|
|
448
|
+
const customers = useCustomers()
|
|
449
|
+
|
|
450
|
+
// Get all open customer views
|
|
451
|
+
const customerViews = useOpenViews('customer')
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<div>
|
|
455
|
+
{customers.map(c => (
|
|
456
|
+
<div
|
|
457
|
+
key={c.id}
|
|
458
|
+
className={customerViews.includes(c.id) ? 'active' : ''}
|
|
459
|
+
>
|
|
460
|
+
{c.id}: {c.name}
|
|
461
|
+
</div>
|
|
462
|
+
))}
|
|
463
|
+
</div>
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
It is also possible to match against params (url path params).
|
|
469
|
+
```jsx
|
|
470
|
+
const specificView = useOpenViews('article', {
|
|
471
|
+
categoryId: 'news',
|
|
472
|
+
articleId: '456'
|
|
473
|
+
})
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Using layout slots
|
|
477
|
+
|
|
478
|
+
A slot is defined in the layout using the component `<Outlet/>` and filled with content in the view using the component `<Fill/>`. The prop `name` is used to identify the slot.
|
|
479
|
+
|
|
480
|
+
The example below is a basic HeroUI Modal displayed in a void view (not in the normal stack). Note how the enabled state of the button can be maintained in the view but still rendered in the footer slot styled by the layout.
|
|
481
|
+
|
|
482
|
+
The layout could be used by many view components but keep all styling of the header and footer in the layout. The order of the slots in the view component is not important.
|
|
483
|
+
|
|
484
|
+
_Also shows how to close a void view, in this case when when the modal closes._
|
|
485
|
+
|
|
486
|
+
**View component**
|
|
487
|
+
|
|
488
|
+
```jsx
|
|
489
|
+
import { Fill } from 'stacked-router'
|
|
490
|
+
|
|
491
|
+
export default function User() {
|
|
492
|
+
const [disabled, setDisabled] = useState(false)
|
|
493
|
+
const userName = 'John Doe'
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<>
|
|
497
|
+
<Fill slot='header'>
|
|
498
|
+
{userName}
|
|
499
|
+
</Fill>
|
|
500
|
+
|
|
501
|
+
<p>User content here</p>
|
|
502
|
+
|
|
503
|
+
<Fill slot='footer'>
|
|
504
|
+
<button disabled={disabled}>Save</button>
|
|
505
|
+
</Fill>
|
|
506
|
+
</>
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Dialog layout**
|
|
512
|
+
|
|
513
|
+
```jsx
|
|
514
|
+
import {
|
|
515
|
+
Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure,
|
|
516
|
+
} from '@heroui/react'
|
|
517
|
+
import { useEffect } from 'react'
|
|
518
|
+
import { useView, Outlet } from 'stacked-router'
|
|
519
|
+
|
|
520
|
+
export default function DialogLayout({ children }: {
|
|
521
|
+
children: React.ReactNode
|
|
522
|
+
}) {
|
|
523
|
+
const {isOpen, onOpen, onClose} = useDisclosure()
|
|
524
|
+
const { close } = useView()
|
|
525
|
+
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
onOpen()
|
|
528
|
+
}, [onOpen])
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<>
|
|
533
|
+
<Modal
|
|
534
|
+
isOpen={isOpen} size='2xl' className='max-h-4/5' onClose={onClose}
|
|
535
|
+
onOpenChange={(isOpen) => {
|
|
536
|
+
if (!isOpen) {
|
|
537
|
+
close()
|
|
538
|
+
}
|
|
539
|
+
}}
|
|
540
|
+
hideCloseButton={true}
|
|
541
|
+
>
|
|
542
|
+
<ModalContent>
|
|
543
|
+
<ModalHeader className='flex flex-row gap-4 border-b border-gray-200'>
|
|
544
|
+
<Outlet slot='header' />{/* Header content from the view */}
|
|
545
|
+
</ModalHeader>
|
|
546
|
+
|
|
547
|
+
<ModalBody className='overflow-y-scroll p-0'>
|
|
548
|
+
{/* All and any User view content is displayed here */}
|
|
549
|
+
{children}
|
|
550
|
+
</ModalBody>
|
|
551
|
+
|
|
552
|
+
<ModalFooter className='border-t border-gray-200 flex justify-end gap-4'>
|
|
553
|
+
<Outlet slot='footer' />{/* Footer content from the view */}
|
|
554
|
+
</ModalFooter>
|
|
555
|
+
</ModalContent>
|
|
556
|
+
</Modal>
|
|
557
|
+
</>
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Error components
|
|
563
|
+
|
|
564
|
+
Create `_error.tsx` files to handle errors at different levels of your application. The router finds the closest error component up the directory tree.
|
|
565
|
+
|
|
566
|
+
**_error.tsx**
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
import type { ErrorComponentProps } from 'stacked-router'
|
|
570
|
+
|
|
571
|
+
export default function MyError({ error, reset }: ErrorComponentProps) {
|
|
572
|
+
return (
|
|
573
|
+
<div>
|
|
574
|
+
<h2>Something went wrong</h2>
|
|
575
|
+
<p>{error.message}</p>
|
|
576
|
+
<button onClick={reset}>Try again</button>
|
|
577
|
+
</div>
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**File structure example**
|
|
583
|
+
|
|
584
|
+
```
|
|
585
|
+
views/
|
|
586
|
+
_error.tsx # Root fallback for all views
|
|
587
|
+
users/
|
|
588
|
+
_error.tsx # Specific to user section
|
|
589
|
+
index.tsx # Uses users/_error.tsx
|
|
590
|
+
plannings/
|
|
591
|
+
index.tsx # Uses root _error.tsx
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
The `reset` function clears the error and re-renders the component. Error boundaries only catch rendering errors, not errors in event handlers or async code.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DefaultLayout.d.ts","sourceRoot":"","sources":["../../lib/components/DefaultLayout.tsx"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAC1C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B,2CAMA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
children?: ReactNode;
|
|
4
|
+
viewUrl: string;
|
|
5
|
+
error?: Error;
|
|
6
|
+
errorCode?: number;
|
|
7
|
+
}
|
|
8
|
+
interface State {
|
|
9
|
+
hasError: boolean;
|
|
10
|
+
errorCode?: number;
|
|
11
|
+
error?: Error;
|
|
12
|
+
errorInfo?: ErrorInfo;
|
|
13
|
+
}
|
|
14
|
+
export declare class ErrorBoundary extends Component<Props, State> {
|
|
15
|
+
constructor(props: Props);
|
|
16
|
+
static getDerivedStateFromError(error: Error): State;
|
|
17
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
18
|
+
reset: () => void;
|
|
19
|
+
render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import('react').ReactPortal | import('react').ReactElement<unknown, string | import('react').JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=ErrorBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ErrorBoundary.d.ts","sourceRoot":"","sources":["../../lib/components/ErrorBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGvD,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,UAAU,KAAK;IACb,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAED,qBAAa,aAAc,SAAQ,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;gBAC5C,KAAK,EAAE,KAAK;IASxB,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK;IAIpD,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;IAMpD,KAAK,aAEJ;IAED,MAAM;CAaP"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ErrorInfo } from 'react';
|
|
2
|
+
export declare function ErrorResolver({ viewUrl, error, errorCode, errorInfo, reset }: {
|
|
3
|
+
viewUrl: string;
|
|
4
|
+
error: Error;
|
|
5
|
+
errorCode?: number;
|
|
6
|
+
errorInfo?: ErrorInfo;
|
|
7
|
+
reset: () => void;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=ErrorResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ErrorResolver.d.ts","sourceRoot":"","sources":["../../lib/components/ErrorResolver.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAIjC,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,KAAK,EACL,SAAS,EACT,SAAS,EACT,KAAK,EACN,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,KAAK,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB,2CAiFA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PropsWithChildren } from 'react';
|
|
2
|
+
interface LinkProps {
|
|
3
|
+
href?: string;
|
|
4
|
+
query?: Record<string, string | number | boolean>;
|
|
5
|
+
props?: Record<string, string | number | boolean>;
|
|
6
|
+
layout?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
target?: '_self' | '_top' | '_blank' | '_void';
|
|
9
|
+
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Link component allows giving parameters in two ways, as part of the query string
|
|
13
|
+
* in the href or as a query key/value object. They will always be combined to a url.
|
|
14
|
+
*
|
|
15
|
+
* Automatically adds rel="noopener noreferrer" to all external links.
|
|
16
|
+
* Props can be used to send arbitrary data to a view.
|
|
17
|
+
*
|
|
18
|
+
* ATTENTION:
|
|
19
|
+
*
|
|
20
|
+
* If using HeroUI, prefer using HeroUI Link component. Utilise the hooks useHref()
|
|
21
|
+
* and useNavigate() to provide HeroUIProvider the necessary components.
|
|
22
|
+
*
|
|
23
|
+
* CAVEAT: Props is not persisted in any way and thus only used in first load of a view.
|
|
24
|
+
* When using target _blank or CMD/CTRL to open a view in a new window props will
|
|
25
|
+
* not be passed to the view.
|
|
26
|
+
*/
|
|
27
|
+
export declare function Link({ href, query, children, className, target, onClick, props, layout }: PropsWithChildren & LinkProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=Link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../lib/components/Link.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAA;AAKzC,UAAU,SAAS;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;IACjD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;IAC9C,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;CAC3D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,KAAU,EACV,QAAQ,EACR,SAAS,EACT,MAAgB,EAChB,OAAO,EACP,KAAK,EACL,MAAM,EACP,EAAE,iBAAiB,GAAG,SAAS,2CAuC/B"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use in layout to render slot content
|
|
3
|
+
*/
|
|
4
|
+
export declare function Outlet({ slot }: {
|
|
5
|
+
slot: string;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
/**
|
|
8
|
+
* Use in view component to define content for a slot
|
|
9
|
+
*/
|
|
10
|
+
export declare function Fill({ slot, children }: {
|
|
11
|
+
slot: string;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}): null;
|
|
14
|
+
//# sourceMappingURL=Slots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Slots.d.ts","sourceRoot":"","sources":["../../lib/components/Slots.tsx"],"names":[],"mappings":"AAIA;;GAEG;AACH,wBAAgB,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,2CAKhD;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B,QAUA"}
|