@furystack/shades 11.1.0 → 12.0.1
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/CHANGELOG.md +312 -0
- package/README.md +13 -13
- package/esm/component-factory.spec.js +13 -5
- package/esm/component-factory.spec.js.map +1 -1
- package/esm/components/index.d.ts +4 -1
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +4 -1
- package/esm/components/index.js.map +1 -1
- package/esm/components/lazy-load.d.ts +2 -4
- package/esm/components/lazy-load.d.ts.map +1 -1
- package/esm/components/lazy-load.js +40 -24
- package/esm/components/lazy-load.js.map +1 -1
- package/esm/components/lazy-load.spec.js +57 -50
- package/esm/components/lazy-load.spec.js.map +1 -1
- package/esm/components/link-to-route.d.ts +2 -0
- package/esm/components/link-to-route.d.ts.map +1 -1
- package/esm/components/link-to-route.js +3 -2
- package/esm/components/link-to-route.js.map +1 -1
- package/esm/components/link-to-route.spec.js +13 -9
- package/esm/components/link-to-route.spec.js.map +1 -1
- package/esm/components/nested-route-link.d.ts +62 -0
- package/esm/components/nested-route-link.d.ts.map +1 -0
- package/esm/components/nested-route-link.js +66 -0
- package/esm/components/nested-route-link.js.map +1 -0
- package/esm/components/nested-route-link.spec.d.ts +2 -0
- package/esm/components/nested-route-link.spec.d.ts.map +1 -0
- package/esm/components/nested-route-link.spec.js +179 -0
- package/esm/components/nested-route-link.spec.js.map +1 -0
- package/esm/components/nested-route-types.d.ts +37 -0
- package/esm/components/nested-route-types.d.ts.map +1 -0
- package/esm/components/nested-route-types.js +2 -0
- package/esm/components/nested-route-types.js.map +1 -0
- package/esm/components/nested-router.d.ts +103 -0
- package/esm/components/nested-router.d.ts.map +1 -0
- package/esm/components/nested-router.js +183 -0
- package/esm/components/nested-router.js.map +1 -0
- package/esm/components/nested-router.spec.d.ts +2 -0
- package/esm/components/nested-router.spec.d.ts.map +1 -0
- package/esm/components/nested-router.spec.js +737 -0
- package/esm/components/nested-router.spec.js.map +1 -0
- package/esm/components/route-link.d.ts +4 -0
- package/esm/components/route-link.d.ts.map +1 -1
- package/esm/components/route-link.js +5 -5
- package/esm/components/route-link.js.map +1 -1
- package/esm/components/route-link.spec.js +16 -12
- package/esm/components/route-link.spec.js.map +1 -1
- package/esm/components/router.d.ts +20 -2
- package/esm/components/router.d.ts.map +1 -1
- package/esm/components/router.js +12 -7
- package/esm/components/router.js.map +1 -1
- package/esm/components/router.spec.js +141 -74
- package/esm/components/router.spec.js.map +1 -1
- package/esm/initialize.d.ts +11 -0
- package/esm/initialize.d.ts.map +1 -1
- package/esm/initialize.js +5 -0
- package/esm/initialize.js.map +1 -1
- package/esm/jsx.d.ts +83 -2
- package/esm/jsx.d.ts.map +1 -1
- package/esm/models/children-list.d.ts +5 -1
- package/esm/models/children-list.d.ts.map +1 -1
- package/esm/models/partial-element.d.ts +12 -2
- package/esm/models/partial-element.d.ts.map +1 -1
- package/esm/models/render-options.d.ts +89 -3
- package/esm/models/render-options.d.ts.map +1 -1
- package/esm/models/selection-state.d.ts +4 -0
- package/esm/models/selection-state.d.ts.map +1 -1
- package/esm/services/location-service.d.ts +11 -0
- package/esm/services/location-service.d.ts.map +1 -1
- package/esm/services/location-service.js +11 -0
- package/esm/services/location-service.js.map +1 -1
- package/esm/services/resource-manager.d.ts +24 -0
- package/esm/services/resource-manager.d.ts.map +1 -1
- package/esm/services/resource-manager.js +36 -1
- package/esm/services/resource-manager.js.map +1 -1
- package/esm/services/resource-manager.spec.js +102 -0
- package/esm/services/resource-manager.spec.js.map +1 -1
- package/esm/services/screen-service.d.ts +81 -4
- package/esm/services/screen-service.d.ts.map +1 -1
- package/esm/services/screen-service.js +75 -4
- package/esm/services/screen-service.js.map +1 -1
- package/esm/services/screen-service.spec.js +91 -7
- package/esm/services/screen-service.spec.js.map +1 -1
- package/esm/shade-component.d.ts +17 -4
- package/esm/shade-component.d.ts.map +1 -1
- package/esm/shade-component.js +67 -5
- package/esm/shade-component.js.map +1 -1
- package/esm/shade-host-props-ref.integration.spec.d.ts +2 -0
- package/esm/shade-host-props-ref.integration.spec.d.ts.map +1 -0
- package/esm/shade-host-props-ref.integration.spec.js +381 -0
- package/esm/shade-host-props-ref.integration.spec.js.map +1 -0
- package/esm/shade-resources.integration.spec.js +208 -39
- package/esm/shade-resources.integration.spec.js.map +1 -1
- package/esm/shade.d.ts +20 -17
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +172 -33
- package/esm/shade.js.map +1 -1
- package/esm/shade.spec.js +31 -30
- package/esm/shade.spec.js.map +1 -1
- package/esm/shades.integration.spec.js +135 -72
- package/esm/shades.integration.spec.js.map +1 -1
- package/esm/style-manager.d.ts +2 -2
- package/esm/style-manager.js +2 -2
- package/esm/svg-types.d.ts +389 -0
- package/esm/svg-types.d.ts.map +1 -0
- package/esm/svg-types.js +9 -0
- package/esm/svg-types.js.map +1 -0
- package/esm/svg.d.ts +15 -0
- package/esm/svg.d.ts.map +1 -0
- package/esm/svg.js +76 -0
- package/esm/svg.js.map +1 -0
- package/esm/svg.spec.d.ts +2 -0
- package/esm/svg.spec.d.ts.map +1 -0
- package/esm/svg.spec.js +80 -0
- package/esm/svg.spec.js.map +1 -0
- package/esm/vnode.d.ts +103 -0
- package/esm/vnode.d.ts.map +1 -0
- package/esm/vnode.integration.spec.d.ts +2 -0
- package/esm/vnode.integration.spec.d.ts.map +1 -0
- package/esm/vnode.integration.spec.js +494 -0
- package/esm/vnode.integration.spec.js.map +1 -0
- package/esm/vnode.js +453 -0
- package/esm/vnode.js.map +1 -0
- package/esm/vnode.spec.d.ts +2 -0
- package/esm/vnode.spec.d.ts.map +1 -0
- package/esm/vnode.spec.js +473 -0
- package/esm/vnode.spec.js.map +1 -0
- package/package.json +8 -9
- package/src/component-factory.spec.tsx +18 -5
- package/src/components/index.ts +4 -1
- package/src/components/lazy-load.spec.tsx +82 -75
- package/src/components/lazy-load.tsx +49 -27
- package/src/components/link-to-route.spec.tsx +25 -21
- package/src/components/link-to-route.tsx +4 -2
- package/src/components/nested-route-link.spec.tsx +303 -0
- package/src/components/nested-route-link.tsx +100 -0
- package/src/components/nested-route-types.ts +42 -0
- package/src/components/nested-router.spec.tsx +918 -0
- package/src/components/nested-router.tsx +260 -0
- package/src/components/route-link.spec.tsx +22 -18
- package/src/components/route-link.tsx +6 -5
- package/src/components/router.spec.tsx +196 -108
- package/src/components/router.tsx +21 -8
- package/src/initialize.ts +12 -0
- package/src/jsx.ts +129 -2
- package/src/models/children-list.ts +7 -1
- package/src/models/partial-element.ts +13 -2
- package/src/models/render-options.ts +90 -3
- package/src/models/selection-state.ts +4 -0
- package/src/services/location-service.tsx +11 -0
- package/src/services/resource-manager.spec.ts +128 -0
- package/src/services/resource-manager.ts +36 -1
- package/src/services/screen-service.spec.ts +109 -7
- package/src/services/screen-service.ts +81 -4
- package/src/shade-component.ts +72 -6
- package/src/shade-host-props-ref.integration.spec.tsx +460 -0
- package/src/shade-resources.integration.spec.tsx +276 -52
- package/src/shade.spec.tsx +40 -39
- package/src/shade.ts +186 -58
- package/src/shades.integration.spec.tsx +154 -80
- package/src/style-manager.ts +2 -2
- package/src/svg-types.ts +437 -0
- package/src/svg.spec.ts +89 -0
- package/src/svg.ts +78 -0
- package/src/vnode.integration.spec.tsx +657 -0
- package/src/vnode.spec.ts +579 -0
- package/src/vnode.ts +508 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Injector } from '@furystack/inject'
|
|
2
|
-
import { sleepAsync } from '@furystack/utils'
|
|
2
|
+
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
3
3
|
import { LazyLoad } from './lazy-load.js'
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
6
|
import { initializeShadeRoot } from '../initialize.js'
|
|
7
7
|
import { createComponent } from '../shade-component.js'
|
|
8
|
+
import { flushUpdates } from '../shade.js'
|
|
8
9
|
|
|
9
10
|
describe('Lazy Load', () => {
|
|
10
11
|
beforeEach(() => {
|
|
@@ -15,93 +16,99 @@ describe('Lazy Load', () => {
|
|
|
15
16
|
})
|
|
16
17
|
|
|
17
18
|
it('Shuld display the loader and completed state', async () => {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
20
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
initializeShadeRoot({
|
|
23
|
+
injector,
|
|
24
|
+
rootElement,
|
|
25
|
+
jsxElement: (
|
|
26
|
+
<LazyLoad
|
|
27
|
+
loader={<div>Loading...</div>}
|
|
28
|
+
component={async () => {
|
|
29
|
+
await sleepAsync(100)
|
|
30
|
+
return <div>Loaded</div>
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
),
|
|
34
|
+
})
|
|
35
|
+
await flushUpdates()
|
|
36
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
37
|
+
await sleepAsync(150)
|
|
38
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loaded</div></lazy-load></div>')
|
|
33
39
|
})
|
|
34
|
-
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
35
|
-
await sleepAsync(150)
|
|
36
|
-
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loaded</div></lazy-load></div>')
|
|
37
40
|
})
|
|
38
41
|
|
|
39
42
|
it('Shuld display the failed state with a retryer', async () => {
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
44
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const load = vi.fn(async () => {
|
|
47
|
+
throw Error(':(')
|
|
48
|
+
})
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
initializeShadeRoot({
|
|
51
|
+
injector,
|
|
52
|
+
rootElement,
|
|
53
|
+
jsxElement: (
|
|
54
|
+
<LazyLoad
|
|
55
|
+
loader={<div>Loading...</div>}
|
|
56
|
+
component={load}
|
|
57
|
+
error={(e, retry) => (
|
|
58
|
+
<button id="retry" onclick={retry}>
|
|
59
|
+
{(e as Error).message}
|
|
60
|
+
</button>
|
|
61
|
+
)}
|
|
62
|
+
/>
|
|
63
|
+
),
|
|
64
|
+
})
|
|
65
|
+
await flushUpdates()
|
|
66
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
67
|
+
await sleepAsync(1)
|
|
68
|
+
expect(load).toBeCalledTimes(1)
|
|
69
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><button id="retry">:(</button></lazy-load></div>')
|
|
70
|
+
document.getElementById('retry')?.click()
|
|
71
|
+
expect(load).toBeCalledTimes(2)
|
|
61
72
|
})
|
|
62
|
-
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
63
|
-
await sleepAsync(1)
|
|
64
|
-
expect(load).toBeCalledTimes(1)
|
|
65
|
-
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><button id="retry">:(</button></lazy-load></div>')
|
|
66
|
-
document.getElementById('retry')?.click()
|
|
67
|
-
expect(load).toBeCalledTimes(2)
|
|
68
73
|
})
|
|
69
74
|
|
|
70
75
|
it('Shuld display the failed state with a retryer', async () => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
77
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
78
|
+
let counter = 0
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const load = vi.fn(async () => {
|
|
81
|
+
if (!counter) {
|
|
82
|
+
counter += 1
|
|
83
|
+
throw Error(':(')
|
|
84
|
+
}
|
|
85
|
+
return <div>success</div>
|
|
86
|
+
})
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
initializeShadeRoot({
|
|
89
|
+
injector,
|
|
90
|
+
rootElement,
|
|
91
|
+
jsxElement: (
|
|
92
|
+
<LazyLoad
|
|
93
|
+
loader={<div>Loading...</div>}
|
|
94
|
+
component={load}
|
|
95
|
+
error={(e, retry) => (
|
|
96
|
+
<button id="retry" onclick={retry}>
|
|
97
|
+
{(e as Error).message}
|
|
98
|
+
</button>
|
|
99
|
+
)}
|
|
100
|
+
/>
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
await flushUpdates()
|
|
104
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
105
|
+
await sleepAsync(1)
|
|
106
|
+
expect(load).toBeCalledTimes(1)
|
|
107
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><button id="retry">:(</button></lazy-load></div>')
|
|
108
|
+
document.getElementById('retry')?.click()
|
|
109
|
+
expect(load).toBeCalledTimes(2)
|
|
110
|
+
await sleepAsync(1)
|
|
111
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>success</div></lazy-load></div>')
|
|
97
112
|
})
|
|
98
|
-
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
99
|
-
await sleepAsync(1)
|
|
100
|
-
expect(load).toBeCalledTimes(1)
|
|
101
|
-
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><button id="retry">:(</button></lazy-load></div>')
|
|
102
|
-
document.getElementById('retry')?.click()
|
|
103
|
-
expect(load).toBeCalledTimes(2)
|
|
104
|
-
await sleepAsync(1)
|
|
105
|
-
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>success</div></lazy-load></div>')
|
|
106
113
|
})
|
|
107
114
|
})
|
|
@@ -6,44 +6,66 @@ export interface LazyLoadProps {
|
|
|
6
6
|
component: () => Promise<JSX.Element>
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export interface LazyLoadState {
|
|
10
|
-
component?: JSX.Element
|
|
11
|
-
error?: unknown
|
|
12
|
-
}
|
|
13
|
-
|
|
14
9
|
export const LazyLoad = Shade<LazyLoadProps>({
|
|
15
10
|
shadowDomName: 'lazy-load',
|
|
16
|
-
|
|
17
|
-
const [_component, setComponent] = useState<JSX.Element | undefined>('component', undefined)
|
|
18
|
-
const [_errorState, setErrorState] = useState<unknown>('error', undefined)
|
|
19
|
-
try {
|
|
20
|
-
const loaded = await props.component()
|
|
21
|
-
if (element.isConnected) {
|
|
22
|
-
setComponent(loaded)
|
|
23
|
-
}
|
|
24
|
-
} catch (error) {
|
|
25
|
-
if (props.error) {
|
|
26
|
-
if (element.isConnected) {
|
|
27
|
-
setErrorState(error)
|
|
28
|
-
}
|
|
29
|
-
} else {
|
|
30
|
-
throw error
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
render: ({ props, useState }) => {
|
|
11
|
+
render: ({ props, useState, useDisposable }) => {
|
|
35
12
|
const [error, setError] = useState<unknown>('error', undefined)
|
|
36
13
|
const [component, setComponent] = useState<JSX.Element | undefined>('component', undefined)
|
|
37
14
|
|
|
15
|
+
const tracker = useDisposable('loadTracker', () => {
|
|
16
|
+
const state: {
|
|
17
|
+
factory: (() => Promise<JSX.Element>) | null
|
|
18
|
+
active: boolean
|
|
19
|
+
[Symbol.dispose](): void
|
|
20
|
+
} = {
|
|
21
|
+
factory: null,
|
|
22
|
+
active: true,
|
|
23
|
+
[Symbol.dispose]() {
|
|
24
|
+
state.active = false
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
return state
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const isNewFactory = tracker.factory !== props.component
|
|
31
|
+
|
|
32
|
+
if (isNewFactory) {
|
|
33
|
+
tracker.factory = props.component
|
|
34
|
+
const factory = props.component
|
|
35
|
+
|
|
36
|
+
factory()
|
|
37
|
+
.then((loaded) => {
|
|
38
|
+
if (tracker.active && tracker.factory === factory) {
|
|
39
|
+
setError(undefined)
|
|
40
|
+
setComponent(loaded)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.catch((err: unknown) => {
|
|
44
|
+
if (tracker.active && tracker.factory === factory) {
|
|
45
|
+
setComponent(undefined)
|
|
46
|
+
if (props.error) {
|
|
47
|
+
setError(err)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return props.loader
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
if (error && props.error) {
|
|
39
56
|
return props.error(error, async () => {
|
|
57
|
+
const factory = props.component
|
|
40
58
|
try {
|
|
41
59
|
setError(undefined)
|
|
42
60
|
setComponent(undefined)
|
|
43
|
-
const loaded = await
|
|
44
|
-
|
|
61
|
+
const loaded = await factory()
|
|
62
|
+
if (tracker.active && tracker.factory === factory) {
|
|
63
|
+
setComponent(loaded)
|
|
64
|
+
}
|
|
45
65
|
} catch (e) {
|
|
46
|
-
|
|
66
|
+
if (tracker.active && tracker.factory === factory) {
|
|
67
|
+
setError(e)
|
|
68
|
+
}
|
|
47
69
|
}
|
|
48
70
|
})
|
|
49
71
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Injector } from '@furystack/inject'
|
|
2
|
+
import { usingAsync } from '@furystack/utils'
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
3
4
|
import { initializeShadeRoot } from '../initialize.js'
|
|
4
5
|
import { createComponent } from '../shade-component.js'
|
|
6
|
+
import { flushUpdates } from '../shade.js'
|
|
5
7
|
import { LinkToRoute } from './link-to-route.js'
|
|
6
8
|
import type { Route } from './router.js'
|
|
7
9
|
|
|
@@ -14,28 +16,30 @@ describe('LinkToRoute', () => {
|
|
|
14
16
|
})
|
|
15
17
|
|
|
16
18
|
it('Shuld display the loader and completed state', async () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
20
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
initializeShadeRoot({
|
|
23
|
+
injector,
|
|
24
|
+
rootElement,
|
|
25
|
+
jsxElement: (
|
|
26
|
+
<LinkToRoute
|
|
27
|
+
route={
|
|
28
|
+
{
|
|
29
|
+
url: '/subroute/:id',
|
|
30
|
+
} as Route<{ id: number }>
|
|
31
|
+
}
|
|
32
|
+
params={{ id: 123 }}
|
|
33
|
+
id="route"
|
|
34
|
+
>
|
|
35
|
+
Link
|
|
36
|
+
</LinkToRoute>
|
|
37
|
+
),
|
|
38
|
+
})
|
|
39
|
+
await flushUpdates()
|
|
40
|
+
expect(document.body.innerHTML).toBe(
|
|
41
|
+
'<div id="root"><a is="link-to-route" id="route" href="/subroute/123">Link</a></div>',
|
|
42
|
+
)
|
|
36
43
|
})
|
|
37
|
-
expect(document.body.innerHTML).toBe(
|
|
38
|
-
'<div id="root"><a is="link-to-route" id="route" href="/subroute/123">Link</a></div>',
|
|
39
|
-
)
|
|
40
44
|
})
|
|
41
45
|
})
|
|
@@ -4,21 +4,23 @@ import { createComponent } from '../shade-component.js'
|
|
|
4
4
|
import { Shade } from '../shade.js'
|
|
5
5
|
import type { Route } from './router.js'
|
|
6
6
|
|
|
7
|
+
/** @deprecated Use `NestedRouteLinkProps` from `nested-route-link` instead */
|
|
7
8
|
export type LinkToRouteProps<T extends object> = {
|
|
8
9
|
route: Route<T>
|
|
9
10
|
params: T
|
|
10
11
|
} & Omit<JSX.IntrinsicElements['a'], 'href'>
|
|
11
12
|
|
|
13
|
+
/** @deprecated Use `NestedRouteLink` from `nested-route-link` instead */
|
|
12
14
|
export const LinkToRoute: <T extends object>(props: LinkToRouteProps<T>, children?: ChildrenList) => JSX.Element =
|
|
13
15
|
Shade({
|
|
14
16
|
shadowDomName: 'link-to-route',
|
|
15
17
|
elementBase: HTMLAnchorElement,
|
|
16
18
|
elementBaseName: 'a',
|
|
17
|
-
render: ({ props,
|
|
19
|
+
render: ({ props, useHostProps, children }) => {
|
|
18
20
|
const { route, params } = props
|
|
19
21
|
|
|
20
22
|
const url = compileRoute(route.url, params)
|
|
21
|
-
|
|
23
|
+
useHostProps({ href: url })
|
|
22
24
|
return <>{children}</>
|
|
23
25
|
},
|
|
24
26
|
})
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject'
|
|
2
|
+
import { usingAsync } from '@furystack/utils'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, expectTypeOf, 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 type { TypedNestedRouteLinkProps } from './nested-route-link.js'
|
|
9
|
+
import { NestedRouteLink, createNestedRouteLink } from './nested-route-link.js'
|
|
10
|
+
import type { ConcatPaths, ExtractRouteParams, ExtractRoutePaths, UrlTree } from './nested-route-types.js'
|
|
11
|
+
import type { NestedRoute } from './nested-router.js'
|
|
12
|
+
|
|
13
|
+
// Minimal route type for type-level tests. Using Pick avoids the
|
|
14
|
+
// `children?: Record<string, NestedRoute<any>>` from NestedRoute<unknown>
|
|
15
|
+
// which would widen literal keys in intersections.
|
|
16
|
+
type TestRoute = Pick<NestedRoute<unknown>, 'component'>
|
|
17
|
+
|
|
18
|
+
describe('NestedRouteLink', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
document.body.innerHTML = '<div id="root"></div>'
|
|
21
|
+
})
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
document.body.innerHTML = ''
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('Should render a link with the correct href', async () => {
|
|
27
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
28
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
29
|
+
|
|
30
|
+
initializeShadeRoot({
|
|
31
|
+
injector,
|
|
32
|
+
rootElement,
|
|
33
|
+
jsxElement: (
|
|
34
|
+
<NestedRouteLink id="link" href="/buttons">
|
|
35
|
+
Buttons
|
|
36
|
+
</NestedRouteLink>
|
|
37
|
+
),
|
|
38
|
+
})
|
|
39
|
+
await flushUpdates()
|
|
40
|
+
expect(document.body.innerHTML).toBe(
|
|
41
|
+
'<div id="root"><a is="nested-route-link" id="link" href="/buttons">Buttons</a></div>',
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('Should trigger SPA navigation on click', async () => {
|
|
47
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
48
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
49
|
+
const onRouteChange = vi.fn()
|
|
50
|
+
|
|
51
|
+
injector.getInstance(LocationService).onLocationPathChanged.subscribe(onRouteChange)
|
|
52
|
+
|
|
53
|
+
initializeShadeRoot({
|
|
54
|
+
injector,
|
|
55
|
+
rootElement,
|
|
56
|
+
jsxElement: (
|
|
57
|
+
<NestedRouteLink id="link" href="/buttons">
|
|
58
|
+
Buttons
|
|
59
|
+
</NestedRouteLink>
|
|
60
|
+
),
|
|
61
|
+
})
|
|
62
|
+
await flushUpdates()
|
|
63
|
+
|
|
64
|
+
expect(onRouteChange).not.toBeCalled()
|
|
65
|
+
document.getElementById('link')?.click()
|
|
66
|
+
expect(onRouteChange).toBeCalledTimes(1)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('Should compile route params in the href', async () => {
|
|
71
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
72
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
73
|
+
|
|
74
|
+
initializeShadeRoot({
|
|
75
|
+
injector,
|
|
76
|
+
rootElement,
|
|
77
|
+
jsxElement: (
|
|
78
|
+
<NestedRouteLink id="link" href="/users/:id" params={{ id: '42' }}>
|
|
79
|
+
User 42
|
|
80
|
+
</NestedRouteLink>
|
|
81
|
+
),
|
|
82
|
+
})
|
|
83
|
+
await flushUpdates()
|
|
84
|
+
expect(document.body.innerHTML).toBe(
|
|
85
|
+
'<div id="root"><a is="nested-route-link" id="link" href="/users/42">User 42</a></div>',
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('Should compile route params with multiple segments', async () => {
|
|
91
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
92
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
93
|
+
|
|
94
|
+
initializeShadeRoot({
|
|
95
|
+
injector,
|
|
96
|
+
rootElement,
|
|
97
|
+
jsxElement: (
|
|
98
|
+
<NestedRouteLink id="link" href="/users/:userId/posts/:postId" params={{ userId: '1', postId: '99' }}>
|
|
99
|
+
Post
|
|
100
|
+
</NestedRouteLink>
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
await flushUpdates()
|
|
104
|
+
expect(document.body.innerHTML).toBe(
|
|
105
|
+
'<div id="root"><a is="nested-route-link" id="link" href="/users/1/posts/99">Post</a></div>',
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('Type utilities', () => {
|
|
112
|
+
describe('ConcatPaths', () => {
|
|
113
|
+
it('Should strip root "/" when concatenating', () => {
|
|
114
|
+
expectTypeOf<ConcatPaths<'/', '/buttons'>>().toEqualTypeOf<'/buttons'>()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('Should concatenate non-root parent paths', () => {
|
|
118
|
+
expectTypeOf<ConcatPaths<'/layout-tests', '/appbar-only'>>().toEqualTypeOf<'/layout-tests/appbar-only'>()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('Should handle deeply nested paths', () => {
|
|
122
|
+
expectTypeOf<ConcatPaths<'/a/b', '/c'>>().toEqualTypeOf<'/a/b/c'>()
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('ExtractRouteParams', () => {
|
|
127
|
+
it('Should return Record<string, never> for paths without params', () => {
|
|
128
|
+
expectTypeOf<ExtractRouteParams<'/buttons'>>().toEqualTypeOf<Record<string, never>>()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('Should extract a single param', () => {
|
|
132
|
+
expectTypeOf<ExtractRouteParams<'/users/:id'>>().toEqualTypeOf<{ id: string }>()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('Should extract multiple params', () => {
|
|
136
|
+
expectTypeOf<ExtractRouteParams<'/users/:userId/posts/:postId'>>().toEqualTypeOf<{
|
|
137
|
+
userId: string
|
|
138
|
+
postId: string
|
|
139
|
+
}>()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('Should handle params at the beginning of the path', () => {
|
|
143
|
+
expectTypeOf<ExtractRouteParams<'/:id'>>().toEqualTypeOf<{ id: string }>()
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('ExtractRoutePaths', () => {
|
|
148
|
+
it('Should extract top-level paths', () => {
|
|
149
|
+
type Routes = {
|
|
150
|
+
'/a': TestRoute
|
|
151
|
+
'/b': TestRoute
|
|
152
|
+
}
|
|
153
|
+
expectTypeOf<ExtractRoutePaths<Routes>>().toEqualTypeOf<'/a' | '/b'>()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('Should extract nested child paths with root parent', () => {
|
|
157
|
+
type Routes = {
|
|
158
|
+
'/': TestRoute & {
|
|
159
|
+
children: {
|
|
160
|
+
'/buttons': TestRoute
|
|
161
|
+
'/inputs': TestRoute
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
expectTypeOf<ExtractRoutePaths<Routes>>().toEqualTypeOf<'/' | '/buttons' | '/inputs'>()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('Should extract nested child paths with non-root parent', () => {
|
|
169
|
+
type Routes = {
|
|
170
|
+
'/layout-tests': TestRoute & {
|
|
171
|
+
children: {
|
|
172
|
+
'/appbar-only': TestRoute
|
|
173
|
+
'/auto-hide': TestRoute
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
expectTypeOf<ExtractRoutePaths<Routes>>().toEqualTypeOf<
|
|
178
|
+
'/layout-tests' | '/layout-tests/appbar-only' | '/layout-tests/auto-hide'
|
|
179
|
+
>()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('Should handle mixed flat and nested routes', () => {
|
|
183
|
+
type Routes = {
|
|
184
|
+
'/standalone': TestRoute
|
|
185
|
+
'/parent': TestRoute & {
|
|
186
|
+
children: {
|
|
187
|
+
'/child': TestRoute
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
expectTypeOf<ExtractRoutePaths<Routes>>().toEqualTypeOf<'/standalone' | '/parent' | '/parent/child'>()
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('UrlTree', () => {
|
|
196
|
+
it('Should accept a flat object of valid paths', () => {
|
|
197
|
+
type Paths = '/a' | '/b'
|
|
198
|
+
const urls = {
|
|
199
|
+
a: '/a',
|
|
200
|
+
b: '/b',
|
|
201
|
+
} satisfies UrlTree<Paths>
|
|
202
|
+
expectTypeOf(urls).toExtend<UrlTree<Paths>>()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('Should accept nested objects of valid paths', () => {
|
|
206
|
+
type Paths = '/' | '/buttons' | '/layout-tests' | '/layout-tests/appbar-only'
|
|
207
|
+
const urls = {
|
|
208
|
+
home: '/',
|
|
209
|
+
buttons: '/buttons',
|
|
210
|
+
layoutTests: {
|
|
211
|
+
index: '/layout-tests',
|
|
212
|
+
appBarOnly: '/layout-tests/appbar-only',
|
|
213
|
+
},
|
|
214
|
+
} satisfies UrlTree<Paths>
|
|
215
|
+
expectTypeOf(urls).toExtend<UrlTree<Paths>>()
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('TypedNestedRouteLinkProps', () => {
|
|
220
|
+
it('Should make params optional for paths without parameters', () => {
|
|
221
|
+
type Props = TypedNestedRouteLinkProps<'/buttons'>
|
|
222
|
+
expectTypeOf<Props['href']>().toEqualTypeOf<'/buttons'>()
|
|
223
|
+
expectTypeOf<Props>().toExtend<{ params?: Record<string, string> }>()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('Should require params for parameterized paths', () => {
|
|
227
|
+
type Props = TypedNestedRouteLinkProps<'/users/:id'>
|
|
228
|
+
expectTypeOf<Props['href']>().toEqualTypeOf<'/users/:id'>()
|
|
229
|
+
expectTypeOf<Props>().toExtend<{ params: { id: string } }>()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('Should require all params for multi-param paths', () => {
|
|
233
|
+
type Props = TypedNestedRouteLinkProps<'/users/:userId/posts/:postId'>
|
|
234
|
+
expectTypeOf<Props>().toExtend<{ params: { userId: string; postId: string } }>()
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe('NestedRouteLink param inference', () => {
|
|
239
|
+
it('Should infer params as optional when href has no parameters', () => {
|
|
240
|
+
expectTypeOf(NestedRouteLink).parameter(0).toHaveProperty('params')
|
|
241
|
+
expectTypeOf(NestedRouteLink<'/buttons'>)
|
|
242
|
+
.parameter(0)
|
|
243
|
+
.toExtend<{ params?: Record<string, string> }>()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('Should infer params as required when href has a parameter', () => {
|
|
247
|
+
expectTypeOf(NestedRouteLink<'/users/:id'>)
|
|
248
|
+
.parameter(0)
|
|
249
|
+
.toExtend<{ params: { id: string } }>()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('Should infer multiple params from href', () => {
|
|
253
|
+
expectTypeOf(NestedRouteLink<'/users/:userId/posts/:postId'>)
|
|
254
|
+
.parameter(0)
|
|
255
|
+
.toExtend<{ params: { userId: string; postId: string } }>()
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe('createNestedRouteLink', () => {
|
|
260
|
+
it('Should constrain href to valid route paths', () => {
|
|
261
|
+
type Routes = {
|
|
262
|
+
'/': TestRoute & {
|
|
263
|
+
children: {
|
|
264
|
+
'/buttons': TestRoute
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const AppLink = createNestedRouteLink<Routes>()
|
|
270
|
+
expectTypeOf(AppLink).parameter(0).toHaveProperty('href')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('Should require params for parameterized routes in the tree', () => {
|
|
274
|
+
type Routes = {
|
|
275
|
+
'/': TestRoute & {
|
|
276
|
+
children: {
|
|
277
|
+
'/users/:userId': TestRoute
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const AppLink = createNestedRouteLink<Routes>()
|
|
283
|
+
expectTypeOf(AppLink<'/users/:userId'>)
|
|
284
|
+
.parameter(0)
|
|
285
|
+
.toExtend<{ params: { userId: string } }>()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('Should require combined params from parent and child route segments', () => {
|
|
289
|
+
type Routes = {
|
|
290
|
+
'/users/:userId': TestRoute & {
|
|
291
|
+
children: {
|
|
292
|
+
'/posts/:postId': TestRoute
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const AppLink = createNestedRouteLink<Routes>()
|
|
298
|
+
expectTypeOf(AppLink<'/users/:userId/posts/:postId'>)
|
|
299
|
+
.parameter(0)
|
|
300
|
+
.toExtend<{ params: { userId: string; postId: string } }>()
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
})
|