@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,12 +1,14 @@
|
|
|
1
1
|
import { ObservableAlreadyDisposedError } from '@furystack/utils'
|
|
2
2
|
import type { MatchOptions, MatchResult } from 'path-to-regexp'
|
|
3
3
|
import { match } from 'path-to-regexp'
|
|
4
|
-
import { Lock } from 'semaphore-async-await'
|
|
5
4
|
import type { RenderOptions } from '../models/render-options.js'
|
|
6
5
|
import { LocationService } from '../services/location-service.js'
|
|
7
6
|
import { createComponent } from '../shade-component.js'
|
|
8
7
|
import { Shade } from '../shade.js'
|
|
9
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated Use NestedRouter instead
|
|
11
|
+
*/
|
|
10
12
|
export interface Route<TMatchResult = unknown> {
|
|
11
13
|
url: string
|
|
12
14
|
component: (options: {
|
|
@@ -14,26 +16,36 @@ export interface Route<TMatchResult = unknown> {
|
|
|
14
16
|
match: MatchResult<TMatchResult extends object ? TMatchResult : object>
|
|
15
17
|
}) => JSX.Element
|
|
16
18
|
routingOptions?: MatchOptions
|
|
17
|
-
onVisit?: (options: RenderOptions<unknown>) => Promise<void>
|
|
18
|
-
onLeave?: (options: RenderOptions<unknown>) => Promise<void>
|
|
19
|
+
onVisit?: (options: RenderOptions<unknown> & { element: JSX.Element }) => Promise<void>
|
|
20
|
+
onLeave?: (options: RenderOptions<unknown> & { element: JSX.Element }) => Promise<void>
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Use NestedRouterProps instead
|
|
25
|
+
*/
|
|
21
26
|
export interface RouterProps {
|
|
22
27
|
style?: CSSStyleDeclaration
|
|
23
28
|
routes: Array<Route<any>>
|
|
24
29
|
notFound?: JSX.Element
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated Use NestedRouterState instead
|
|
34
|
+
*/
|
|
27
35
|
export interface RouterState {
|
|
28
36
|
activeRoute?: Route<unknown> | null
|
|
29
37
|
activeRouteParams?: unknown
|
|
30
38
|
jsx: JSX.Element
|
|
31
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Use NestedRouter instead
|
|
43
|
+
*/
|
|
32
44
|
export const Router = Shade<RouterProps>({
|
|
33
45
|
shadowDomName: 'shade-router',
|
|
34
46
|
render: (options) => {
|
|
35
47
|
const { useState, useObservable, injector } = options
|
|
36
|
-
const [
|
|
48
|
+
const [versionRef] = useState('navVersion', { current: 0 })
|
|
37
49
|
const [state, setState] = useState<RouterState>('routerState', {
|
|
38
50
|
jsx: <div />,
|
|
39
51
|
})
|
|
@@ -42,23 +54,27 @@ export const Router = Shade<RouterProps>({
|
|
|
42
54
|
const [lastState] = useState<RouterState>('routerState', state)
|
|
43
55
|
const { activeRoute: lastRoute, activeRouteParams: lastRouteParams, jsx: lastJsx } = lastState
|
|
44
56
|
try {
|
|
45
|
-
await lock.acquire()
|
|
46
57
|
for (const route of options.props.routes) {
|
|
47
58
|
const matchFn = match(route.url, route.routingOptions)
|
|
48
59
|
const matchResult = matchFn(currentUrl)
|
|
49
60
|
if (matchResult) {
|
|
50
61
|
if (route !== lastRoute || JSON.stringify(lastRouteParams) !== JSON.stringify(matchResult.params)) {
|
|
62
|
+
const version = ++versionRef.current
|
|
51
63
|
await lastRoute?.onLeave?.({ ...options, element: lastState.jsx })
|
|
64
|
+
if (version !== versionRef.current) return
|
|
52
65
|
const newJsx = route.component({ currentUrl, match: matchResult })
|
|
53
66
|
setState({ jsx: newJsx, activeRoute: route, activeRouteParams: matchResult.params })
|
|
54
67
|
await route.onVisit?.({ ...options, element: newJsx })
|
|
68
|
+
if (version !== versionRef.current) return
|
|
55
69
|
}
|
|
56
70
|
return
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
if (lastRoute !== null) {
|
|
75
|
+
const version = ++versionRef.current
|
|
61
76
|
await lastRoute?.onLeave?.({ ...options, element: lastJsx })
|
|
77
|
+
if (version !== versionRef.current) return
|
|
62
78
|
setState({
|
|
63
79
|
jsx: options.props.notFound || <div />,
|
|
64
80
|
activeRoute: null,
|
|
@@ -66,12 +82,9 @@ export const Router = Shade<RouterProps>({
|
|
|
66
82
|
})
|
|
67
83
|
}
|
|
68
84
|
} catch (e) {
|
|
69
|
-
// path updates can be async, this can be ignored
|
|
70
85
|
if (!(e instanceof ObservableAlreadyDisposedError)) {
|
|
71
86
|
throw e
|
|
72
87
|
}
|
|
73
|
-
} finally {
|
|
74
|
-
lock?.release()
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
90
|
|
package/src/initialize.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { Injector } from '@furystack/inject'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Options for bootstrapping a Shades application.
|
|
5
|
+
*/
|
|
3
6
|
export interface InitializeOptions {
|
|
7
|
+
/** The DOM element that will host the application */
|
|
4
8
|
rootElement: HTMLElement
|
|
9
|
+
/** The root JSX element to render */
|
|
5
10
|
jsxElement: JSX.Element
|
|
11
|
+
/** The root injector instance for dependency injection */
|
|
6
12
|
injector: Injector
|
|
7
13
|
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Bootstraps a Shades application by attaching the root JSX element to a DOM node
|
|
17
|
+
* and wiring up the dependency injection context.
|
|
18
|
+
* @param options The initialization options
|
|
19
|
+
*/
|
|
8
20
|
export const initializeShadeRoot = (options: InitializeOptions) => {
|
|
9
21
|
options.jsxElement.injector = options.injector
|
|
10
22
|
options.rootElement.appendChild(options.jsxElement)
|
package/src/jsx.ts
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import type { Injector } from '@furystack/inject'
|
|
2
2
|
import type { ChildrenList, PartialElement } from './models/index.js'
|
|
3
3
|
import type { ResourceManager } from './services/resource-manager.js'
|
|
4
|
+
import type {
|
|
5
|
+
SvgAnimateAttributes,
|
|
6
|
+
SvgAnimateMotionAttributes,
|
|
7
|
+
SvgAnimateTransformAttributes,
|
|
8
|
+
SvgCircleAttributes,
|
|
9
|
+
SvgClipPathAttributes,
|
|
10
|
+
SvgCoreAttributes,
|
|
11
|
+
SvgDefsAttributes,
|
|
12
|
+
SvgDescAttributes,
|
|
13
|
+
SvgEllipseAttributes,
|
|
14
|
+
SvgFeBlendAttributes,
|
|
15
|
+
SvgFeColorMatrixAttributes,
|
|
16
|
+
SvgFeCompositeAttributes,
|
|
17
|
+
SvgFeFloodAttributes,
|
|
18
|
+
SvgFeGaussianBlurAttributes,
|
|
19
|
+
SvgFeMergeAttributes,
|
|
20
|
+
SvgFeMergeNodeAttributes,
|
|
21
|
+
SvgFeOffsetAttributes,
|
|
22
|
+
SvgFilterAttributes,
|
|
23
|
+
SvgForeignObjectAttributes,
|
|
24
|
+
SvgGAttributes,
|
|
25
|
+
SvgImageAttributes,
|
|
26
|
+
SvgLinearGradientAttributes,
|
|
27
|
+
SvgLineAttributes,
|
|
28
|
+
SvgMarkerAttributes,
|
|
29
|
+
SvgMaskAttributes,
|
|
30
|
+
SvgPathAttributes,
|
|
31
|
+
SvgPatternAttributes,
|
|
32
|
+
SvgPolygonAttributes,
|
|
33
|
+
SvgPolylineAttributes,
|
|
34
|
+
SvgRadialGradientAttributes,
|
|
35
|
+
SvgRectAttributes,
|
|
36
|
+
SvgSetAttributes,
|
|
37
|
+
SvgStopAttributes,
|
|
38
|
+
SvgSvgAttributes,
|
|
39
|
+
SvgSymbolAttributes,
|
|
40
|
+
SvgTextAttributes,
|
|
41
|
+
SvgTextPathAttributes,
|
|
42
|
+
SvgTitleAttributes,
|
|
43
|
+
SvgTspanAttributes,
|
|
44
|
+
SvgUseAttributes,
|
|
45
|
+
} from './svg-types.js'
|
|
4
46
|
|
|
5
47
|
declare global {
|
|
6
48
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
@@ -10,7 +52,6 @@ declare global {
|
|
|
10
52
|
props: TProps
|
|
11
53
|
updateComponent: () => void
|
|
12
54
|
shadeChildren?: ChildrenList
|
|
13
|
-
callConstructed: () => void
|
|
14
55
|
resourceManager: ResourceManager
|
|
15
56
|
getRenderCount(): number
|
|
16
57
|
}
|
|
@@ -423,7 +464,7 @@ declare global {
|
|
|
423
464
|
* The <svg> tag defines a container for SVG graphics.
|
|
424
465
|
* SVG has several methods for drawing paths, boxes, circles, text, and graphic images.
|
|
425
466
|
*/
|
|
426
|
-
svg:
|
|
467
|
+
svg: SvgSvgAttributes
|
|
427
468
|
/**
|
|
428
469
|
* The <table> tag defines an HTML table.
|
|
429
470
|
* An HTML table consists of the <table> element and one or more <tr>, <th>, and <td> elements.
|
|
@@ -519,6 +560,92 @@ declare global {
|
|
|
519
560
|
* Tip: When a word is too long, or you are afraid that the browser will break your lines at the wrong place, you can use the <wbr> element to add word break opportunities.
|
|
520
561
|
*/
|
|
521
562
|
wbr: PartialElement<HTMLElement>
|
|
563
|
+
|
|
564
|
+
// -------------------------------------------------------------------
|
|
565
|
+
// SVG Elements
|
|
566
|
+
// -------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
/** The `<g>` element groups SVG elements together. */
|
|
569
|
+
g: SvgGAttributes
|
|
570
|
+
/** The `<defs>` element stores graphical objects for later reuse. */
|
|
571
|
+
defs: SvgDefsAttributes
|
|
572
|
+
/** The `<symbol>` element defines a reusable graphical template. */
|
|
573
|
+
symbol: SvgSymbolAttributes
|
|
574
|
+
/** The `<use>` element references another element for rendering. */
|
|
575
|
+
use: SvgUseAttributes
|
|
576
|
+
/** The `<path>` element defines a shape via SVG path commands. */
|
|
577
|
+
path: SvgPathAttributes
|
|
578
|
+
/** The `<rect>` element draws a rectangle. */
|
|
579
|
+
rect: SvgRectAttributes
|
|
580
|
+
/** The `<circle>` element draws a circle. */
|
|
581
|
+
circle: SvgCircleAttributes
|
|
582
|
+
/** The `<ellipse>` element draws an ellipse. */
|
|
583
|
+
ellipse: SvgEllipseAttributes
|
|
584
|
+
/** The `<line>` element draws a straight line between two points. */
|
|
585
|
+
line: SvgLineAttributes
|
|
586
|
+
/** The `<polyline>` element draws connected straight line segments. */
|
|
587
|
+
polyline: SvgPolylineAttributes
|
|
588
|
+
/** The `<polygon>` element draws a closed shape of connected line segments. */
|
|
589
|
+
polygon: SvgPolygonAttributes
|
|
590
|
+
/** The `<text>` element renders text in SVG. */
|
|
591
|
+
text: SvgTextAttributes
|
|
592
|
+
/** The `<tspan>` element defines a subtext within a `<text>` element. */
|
|
593
|
+
tspan: SvgTspanAttributes
|
|
594
|
+
/** The `<textPath>` element renders text along a path. */
|
|
595
|
+
textPath: SvgTextPathAttributes
|
|
596
|
+
/** The `<clipPath>` element defines a clipping region. */
|
|
597
|
+
clipPath: SvgClipPathAttributes
|
|
598
|
+
/** The `<mask>` element defines an alpha mask for compositing. */
|
|
599
|
+
mask: SvgMaskAttributes
|
|
600
|
+
/** The `<linearGradient>` element defines a linear color gradient. */
|
|
601
|
+
linearGradient: SvgLinearGradientAttributes
|
|
602
|
+
/** The `<radialGradient>` element defines a radial color gradient. */
|
|
603
|
+
radialGradient: SvgRadialGradientAttributes
|
|
604
|
+
/** The `<stop>` element defines a color stop in a gradient. */
|
|
605
|
+
stop: SvgStopAttributes
|
|
606
|
+
/** The `<pattern>` element defines a repeating graphic pattern. */
|
|
607
|
+
pattern: SvgPatternAttributes
|
|
608
|
+
/** The `<marker>` element defines a graphic for drawing on edges of shapes. */
|
|
609
|
+
marker: SvgMarkerAttributes
|
|
610
|
+
/** The `<filter>` element defines a set of filter operations. */
|
|
611
|
+
filter: SvgFilterAttributes
|
|
612
|
+
/** The `<feGaussianBlur>` filter primitive blurs the input image. */
|
|
613
|
+
feGaussianBlur: SvgFeGaussianBlurAttributes
|
|
614
|
+
/** The `<feBlend>` filter primitive composites two inputs. */
|
|
615
|
+
feBlend: SvgFeBlendAttributes
|
|
616
|
+
/** The `<feColorMatrix>` filter primitive applies a matrix color transform. */
|
|
617
|
+
feColorMatrix: SvgFeColorMatrixAttributes
|
|
618
|
+
/** The `<feOffset>` filter primitive offsets the input image. */
|
|
619
|
+
feOffset: SvgFeOffsetAttributes
|
|
620
|
+
/** The `<feFlood>` filter primitive fills with a solid color. */
|
|
621
|
+
feFlood: SvgFeFloodAttributes
|
|
622
|
+
/** The `<feMerge>` filter primitive composites multiple inputs. */
|
|
623
|
+
feMerge: SvgFeMergeAttributes
|
|
624
|
+
/** The `<feMergeNode>` element defines an input for `<feMerge>`. */
|
|
625
|
+
feMergeNode: SvgFeMergeNodeAttributes
|
|
626
|
+
/** The `<feComposite>` filter primitive combines images using Porter-Duff operations. */
|
|
627
|
+
feComposite: SvgFeCompositeAttributes
|
|
628
|
+
/** The `<image>` element (SVG) embeds a raster image. */
|
|
629
|
+
image: SvgImageAttributes
|
|
630
|
+
/** The `<foreignObject>` element embeds external XML (e.g. HTML) in SVG. */
|
|
631
|
+
foreignObject: SvgForeignObjectAttributes
|
|
632
|
+
/** The `<animate>` element animates an attribute over time. */
|
|
633
|
+
animate: SvgAnimateAttributes
|
|
634
|
+
/** The `<animateMotion>` element animates an element along a path. */
|
|
635
|
+
animateMotion: SvgAnimateMotionAttributes
|
|
636
|
+
/** The `<animateTransform>` element animates a transformation attribute. */
|
|
637
|
+
animateTransform: SvgAnimateTransformAttributes
|
|
638
|
+
/** The `<set>` element sets an attribute to a value for a duration. */
|
|
639
|
+
set: SvgSetAttributes
|
|
640
|
+
/** The `<title>` element (SVG) provides an accessible title. */
|
|
641
|
+
title: SvgTitleAttributes
|
|
642
|
+
/** The `<desc>` element provides an accessible description. */
|
|
643
|
+
desc: SvgDescAttributes
|
|
644
|
+
/**
|
|
645
|
+
* Catch-all for SVG filter primitives and other SVG elements
|
|
646
|
+
* not explicitly listed above.
|
|
647
|
+
*/
|
|
648
|
+
[key: `fe${string}`]: SvgCoreAttributes
|
|
522
649
|
}
|
|
523
650
|
}
|
|
524
651
|
}
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The type for children passed to Shade components and intrinsic JSX elements.
|
|
3
|
+
* Supports strings, HTML/SVG elements, JSX elements, and nested arrays of these.
|
|
4
|
+
*/
|
|
5
|
+
export type ChildrenList = Array<
|
|
6
|
+
string | HTMLElement | SVGElement | JSX.Element | string[] | HTMLElement[] | SVGElement[] | JSX.Element[]
|
|
7
|
+
>
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RefObject } from './render-options.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Makes all properties of an HTML element type optional, with `style` narrowed
|
|
5
|
+
* to `Partial<CSSStyleDeclaration>` and a `ref` prop for capturing DOM references.
|
|
6
|
+
* Used as the props type for intrinsic JSX elements and component host element overrides.
|
|
7
|
+
* @typeParam T - The base HTML element type
|
|
8
|
+
*/
|
|
9
|
+
export type PartialElement<T> = (T extends { style?: CSSStyleDeclaration }
|
|
2
10
|
? Omit<Partial<T>, 'style'> & {
|
|
3
11
|
style?: Partial<CSSStyleDeclaration>
|
|
4
12
|
}
|
|
5
|
-
: Partial<T>
|
|
13
|
+
: Partial<T>) & {
|
|
14
|
+
/** Ref object to capture a reference to the underlying DOM element. */
|
|
15
|
+
ref?: RefObject<Element>
|
|
16
|
+
}
|
|
@@ -3,12 +3,81 @@ import type { ObservableValue, ValueObserverOptions } from '@furystack/utils'
|
|
|
3
3
|
import type { ChildrenList } from './children-list.js'
|
|
4
4
|
import type { PartialElement } from './partial-element.js'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A reference object returned by `useRef`.
|
|
8
|
+
* `current` is set to the DOM element when it is mounted, and `null` when unmounted.
|
|
9
|
+
* The `readonly` modifier ensures covariance so that `RefObject<HTMLInputElement>`
|
|
10
|
+
* is assignable to `RefObject<Element>`.
|
|
11
|
+
*/
|
|
12
|
+
export type RefObject<T extends Element = HTMLElement> = {
|
|
13
|
+
readonly current: T | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options provided to a Shade component's `render` function.
|
|
18
|
+
* Contains the current props, injector, children, and hooks for managing state, side effects, and host element attributes.
|
|
19
|
+
* @typeParam TProps - The component's props type
|
|
20
|
+
* @typeParam TElementBase - The base HTML element type (defaults to HTMLElement)
|
|
21
|
+
*/
|
|
6
22
|
export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement> = {
|
|
7
23
|
readonly props: TProps & PartialElement<TElementBase>
|
|
8
24
|
renderCount: number
|
|
9
25
|
injector: Injector
|
|
10
26
|
children?: ChildrenList
|
|
11
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Declaratively sets attributes and styles on the host custom element.
|
|
29
|
+
* Can be called multiple times per render; each call merges into the previous values.
|
|
30
|
+
*
|
|
31
|
+
* CSS custom properties (e.g. `--my-color`) are applied via `setProperty`.
|
|
32
|
+
* The `style` property accepts both standard camelCase properties and CSS custom properties.
|
|
33
|
+
*
|
|
34
|
+
* **Best practice:** Use `useHostProps` for data attributes, ARIA attributes, CSS variables,
|
|
35
|
+
* and event handlers on the host element instead of imperative DOM manipulation.
|
|
36
|
+
*
|
|
37
|
+
* @param hostProps An object of attribute key-value pairs, optionally including a `style` record
|
|
38
|
+
*
|
|
39
|
+
* **Note:** Object and function values are assigned as properties on the host element
|
|
40
|
+
* (not as attributes). This means you can set event handlers (e.g. `onclick`) and
|
|
41
|
+
* even class properties like `injector` via `useHostProps`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* useHostProps({
|
|
46
|
+
* 'data-variant': props.variant,
|
|
47
|
+
* role: 'progressbar',
|
|
48
|
+
* 'aria-valuenow': String(value),
|
|
49
|
+
* style: {
|
|
50
|
+
* '--btn-color-main': colors.main,
|
|
51
|
+
* display: 'flex',
|
|
52
|
+
* },
|
|
53
|
+
* })
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
useHostProps: (hostProps: Record<string, unknown> & { style?: Record<string, string> }) => void
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a mutable ref object that can be attached to intrinsic JSX elements via the `ref` prop.
|
|
60
|
+
* The ref's `current` property will be set to the DOM element after mount and `null` on unmount.
|
|
61
|
+
*
|
|
62
|
+
* Refs are cached by key, so calling `useRef` with the same key returns the same object across renders.
|
|
63
|
+
*
|
|
64
|
+
* **Best practice:** Prefer declarative JSX and `useHostProps` when possible.
|
|
65
|
+
* Use refs sparingly for imperative needs like focus management or measuring elements.
|
|
66
|
+
*
|
|
67
|
+
* @param key A unique key for caching the ref object
|
|
68
|
+
* @returns A ref object with a `current` property
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const inputRef = useRef<HTMLInputElement>('input')
|
|
73
|
+
* // In JSX:
|
|
74
|
+
* <input ref={inputRef} />
|
|
75
|
+
* // Later:
|
|
76
|
+
* inputRef.current?.focus()
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
useRef: <T extends Element = HTMLElement>(key: string) => RefObject<T>
|
|
80
|
+
|
|
12
81
|
/**
|
|
13
82
|
* Creates and disposes a resource after the component has been detached from the DOM
|
|
14
83
|
* @param key The key for caching the disposable resource
|
|
@@ -18,11 +87,29 @@ export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement
|
|
|
18
87
|
useDisposable: <T extends Disposable | AsyncDisposable>(key: string, factory: () => T) => T
|
|
19
88
|
|
|
20
89
|
/**
|
|
21
|
-
* Creates a state object from an existing observable value
|
|
90
|
+
* Creates a state object from an existing observable value.
|
|
91
|
+
*
|
|
92
|
+
* **Important:** By default, this will trigger a full component re-render when the observable value changes.
|
|
93
|
+
* To prevent re-renders (e.g., for manual DOM updates or animations), provide a custom `onChange` callback.
|
|
94
|
+
*
|
|
22
95
|
* @param key The key for caching the observable value
|
|
23
96
|
* @param observable The observable value to observe
|
|
24
|
-
* @param options Optional options for the observer
|
|
97
|
+
* @param options Optional options for the observer
|
|
98
|
+
* @param options.onChange Custom callback when value changes. If not provided, the component will re-render on each change.
|
|
25
99
|
* @returns tuple with the current value and a setter function
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* // Default behavior: re-renders component on change
|
|
103
|
+
* const [count] = useObservable('count', countObservable)
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* // Custom onChange: no re-render, update host element via useHostProps
|
|
107
|
+
* useHostProps({ 'data-active': count > 0 ? '' : undefined })
|
|
108
|
+
* const [count] = useObservable('count', countObservable, {
|
|
109
|
+
* onChange: () => {
|
|
110
|
+
* // Triggers a re-render so useHostProps above picks up the new value
|
|
111
|
+
* }
|
|
112
|
+
* })
|
|
26
113
|
*/
|
|
27
114
|
useObservable: <T>(
|
|
28
115
|
key: string,
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
serializeToQueryString as defaultSerializeToQueryString,
|
|
5
5
|
} from '@furystack/rest'
|
|
6
6
|
import { ObservableValue } from '@furystack/utils'
|
|
7
|
+
/**
|
|
8
|
+
* Singleton service that tracks browser location changes (pathname, search, hash)
|
|
9
|
+
* and exposes them as observable values for reactive routing and URL-driven state.
|
|
10
|
+
*/
|
|
7
11
|
@Injectable({ lifetime: 'singleton' })
|
|
8
12
|
export class LocationService implements Disposable {
|
|
9
13
|
constructor(
|
|
@@ -119,6 +123,13 @@ export class LocationService implements Disposable {
|
|
|
119
123
|
}).bind(this)
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Configures custom serialization for URL search state.
|
|
128
|
+
* Must be called **before** `LocationService` is first instantiated by the injector.
|
|
129
|
+
* @param injector The root injector
|
|
130
|
+
* @param serialize Function to serialize state to a query string
|
|
131
|
+
* @param deserialize Function to deserialize a query string to state
|
|
132
|
+
*/
|
|
122
133
|
export const useCustomSearchStateSerializer = (
|
|
123
134
|
injector: Injector,
|
|
124
135
|
serialize: typeof defaultSerializeToQueryString,
|
|
@@ -19,6 +19,122 @@ describe('ResourceManager', () => {
|
|
|
19
19
|
})
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
+
it('Should switch to a new observable when a different reference is passed for the same key', async () => {
|
|
23
|
+
await usingAsync(new ResourceManager(), async (rm) => {
|
|
24
|
+
const o1 = new ObservableValue(1)
|
|
25
|
+
const o2 = new ObservableValue(42)
|
|
26
|
+
const onChange = vi.fn()
|
|
27
|
+
|
|
28
|
+
const [value1] = rm.useObservable('test', o1, onChange)
|
|
29
|
+
expect(value1).toBe(1)
|
|
30
|
+
expect(o1.getObservers().length).toBe(1)
|
|
31
|
+
|
|
32
|
+
const [value2] = rm.useObservable('test', o2, onChange)
|
|
33
|
+
expect(value2).toBe(42)
|
|
34
|
+
expect(o1.getObservers().length).toBe(0)
|
|
35
|
+
expect(o2.getObservers().length).toBe(1)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('Should subscribe with the new onChange callback when switching observables', async () => {
|
|
40
|
+
await usingAsync(new ResourceManager(), async (rm) => {
|
|
41
|
+
const o1 = new ObservableValue('a')
|
|
42
|
+
const o2 = new ObservableValue('x')
|
|
43
|
+
const onChange1 = vi.fn()
|
|
44
|
+
const onChange2 = vi.fn()
|
|
45
|
+
|
|
46
|
+
rm.useObservable('test', o1, onChange1)
|
|
47
|
+
rm.useObservable('test', o2, onChange2)
|
|
48
|
+
|
|
49
|
+
o2.setValue('y')
|
|
50
|
+
expect(onChange2).toHaveBeenCalledWith('y')
|
|
51
|
+
expect(onChange1).not.toHaveBeenCalled()
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('Should not re-subscribe when the same observable reference is passed', async () => {
|
|
56
|
+
await usingAsync(new ResourceManager(), async (rm) => {
|
|
57
|
+
const o = new ObservableValue(1)
|
|
58
|
+
const onChange = vi.fn()
|
|
59
|
+
|
|
60
|
+
rm.useObservable('test', o, onChange)
|
|
61
|
+
rm.useObservable('test', o, onChange)
|
|
62
|
+
rm.useObservable('test', o, onChange)
|
|
63
|
+
|
|
64
|
+
expect(o.getObservers().length).toBe(1)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('Should return a setValue bound to the new observable after switching', async () => {
|
|
69
|
+
await usingAsync(new ResourceManager(), async (rm) => {
|
|
70
|
+
const o1 = new ObservableValue(1)
|
|
71
|
+
const o2 = new ObservableValue(10)
|
|
72
|
+
const onChange = vi.fn()
|
|
73
|
+
|
|
74
|
+
const [, setValue1] = rm.useObservable('test', o1, onChange)
|
|
75
|
+
setValue1(5)
|
|
76
|
+
expect(o1.getValue()).toBe(5)
|
|
77
|
+
|
|
78
|
+
const [, setValue2] = rm.useObservable('test', o2, onChange)
|
|
79
|
+
setValue2(99)
|
|
80
|
+
expect(o2.getValue()).toBe(99)
|
|
81
|
+
expect(o1.getValue()).toBe(5)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('Should handle multiple sequential observable switches for the same key', async () => {
|
|
86
|
+
await usingAsync(new ResourceManager(), async (rm) => {
|
|
87
|
+
const o1 = new ObservableValue('a')
|
|
88
|
+
const o2 = new ObservableValue('b')
|
|
89
|
+
const o3 = new ObservableValue('c')
|
|
90
|
+
const onChange = vi.fn()
|
|
91
|
+
|
|
92
|
+
const [v1] = rm.useObservable('test', o1, onChange)
|
|
93
|
+
expect(v1).toBe('a')
|
|
94
|
+
|
|
95
|
+
const [v2] = rm.useObservable('test', o2, onChange)
|
|
96
|
+
expect(v2).toBe('b')
|
|
97
|
+
expect(o1.getObservers().length).toBe(0)
|
|
98
|
+
|
|
99
|
+
const [v3] = rm.useObservable('test', o3, onChange)
|
|
100
|
+
expect(v3).toBe('c')
|
|
101
|
+
expect(o2.getObservers().length).toBe(0)
|
|
102
|
+
expect(o3.getObservers().length).toBe(1)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('Should not trigger callbacks on the old observable after switching', async () => {
|
|
107
|
+
await usingAsync(new ResourceManager(), async (rm) => {
|
|
108
|
+
const o1 = new ObservableValue(1)
|
|
109
|
+
const o2 = new ObservableValue(2)
|
|
110
|
+
const onChange = vi.fn()
|
|
111
|
+
|
|
112
|
+
rm.useObservable('test', o1, onChange)
|
|
113
|
+
rm.useObservable('test', o2, onChange)
|
|
114
|
+
|
|
115
|
+
onChange.mockClear()
|
|
116
|
+
o1.setValue(999)
|
|
117
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
118
|
+
|
|
119
|
+
o2.setValue(100)
|
|
120
|
+
expect(onChange).toHaveBeenCalledWith(100)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('Should clean up switched observers on dispose', async () => {
|
|
125
|
+
const o1 = new ObservableValue(1)
|
|
126
|
+
const o2 = new ObservableValue(2)
|
|
127
|
+
const onChange = vi.fn()
|
|
128
|
+
|
|
129
|
+
await usingAsync(new ResourceManager(), async (rm) => {
|
|
130
|
+
rm.useObservable('test', o1, onChange)
|
|
131
|
+
rm.useObservable('test', o2, onChange)
|
|
132
|
+
expect(o2.getObservers().length).toBe(1)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(o2.getObservers().length).toBe(0)
|
|
136
|
+
})
|
|
137
|
+
|
|
22
138
|
it('Should return a disposable from cache', async () => {
|
|
23
139
|
await usingAsync(new ResourceManager(), async (rm) => {
|
|
24
140
|
const factory = vi.fn(() => ({
|
|
@@ -80,6 +196,18 @@ describe('ResourceManager', () => {
|
|
|
80
196
|
expect(disposable[Symbol.dispose]).toHaveBeenCalledTimes(1)
|
|
81
197
|
})
|
|
82
198
|
|
|
199
|
+
it('Should silently ignore useState setter calls after disposal', async () => {
|
|
200
|
+
let setValueFn: (value: number) => void
|
|
201
|
+
|
|
202
|
+
await usingAsync(new ResourceManager(), async (rm) => {
|
|
203
|
+
const [, setValue] = rm.useState<number>('count', 0, vi.fn())
|
|
204
|
+
setValueFn = setValue
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// After disposal, calling the setter should not throw
|
|
208
|
+
expect(() => setValueFn!(42)).not.toThrow()
|
|
209
|
+
})
|
|
210
|
+
|
|
83
211
|
it('Should throw an aggregated error when failed to async dispose something', async () => {
|
|
84
212
|
const disposable = {
|
|
85
213
|
[Symbol.asyncDispose]: vi.fn(async () => {
|
|
@@ -8,6 +8,13 @@ import { ObservableValue, isAsyncDisposable, isDisposable } from '@furystack/uti
|
|
|
8
8
|
export class ResourceManager implements AsyncDisposable {
|
|
9
9
|
private readonly disposables = new Map<string, Disposable | AsyncDisposable>()
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Returns an existing disposable resource by key, or creates and caches a new one.
|
|
13
|
+
* Resources are automatically disposed when the component is removed from the DOM.
|
|
14
|
+
* @param key Unique key for caching this resource
|
|
15
|
+
* @param factory Factory function called once to create the resource
|
|
16
|
+
* @returns The cached or newly created resource
|
|
17
|
+
*/
|
|
11
18
|
public useDisposable<T extends Disposable | AsyncDisposable>(key: string, factory: () => T): T {
|
|
12
19
|
const existing = this.disposables.get(key)
|
|
13
20
|
if (!existing) {
|
|
@@ -20,6 +27,15 @@ export class ResourceManager implements AsyncDisposable {
|
|
|
20
27
|
|
|
21
28
|
public readonly observers = new Map<string, ValueObserver<any>>()
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Subscribes to an observable value by key. If the observable changes between renders,
|
|
32
|
+
* the previous subscription is disposed and a new one is created.
|
|
33
|
+
* @param key Unique key for caching this subscription
|
|
34
|
+
* @param observable The observable to subscribe to
|
|
35
|
+
* @param onChange Callback invoked when the value changes
|
|
36
|
+
* @param options Additional observer options
|
|
37
|
+
* @returns Tuple of [currentValue, setValue]
|
|
38
|
+
*/
|
|
23
39
|
public useObservable = <T>(
|
|
24
40
|
key: string,
|
|
25
41
|
observable: ObservableValue<T>,
|
|
@@ -28,6 +44,12 @@ export class ResourceManager implements AsyncDisposable {
|
|
|
28
44
|
): [value: T, setValue: (newValue: T) => void] => {
|
|
29
45
|
const alreadyUsed = this.observers.get(key) as ValueObserver<T> | undefined
|
|
30
46
|
if (alreadyUsed) {
|
|
47
|
+
if (alreadyUsed.observable !== observable) {
|
|
48
|
+
alreadyUsed[Symbol.dispose]()
|
|
49
|
+
const observer = observable.subscribe(onChange, options)
|
|
50
|
+
this.observers.set(key, observer)
|
|
51
|
+
return [observable.getValue(), observable.setValue.bind(observable)]
|
|
52
|
+
}
|
|
31
53
|
return [alreadyUsed.observable.getValue(), alreadyUsed.observable.setValue.bind(alreadyUsed.observable)]
|
|
32
54
|
}
|
|
33
55
|
const observer = observable.subscribe(onChange, options)
|
|
@@ -37,6 +59,14 @@ export class ResourceManager implements AsyncDisposable {
|
|
|
37
59
|
|
|
38
60
|
public readonly stateObservers = new Map<string, ObservableValue<any>>()
|
|
39
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Creates or retrieves a local state observable by key.
|
|
64
|
+
* State is persisted across re-renders and disposed with the component.
|
|
65
|
+
* @param key Unique key for caching this state
|
|
66
|
+
* @param initialValue Initial value used on first call
|
|
67
|
+
* @param callback Callback invoked when the state changes
|
|
68
|
+
* @returns Tuple of [currentValue, setValue]
|
|
69
|
+
*/
|
|
40
70
|
public useState = <T>(
|
|
41
71
|
key: string,
|
|
42
72
|
initialValue: T,
|
|
@@ -48,7 +78,12 @@ export class ResourceManager implements AsyncDisposable {
|
|
|
48
78
|
newObservable.subscribe(callback)
|
|
49
79
|
}
|
|
50
80
|
const observable = this.stateObservers.get(key) as ObservableValue<T>
|
|
51
|
-
|
|
81
|
+
const setValue = (newValue: T) => {
|
|
82
|
+
if (!observable.isDisposed) {
|
|
83
|
+
observable.setValue(newValue)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [observable.getValue(), setValue]
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
public async [Symbol.asyncDispose]() {
|