@furystack/shades 6.1.5 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/component-factory.spec.js +6 -6
  2. package/dist/component-factory.spec.js.map +1 -1
  3. package/dist/components/lazy-load.js +17 -14
  4. package/dist/components/lazy-load.js.map +1 -1
  5. package/dist/components/route-link.js +7 -3
  6. package/dist/components/route-link.js.map +1 -1
  7. package/dist/components/route-link.spec.js +2 -2
  8. package/dist/components/route-link.spec.js.map +1 -1
  9. package/dist/components/router.js +25 -26
  10. package/dist/components/router.js.map +1 -1
  11. package/dist/components/router.spec.js +2 -2
  12. package/dist/components/router.spec.js.map +1 -1
  13. package/dist/services/location-service.js +52 -4
  14. package/dist/services/location-service.js.map +1 -1
  15. package/dist/services/location-service.spec.js +63 -5
  16. package/dist/services/location-service.spec.js.map +1 -1
  17. package/dist/services/resource-manager.js +48 -0
  18. package/dist/services/resource-manager.js.map +1 -0
  19. package/dist/services/resource-manager.spec.js +32 -0
  20. package/dist/services/resource-manager.spec.js.map +1 -0
  21. package/dist/shade-component.js +28 -14
  22. package/dist/shade-component.js.map +1 -1
  23. package/dist/shade-resources.integration.spec.js +7 -8
  24. package/dist/shade-resources.integration.spec.js.map +1 -1
  25. package/dist/shade.js +79 -69
  26. package/dist/shade.js.map +1 -1
  27. package/dist/shades.integration.spec.js +197 -187
  28. package/dist/shades.integration.spec.js.map +1 -1
  29. package/package.json +6 -6
  30. package/src/component-factory.spec.tsx +7 -7
  31. package/src/components/lazy-load.tsx +19 -15
  32. package/src/components/route-link.spec.tsx +2 -2
  33. package/src/components/route-link.tsx +11 -10
  34. package/src/components/router.spec.tsx +2 -2
  35. package/src/components/router.tsx +32 -32
  36. package/src/jsx.ts +3 -2
  37. package/src/models/render-options.ts +59 -9
  38. package/src/services/location-service.spec.ts +75 -6
  39. package/src/services/location-service.tsx +58 -4
  40. package/src/services/resource-manager.spec.ts +33 -0
  41. package/src/services/resource-manager.ts +60 -0
  42. package/src/shade-component.ts +35 -15
  43. package/src/shade-resources.integration.spec.tsx +8 -14
  44. package/src/shade.ts +95 -104
  45. package/src/shades.integration.spec.tsx +265 -252
  46. package/types/components/lazy-load.d.ts +1 -1
  47. package/types/components/lazy-load.d.ts.map +1 -1
  48. package/types/components/route-link.d.ts +1 -1
  49. package/types/components/route-link.d.ts.map +1 -1
  50. package/types/components/router.d.ts +6 -8
  51. package/types/components/router.d.ts.map +1 -1
  52. package/types/jsx.d.ts +3 -2
  53. package/types/jsx.d.ts.map +1 -1
  54. package/types/models/render-options.d.ts +46 -7
  55. package/types/models/render-options.d.ts.map +1 -1
  56. package/types/services/location-service.d.ts +21 -1
  57. package/types/services/location-service.d.ts.map +1 -1
  58. package/types/services/resource-manager.d.ts +16 -0
  59. package/types/services/resource-manager.d.ts.map +1 -0
  60. package/types/services/resource-manager.spec.d.ts +2 -0
  61. package/types/services/resource-manager.spec.d.ts.map +1 -0
  62. package/types/shade-component.d.ts +16 -5
  63. package/types/shade-component.d.ts.map +1 -1
  64. package/types/shade.d.ts +8 -27
  65. package/types/shade.d.ts.map +1 -1
package/src/shade.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import type { Disposable } from '@furystack/utils'
2
+ import { ObservableValue } from '@furystack/utils'
2
3
  import { Injector } from '@furystack/inject'
3
- import type { ChildrenList, PartialElement, RenderOptions } from './models'
4
+ import type { ChildrenList, RenderOptions } from './models'
5
+ import { ResourceManager } from './services/resource-manager'
6
+ import { LocationService } from './services'
4
7
 
5
- export type ShadeOptions<TProps, TState> = {
8
+ export type ShadeOptions<TProps> = {
6
9
  /**
7
10
  * Explicit shadow dom name. Will fall back to 'shade-{guid}' if not provided
8
11
  */
@@ -11,48 +14,30 @@ export type ShadeOptions<TProps, TState> = {
11
14
  /**
12
15
  * Render hook, this method will be executed on each and every render.
13
16
  */
14
- render: (options: RenderOptions<TProps, TState>) => JSX.Element | string | null
17
+ render: (options: RenderOptions<TProps>) => JSX.Element | string | null
15
18
 
16
19
  /**
17
20
  * Construct hook. Will be executed once when the element has been constructed and initialized
18
21
  */
19
22
  constructed?: (
20
- options: RenderOptions<TProps, TState>,
23
+ options: RenderOptions<TProps>,
21
24
  ) => void | undefined | (() => void) | Promise<void | undefined | (() => void)>
22
25
 
23
26
  /**
24
27
  * Will be executed when the element is attached to the DOM.
25
28
  */
26
- onAttach?: (options: RenderOptions<TProps, TState>) => void
29
+ onAttach?: (options: RenderOptions<TProps>) => void
27
30
 
28
31
  /**
29
32
  * Will be executed when the element is detached from the DOM.
30
33
  */
31
- onDetach?: (options: RenderOptions<TProps, TState>) => void
34
+ onDetach?: (options: RenderOptions<TProps>) => void
32
35
 
33
36
  /**
34
37
  * A factory method that creates a list of disposable resources that will be disposed when the element is detached.
35
38
  */
36
- resources?: (options: RenderOptions<TProps, TState>) => Disposable[]
37
-
38
- /**
39
- * An optional method that checks the state for changes and returns true if the element should be rerendered. This will not be called if `skipRender` is set to true in the relevant `updateState()` call.
40
- */
41
- compareState?: (options: {
42
- oldState: TState
43
- newState: TState
44
- props: TProps
45
- element: HTMLElement
46
- injector: Injector
47
- }) => boolean
48
- } & (unknown extends TState
49
- ? {}
50
- : {
51
- /**
52
- * The initial state of the component
53
- */
54
- getInitialState: (options: { injector: Injector; props: TProps }) => TState
55
- })
39
+ resources?: (options: RenderOptions<TProps>) => Disposable[]
40
+ }
56
41
 
57
42
  /**
58
43
  * Factory method for creating Shade components
@@ -60,7 +45,7 @@ export type ShadeOptions<TProps, TState> = {
60
45
  * @param o for component creation
61
46
  * @returns the JSX element
62
47
  */
63
- export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>) => {
48
+ export const Shade = <TProps>(o: ShadeOptions<TProps>) => {
64
49
  // register shadow-dom element
65
50
  const customElementName = o.shadowDomName
66
51
 
@@ -69,11 +54,7 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
69
54
  customElements.define(
70
55
  customElementName,
71
56
  class extends HTMLElement implements JSX.Element {
72
- private compareState =
73
- o.compareState ||
74
- (({ oldState, newState }: { oldState: TState; newState: TState }) =>
75
- Object.entries(oldState as object).some(([key, value]) => value !== newState[key as keyof TState]) ||
76
- Object.entries(newState as object).some(([key, value]) => value !== oldState[key as keyof TState]))
57
+ public resourceManager = new ResourceManager()
77
58
 
78
59
  public connectedCallback() {
79
60
  o.onAttach && o.onAttach(this.getRenderOptions())
@@ -82,19 +63,14 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
82
63
 
83
64
  public disconnectedCallback() {
84
65
  o.onDetach && o.onDetach(this.getRenderOptions())
85
- Object.values(this.resources).forEach((s) => s.dispose())
66
+ this.resourceManager.dispose()
86
67
  this.cleanup && this.cleanup()
87
68
  }
88
69
 
89
70
  /**
90
71
  * Will be triggered when updating the external props object
91
72
  */
92
- public props: TProps & { children?: JSX.Element[] }
93
-
94
- /**
95
- * Will be triggered on state update
96
- */
97
- public state!: TState
73
+ public props!: TProps & { children?: JSX.Element[] }
98
74
 
99
75
  /**
100
76
  * Will be updated when on children change
@@ -105,55 +81,88 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
105
81
  * @param options Options for rendering the component
106
82
  * @returns the JSX element
107
83
  */
108
- public render = (options: RenderOptions<TProps, TState>) => o.render(options)
84
+ public render = (options: RenderOptions<TProps>) => o.render(options)
109
85
 
110
86
  /**
111
87
  * @returns values for the current render options
112
88
  */
113
- private getRenderOptions = (): RenderOptions<TProps, TState> => {
114
- const props: TProps = { ...this.props }
115
- const getState = () => ({ ...this.state })
116
- const updateState = (stateChanges: PartialElement<TState>, skipRender?: boolean) => {
117
- const oldState = { ...this.state }
118
- const newState = { ...oldState, ...stateChanges }
119
-
120
- this.state = newState
121
-
122
- if (
123
- !skipRender &&
124
- this.compareState({
125
- oldState,
126
- newState,
127
- props,
128
- element: this,
129
- injector: this.injector,
89
+ private getRenderOptions = (): RenderOptions<TProps> => {
90
+ const renderOptions: RenderOptions<TProps> = {
91
+ props: this.props,
92
+ injector: this.injector,
93
+
94
+ children: this.shadeChildren,
95
+ element: this,
96
+ useObservable: (key, obesrvable, callback, getLast) =>
97
+ this.resourceManager.useObservable(key, obesrvable, callback || (() => this.updateComponent()), getLast),
98
+ useState: (key, initialValue) =>
99
+ this.resourceManager.useState(key, initialValue, this.updateComponent.bind(this)),
100
+ useSearchState: (key, initialValue) =>
101
+ this.resourceManager.useObservable(
102
+ `useSearchState-${key}`,
103
+ this.injector.getInstance(LocationService).useSearchParam(key, initialValue),
104
+ () => this.updateComponent(),
105
+ ),
106
+
107
+ useStoredState: <T>(key: string, initialValue: T, storageArea = localStorage) => {
108
+ const getFromStorage = () => {
109
+ const value = storageArea?.getItem(key)
110
+ return value ? JSON.parse(value) : initialValue
111
+ }
112
+
113
+ const setToStorage = (value: T) => {
114
+ if (JSON.stringify(value) !== storageArea?.getItem(key)) {
115
+ const newValue = JSON.stringify(value)
116
+ storageArea?.setItem(key, newValue)
117
+ }
118
+ if (JSON.stringify(observable.getValue()) !== JSON.stringify(value)) {
119
+ observable.setValue(value)
120
+ }
121
+ }
122
+
123
+ const observable = this.resourceManager.useDisposable(
124
+ `useStoredState-${key}`,
125
+ () => new ObservableValue(getFromStorage()),
126
+ )
127
+
128
+ const updateFromStorageEvent = (e: StorageEvent) => {
129
+ e.key === key &&
130
+ e.storageArea === storageArea &&
131
+ setToStorage((e.newValue && JSON.parse(e.newValue)) || initialValue)
132
+ }
133
+
134
+ this.resourceManager.useDisposable(`useStoredState-${key}-storage-event`, () => {
135
+ window.addEventListener('storage', updateFromStorageEvent)
136
+ const channelName = `useStoredState-broadcast-channel`
137
+ const messageChannel = new BroadcastChannel(channelName)
138
+ messageChannel.onmessage = (e) => {
139
+ if (e.data.key === key) {
140
+ setToStorage(e.data.value)
141
+ }
142
+ }
143
+ const subscription = observable.subscribe((value) => {
144
+ messageChannel.postMessage({ key, value })
145
+ })
146
+
147
+ return {
148
+ dispose: () => {
149
+ window.removeEventListener('storage', updateFromStorageEvent)
150
+ subscription.dispose()
151
+ messageChannel.close()
152
+ },
153
+ }
130
154
  })
131
- ) {
132
- this.updateComponent()
133
- }
134
- }
135
155
 
136
- const returnValue = {
137
- ...{
138
- props,
139
- injector: this.injector,
156
+ observable.subscribe(setToStorage)
140
157
 
141
- children: this.shadeChildren,
142
- element: this,
158
+ return this.resourceManager.useObservable(`useStoredState-${key}`, observable, () =>
159
+ this.updateComponent(),
160
+ )
143
161
  },
144
- ...((o as any).getInitialState
145
- ? {
146
- getState,
147
- updateState,
148
- }
149
- : {}),
150
- } as any as RenderOptions<TProps, TState>
151
-
152
- return returnValue
153
- }
162
+ useDisposable: this.resourceManager.useDisposable.bind(this.resourceManager),
163
+ }
154
164
 
155
- private createResources() {
156
- this.resources.push(...(o.resources?.(this.getRenderOptions()) || []))
165
+ return renderOptions as RenderOptions<TProps>
157
166
  }
158
167
 
159
168
  /**
@@ -162,32 +171,27 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
162
171
  public updateComponent() {
163
172
  const renderResult = this.render(this.getRenderOptions())
164
173
 
165
- if (renderResult === null) {
174
+ if (renderResult === null || renderResult === undefined) {
166
175
  this.innerHTML = ''
167
176
  }
168
177
 
169
- if (typeof renderResult === 'string') {
178
+ if (typeof renderResult === 'string' || typeof renderResult === 'number') {
170
179
  this.innerHTML = renderResult
171
180
  }
172
181
 
173
- if (renderResult instanceof DocumentFragment) {
174
- this.replaceChildren(...renderResult.children)
175
- }
176
-
177
182
  if (renderResult instanceof HTMLElement) {
178
183
  this.replaceChildren(renderResult)
179
184
  }
185
+ if (renderResult instanceof DocumentFragment) {
186
+ this.replaceChildren(renderResult)
187
+ }
180
188
  }
181
189
 
182
190
  /**
183
191
  * Finialize the component initialization after it gets the Props. Called by the framework internally
184
192
  */
185
193
  public callConstructed() {
186
- ;(o as any).getInitialState &&
187
- (this.state = (o as any).getInitialState({ props: { ...this.props }, injector: this.injector }))
188
-
189
194
  this.updateComponent()
190
- this.createResources()
191
195
  const cleanupResult = o.constructed && o.constructed(this.getRenderOptions())
192
196
  if (cleanupResult instanceof Promise) {
193
197
  cleanupResult.then((cleanup) => (this.cleanup = cleanup))
@@ -216,11 +220,6 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
216
220
  return this._injector
217
221
  }
218
222
 
219
- const fromState = (this.state as any)?.injector
220
- if (fromState && fromState instanceof Injector) {
221
- return fromState
222
- }
223
-
224
223
  const fromProps = (this.props as any)?.injector
225
224
  if (fromProps && fromProps instanceof Injector) {
226
225
  return fromProps
@@ -238,13 +237,6 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
238
237
  public set injector(i: Injector) {
239
238
  this._injector = i
240
239
  }
241
-
242
- private resources: Disposable[] = []
243
-
244
- constructor(_props: TProps & { children?: JSX.Element[] }) {
245
- super()
246
- this.props = _props
247
- }
248
240
  },
249
241
  )
250
242
  } else {
@@ -254,9 +246,8 @@ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>)
254
246
  return (props: TProps, children: ChildrenList) => {
255
247
  const el = document.createElement(customElementName, {
256
248
  ...(props as TProps & ElementCreationOptions),
257
- }) as JSX.Element<TProps, TState>
258
- el.props = props
259
-
249
+ }) as JSX.Element<TProps>
250
+ el.props = props || ({} as TProps)
260
251
  el.shadeChildren = children
261
252
  return el as JSX.Element
262
253
  }