@furystack/shades 6.1.4 → 7.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/dist/component-factory.spec.js +6 -6
- package/dist/component-factory.spec.js.map +1 -1
- package/dist/components/lazy-load.js +17 -14
- package/dist/components/lazy-load.js.map +1 -1
- package/dist/components/route-link.js +7 -3
- package/dist/components/route-link.js.map +1 -1
- package/dist/components/route-link.spec.js +2 -2
- package/dist/components/route-link.spec.js.map +1 -1
- package/dist/components/router.js +25 -26
- package/dist/components/router.js.map +1 -1
- package/dist/components/router.spec.js +2 -2
- package/dist/components/router.spec.js.map +1 -1
- package/dist/models/render-options.js +23 -0
- package/dist/models/render-options.js.map +1 -1
- package/dist/services/location-service.js +16 -4
- package/dist/services/location-service.js.map +1 -1
- package/dist/services/location-service.spec.js +1 -1
- package/dist/services/location-service.spec.js.map +1 -1
- package/dist/services/resource-manager.js +48 -0
- package/dist/services/resource-manager.js.map +1 -0
- package/dist/services/resource-manager.spec.js +32 -0
- package/dist/services/resource-manager.spec.js.map +1 -0
- package/dist/shade-component.js +28 -14
- package/dist/shade-component.js.map +1 -1
- package/dist/shade-resources.integration.spec.js +7 -8
- package/dist/shade-resources.integration.spec.js.map +1 -1
- package/dist/shade.js +34 -75
- package/dist/shade.js.map +1 -1
- package/dist/shades.integration.spec.js +197 -187
- package/dist/shades.integration.spec.js.map +1 -1
- package/package.json +5 -5
- package/src/component-factory.spec.tsx +7 -7
- package/src/components/lazy-load.tsx +19 -15
- package/src/components/route-link.spec.tsx +2 -2
- package/src/components/route-link.tsx +11 -10
- package/src/components/router.spec.tsx +2 -2
- package/src/components/router.tsx +32 -32
- package/src/jsx.ts +3 -2
- package/src/models/render-options.ts +37 -9
- package/src/services/location-service.spec.ts +1 -1
- package/src/services/location-service.tsx +18 -4
- package/src/services/resource-manager.spec.ts +33 -0
- package/src/services/resource-manager.ts +60 -0
- package/src/shade-component.ts +35 -15
- package/src/shade-resources.integration.spec.tsx +8 -14
- package/src/shade.ts +35 -112
- package/src/shades.integration.spec.tsx +265 -252
- package/types/components/lazy-load.d.ts +1 -1
- package/types/components/lazy-load.d.ts.map +1 -1
- package/types/components/route-link.d.ts +1 -1
- package/types/components/route-link.d.ts.map +1 -1
- package/types/components/router.d.ts +6 -8
- package/types/components/router.d.ts.map +1 -1
- package/types/jsx.d.ts +3 -2
- package/types/jsx.d.ts.map +1 -1
- package/types/models/render-options.d.ts +7 -7
- package/types/models/render-options.d.ts.map +1 -1
- package/types/services/location-service.d.ts +12 -1
- package/types/services/location-service.d.ts.map +1 -1
- package/types/services/resource-manager.d.ts +16 -0
- package/types/services/resource-manager.d.ts.map +1 -0
- package/types/services/resource-manager.spec.d.ts +2 -0
- package/types/services/resource-manager.spec.d.ts.map +1 -0
- package/types/shade-component.d.ts +16 -5
- package/types/shade-component.d.ts.map +1 -1
- package/types/shade.d.ts +8 -27
- package/types/shade.d.ts.map +1 -1
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { Shade } from '../shade'
|
|
2
2
|
import type { PartialElement } from '../models'
|
|
3
3
|
import { LocationService } from '../services'
|
|
4
|
-
import { createComponent } from '..'
|
|
4
|
+
import { attachProps, createComponent } from '..'
|
|
5
5
|
|
|
6
6
|
export type RouteLinkProps = PartialElement<HTMLAnchorElement>
|
|
7
7
|
|
|
8
8
|
export const RouteLink = Shade<RouteLinkProps>({
|
|
9
9
|
shadowDomName: 'route-link',
|
|
10
|
-
render: ({ children, props, injector }) => {
|
|
10
|
+
render: ({ children, props, injector, element }) => {
|
|
11
|
+
attachProps(element, {
|
|
12
|
+
...props,
|
|
13
|
+
onclick: (ev: MouseEvent) => {
|
|
14
|
+
ev.preventDefault()
|
|
15
|
+
history.pushState('', props.title || '', props.href)
|
|
16
|
+
injector.getInstance(LocationService).updateState()
|
|
17
|
+
},
|
|
18
|
+
})
|
|
11
19
|
return (
|
|
12
|
-
<a
|
|
13
|
-
{...props}
|
|
14
|
-
onclick={(ev: MouseEvent) => {
|
|
15
|
-
ev.preventDefault()
|
|
16
|
-
history.pushState('', props.title || '', props.href)
|
|
17
|
-
injector.getInstance(LocationService).updateState()
|
|
18
|
-
}}
|
|
19
|
-
>
|
|
20
|
+
<a href={props.href} style={{ color: 'inherit', textDecoration: 'inherit' }} onclick={(e) => e.preventDefault()}>
|
|
20
21
|
{children}
|
|
21
22
|
</a>
|
|
22
23
|
)
|
|
@@ -25,7 +25,7 @@ describe('Router', () => {
|
|
|
25
25
|
|
|
26
26
|
const onRouteChange = jest.fn()
|
|
27
27
|
|
|
28
|
-
injector.getInstance(LocationService).
|
|
28
|
+
injector.getInstance(LocationService).onLocationPathChanged.subscribe(onRouteChange)
|
|
29
29
|
|
|
30
30
|
initializeShadeRoot({
|
|
31
31
|
injector,
|
|
@@ -61,7 +61,7 @@ describe('Router', () => {
|
|
|
61
61
|
},
|
|
62
62
|
{ url: '/', component: () => <div id="content">home</div> },
|
|
63
63
|
]}
|
|
64
|
-
notFound={
|
|
64
|
+
notFound={<div id="content">not found</div>}
|
|
65
65
|
/>
|
|
66
66
|
</div>
|
|
67
67
|
),
|
|
@@ -11,67 +11,67 @@ export interface Route<TMatchResult extends object> {
|
|
|
11
11
|
url: string
|
|
12
12
|
component: (options: { currentUrl: string; match: MatchResult<TMatchResult> }) => JSX.Element
|
|
13
13
|
routingOptions?: TokensToRegexpOptions
|
|
14
|
-
onVisit?: (options: RenderOptions<
|
|
15
|
-
onLeave?: (options: RenderOptions<
|
|
14
|
+
onVisit?: (options: RenderOptions<unknown>) => Promise<void>
|
|
15
|
+
onLeave?: (options: RenderOptions<unknown>) => Promise<void>
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface RouterProps {
|
|
19
19
|
style?: CSSStyleDeclaration
|
|
20
20
|
routes: Array<Route<any>>
|
|
21
|
-
notFound?:
|
|
21
|
+
notFound?: JSX.Element
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface RouterState {
|
|
25
|
-
activeRoute?: Route<any>
|
|
25
|
+
activeRoute?: Route<any> | null
|
|
26
26
|
activeRouteParams?: any
|
|
27
|
-
jsx
|
|
28
|
-
lock: Semaphore
|
|
27
|
+
jsx: JSX.Element
|
|
29
28
|
}
|
|
30
|
-
export const Router = Shade<RouterProps
|
|
29
|
+
export const Router = Shade<RouterProps>({
|
|
31
30
|
shadowDomName: 'shade-router',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
render: (options) => {
|
|
32
|
+
const { useState, useObservable, injector } = options
|
|
33
|
+
const [lock] = useState('lock', new Semaphore(1))
|
|
34
|
+
const [state, setState] = useState<RouterState>('routerState', {
|
|
35
|
+
jsx: <div />,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const updateUrl = async (currentUrl: string) => {
|
|
39
|
+
const [lastState] = useState<RouterState>('routerState', state)
|
|
40
|
+
const { activeRoute: lastRoute, activeRouteParams: lastRouteParams, jsx: lastJsx } = lastState
|
|
38
41
|
try {
|
|
39
42
|
await lock.acquire()
|
|
40
|
-
for (const route of props.routes) {
|
|
43
|
+
for (const route of options.props.routes) {
|
|
41
44
|
const matchFn = match(route.url, route.routingOptions)
|
|
42
45
|
const matchResult = matchFn(currentUrl)
|
|
43
46
|
if (matchResult) {
|
|
44
|
-
if (route !== lastRoute || JSON.stringify(
|
|
45
|
-
await lastRoute?.onLeave?.({
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
activeRouteParams: matchResult.params,
|
|
50
|
-
})
|
|
51
|
-
await route.onVisit?.({ children, props, injector, updateState, getState, element })
|
|
47
|
+
if (route !== lastRoute || JSON.stringify(lastRouteParams) !== JSON.stringify(matchResult.params)) {
|
|
48
|
+
await lastRoute?.onLeave?.({ ...options, element: lastState.jsx })
|
|
49
|
+
const newJsx = route.component({ currentUrl, match: matchResult })
|
|
50
|
+
setState({ jsx: newJsx, activeRoute: route, activeRouteParams: matchResult.params })
|
|
51
|
+
await route.onVisit?.({ ...options, element: newJsx })
|
|
52
52
|
}
|
|
53
53
|
return
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
if (lastRoute?.onLeave) {
|
|
57
|
-
await lastRoute.onLeave({
|
|
57
|
+
await lastRoute.onLeave({ ...options, element: lastJsx })
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
setState({
|
|
60
|
+
jsx: options.props.notFound || <div />,
|
|
61
|
+
activeRoute: null,
|
|
62
|
+
activeRouteParams: null,
|
|
63
|
+
})
|
|
60
64
|
} catch (e) {
|
|
61
65
|
// path updates can be async, this can be ignored
|
|
62
66
|
if (!(e instanceof ObservableAlreadyDisposedError)) {
|
|
63
67
|
throw e
|
|
64
68
|
}
|
|
65
69
|
} finally {
|
|
66
|
-
lock
|
|
70
|
+
lock?.release()
|
|
67
71
|
}
|
|
68
|
-
}, true),
|
|
69
|
-
],
|
|
70
|
-
render: ({ getState }) => {
|
|
71
|
-
const { jsx } = getState()
|
|
72
|
-
if (jsx) {
|
|
73
|
-
return jsx
|
|
74
72
|
}
|
|
75
|
-
|
|
73
|
+
|
|
74
|
+
useObservable('locationPathChanged', injector.getInstance(LocationService).onLocationPathChanged, updateUrl, true)
|
|
75
|
+
return state.jsx
|
|
76
76
|
},
|
|
77
77
|
})
|
package/src/jsx.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { Injector } from '@furystack/inject'
|
|
2
2
|
import type { ChildrenList, PartialElement } from './models'
|
|
3
|
+
import type { ResourceManager } from './services/resource-manager'
|
|
3
4
|
|
|
4
5
|
declare global {
|
|
5
6
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
7
|
export namespace JSX {
|
|
7
|
-
interface Element<TProps = any
|
|
8
|
+
interface Element<TProps = any> extends HTMLElement {
|
|
8
9
|
injector: Injector
|
|
9
|
-
state: TState
|
|
10
10
|
props: TProps
|
|
11
11
|
updateComponent: () => void
|
|
12
12
|
shadeChildren?: ChildrenList
|
|
13
13
|
callConstructed: () => void
|
|
14
|
+
resourceManager: ResourceManager
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
interface IntrinsicElements {
|
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import type { Injector } from '@furystack/inject'
|
|
2
|
-
import type { PartialElement } from './partial-element'
|
|
3
2
|
import type { ChildrenList } from './children-list'
|
|
3
|
+
import type { Disposable, ObservableValue } from '@furystack/utils'
|
|
4
4
|
|
|
5
|
-
export type RenderOptions<TProps
|
|
5
|
+
export type RenderOptions<TProps> = {
|
|
6
6
|
readonly props: TProps
|
|
7
7
|
|
|
8
8
|
injector: Injector
|
|
9
9
|
children?: ChildrenList
|
|
10
|
-
element: JSX.Element<TProps
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
element: JSX.Element<TProps>
|
|
11
|
+
useDisposable: <T extends Disposable>(key: string, factory: () => T) => T
|
|
12
|
+
useObservable: <T>(
|
|
13
|
+
key: string,
|
|
14
|
+
observable: ObservableValue<T>,
|
|
15
|
+
callback?: (newValue: T) => void,
|
|
16
|
+
getLast?: boolean,
|
|
17
|
+
) => [value: T, setValue: (newValue: T) => void]
|
|
18
|
+
useState: <T>(key: string, initialValue: T) => [value: T, setValue: (newValue: T) => void]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// export type RenderOptionsState<TState> = unknown extends TState
|
|
22
|
+
// ? {}
|
|
23
|
+
// : {
|
|
24
|
+
// /**
|
|
25
|
+
// * @returns the current state object
|
|
26
|
+
// */
|
|
27
|
+
// getState: () => TState
|
|
28
|
+
// /**
|
|
29
|
+
// * Update the current component state's multiple properties in one-shot
|
|
30
|
+
// *
|
|
31
|
+
// * @param newState The partial new state object
|
|
32
|
+
// * @param skipRender Option to skip the render process
|
|
33
|
+
// */
|
|
34
|
+
// updateState: (newState: PartialElement<TState>, skipRender?: boolean) => void
|
|
35
|
+
// /**
|
|
36
|
+
// * @param key The key on the state object
|
|
37
|
+
// * @returns A tuple with the value and the setter function
|
|
38
|
+
// */
|
|
39
|
+
// useState: <T extends keyof TState>(
|
|
40
|
+
// key: T,
|
|
41
|
+
// ) => [value: TState[T], setValue: (newValue: TState[T], skipRender?: boolean) => void]
|
|
42
|
+
// }
|
|
43
|
+
|
|
44
|
+
// export type RenderOptions<TProps, TState> = RenderOptionsBase<TProps, TState> & RenderOptionsState<TState>
|
|
@@ -22,7 +22,7 @@ describe('LocationService', () => {
|
|
|
22
22
|
await usingAsync(new Injector(), async (i) => {
|
|
23
23
|
const onLocaionChanged = jest.fn()
|
|
24
24
|
const s = i.getInstance(LocationService)
|
|
25
|
-
s.
|
|
25
|
+
s.onLocationPathChanged.subscribe(onLocaionChanged)
|
|
26
26
|
expect(onLocaionChanged).toBeCalledTimes(0)
|
|
27
27
|
history.pushState(null, '', '/loc1')
|
|
28
28
|
expect(onLocaionChanged).toBeCalledTimes(1)
|
|
@@ -8,14 +8,28 @@ export class LocationService implements Disposable {
|
|
|
8
8
|
window.removeEventListener('hashchange', this.updateState)
|
|
9
9
|
this.pushStateTracer.dispose()
|
|
10
10
|
this.replaceStateTracer.dispose()
|
|
11
|
-
this.
|
|
11
|
+
this.onLocationPathChanged.dispose()
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Observable value that will be updated when the location pathname (e.g. /page/1) changes
|
|
16
|
+
*/
|
|
17
|
+
public onLocationPathChanged = new ObservableValue(new URL(location.href).pathname)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Observable value that will be updated when the location hash (e.g. #hash) changes
|
|
21
|
+
*/
|
|
22
|
+
public onLocationHashChanged = new ObservableValue(location.hash)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Observable value that will be updated when the location search (e.g. ?search=1) changes
|
|
26
|
+
*/
|
|
27
|
+
public onLocationSearchChanged = new ObservableValue(location.search)
|
|
15
28
|
|
|
16
29
|
public updateState() {
|
|
17
|
-
|
|
18
|
-
this.
|
|
30
|
+
this.onLocationPathChanged.setValue(location.pathname)
|
|
31
|
+
this.onLocationHashChanged.setValue(location.hash)
|
|
32
|
+
this.onLocationSearchChanged.setValue(location.search)
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
private pushStateTracer: Disposable
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ObservableValue, using } from '@furystack/utils'
|
|
2
|
+
import { ResourceManager } from './resource-manager'
|
|
3
|
+
describe('ResourceManager', () => {
|
|
4
|
+
it('Should return an observable from cache', () => {
|
|
5
|
+
using(new ResourceManager(), (rm) => {
|
|
6
|
+
const o = new ObservableValue(1)
|
|
7
|
+
const [value1] = rm.useObservable('test', o, () => {
|
|
8
|
+
/** ignore */
|
|
9
|
+
})
|
|
10
|
+
const [value2] = rm.useObservable('test', o, () => {
|
|
11
|
+
/** ignore */
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
expect(value1).toBe(value2)
|
|
15
|
+
|
|
16
|
+
expect(o.getObservers().length).toBe(1)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('Should return a disposable from cache', () => {
|
|
21
|
+
using(new ResourceManager(), (rm) => {
|
|
22
|
+
const d = {
|
|
23
|
+
dispose: () => {
|
|
24
|
+
/** ignore */
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
const d1 = rm.useDisposable('test', () => d)
|
|
28
|
+
const d2 = rm.useDisposable('test', () => d)
|
|
29
|
+
|
|
30
|
+
expect(d1).toBe(d2)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ObservableValue } from '@furystack/utils'
|
|
2
|
+
import type { Disposable, ValueChangeCallback } from '@furystack/utils'
|
|
3
|
+
import type { ValueObserver } from '@furystack/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Class for managing observables and disposables for components, based on key-value maps
|
|
7
|
+
*/
|
|
8
|
+
export class ResourceManager {
|
|
9
|
+
private readonly disposables = new Map<string, Disposable>()
|
|
10
|
+
|
|
11
|
+
public useDisposable<T extends Disposable>(key: string, factory: () => T): T {
|
|
12
|
+
if (!this.disposables.has(key)) {
|
|
13
|
+
this.disposables.set(key, factory())
|
|
14
|
+
}
|
|
15
|
+
return this.disposables.get(key) as T
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public readonly observers = new Map<string, ValueObserver<any>>()
|
|
19
|
+
|
|
20
|
+
public useObservable = <T>(
|
|
21
|
+
key: string,
|
|
22
|
+
observable: ObservableValue<T>,
|
|
23
|
+
callback: ValueChangeCallback<T>,
|
|
24
|
+
getLast?: boolean,
|
|
25
|
+
): [value: T, setValue: (newValue: T) => void] => {
|
|
26
|
+
const alreadyUsed = this.observers.get(key) as ValueObserver<T> | undefined
|
|
27
|
+
if (alreadyUsed) {
|
|
28
|
+
return [alreadyUsed.observable.getValue(), alreadyUsed.observable.setValue.bind(alreadyUsed.observable)]
|
|
29
|
+
}
|
|
30
|
+
const observer = observable.subscribe(callback, getLast)
|
|
31
|
+
this.observers.set(key, observer)
|
|
32
|
+
return [observable.getValue(), observable.setValue.bind(observable)]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public readonly stateObservers = new Map<string, ObservableValue<any>>()
|
|
36
|
+
|
|
37
|
+
public useState = <T>(
|
|
38
|
+
key: string,
|
|
39
|
+
initialValue: T,
|
|
40
|
+
callback: ValueChangeCallback<T>,
|
|
41
|
+
): [value: T, setValue: (newValue: T) => void] => {
|
|
42
|
+
if (!this.stateObservers.has(key)) {
|
|
43
|
+
const newObservable = new ObservableValue<T>(initialValue)
|
|
44
|
+
this.stateObservers.set(key, newObservable)
|
|
45
|
+
newObservable.subscribe(callback)
|
|
46
|
+
}
|
|
47
|
+
const observable = this.stateObservers.get(key) as ObservableValue<T>
|
|
48
|
+
return [observable.getValue(), observable.setValue.bind(observable)]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public dispose() {
|
|
52
|
+
this.disposables.forEach((r) => r.dispose())
|
|
53
|
+
this.disposables.clear()
|
|
54
|
+
this.observers.forEach((r) => r.dispose())
|
|
55
|
+
this.observers.clear()
|
|
56
|
+
|
|
57
|
+
this.stateObservers.forEach((r) => r.dispose())
|
|
58
|
+
this.stateObservers.clear()
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/shade-component.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { isShadeComponent } from './models'
|
|
|
9
9
|
*/
|
|
10
10
|
export const appendChild = (el: HTMLElement | DocumentFragment, children: ChildrenList) => {
|
|
11
11
|
for (const child of children) {
|
|
12
|
-
if (typeof child === 'string') {
|
|
12
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
13
13
|
el.appendChild(document.createTextNode(child))
|
|
14
14
|
} else {
|
|
15
15
|
if (child instanceof HTMLElement || child instanceof DocumentFragment) {
|
|
@@ -46,26 +46,37 @@ export const attachDataAttributes = (el: HTMLElement, props: any) => {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Factory method that creates a component. This should be configured as a default JSX Factory in tsconfig.
|
|
50
49
|
*
|
|
51
|
-
* @param
|
|
52
|
-
* @param props The
|
|
53
|
-
* @param children additional rest parameters will be parsed as children objects
|
|
54
|
-
* @returns the created JSX element
|
|
50
|
+
* @param el The Target HTML Element
|
|
51
|
+
* @param props The Props to attach
|
|
55
52
|
*/
|
|
56
|
-
export const
|
|
53
|
+
export const attachProps = (el: HTMLElement, props: any) => {
|
|
54
|
+
Object.assign(el, props)
|
|
55
|
+
|
|
56
|
+
if (props && (props as any).style) {
|
|
57
|
+
attachStyles(el, props)
|
|
58
|
+
}
|
|
59
|
+
attachDataAttributes(el, props)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type CreateComponentArgs<TProps> = [
|
|
57
63
|
elementType: string | ShadeComponent<TProps>,
|
|
58
64
|
props: TProps,
|
|
59
|
-
...children: ChildrenList
|
|
60
|
-
|
|
65
|
+
...children: ChildrenList,
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
// eslint-disable-next-line jsdoc/require-param
|
|
69
|
+
/**
|
|
70
|
+
* Factory method that creates a component. This should be configured as a default JSX Factory in tsconfig.
|
|
71
|
+
*
|
|
72
|
+
* @returns the created JSX element
|
|
73
|
+
*/
|
|
74
|
+
export const createComponentInner = <TProps>(...[elementType, props, ...children]: CreateComponentArgs<TProps>) => {
|
|
61
75
|
if (typeof elementType === 'string') {
|
|
62
76
|
const el = document.createElement(elementType)
|
|
63
|
-
Object.assign(el, props)
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
attachDataAttributes(el, props)
|
|
78
|
+
attachProps(el, props)
|
|
79
|
+
|
|
69
80
|
if (children) {
|
|
70
81
|
appendChild(el, children)
|
|
71
82
|
}
|
|
@@ -78,8 +89,17 @@ export const createComponent = <TProps>(
|
|
|
78
89
|
return undefined
|
|
79
90
|
}
|
|
80
91
|
|
|
81
|
-
|
|
92
|
+
type CreateFragmentArgs = [props: null, ...children: ChildrenList]
|
|
93
|
+
|
|
94
|
+
export const createFragmentInner = (...[_props, ...children]: CreateFragmentArgs) => {
|
|
82
95
|
const fragment = document.createDocumentFragment()
|
|
83
96
|
appendChild(fragment, children)
|
|
84
97
|
return fragment
|
|
85
98
|
}
|
|
99
|
+
|
|
100
|
+
export const createComponent = <TProps>(...args: CreateComponentArgs<TProps> | CreateFragmentArgs) => {
|
|
101
|
+
if (args[0] === null) {
|
|
102
|
+
return createFragmentInner(...args)
|
|
103
|
+
}
|
|
104
|
+
return createComponentInner(...args)
|
|
105
|
+
}
|
|
@@ -24,22 +24,15 @@ describe('Shade Resources integration tests', () => {
|
|
|
24
24
|
const obs2 = new ObservableValue('a')
|
|
25
25
|
|
|
26
26
|
const ExampleComponent = Shade({
|
|
27
|
-
|
|
28
|
-
obs1
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
),
|
|
32
|
-
obs2.subscribe(
|
|
33
|
-
(val2) => ((element.querySelector('#val2') as HTMLDivElement).innerHTML = val2.toString()),
|
|
34
|
-
true,
|
|
35
|
-
),
|
|
36
|
-
],
|
|
37
|
-
render: () => {
|
|
27
|
+
render: ({ useObservable }) => {
|
|
28
|
+
const [value1] = useObservable('obs1', obs1)
|
|
29
|
+
const [value2] = useObservable('obs2', obs2)
|
|
30
|
+
|
|
38
31
|
renderCounter()
|
|
39
32
|
return (
|
|
40
33
|
<div>
|
|
41
|
-
<div id="val1"
|
|
42
|
-
<div id="val2"
|
|
34
|
+
<div id="val1">{value1}</div>
|
|
35
|
+
<div id="val2">{value2}</div>
|
|
43
36
|
</div>
|
|
44
37
|
)
|
|
45
38
|
},
|
|
@@ -67,6 +60,7 @@ describe('Shade Resources integration tests', () => {
|
|
|
67
60
|
expect(document.body.innerHTML).toBe(
|
|
68
61
|
'<div id="root"><shades-example-resource><div><div id="val1">1</div><div id="val2">a</div></div></shades-example-resource></div>',
|
|
69
62
|
)
|
|
63
|
+
expect(renderCounter).toBeCalledTimes(2)
|
|
70
64
|
|
|
71
65
|
obs2.setValue('b')
|
|
72
66
|
expect(document.body.innerHTML).toBe(
|
|
@@ -78,6 +72,6 @@ describe('Shade Resources integration tests', () => {
|
|
|
78
72
|
expect(obs1.getObservers().length).toBe(0)
|
|
79
73
|
expect(obs2.getObservers().length).toBe(0)
|
|
80
74
|
|
|
81
|
-
expect(renderCounter).toBeCalledTimes(
|
|
75
|
+
expect(renderCounter).toBeCalledTimes(3)
|
|
82
76
|
})
|
|
83
77
|
})
|