@furystack/shades 3.7.3 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/component-factory.spec.js +68 -11
  2. package/dist/component-factory.spec.js.map +1 -1
  3. package/dist/components/lazy-load.spec.d.ts +2 -0
  4. package/dist/components/lazy-load.spec.d.ts.map +1 -0
  5. package/dist/components/lazy-load.spec.js +83 -0
  6. package/dist/components/lazy-load.spec.js.map +1 -0
  7. package/dist/components/route-link.d.ts.map +1 -1
  8. package/dist/components/route-link.js.map +1 -1
  9. package/dist/components/route-link.spec.d.ts +2 -0
  10. package/dist/components/route-link.spec.d.ts.map +1 -0
  11. package/dist/components/route-link.spec.js +38 -0
  12. package/dist/components/route-link.spec.js.map +1 -0
  13. package/dist/components/router.d.ts +4 -2
  14. package/dist/components/router.d.ts.map +1 -1
  15. package/dist/components/router.js +35 -26
  16. package/dist/components/router.js.map +1 -1
  17. package/dist/components/router.spec.d.ts +2 -0
  18. package/dist/components/router.spec.d.ts.map +1 -0
  19. package/dist/components/router.spec.js +93 -0
  20. package/dist/components/router.spec.js.map +1 -0
  21. package/dist/services/location-service.d.ts +1 -1
  22. package/dist/services/location-service.d.ts.map +1 -1
  23. package/dist/services/location-service.js +3 -2
  24. package/dist/services/location-service.js.map +1 -1
  25. package/dist/services/location-service.spec.js +29 -3
  26. package/dist/services/location-service.spec.js.map +1 -1
  27. package/dist/services/screen-service.d.ts +1 -1
  28. package/dist/services/screen-service.d.ts.map +1 -1
  29. package/dist/services/screen-service.js +5 -5
  30. package/dist/services/screen-service.js.map +1 -1
  31. package/dist/services/screen-service.spec.d.ts +2 -0
  32. package/dist/services/screen-service.spec.d.ts.map +1 -0
  33. package/dist/services/screen-service.spec.js +34 -0
  34. package/dist/services/screen-service.spec.js.map +1 -0
  35. package/dist/shade-resources.integration.spec.d.ts +2 -0
  36. package/dist/shade-resources.integration.spec.d.ts.map +1 -0
  37. package/dist/shade-resources.integration.spec.js +62 -0
  38. package/dist/shade-resources.integration.spec.js.map +1 -0
  39. package/dist/shade.d.ts +2 -0
  40. package/dist/shade.d.ts.map +1 -1
  41. package/dist/shade.js +22 -6
  42. package/dist/shade.js.map +1 -1
  43. package/dist/shades.integration.spec.d.ts +2 -0
  44. package/dist/shades.integration.spec.d.ts.map +1 -0
  45. package/dist/shades.integration.spec.js +123 -0
  46. package/dist/shades.integration.spec.js.map +1 -0
  47. package/package.json +8 -5
  48. package/src/component-factory.spec.tsx +97 -14
  49. package/src/components/lazy-load.spec.tsx +117 -0
  50. package/src/components/route-link.spec.tsx +50 -0
  51. package/src/components/route-link.tsx +2 -1
  52. package/src/components/router.spec.tsx +130 -0
  53. package/src/components/router.tsx +38 -29
  54. package/src/services/location-service.spec.ts +37 -3
  55. package/src/services/location-service.tsx +3 -2
  56. package/src/services/screen-service.spec.ts +39 -0
  57. package/src/services/screen-service.ts +1 -1
  58. package/src/shade-resources.integration.spec.tsx +94 -0
  59. package/src/shade.ts +29 -9
  60. package/src/shades.integration.spec.tsx +156 -0
package/src/shade.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { ObservableValue } from '@furystack/utils'
1
+ import { Disposable, ObservableValue } from '@furystack/utils'
2
2
  import { v4 } from 'uuid'
3
3
  import { Injector } from '@furystack/inject'
4
- import { ChildrenList, RenderOptions } from './models'
4
+ import { ChildrenList, PartialElement, RenderOptions } from './models'
5
5
 
6
6
  export type ShadeOptions<TProps, TState> = {
7
7
  /**
@@ -29,6 +29,8 @@ export type ShadeOptions<TProps, TState> = {
29
29
  * Will be executed when the element is detached from the DOM.
30
30
  */
31
31
  onDetach?: (options: RenderOptions<TProps, TState>) => void
32
+
33
+ resources?: (options: RenderOptions<TProps, TState>) => Disposable[]
32
34
  } & (unknown extends TState
33
35
  ? {}
34
36
  : {
@@ -64,6 +66,7 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
64
66
  this.state.dispose()
65
67
  this.shadeChildren.dispose()
66
68
  this.cleanup && this.cleanup()
69
+ Object.values(this.resources).forEach((s) => s.dispose())
67
70
  }
68
71
 
69
72
  /**
@@ -93,23 +96,35 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
93
96
  private getRenderOptions = () => {
94
97
  const props = this.props.getValue() || {}
95
98
  const getState = () => this.state.getValue()
96
- return {
99
+ const updateState = (stateChanges: PartialElement<TState>, skipRender?: boolean) => {
100
+ const currentState = this.state.getValue()
101
+ const newState = { ...currentState, ...stateChanges }
102
+ if (JSON.stringify(currentState) !== JSON.stringify(newState)) {
103
+ this.state.setValue(newState)
104
+ !skipRender && this.updateComponent()
105
+ }
106
+ }
107
+
108
+ const returnValue: RenderOptions<TProps, TState> = {
97
109
  props,
98
110
  getState,
99
111
  injector: this.injector,
100
- updateState: (newState: TState, skipRender: boolean) => {
101
- this.state.setValue({ ...this.state.getValue(), ...newState })
102
- !skipRender && this.updateComponent()
103
- },
112
+ updateState,
104
113
  children: this.shadeChildren.getValue(),
105
114
  element: this,
106
- } as any as RenderOptions<TProps, TState>
115
+ }
116
+
117
+ return returnValue
118
+ }
119
+
120
+ private createResources() {
121
+ this.resources.push(...(o.resources?.(this.getRenderOptions()) || []))
107
122
  }
108
123
 
109
124
  /**
110
125
  * Updates the component in the DOM.
111
126
  */
112
- public async updateComponent() {
127
+ public updateComponent() {
113
128
  const newJsx = this.render(this.getRenderOptions())
114
129
 
115
130
  // const selectionState = this.getSelectionState()
@@ -130,6 +145,7 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
130
145
  this.state.setValue((o as any).getInitialState({ props: this.props.getValue(), injector: this.injector }))
131
146
 
132
147
  this.updateComponent()
148
+ this.createResources()
133
149
  const cleanupResult = o.constructed && o.constructed(this.getRenderOptions())
134
150
  if (cleanupResult instanceof Promise) {
135
151
  cleanupResult.then((cleanup) => (this.cleanup = cleanup))
@@ -180,6 +196,8 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
180
196
  this._injector = i
181
197
  }
182
198
 
199
+ private resources: Disposable[] = []
200
+
183
201
  constructor(_props: TProps) {
184
202
  super()
185
203
  this.props = new ObservableValue()
@@ -187,6 +205,8 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
187
205
  }
188
206
  } as any as CustomElementConstructor,
189
207
  )
208
+ } else {
209
+ throw Error(`A custom shade with shadow DOM name '${o.shadowDomName}' has already been registered!`)
190
210
  }
191
211
 
192
212
  return (props: TProps, children: ChildrenList) => {
@@ -0,0 +1,156 @@
1
+ import { Injector } from '@furystack/inject'
2
+
3
+ import { TextEncoder, TextDecoder } from 'util'
4
+
5
+ global.TextEncoder = TextEncoder
6
+ global.TextDecoder = TextDecoder as any
7
+
8
+ import { JSDOM } from 'jsdom'
9
+ import { initializeShadeRoot } from './initialize'
10
+ import { Shade } from './shade'
11
+ import { createComponent } from './shade-component'
12
+
13
+ describe('Shades integration tests', () => {
14
+ const oldDoc = document
15
+
16
+ beforeAll(() => {
17
+ globalThis.document = new JSDOM().window.document
18
+ })
19
+
20
+ afterAll(() => {
21
+ globalThis.document = oldDoc
22
+ })
23
+
24
+ beforeEach(() => (document.body.innerHTML = '<div id="root"></div>'))
25
+ afterEach(() => (document.body.innerHTML = ''))
26
+
27
+ it('Should mount a custom component to a Shade root', () => {
28
+ const injector = new Injector()
29
+ const rootElement = document.getElementById('root') as HTMLDivElement
30
+
31
+ const ExampleComponent = Shade({ render: () => <div>Hello</div>, shadowDomName: 'shades-example' })
32
+
33
+ initializeShadeRoot({
34
+ injector,
35
+ rootElement,
36
+ jsxElement: <ExampleComponent />,
37
+ })
38
+ expect(document.body.innerHTML).toBe('<div id="root"><shades-example><div>Hello</div></shades-example></div>')
39
+ })
40
+
41
+ it('Should mount nested Shades components', () => {
42
+ const injector = new Injector()
43
+ const rootElement = document.getElementById('root') as HTMLDivElement
44
+
45
+ const ExampleComponent = Shade({
46
+ render: ({ children }) => <div>{children}</div>,
47
+ shadowDomName: 'shades-example-2',
48
+ })
49
+
50
+ const ExampleSubs = Shade<{ no: number }>({
51
+ render: ({ props }) => <div>{props.no}</div>,
52
+ shadowDomName: 'shades-example-sub',
53
+ })
54
+
55
+ initializeShadeRoot({
56
+ injector,
57
+ rootElement,
58
+ jsxElement: (
59
+ <ExampleComponent>
60
+ <ExampleSubs no={1} />
61
+ <ExampleSubs no={2} />
62
+ <ExampleSubs no={3} />
63
+ </ExampleComponent>
64
+ ),
65
+ })
66
+ expect(document.body.innerHTML).toBe(
67
+ '<div id="root"><shades-example-2><div><shades-example-sub><div></div></shades-example-sub><shades-example-sub><div></div></shades-example-sub><shades-example-sub><div></div></shades-example-sub></div></shades-example-2></div>',
68
+ )
69
+ })
70
+
71
+ it("Should execute the constructed and constructed's cleanup callback", () => {
72
+ const injector = new Injector()
73
+ const rootElement = document.getElementById('root') as HTMLDivElement
74
+ const cleanup = jest.fn()
75
+ const constructed = jest.fn(() => cleanup)
76
+
77
+ const ExampleComponent = Shade({
78
+ constructed,
79
+ render: () => <div>Hello</div>,
80
+ })
81
+
82
+ initializeShadeRoot({
83
+ injector,
84
+ rootElement,
85
+ jsxElement: <ExampleComponent />,
86
+ })
87
+ expect(constructed).toBeCalled()
88
+ expect(cleanup).not.toBeCalled()
89
+ document.body.innerHTML = ''
90
+ expect(cleanup).toBeCalled()
91
+ })
92
+
93
+ it('Should execute the onAttach and onDetach callbacks', () => {
94
+ const injector = new Injector()
95
+ const rootElement = document.getElementById('root') as HTMLDivElement
96
+ const onAttach = jest.fn()
97
+ const onDetach = jest.fn()
98
+
99
+ const ExampleComponent = Shade({
100
+ onAttach,
101
+ onDetach,
102
+ render: () => <div>Hello</div>,
103
+ })
104
+
105
+ initializeShadeRoot({
106
+ injector,
107
+ rootElement,
108
+ jsxElement: <ExampleComponent />,
109
+ })
110
+ expect(onAttach).toBeCalled()
111
+ expect(onDetach).not.toBeCalled()
112
+ document.body.innerHTML = ''
113
+ expect(onDetach).toBeCalled()
114
+ })
115
+
116
+ it('Should update state', () => {
117
+ const injector = new Injector()
118
+ const rootElement = document.getElementById('root') as HTMLDivElement
119
+
120
+ const ExampleComponent = Shade({
121
+ getInitialState: () => ({ count: 0 }),
122
+ render: ({ getState, updateState }) => {
123
+ const { count } = getState()
124
+ return (
125
+ <div>
126
+ Count is {getState().count.toString()}
127
+ <button id="plus" onclick={() => updateState({ count: count + 1 })}>
128
+ +
129
+ </button>
130
+ <button id="minus" onclick={() => updateState({ count: count - 1 })}>
131
+ -
132
+ </button>
133
+ </div>
134
+ )
135
+ },
136
+ })
137
+ initializeShadeRoot({
138
+ injector,
139
+ rootElement,
140
+ jsxElement: <ExampleComponent />,
141
+ })
142
+
143
+ const plus = () => document.getElementById('plus')?.click()
144
+ const minus = () => document.getElementById('minus')?.click()
145
+
146
+ expect(document.body.innerHTML).toContain('Count is 0')
147
+ plus()
148
+ expect(document.body.innerHTML).toContain('Count is 1')
149
+ plus()
150
+ expect(document.body.innerHTML).toContain('Count is 2')
151
+
152
+ minus()
153
+ minus()
154
+ expect(document.body.innerHTML).toContain('Count is 0')
155
+ })
156
+ })