@furystack/shades 13.2.2 → 14.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.
Files changed (111) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/esm/components/define-nested-routes.d.ts +27 -0
  3. package/esm/components/define-nested-routes.d.ts.map +1 -0
  4. package/esm/components/define-nested-routes.js +26 -0
  5. package/esm/components/define-nested-routes.js.map +1 -0
  6. package/esm/components/define-nested-routes.spec.d.ts +2 -0
  7. package/esm/components/define-nested-routes.spec.d.ts.map +1 -0
  8. package/esm/components/define-nested-routes.spec.js +81 -0
  9. package/esm/components/define-nested-routes.spec.js.map +1 -0
  10. package/esm/components/index.d.ts +3 -3
  11. package/esm/components/index.d.ts.map +1 -1
  12. package/esm/components/index.js +3 -3
  13. package/esm/components/index.js.map +1 -1
  14. package/esm/components/nested-hooks.d.ts +62 -0
  15. package/esm/components/nested-hooks.d.ts.map +1 -0
  16. package/esm/components/nested-hooks.js +73 -0
  17. package/esm/components/nested-hooks.js.map +1 -0
  18. package/esm/components/nested-hooks.spec.d.ts +2 -0
  19. package/esm/components/nested-hooks.spec.d.ts.map +1 -0
  20. package/esm/components/nested-hooks.spec.js +211 -0
  21. package/esm/components/nested-hooks.spec.js.map +1 -0
  22. package/esm/components/nested-navigate.d.ts +52 -21
  23. package/esm/components/nested-navigate.d.ts.map +1 -1
  24. package/esm/components/nested-navigate.js +32 -17
  25. package/esm/components/nested-navigate.js.map +1 -1
  26. package/esm/components/nested-navigate.spec.js +102 -12
  27. package/esm/components/nested-navigate.spec.js.map +1 -1
  28. package/esm/components/nested-replace.d.ts +36 -0
  29. package/esm/components/nested-replace.d.ts.map +1 -0
  30. package/esm/components/nested-replace.js +38 -0
  31. package/esm/components/nested-replace.js.map +1 -0
  32. package/esm/components/nested-replace.spec.d.ts +2 -0
  33. package/esm/components/nested-replace.spec.d.ts.map +1 -0
  34. package/esm/components/nested-replace.spec.js +59 -0
  35. package/esm/components/nested-replace.spec.js.map +1 -0
  36. package/esm/components/nested-route-link.d.ts +36 -26
  37. package/esm/components/nested-route-link.d.ts.map +1 -1
  38. package/esm/components/nested-route-link.js +17 -19
  39. package/esm/components/nested-route-link.js.map +1 -1
  40. package/esm/components/nested-route-link.spec.js +61 -11
  41. package/esm/components/nested-route-link.spec.js.map +1 -1
  42. package/esm/components/nested-route-types.d.ts +79 -2
  43. package/esm/components/nested-route-types.d.ts.map +1 -1
  44. package/esm/components/nested-router.d.ts +77 -7
  45. package/esm/components/nested-router.d.ts.map +1 -1
  46. package/esm/components/nested-router.js +137 -11
  47. package/esm/components/nested-router.js.map +1 -1
  48. package/esm/components/nested-router.spec.js +265 -34
  49. package/esm/components/nested-router.spec.js.map +1 -1
  50. package/esm/services/location-service.spec.js +33 -0
  51. package/esm/services/location-service.spec.js.map +1 -1
  52. package/esm/services/route-match-service.spec.js +4 -0
  53. package/esm/services/route-match-service.spec.js.map +1 -1
  54. package/esm/services/route-meta-utils.d.ts +28 -6
  55. package/esm/services/route-meta-utils.d.ts.map +1 -1
  56. package/esm/services/route-meta-utils.js +18 -4
  57. package/esm/services/route-meta-utils.js.map +1 -1
  58. package/esm/services/route-meta-utils.spec.js +57 -2
  59. package/esm/services/route-meta-utils.spec.js.map +1 -1
  60. package/esm/vnode.d.ts +1 -1
  61. package/esm/vnode.js +1 -1
  62. package/package.json +2 -2
  63. package/src/components/define-nested-routes.spec.ts +91 -0
  64. package/src/components/define-nested-routes.ts +27 -0
  65. package/src/components/index.ts +3 -3
  66. package/src/components/nested-hooks.spec.ts +254 -0
  67. package/src/components/nested-hooks.ts +109 -0
  68. package/src/components/nested-navigate.spec.ts +128 -25
  69. package/src/components/nested-navigate.ts +75 -26
  70. package/src/components/nested-replace.spec.ts +88 -0
  71. package/src/components/nested-replace.ts +46 -0
  72. package/src/components/nested-route-link.spec.tsx +84 -16
  73. package/src/components/nested-route-link.tsx +53 -28
  74. package/src/components/nested-route-types.ts +82 -2
  75. package/src/components/nested-router.spec.tsx +316 -33
  76. package/src/components/nested-router.tsx +182 -22
  77. package/src/services/location-service.spec.ts +39 -0
  78. package/src/services/route-match-service.spec.ts +4 -0
  79. package/src/services/route-meta-utils.spec.ts +75 -7
  80. package/src/services/route-meta-utils.ts +44 -15
  81. package/src/vnode.ts +1 -1
  82. package/esm/components/link-to-route.d.ts +0 -10
  83. package/esm/components/link-to-route.d.ts.map +0 -1
  84. package/esm/components/link-to-route.js +0 -16
  85. package/esm/components/link-to-route.js.map +0 -1
  86. package/esm/components/link-to-route.spec.d.ts +0 -2
  87. package/esm/components/link-to-route.spec.d.ts.map +0 -1
  88. package/esm/components/link-to-route.spec.js +0 -30
  89. package/esm/components/link-to-route.spec.js.map +0 -1
  90. package/esm/components/route-link.d.ts +0 -10
  91. package/esm/components/route-link.d.ts.map +0 -1
  92. package/esm/components/route-link.js +0 -25
  93. package/esm/components/route-link.js.map +0 -1
  94. package/esm/components/route-link.spec.d.ts +0 -2
  95. package/esm/components/route-link.spec.d.ts.map +0 -1
  96. package/esm/components/route-link.spec.js +0 -34
  97. package/esm/components/route-link.spec.js.map +0 -1
  98. package/esm/components/router.d.ts +0 -44
  99. package/esm/components/router.d.ts.map +0 -1
  100. package/esm/components/router.js +0 -66
  101. package/esm/components/router.js.map +0 -1
  102. package/esm/components/router.spec.d.ts +0 -2
  103. package/esm/components/router.spec.d.ts.map +0 -1
  104. package/esm/components/router.spec.js +0 -161
  105. package/esm/components/router.spec.js.map +0 -1
  106. package/src/components/link-to-route.spec.tsx +0 -45
  107. package/src/components/link-to-route.tsx +0 -26
  108. package/src/components/route-link.spec.tsx +0 -44
  109. package/src/components/route-link.tsx +0 -29
  110. package/src/components/router.spec.tsx +0 -217
  111. package/src/components/router.tsx +0 -103
@@ -1,217 +0,0 @@
1
- import { Injector } from '@furystack/inject'
2
- import { sleepAsync, usingAsync } from '@furystack/utils'
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
- import { initializeShadeRoot } from '../initialize.js'
5
- import { LocationService } from '../services/location-service.js'
6
- import { createComponent } from '../shade-component.js'
7
- import { flushUpdates } from '../shade.js'
8
- import { RouteLink } from './route-link.js'
9
- import { Router, type Route } from './router.js'
10
-
11
- describe('Router', () => {
12
- beforeEach(() => {
13
- document.body.innerHTML = '<div id="root"></div>'
14
- })
15
- afterEach(() => {
16
- document.body.innerHTML = ''
17
- })
18
-
19
- it('should skip intermediate route when navigating rapidly (latest wins)', async () => {
20
- await usingAsync(new Injector(), async (injector) => {
21
- history.pushState(null, '', '/route-a')
22
-
23
- const callOrder: string[] = []
24
-
25
- const onVisitA = vi.fn(async () => {
26
- callOrder.push('visit-a')
27
- })
28
- const onLeaveA = vi.fn(async () => {
29
- callOrder.push('leave-a')
30
- })
31
- const onVisitB = vi.fn(async () => {
32
- await sleepAsync(200)
33
- callOrder.push('visit-b')
34
- })
35
- const onLeaveB = vi.fn(async () => {
36
- callOrder.push('leave-b')
37
- })
38
- const onVisitC = vi.fn(async () => {
39
- callOrder.push('visit-c')
40
- })
41
-
42
- const routeA: Route = {
43
- url: '/route-a',
44
- component: () => <div id="content">route-a</div>,
45
- onVisit: onVisitA,
46
- onLeave: onLeaveA,
47
- }
48
- const routeB: Route = {
49
- url: '/route-b',
50
- component: () => <div id="content">route-b</div>,
51
- onVisit: onVisitB,
52
- onLeave: onLeaveB,
53
- }
54
- const routeC: Route = {
55
- url: '/route-c',
56
- component: () => <div id="content">route-c</div>,
57
- onVisit: onVisitC,
58
- }
59
-
60
- const rootElement = document.getElementById('root') as HTMLDivElement
61
-
62
- initializeShadeRoot({
63
- injector,
64
- rootElement,
65
- jsxElement: (
66
- <div>
67
- <RouteLink id="go-a" href="/route-a">
68
- a
69
- </RouteLink>
70
- <RouteLink id="go-b" href="/route-b">
71
- b
72
- </RouteLink>
73
- <RouteLink id="go-c" href="/route-c">
74
- c
75
- </RouteLink>
76
- <Router routes={[routeA, routeB, routeC]} />
77
- </div>
78
- ),
79
- })
80
-
81
- const getContent = () => document.getElementById('content')?.innerHTML
82
- const clickOn = (name: string) => document.getElementById(name)?.click()
83
-
84
- // --- Initial load at /route-a ---
85
- await flushUpdates()
86
- expect(getContent()).toBe('route-a')
87
- expect(onVisitA).toHaveBeenCalledTimes(1)
88
-
89
- // --- Rapid navigation: click B then immediately C ---
90
- callOrder.length = 0
91
- clickOn('go-b')
92
- clickOn('go-c')
93
-
94
- // Wait long enough for both transitions to settle (onVisitB has 200ms delay)
95
- await sleepAsync(500)
96
-
97
- // The final destination should be route-c
98
- expect(getContent()).toBe('route-c')
99
- expect(onVisitC).toHaveBeenCalledTimes(1)
100
-
101
- // route-b's onVisit should have been abandoned
102
- expect(callOrder).not.toContain('visit-b')
103
- })
104
- })
105
-
106
- it('Shuld display the loader and completed state', async () => {
107
- await usingAsync(new Injector(), async (injector) => {
108
- history.pushState(null, '', '/')
109
-
110
- const onVisit = vi.fn()
111
- const onLeave = vi.fn()
112
- const onLastLeave = vi.fn()
113
-
114
- const rootElement = document.getElementById('root') as HTMLDivElement
115
-
116
- const onRouteChange = vi.fn()
117
-
118
- injector.getInstance(LocationService).onLocationPathChanged.subscribe(onRouteChange)
119
-
120
- const route1: Route = {
121
- url: '/route-a',
122
- component: () => <div id="content">route-a</div>,
123
- onVisit,
124
- onLeave,
125
- }
126
-
127
- const route2: Route<{ id: string }> = {
128
- url: '/route-b{/:id}',
129
- component: ({ match }) => <div id="content">route-b{match.params.id}</div>,
130
- }
131
-
132
- const route3: Route = {
133
- url: '/route-c',
134
- component: () => <div id="content">route-c</div>,
135
- onLeave: onLastLeave,
136
- }
137
-
138
- const route4 = { url: '/', component: () => <div id="content">home</div> }
139
-
140
- initializeShadeRoot({
141
- injector,
142
- rootElement,
143
- jsxElement: (
144
- <div>
145
- <RouteLink id="home" href="/">
146
- home
147
- </RouteLink>
148
- <RouteLink id="a" href="/route-a">
149
- a
150
- </RouteLink>
151
- <RouteLink id="b" href="/route-b">
152
- b
153
- </RouteLink>
154
- <RouteLink id="b-with-id" href="/route-b/123">
155
- b-with-id
156
- </RouteLink>
157
- <RouteLink id="c" href="/route-c">
158
- c
159
- </RouteLink>
160
- <RouteLink id="x" href="/route-x">
161
- x
162
- </RouteLink>
163
- <Router routes={[route1, route2, route3, route4]} notFound={<div id="content">not found</div>} />
164
- </div>
165
- ),
166
- })
167
-
168
- const getContent = () => document.getElementById('content')?.innerHTML
169
- const getLocation = () => location.pathname
170
-
171
- const clickOn = (name: string) => document.getElementById(name)?.click()
172
-
173
- await flushUpdates()
174
-
175
- expect(getLocation()).toBe('/')
176
- expect(getContent()).toBe('home')
177
-
178
- expect(onVisit).not.toBeCalled()
179
-
180
- clickOn('a')
181
- await flushUpdates()
182
- expect(getContent()).toBe('route-a')
183
- expect(getLocation()).toBe('/route-a')
184
- expect(onRouteChange).toBeCalledTimes(1)
185
- expect(onVisit).toBeCalledTimes(1)
186
-
187
- clickOn('a')
188
- await flushUpdates()
189
- expect(onVisit).toBeCalledTimes(1)
190
- expect(onLeave).not.toBeCalled()
191
-
192
- clickOn('b')
193
- await flushUpdates()
194
- expect(onLeave).toBeCalledTimes(1)
195
-
196
- expect(getContent()).toBe('route-b')
197
- expect(getLocation()).toBe('/route-b')
198
-
199
- clickOn('b-with-id')
200
- await flushUpdates()
201
- expect(getContent()).toBe('route-b123')
202
- expect(getLocation()).toBe('/route-b/123')
203
-
204
- clickOn('c')
205
- await flushUpdates()
206
- expect(getContent()).toBe('route-c')
207
- expect(getLocation()).toBe('/route-c')
208
-
209
- expect(onLastLeave).not.toBeCalled()
210
- clickOn('x')
211
- await flushUpdates()
212
- expect(getContent()).toBe('not found')
213
- expect(getLocation()).toBe('/route-x')
214
- expect(onLastLeave).toBeCalledTimes(1)
215
- })
216
- })
217
- })
@@ -1,103 +0,0 @@
1
- import { ObservableAlreadyDisposedError } from '@furystack/utils'
2
- import type { MatchOptions, MatchResult } from 'path-to-regexp'
3
- import { match } from 'path-to-regexp'
4
- import type { RenderOptions } from '../models/render-options.js'
5
- import { LocationService } from '../services/location-service.js'
6
- import { createComponent } from '../shade-component.js'
7
- import { Shade } from '../shade.js'
8
-
9
- /**
10
- * @deprecated Use NestedRouter instead
11
- */
12
- export interface Route<TMatchResult = unknown> {
13
- url: string
14
- component: (options: {
15
- currentUrl: string
16
- match: MatchResult<TMatchResult extends object ? TMatchResult : object>
17
- }) => JSX.Element
18
- routingOptions?: MatchOptions
19
- onVisit?: (options: RenderOptions<unknown> & { element: JSX.Element }) => Promise<void>
20
- onLeave?: (options: RenderOptions<unknown> & { element: JSX.Element }) => Promise<void>
21
- }
22
-
23
- /**
24
- * @deprecated Use NestedRouterProps instead
25
- */
26
- export interface RouterProps {
27
- style?: CSSStyleDeclaration
28
- routes: Array<Route<any>>
29
- notFound?: JSX.Element
30
- }
31
-
32
- /**
33
- * @deprecated Use NestedRouterState instead
34
- */
35
- export interface RouterState {
36
- activeRoute?: Route<unknown> | null
37
- activeRouteParams?: unknown
38
- jsx: JSX.Element
39
- }
40
-
41
- /**
42
- * @deprecated Use NestedRouter instead
43
- */
44
- export const Router = Shade<RouterProps>({
45
- customElementName: 'shade-router',
46
- render: (options) => {
47
- const { useState, useObservable, injector } = options
48
- const [versionRef] = useState('navVersion', { current: 0 })
49
- const [state, setState] = useState<RouterState>('routerState', {
50
- jsx: <div />,
51
- })
52
-
53
- const updateUrl = async (currentUrl: string) => {
54
- const [lastState] = useState<RouterState>('routerState', state)
55
- const { activeRoute: lastRoute, activeRouteParams: lastRouteParams, jsx: lastJsx } = lastState
56
- try {
57
- for (const route of options.props.routes) {
58
- const matchFn = match(route.url, route.routingOptions)
59
- const matchResult = matchFn(currentUrl)
60
- if (matchResult) {
61
- if (route !== lastRoute || JSON.stringify(lastRouteParams) !== JSON.stringify(matchResult.params)) {
62
- const version = ++versionRef.current
63
- await lastRoute?.onLeave?.({ ...options, element: lastState.jsx })
64
- if (version !== versionRef.current) return
65
- const newJsx = route.component({ currentUrl, match: matchResult })
66
- setState({ jsx: newJsx, activeRoute: route, activeRouteParams: matchResult.params })
67
- await route.onVisit?.({ ...options, element: newJsx })
68
- if (version !== versionRef.current) return
69
- }
70
- return
71
- }
72
- }
73
-
74
- if (lastRoute !== null) {
75
- const version = ++versionRef.current
76
- await lastRoute?.onLeave?.({ ...options, element: lastJsx })
77
- if (version !== versionRef.current) return
78
- setState({
79
- jsx: options.props.notFound || <div />,
80
- activeRoute: null,
81
- activeRouteParams: null,
82
- })
83
- }
84
- } catch (e) {
85
- if (!(e instanceof ObservableAlreadyDisposedError)) {
86
- throw e
87
- }
88
- }
89
- }
90
-
91
- const [locationPath] = useObservable(
92
- 'locationPathChanged',
93
- injector.getInstance(LocationService).onLocationPathChanged,
94
- {
95
- onChange: (newValue) => {
96
- void updateUrl(newValue)
97
- },
98
- },
99
- )
100
- void updateUrl(locationPath)
101
- return state.jsx
102
- },
103
- })