@sitecore-feaas/clientside 0.2.0 → 0.2.2

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 (120) hide show
  1. package/README.md +56 -36
  2. package/build/components/FEAASComponent.js +71 -11
  3. package/build/components/FEAASComponent.js.map +1 -1
  4. package/build/components/FEAASElement.js +411 -37
  5. package/build/components/FEAASElement.js.map +1 -1
  6. package/build/components/FEAASLoader.js +2 -2
  7. package/build/components/FEAASLoader.js.map +1 -1
  8. package/build/components/FEAASReactElement.js +47 -0
  9. package/build/components/FEAASReactElement.js.map +1 -0
  10. package/build/components/FEAASStylesheet.js +2 -2
  11. package/build/components/FEAASStylesheet.js.map +1 -1
  12. package/build/headless.js +4 -3
  13. package/build/headless.js.map +1 -1
  14. package/build/ui/FEAAS.js +5 -0
  15. package/build/ui/FEAAS.js.map +1 -0
  16. package/build/ui/FEAASContext.js +20 -8
  17. package/build/ui/FEAASContext.js.map +1 -1
  18. package/build/ui/FEAASEditor.js +2 -2
  19. package/build/ui/FEAASEditor.js.map +1 -1
  20. package/build/ui/FEAASExternal.js +128 -24
  21. package/build/ui/FEAASExternal.js.map +1 -1
  22. package/build/ui/FEAASPicker.js +7 -2
  23. package/build/ui/FEAASPicker.js.map +1 -1
  24. package/build/ui/index.js +3 -4
  25. package/build/ui/index.js.map +1 -1
  26. package/build/ui/react.js.map +1 -1
  27. package/build/utils/fetch.js +26 -0
  28. package/build/utils/fetch.js.map +1 -0
  29. package/dist/browser/headless.cjs +6 -6
  30. package/dist/browser/headless.cjs.map +4 -4
  31. package/dist/browser/headless.esm.js +6 -6
  32. package/dist/browser/headless.esm.js.map +1 -1
  33. package/dist/browser/index.cjs +7 -7
  34. package/dist/browser/index.cjs.map +4 -4
  35. package/dist/browser/index.esm.js +7 -7
  36. package/dist/browser/index.esm.js.map +1 -1
  37. package/dist/browser/react.cjs +6 -6
  38. package/dist/browser/react.cjs.map +4 -4
  39. package/dist/browser/react.esm.js +6 -6
  40. package/dist/browser/react.esm.js.map +1 -1
  41. package/dist/node/headless.cjs +4 -4
  42. package/dist/node/headless.cjs.map +4 -4
  43. package/dist/node/headless.esm.js +4 -4
  44. package/dist/node/headless.esm.js.map +4 -4
  45. package/dist/node/index.cjs +4 -4
  46. package/dist/node/index.cjs.map +4 -4
  47. package/dist/node/index.esm.js +4 -4
  48. package/dist/node/index.esm.js.map +4 -4
  49. package/dist/node/react.cjs +4 -4
  50. package/dist/node/react.cjs.map +4 -4
  51. package/dist/node/react.esm.js +4 -4
  52. package/dist/node/react.esm.js.map +4 -4
  53. package/headless.cjs +4 -4
  54. package/headless.cjs.map +4 -4
  55. package/headless.js +4 -4
  56. package/headless.js.map +4 -4
  57. package/package.json +11 -3
  58. package/react.cjs +6 -6
  59. package/react.cjs.map +4 -4
  60. package/react.js +6 -6
  61. package/react.js.map +4 -4
  62. package/scripts/build.js +1 -1
  63. package/scripts/test-setup.ts +1 -0
  64. package/src/components/FEAASComponent.tsx +96 -12
  65. package/src/components/FEAASElement.tsx +479 -41
  66. package/src/components/FEAASLoader.tsx +2 -2
  67. package/src/components/FEAASReactElement.tsx +61 -0
  68. package/src/components/FEAASStylesheet.tsx +3 -3
  69. package/src/headless.ts +5 -4
  70. package/src/ui/FEAAS.tsx +4 -0
  71. package/src/ui/FEAASContext.tsx +25 -7
  72. package/src/ui/FEAASEditor.tsx +7 -7
  73. package/src/ui/FEAASExternal.tsx +134 -24
  74. package/src/ui/FEAASPicker.tsx +9 -5
  75. package/src/ui/index.ts +4 -4
  76. package/src/ui/react.tsx +5 -0
  77. package/src/ui/tsconfig.json +1 -1
  78. package/src/utils/fetch.ts +44 -0
  79. package/test/FEAASBase.test.tsx +2 -2
  80. package/test/FEAASComponent.test.tsx +4 -1
  81. package/test/FEAASComponent2.test.tsx +215 -0
  82. package/test/FEAASContext.test.tsx +206 -29
  83. package/test/FEAASExternal.test.tsx +1 -8
  84. package/test/FEAASPicker.test.tsx +4 -2
  85. package/test/fetch-data.ts +55 -0
  86. package/types/components/FEAASComponent.d.ts +33 -3
  87. package/types/components/FEAASComponent.d.ts.map +1 -1
  88. package/types/components/FEAASElement.d.ts +299 -22
  89. package/types/components/FEAASElement.d.ts.map +1 -1
  90. package/types/components/FEAASLoader.d.ts +2 -2
  91. package/types/components/FEAASLoader.d.ts.map +1 -1
  92. package/types/components/FEAASReactElement.d.ts +30 -0
  93. package/types/components/FEAASReactElement.d.ts.map +1 -0
  94. package/types/components/FEAASStylesheet.d.ts +3 -3
  95. package/types/components/FEAASStylesheet.d.ts.map +1 -1
  96. package/types/headless.d.ts +4 -3
  97. package/types/headless.d.ts.map +1 -1
  98. package/types/tsbuildinfo +1 -1
  99. package/types/ui/FEAAS.d.ts +5 -0
  100. package/types/ui/FEAAS.d.ts.map +1 -0
  101. package/types/ui/FEAASContext.d.ts +16 -7
  102. package/types/ui/FEAASContext.d.ts.map +1 -1
  103. package/types/ui/FEAASEditor.d.ts +6 -6
  104. package/types/ui/FEAASEditor.d.ts.map +1 -1
  105. package/types/ui/FEAASExternal.d.ts +59 -10
  106. package/types/ui/FEAASExternal.d.ts.map +1 -1
  107. package/types/ui/FEAASPicker.d.ts +11 -9
  108. package/types/ui/FEAASPicker.d.ts.map +1 -1
  109. package/types/ui/index.d.ts +1 -4
  110. package/types/ui/index.d.ts.map +1 -1
  111. package/types/ui/react.d.ts.map +1 -1
  112. package/types/ui/tsbuildinfo +1 -0
  113. package/types/utils/fetch.d.ts +9 -0
  114. package/types/utils/fetch.d.ts.map +1 -0
  115. package/build/components/FEAASBase.js +0 -416
  116. package/build/components/FEAASBase.js.map +0 -1
  117. package/src/components/FEAASBase.tsx +0 -485
  118. package/types/components/FEAASBase.d.ts +0 -299
  119. package/types/components/FEAASBase.d.ts.map +0 -1
  120. package/types/tsbuildinfo_ui +0 -1
@@ -1,61 +1,499 @@
1
- import type { ReactNode, ReactElement, JSXElementConstructor, PropsWithChildren } from 'react'
2
- import { FEAASBase } from './FEAASBase.js'
1
+ import type { JSXElementConstructor, ReactElement, ReactNode } from 'react'
2
+ import type ReactDOMClient from 'react-dom/client'
3
+ import type ReactDOM from 'react-dom'
4
+ import type React from 'react'
5
+ import { ControlledPromise } from '../utils/promise.js'
3
6
 
4
- export type ReactModules = {
7
+ export type OptionalExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>
8
+
9
+ // Shim in case it's required in node.js environment
10
+ var WebComponent =
11
+ typeof HTMLElement == 'undefined'
12
+ ? // @ts-ignore
13
+ typeof windowJSDOM != 'undefined'
14
+ ? // @ts-ignore
15
+ (windowJSDOM.HTMLElement as typeof HTMLElement)
16
+ : (class {} as unknown as typeof HTMLElement)
17
+ : HTMLElement
18
+
19
+ export interface ReactPayload {
5
20
  React: typeof React
6
21
  ReactDOM: typeof ReactDOM
22
+ ReactDOMClient: typeof ReactDOMClient
7
23
  }
8
- export type ReactJSX = ReactNode | ReactElement<any, string | JSXElementConstructor<any>>
9
- export type FEAASReactRender<P> = {
10
- (this: FEAASElement<P>, props: P, payload: ReactModules): ReactJSX
24
+
25
+ declare global {
26
+ interface DocumentEventMap {
27
+ feaasMount: Event
28
+ feaasUnmount: Event
29
+ feaasLoad: Event
30
+ feaasError: CustomEvent<Error>
31
+ }
11
32
  }
12
33
 
13
- export default class FEAASElement<P> extends FEAASBase<P, ReactModules> {
14
- load() {
15
- return Promise.all([
16
- // @ts-ignore @webpack-ignore
17
- import('https://cdn.skypack.dev/react'),
18
- // @ts-ignore
19
- import('https://cdn.skypack.dev/react-dom')
20
- ]).then(([React, ReactDOM]) => {
21
- return { React, ReactDOM }
22
- })
34
+ /**
35
+ * FEAASElement is a superclass for FEAAS components with optional react support.
36
+ *
37
+ * Initialization lifecycle:
38
+ *
39
+ * - `set(props)`
40
+ * - `scheduleRender()` - any attribute change causes render to schedule to next frame
41
+ * - `whenRendered.restart()` - rendering promise is reset and will resolve after render
42
+ * - `advance()` - called on next frame
43
+ * - `if (isReadyToLoad()) => boolean` - check if component has all necessary properties to load
44
+ * - `load() => payload` => component is loaded
45
+ * - `onLoad?(payload)` => invoke loaded callback
46
+ * - `advance()` => going further in state machine
47
+ * - `flush()` => flushes all react changes if needed
48
+ * - `update()` => component-specific method (or react) to render changes to dom
49
+ * - `render(payload, props)` => render means react
50
+ * - `whenRendered.resolve()` - rendering promise is resolved after rendering
51
+ * - `whenLoaded.resolved()` - resolved after loading and rendering
52
+ */
53
+ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
54
+ static observedAttributes: string[]
55
+
56
+ defaultProps = {} as Partial<Props>
57
+
58
+ /** Return attributes as parsed values */
59
+ getProps(context?: HTMLElement): any {
60
+ return {}
23
61
  }
24
62
 
25
- render({}: P, { React }: ReactModules): ReactJSX {
26
- return <></>
63
+ /** Properties parsed in current tick */
64
+ props: Props
65
+ overrides: Props
66
+
67
+ /** Timer until next tick */
68
+ nextRender: number
69
+
70
+ /** Is/was component in the dom? */
71
+ connected = false
72
+
73
+ refs: Record<string, FEAASElement> = {}
74
+ descendants: FEAASElement[] = []
75
+
76
+ /** Result of lazy loading */
77
+ payload: Payload
78
+ whenLoaded = ControlledPromise<Payload>((payload) => {
79
+ this.payload = payload
80
+ })
81
+
82
+ constructor() {
83
+ super()
84
+ this.addEventListener('feaasMount', this)
85
+ this.addEventListener('feaasUnmount', this)
27
86
  }
28
- }
29
87
 
30
- interface MyComponentProps {
31
- counter: string
32
- }
88
+ /** Attaches shadow root if getShadowRootOptions any settings, or uses element itself */
89
+ getRoot(): FEAASElement | HTMLElement | ShadowRoot {
90
+ return this
91
+ }
92
+
93
+ /** Attribute changes trigger props parsing and schedules re-rendering */
94
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
95
+ this.set()
96
+ }
97
+
98
+ /** Components parses its attributes when connected, and schedules rendeirng */
99
+ connectedCallback() {
100
+ this.mount()
101
+ }
102
+ disconnectedCallback() {
103
+ this.unmount()
104
+ }
105
+ /**
106
+ * Web components are initialized depth-first, making child elements render before its parents. This could here
107
+ * attempts to avoid this and inverts the initialization logic, so that children are initialized in the context of its
108
+ * already-initialized parents.
109
+ *
110
+ * FEAAS components dispatch `feaasMount` event that allows components to store references to each other. In addition
111
+ * it allows external scripts to listen for initialization of FEAAS components
112
+ */
113
+ mount() {
114
+ try {
115
+ const parent = this.getParent() as FEAASElement
116
+ // Allow parent component to initialize first
117
+ if (parent && !parent.connected) return
118
+ if (this.connected) return
119
+ this.connected = true
120
+ this.log('FEAAS: Mount', this.tagName, this)
121
+ this.dispatchEvent(new Event('feaasMount', { bubbles: true, composed: true }))
122
+ this.getElements().map((element: FEAASElement) => element.mount?.())
123
+ this.set()
124
+ } catch (e) {
125
+ this.onError(e)
126
+ }
127
+ }
128
+
129
+ log(...args: any) {
130
+ if (typeof process != 'undefined' && process.env.TESTING) return
131
+ console.log(...args)
132
+ }
133
+
134
+ /** Nothing special happens on unmount, except setting a flag */
135
+ unmount() {
136
+ if (!this.connected) return
137
+ this.log('FEAAS: Unmount', this)
138
+ this.dispatchEvent(new Event('feaasUnmount', { bubbles: true, composed: true }))
139
+ this.connected = false
140
+ }
141
+
142
+ getParent(onlyFeaas = true) {
143
+ for (
144
+ var p: HTMLElement = this;
145
+ (p = p.parentElement || ((p.parentNode instanceof ShadowRoot ? p.parentNode.host : null) as HTMLElement));
146
+
147
+ ) {
148
+ if (!onlyFeaas || p.tagName.startsWith('FEAAS-')) return p
149
+ }
150
+ // feaas context anywhere in the dom acts as a root element
151
+ if (this.tagName != 'FEAAS-CONTEXT') return document.querySelector('feaas-context')
152
+ }
153
+
154
+ getElements(root: HTMLElement | ShadowRoot = this.getRoot(), onlyFeaas = true): HTMLElement[] {
155
+ return Array.from(root.querySelectorAll('*'))
156
+ .map(
157
+ (el) =>
158
+ [
159
+ !onlyFeaas || el.tagName.startsWith('FEAAS-') ? el : null,
160
+ el.shadowRoot ? this.getElements(el.shadowRoot, onlyFeaas) : null
161
+ ] as HTMLElement[]
162
+ )
163
+ .flat()
164
+ .filter(Boolean)
165
+ }
166
+
167
+ /** Catch-all event handler that dispatches events to callback */
168
+ handleEvent(event: Event) {
169
+ const target = event.composedPath()[0] as FEAASElement
170
+ switch (event.type) {
171
+ case 'feaasMount':
172
+ if (target != this) this.nestedCallback(target)
173
+ break
174
+ case 'feaasUnmount':
175
+ if (target != this) this.unnestedCallback(target)
176
+ break
177
+ }
178
+ }
179
+
180
+ /** Act on children element being removed */
181
+ unnestedCallback(target: FEAASElement) {
182
+ const index = this.descendants.indexOf(target)
183
+ if (index == -1) return
184
+ this.log('FEAAS: Unnest', this, target)
185
+ this.descendants.splice(index, -1)
186
+ this.refs[target.getComponentName()] = null
187
+ if (target.refs.parent == this) target.refs.parent = null
188
+ if (target.refs.top == this) {
189
+ target.refs.top = target.refs.parent
190
+ }
191
+ }
192
+
193
+ /** Act on children element being added */
194
+ nestedCallback(target: FEAASElement): boolean | void {
195
+ const index = this.descendants.indexOf(target)
196
+ if (index != -1) return
197
+ this.descendants.push(target)
198
+ this.log('FEAAS: Nest', this, target)
199
+ this.refs[target.getComponentName()] = target
200
+ target.refs.top = this
201
+ target.refs.parent ||= this
202
+ if (target.render) {
203
+ for (var p: FEAASElement = target.refs.parent; p; p = p.refs.parent) {
204
+ if (p.render) {
205
+ target.refs.parentReact ||= p
206
+ target.refs.topReact = p
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ getComponentName() {
213
+ return this.tagName.replace('FEAAS-', '').toLowerCase()
214
+ }
215
+
216
+ /**
217
+ * Check if lazy component is ready to be loaded. For example a component may stay unloaded unless certain attributes
218
+ * are set
219
+ */
220
+ isReadyToLoad() {
221
+ return this.payload === undefined && this.getAttribute('hidden') == null
222
+ }
223
+
224
+ /** Generic error handler */
225
+ onError(error: Error, where: string = 'uncaught', handled = false) {
226
+ const event = new CustomEvent('feaasError', { bubbles: true, composed: true, detail: error })
227
+ this.dispatchEvent(event)
228
+ this.whenLoaded.reject(error)
229
+ if (!event.defaultPrevented && !handled) {
230
+ this.whenRendered.reject(error)
231
+ try {
232
+ this.unmount()
233
+ } catch (e) {}
234
+ return true
235
+ }
236
+ return false
237
+ }
238
+
239
+ shouldUpdateOnLoad() {
240
+ return true
241
+ }
242
+
243
+ onLoad: (payload: Payload) => void
244
+
245
+ /** Load lazy component & render */
246
+ async advance() {
247
+ try {
248
+ if (this.payload === undefined) {
249
+ if (!this.isReadyToLoad()) return
250
+ this.payload = null
251
+ this.dispatchEvent(new Event('feaasLoad', { bubbles: true, composed: true }))
252
+ const loaded = await this.load().catch((e) => {
253
+ this.payload = undefined
254
+ throw e
255
+ })
256
+
257
+ this.whenLoaded.resolve(loaded)
33
258
 
34
- export class MyComponent extends FEAASElement<MyComponentProps> {
35
- // will re-render component if any of these attributes have changed
36
- static observedAttributes = ['counter']
259
+ // Allow component to handle the post-loading
260
+ if (this.onLoad) {
261
+ return
262
+ }
263
+ } else if (this.payload == null) {
264
+ return
265
+ }
37
266
 
38
- useShadowRoot = true
267
+ this.flush()
268
+ this.whenRendered.resolve(this)
269
+ } catch (e) {
270
+ this.onError(e)
271
+ }
272
+ }
273
+
274
+ flush() {
275
+ const flushSync = (this.payload as ReactPayload)?.ReactDOM?.flushSync
276
+ if (flushSync) {
277
+ flushSync(() => this.update())
278
+ } else {
279
+ this.update()
280
+ }
281
+ }
282
+
283
+ /** Invoke preloading logic that needs to finish before component can render */
284
+ load() {
285
+ return Promise.resolve(null)
286
+ }
39
287
 
40
- // parse attributes from web component to react-ready form
41
- getProps() {
42
- return {
43
- counter: parseFloat(this.getAttribute('counter'))
288
+ /**
289
+ * Component-specific logic that updates the DOM. Default render implementation assume react rendering, in that case
290
+ * the component needs to provide React & ReactDOM references in its payload. Web component redefine its render to not
291
+ * use react at all
292
+ */
293
+ update(): any {
294
+ //this.log('FEAAS: React', this.tagName, this.payload)
295
+ if (this.payload == null) return
296
+ if (!this.render) return
297
+ const { React, ReactDOM, ReactDOMClient } = (this.payload as ReactPayload) || {}
298
+ if (!React) throw new Error(`${this.tagName}: React is not present in payload`)
299
+ if (!ReactDOM) throw new Error(`${this.tagName}: ReactDOM is not present in payload`)
300
+ if (!this.refs.topReact && !this.forceUpdateReact) {
301
+ if (ReactDOMClient) {
302
+ this.reactRoot ||= ReactDOMClient.createRoot(this.getReactRootElement())
303
+ this.reactRoot.render(this.getReactElement())
304
+ } else {
305
+ ReactDOM.render(this.getReactElement(), this.getReactRootElement())
306
+ }
307
+ } else if (this.forceUpdateReact) {
308
+ this.forceUpdateReact?.()
309
+ } else {
310
+ this.refs.topReact.forceUpdateReact?.()
44
311
  }
45
312
  }
46
313
 
47
- // First argument is props, second arguments is React stuff
48
- // Destructuring React is important for JSX to work
49
- render({ counter }: MyComponentProps, { React }: ReactModules): ReactJSX {
50
- const [updates, setUpdates] = React.useState(0)
51
- React.useEffect(() => {
52
- setUpdates((u) => u + 1)
53
- }, [counter])
314
+ invokeReactRendering(): ReactNode {
315
+ const { React, ReactDOM } = (this.payload as ReactPayload) || {}
316
+ if (!this.connected) return null
317
+ const children = this.descendants
318
+ .filter((r) => {
319
+ return r.refs.parentReact == this && r.payload
320
+ })
321
+ .map((child: FEAASElement<any, any>, index) => {
322
+ return this.getReactElement(child)
323
+ })
324
+ return this.render({ ...this.props, children }, this.payload)
325
+ }
326
+
327
+ reactWrapper: React.FunctionComponent
328
+ reactErrorBoundary: new (...args: any) => React.Component<{ children: any; slot: string }, { error: Error }>
329
+ reactRoot: ReactDOMClient.Root
330
+
331
+ getReactErrorBoundary(target: FEAASElement = this, displayName = target.tagName) {
332
+ if (this.reactErrorBoundary) return this.reactErrorBoundary
333
+
334
+ const { React, ReactDOM } = (target.payload as ReactPayload) || {}
335
+ return (this.reactErrorBoundary ||= class FEAASErrorBoundary extends React.Component<
336
+ { children: any; slot: string },
337
+ { error: Error }
338
+ > {
339
+ state = {
340
+ error: null as Error
341
+ }
342
+
343
+ static getDerivedStateFromError(error: Error) {
344
+ return { error: error }
345
+ }
346
+
347
+ componentDidCatch(error: Error, errorInfo: any) {
348
+ console.error(`FEAAS: ${target.tagName} Error:`, error, errorInfo)
349
+ target.onError(error, 'react')
350
+ //this.setState({ error: error })
351
+ }
352
+
353
+ render() {
354
+ if (this.state.error) {
355
+ // target.classList.add('feaas-error')
356
+ return
357
+ //return <h1>There seems to be a problem.</h1>
358
+ }
359
+ //if (target.classList.contains('feaas-error')) target.classList.remove('feaas-error')
360
+
361
+ return this.props.children
362
+ }
363
+ })
364
+ }
365
+
366
+ getReactElement(target: FEAASElement = this, props?: any) {
367
+ const { React, ReactDOM } = (target.payload as ReactPayload) || {}
368
+ const ErrorBoundary = this.getReactErrorBoundary()
369
+ const Wrapper = this.getReactWrapper(target)
54
370
  return (
55
- <>
56
- <p>Rendering attribute: {counter}</p>
57
- <p>Triggered updates: {updates}</p>
58
- </>
371
+ <ErrorBoundary key={target.tagName + '-' + target.getUID()} slot={target.getAttribute('slot')}>
372
+ <Wrapper />
373
+ </ErrorBoundary>
59
374
  )
60
375
  }
376
+
377
+ getReactWrapper(target: FEAASElement = this, displayName = target.tagName) {
378
+ if (target.reactWrapper) return target.reactWrapper
379
+
380
+ const { React, ReactDOM } = (target.payload as ReactPayload) || {}
381
+
382
+ target.reactWrapper = (props?: any) => {
383
+ if (target.render) {
384
+ const [value, forceUpdateReact] = React.useReducer((r) => r + 1, 0)
385
+ target.forceUpdateReact = forceUpdateReact
386
+ return ReactDOM.createPortal(<>{target.invokeReactRendering()}</>, target.getReactRootElement())
387
+ } else {
388
+ return target.update()
389
+ }
390
+ }
391
+
392
+ Object.assign(target.reactWrapper, {
393
+ key: target.tagName + '-' + target.getUID(),
394
+ displayName: displayName
395
+ })
396
+
397
+ return target.reactWrapper
398
+ }
399
+
400
+ uid: string
401
+ getUID() {
402
+ return (this.uid ||= String(Math.random()))
403
+ }
404
+ forceUpdateReact: (node?: ReactNode) => void
405
+
406
+ /**
407
+ * Render method that is pretty much the same as React Functional component, but with extra second argument for
408
+ * payload (usually result of import, it is expected to have React in scope).
409
+ */
410
+ render?(props: React.PropsWithChildren<Props>, payload: Payload): ReactNode | ReactElement
411
+
412
+ useShadowRoot: boolean
413
+ getShadowRoot() {
414
+ if (!this.shadowRoot) {
415
+ this.attachShadow({ mode: 'open' })
416
+ }
417
+ return this.shadowRoot
418
+ }
419
+ getReactRootElement(): HTMLElement | ShadowRoot {
420
+ return this.useShadowRoot ? this.getShadowRoot() : this.getRoot()
421
+ }
422
+
423
+ /** Set props and return values on new next tick */
424
+ set(overrides?: Partial<Props>) {
425
+ try {
426
+ this.scheduleRender()
427
+ this.overrides = {
428
+ ...this.overrides,
429
+ ...overrides
430
+ }
431
+ this.props = {
432
+ ...this.getProps(),
433
+ ...this.overrides
434
+ }
435
+ for (var property in this.props) {
436
+ const value = this.props[property]
437
+ const attribute = property.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
438
+ if (value == null || value === this.defaultProps[property as keyof typeof this.defaultProps]) {
439
+ this.removeAttribute(attribute)
440
+ } else if (typeof value == 'string' || typeof value == 'number' || typeof value == 'boolean') {
441
+ if (this.getAttribute(attribute) != String(value)) this.setAttribute(attribute, String(value))
442
+ }
443
+ }
444
+ } catch (e) {
445
+ this.whenRendered.reject(e)
446
+ this.onError(e, 'set')
447
+ }
448
+ return this
449
+ }
450
+
451
+ import(path: string): Promise<Payload> {
452
+ return import(/* @vite-ignore */ /* webpackIgnore: true */ path)
453
+ }
454
+
455
+ /** Schedule rendering on next tick, to batch multiple attribute changes */
456
+ initialized: boolean
457
+ whenRendered = ControlledPromise<this>(
458
+ () => (this.initialized = true),
459
+ () => cancelAnimationFrame(this.nextRender)
460
+ )
461
+ scheduleRender() {
462
+ if (this.nextRender == null) {
463
+ this.whenRendered = this.whenRendered.restart()
464
+ }
465
+ cancelAnimationFrame(this.nextRender)
466
+ this.nextRender = requestAnimationFrame(() => {
467
+ this.nextRender = null
468
+ this.advance().catch((e) => {})
469
+ })
470
+ }
471
+
472
+ formatURL(src: string, hostname: string) {
473
+ if (typeof src == 'string' && src.match(/^\/[^\/]/)) {
474
+ return hostname + src
475
+ } else {
476
+ return src
477
+ }
478
+ }
479
+
480
+ getContextAttribute(name: string, element = this) {
481
+ for (
482
+ var p: HTMLElement = this;
483
+ (p = p.parentElement || ((p.parentNode instanceof ShadowRoot ? p.parentNode.host : null) as HTMLElement));
484
+
485
+ ) {
486
+ if (p.getAttribute(name) != null) return p.getAttribute(name)
487
+ }
488
+ return this.getAttribute(name)
489
+ }
490
+
491
+ static register(tagName: string, win?: Window) {
492
+ if (win == null) win = typeof window != 'undefined' ? window : null
493
+ if (win && !win.customElements.get(tagName)) {
494
+ win.customElements.define(tagName, this)
495
+ }
496
+ }
61
497
  }
498
+
499
+ export { FEAASElement as Element }
@@ -1,4 +1,4 @@
1
- import { FEAASBase } from './FEAASBase.js'
1
+ import { FEAASElement } from './FEAASElement.js'
2
2
 
3
3
  export function FEAASLoaderProps(element: HTMLElement) {
4
4
  return {
@@ -8,7 +8,7 @@ export function FEAASLoaderProps(element: HTMLElement) {
8
8
  }
9
9
  type FEAASLoaderProps = ReturnType<typeof FEAASLoaderProps>
10
10
 
11
- export class FEAASLoader extends FEAASBase<FEAASLoaderProps, string> {
11
+ export class FEAASLoader extends FEAASElement<FEAASLoaderProps, string> {
12
12
  stylesheet: HTMLStyleElement
13
13
  loader: HTMLDivElement
14
14
 
@@ -0,0 +1,61 @@
1
+ import type { ReactNode, ReactElement, JSXElementConstructor, PropsWithChildren } from 'react'
2
+ import { FEAASElement } from './FEAASElement.js'
3
+
4
+ export type ReactModules = {
5
+ React: typeof React
6
+ ReactDOM: typeof ReactDOM
7
+ }
8
+ export type ReactJSX = ReactNode | ReactElement<any, string | JSXElementConstructor<any>>
9
+ export type FEAASReactRender<P> = {
10
+ (this: FEAASReactElement<P>, props: P, payload: ReactModules): ReactJSX
11
+ }
12
+
13
+ export default class FEAASReactElement<P> extends FEAASElement<P, ReactModules> {
14
+ load() {
15
+ return Promise.all([
16
+ // @ts-ignore @webpack-ignore
17
+ import('https://cdn.skypack.dev/react'),
18
+ // @ts-ignore
19
+ import('https://cdn.skypack.dev/react-dom')
20
+ ]).then(([React, ReactDOM]) => {
21
+ return { React, ReactDOM }
22
+ })
23
+ }
24
+
25
+ render({}: P, { React }: ReactModules): ReactJSX {
26
+ return <></>
27
+ }
28
+ }
29
+
30
+ interface MyComponentProps {
31
+ counter: string
32
+ }
33
+
34
+ export class MyComponent extends FEAASReactElement<MyComponentProps> {
35
+ // will re-render component if any of these attributes have changed
36
+ static observedAttributes = ['counter']
37
+
38
+ useShadowRoot = true
39
+
40
+ // parse attributes from web component to react-ready form
41
+ getProps() {
42
+ return {
43
+ counter: parseFloat(this.getAttribute('counter'))
44
+ }
45
+ }
46
+
47
+ // First argument is props, second arguments is React stuff
48
+ // Destructuring React is important for JSX to work
49
+ render({ counter }: MyComponentProps, { React }: ReactModules): ReactJSX {
50
+ const [updates, setUpdates] = React.useState(0)
51
+ React.useEffect(() => {
52
+ setUpdates((u) => u + 1)
53
+ }, [counter])
54
+ return (
55
+ <>
56
+ <p>Rendering attribute: {counter}</p>
57
+ <p>Triggered updates: {updates}</p>
58
+ </>
59
+ )
60
+ }
61
+ }
@@ -1,8 +1,8 @@
1
1
  import { DEFAULT_CDN_URL, FEAASCDNStylesheetParams, parseStylesheetSource } from '../cdn.js'
2
2
  import { loadStylesheet } from '../dom/stylesheet.js'
3
- import { FEAASBase } from './FEAASBase.js'
3
+ import { FEAASElement } from './FEAASElement.js'
4
4
 
5
- export function FEAASStylesheetProps(element: FEAASBase) {
5
+ export function FEAASStylesheetProps(element: FEAASElement) {
6
6
  const src = element.getAttribute('src')
7
7
  if (src) {
8
8
  var { cdn, library, revision } = parseStylesheetSource(src)
@@ -15,7 +15,7 @@ export function FEAASStylesheetProps(element: FEAASBase) {
15
15
  }
16
16
  export type FEAASStylesheetProps = ReturnType<typeof FEAASStylesheetProps>
17
17
 
18
- export class FEAASStylesheet extends FEAASBase<FEAASStylesheetProps, string> {
18
+ export class FEAASStylesheet extends FEAASElement<FEAASStylesheetProps, string> {
19
19
  getProps() {
20
20
  return FEAASStylesheetProps(this)
21
21
  }
package/src/headless.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * avoid circular dependency. Regular clientside build entry point is in ui/index.ts
4
4
  */
5
5
 
6
- import FEAASElement from './components/FEAASElement.js'
6
+ import FEAASReactElement from './components/FEAASReactElement.js'
7
7
 
8
8
  export * from './dom/rendering.js'
9
9
  export * from './dom/data.js'
@@ -11,9 +11,10 @@ export * from './dom/scripts.js'
11
11
  export * from './dom/stylesheet.js'
12
12
  export * from './dom/component.js'
13
13
  export * from './fetch.js'
14
+ export * from './utils/promise.js'
14
15
  export * from './cdn.js'
15
- export * from './components/FEAASBase.js'
16
16
  export * from './components/FEAASElement.js'
17
+ export * from './components/FEAASReactElement.js'
17
18
  export * from './components/FEAASComponent.js'
18
19
  export * from './components/FEAASLoader.js'
19
20
  export * from './components/FEAASStylesheet.js'
@@ -23,11 +24,11 @@ export * as Thumbnail from './dom/thumbnail.js'
23
24
  declare global {
24
25
  interface Window {
25
26
  FEAASLoading: Promise<any>
26
- FEAASElement: typeof FEAASElement
27
+ FEAASReactElement: typeof FEAASReactElement
27
28
  }
28
29
  }
29
30
 
30
31
  if (typeof window != 'undefined') {
31
- window.FEAASElement = FEAASElement
32
+ window.FEAASReactElement = FEAASReactElement
32
33
  window.FEAASLoading ??= Promise.resolve()
33
34
  }
@@ -0,0 +1,4 @@
1
+ export * from '../headless.js'
2
+ export * from './FEAASEditor.js'
3
+ export * from './FEAASPicker.js'
4
+ export * from './FEAASContext.js'