@sitecore-feaas/clientside 0.1.28 → 0.1.29

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 (156) hide show
  1. package/build/cdn.js +115 -0
  2. package/build/cdn.js.map +1 -0
  3. package/build/components/FEAASBase.js +212 -79
  4. package/build/components/FEAASBase.js.map +1 -1
  5. package/build/components/FEAASComponent.js +50 -17
  6. package/build/components/FEAASComponent.js.map +1 -1
  7. package/build/components/FEAASLoader.js +6 -0
  8. package/build/components/FEAASLoader.js.map +1 -1
  9. package/build/components/FEAASStylesheet.js +18 -13
  10. package/build/components/FEAASStylesheet.js.map +1 -1
  11. package/build/dom/component.js +34 -0
  12. package/build/dom/component.js.map +1 -0
  13. package/build/dom/data.js +105 -0
  14. package/build/dom/data.js.map +1 -0
  15. package/build/{dom.js → dom/rendering.js} +3 -194
  16. package/build/dom/rendering.js.map +1 -0
  17. package/build/dom/scripts.js +48 -0
  18. package/build/dom/scripts.js.map +1 -0
  19. package/build/dom/stylesheet.js +39 -0
  20. package/build/dom/stylesheet.js.map +1 -0
  21. package/build/fetch.js +72 -22
  22. package/build/fetch.js.map +1 -1
  23. package/build/{basic.js → headless.js} +11 -3
  24. package/build/headless.js.map +1 -0
  25. package/build/ui/FEAASContext.js +10 -3
  26. package/build/ui/FEAASContext.js.map +1 -1
  27. package/build/ui/FEAASEditor.js +5 -0
  28. package/build/ui/FEAASEditor.js.map +1 -1
  29. package/build/ui/FEAASPicker.js +7 -1
  30. package/build/ui/FEAASPicker.js.map +1 -1
  31. package/build/ui/index.js +1 -1
  32. package/build/ui/index.js.map +1 -1
  33. package/build/ui/react.js +6 -0
  34. package/build/ui/react.js.map +1 -0
  35. package/build/utils/promise.js +26 -0
  36. package/build/utils/promise.js.map +1 -0
  37. package/dist/browser/headless.esm.js +69 -0
  38. package/dist/browser/headless.esm.js.map +1 -0
  39. package/dist/browser/headless.js +69 -0
  40. package/dist/browser/headless.js.map +1 -0
  41. package/dist/browser/index.esm.js +3 -5
  42. package/dist/browser/index.esm.js.map +1 -1
  43. package/dist/browser/index.js +5 -5
  44. package/dist/browser/index.js.map +1 -1
  45. package/dist/browser/react.js +67 -0
  46. package/dist/browser/react.js.map +1 -0
  47. package/dist/node/headless.esm.js +67 -0
  48. package/dist/node/headless.esm.js.map +7 -0
  49. package/dist/node/headless.js +67 -0
  50. package/dist/node/headless.js.map +7 -0
  51. package/dist/node/index.esm.js +3 -3
  52. package/dist/node/index.esm.js.map +4 -4
  53. package/dist/node/index.js +3 -3
  54. package/dist/node/index.js.map +4 -4
  55. package/dist/node/react.js +67 -0
  56. package/dist/node/react.js.map +7 -0
  57. package/examples/index.html +0 -5
  58. package/package.json +23 -12
  59. package/scripts/build.js +17 -6
  60. package/scripts/test-setup.ts +1 -0
  61. package/src/cdn.ts +188 -0
  62. package/src/components/FEAASBase.tsx +234 -82
  63. package/src/components/FEAASComponent.tsx +56 -21
  64. package/src/components/FEAASLoader.tsx +4 -0
  65. package/src/components/FEAASStylesheet.tsx +14 -13
  66. package/src/dom/component.ts +35 -0
  67. package/src/dom/data.ts +124 -0
  68. package/src/{dom.ts → dom/rendering.ts} +4 -218
  69. package/src/dom/scripts.ts +47 -0
  70. package/src/dom/stylesheet.ts +48 -0
  71. package/src/fetch.ts +79 -25
  72. package/src/{basic.ts → headless.ts} +11 -2
  73. package/src/ui/FEAASContext.tsx +12 -4
  74. package/src/ui/FEAASEditor.tsx +5 -0
  75. package/src/ui/FEAASPicker.tsx +6 -2
  76. package/src/ui/index.ts +1 -1
  77. package/src/ui/react.tsx +8 -0
  78. package/src/utils/promise.ts +34 -0
  79. package/test/FEAASBase.test.tsx +195 -0
  80. package/test/FEAASComponent.test.tsx +284 -0
  81. package/test/FEAASContext.test.tsx +76 -0
  82. package/test/FEAASPicker.test.tsx +40 -0
  83. package/test/FEAASStylesheet.test.tsx +159 -0
  84. package/test/cdn.ts +39 -0
  85. package/test/{render.ts → dom.test.ts} +12 -6
  86. package/test/extras.ts +1 -1
  87. package/test/fetch.ts +254 -0
  88. package/test/tsconfig.json +4 -1
  89. package/types/cdn.d.ts +57 -0
  90. package/types/cdn.d.ts.map +1 -0
  91. package/types/components/FEAASBase.d.ts +220 -16
  92. package/types/components/FEAASBase.d.ts.map +1 -1
  93. package/types/components/FEAASComponent.d.ts +28 -7
  94. package/types/components/FEAASComponent.d.ts.map +1 -1
  95. package/types/components/FEAASLoader.d.ts +4 -0
  96. package/types/components/FEAASLoader.d.ts.map +1 -1
  97. package/types/components/FEAASStylesheet.d.ts +5 -13
  98. package/types/components/FEAASStylesheet.d.ts.map +1 -1
  99. package/types/dom/component.d.ts +6 -0
  100. package/types/dom/component.d.ts.map +1 -0
  101. package/types/dom/data.d.ts +15 -0
  102. package/types/dom/data.d.ts.map +1 -0
  103. package/types/dom/rendering.d.ts +20 -0
  104. package/types/dom/rendering.d.ts.map +1 -0
  105. package/types/dom/scripts.d.ts +4 -0
  106. package/types/dom/scripts.d.ts.map +1 -0
  107. package/types/dom/stylesheet.d.ts +8 -0
  108. package/types/dom/stylesheet.d.ts.map +1 -0
  109. package/types/fetch.d.ts +25 -11
  110. package/types/fetch.d.ts.map +1 -1
  111. package/types/headless.d.ts +24 -0
  112. package/types/headless.d.ts.map +1 -0
  113. package/types/tsbuildinfo +1 -1
  114. package/types/tsbuildinfo_ui +1 -1
  115. package/types/ui/FEAASContext.d.ts +15 -5
  116. package/types/ui/FEAASContext.d.ts.map +1 -1
  117. package/types/ui/FEAASEditor.d.ts +18 -0
  118. package/types/ui/FEAASEditor.d.ts.map +1 -1
  119. package/types/ui/FEAASPicker.d.ts +15 -3
  120. package/types/ui/FEAASPicker.d.ts.map +1 -1
  121. package/types/ui/index.d.ts +1 -1
  122. package/types/ui/index.d.ts.map +1 -1
  123. package/types/ui/react.d.ts +7 -0
  124. package/types/ui/react.d.ts.map +1 -0
  125. package/types/utils/promise.d.ts +8 -0
  126. package/types/utils/promise.d.ts.map +1 -0
  127. package/build/basic.js.map +0 -1
  128. package/build/cloud.js +0 -71
  129. package/build/cloud.js.map +0 -1
  130. package/build/dom.js.map +0 -1
  131. package/build/node.js +0 -10
  132. package/build/node.js.map +0 -1
  133. package/build/ui/node.js +0 -3
  134. package/build/ui/node.js.map +0 -1
  135. package/dist/browser/basic.esm.js +0 -69
  136. package/dist/browser/basic.esm.js.map +0 -1
  137. package/dist/browser/basic.js +0 -69
  138. package/dist/browser/basic.js.map +0 -1
  139. package/dist/node/basic.esm.js +0 -67
  140. package/dist/node/basic.esm.js.map +0 -7
  141. package/dist/node/basic.js +0 -67
  142. package/dist/node/basic.js.map +0 -7
  143. package/src/cloud.ts +0 -119
  144. package/src/node.ts +0 -10
  145. package/src/ui/node.ts +0 -2
  146. package/test/components.ts +0 -31
  147. package/types/basic.d.ts +0 -16
  148. package/types/basic.d.ts.map +0 -1
  149. package/types/cloud.d.ts +0 -39
  150. package/types/cloud.d.ts.map +0 -1
  151. package/types/dom.d.ts +0 -41
  152. package/types/dom.d.ts.map +0 -1
  153. package/types/node.d.ts +0 -4
  154. package/types/node.d.ts.map +0 -1
  155. package/types/ui/node.d.ts +0 -3
  156. package/types/ui/node.d.ts.map +0 -1
package/src/cdn.ts ADDED
@@ -0,0 +1,188 @@
1
+ import {
2
+ fetchAndRevalidate,
3
+ fetchWithCacheAwareness,
4
+ fetchBypassingCache,
5
+ fetchToValidateCache,
6
+ ValidatableRequestPhase
7
+ } from './fetch.js'
8
+
9
+ export const DEFAULT_CDN_URL = `https://feaascomponentsapidev.blob.core.windows.net`
10
+ export type FEAASCDNComponentParams = {
11
+ cdn?: string
12
+ revision?: 'staging' | 'published' | 'saved' | number
13
+ library: string
14
+ component: string
15
+ version: string
16
+
17
+ /** @deprecated Use `cdn` instead */
18
+ hostname?: string
19
+ }
20
+ export type FEAASCDNComponentSource = {
21
+ src: string
22
+ }
23
+ export type FEAASCDNComponentParamsCustomizations = {
24
+ instance?: string
25
+ }
26
+ export type FEAASCDNComponentProps = (FEAASCDNComponentParams | FEAASCDNComponentSource) &
27
+ FEAASCDNComponentParamsCustomizations
28
+
29
+ export interface FEAASCDNStylesheetParams {
30
+ cdn?: string
31
+ library: string
32
+ revision?: 'staging' | 'published' | 'saved' | number
33
+
34
+ /** @deprecated Use `cdn` instead */
35
+ hostname?: string
36
+ }
37
+
38
+ export type FEAASCDNStylesheetSource = {
39
+ src: string
40
+ }
41
+ export type FEAASCDNStylesheetProps = FEAASCDNStylesheetParams | FEAASCDNStylesheetSource
42
+
43
+ export function parseComponentSource(source: string): FEAASCDNComponentParams {
44
+ const match = source.match(/^(?:(.*?)\/components)?\/?([^\/]+)\/([^\/]+)(?:\/([^\/]+)(?:\/([^\/]+?)(?:\.html?)?)?)?$/)
45
+ if (match) {
46
+ var [, cdn, library, component, version, revision] = match
47
+ return { cdn, library, component, version, revision: revision as any }
48
+ }
49
+ }
50
+ export function getComponentSource(props: FEAASCDNComponentParams) {
51
+ if (!props || !props.library || !props.component)
52
+ throw new Error('Component requires `library` and `component` properties')
53
+ return `${props.cdn && props.cdn != DEFAULT_CDN_URL ? `${props.cdn}/components` : ''}/${props.library}/${
54
+ props.component
55
+ }/${
56
+ !props.version || (props.version == 'responsive' && (!props.revision || props.revision == 'published'))
57
+ ? ''
58
+ : props.version
59
+ }/${!props.revision || props.revision == 'published' ? '' : props.revision}`.replace(
60
+ /(^\/|\/$|(?<!:)\/\/)(?!.+\/components\/)/g,
61
+ ''
62
+ )
63
+ }
64
+
65
+ export function getStylesheetSource(props: FEAASCDNStylesheetParams) {
66
+ return `${props.cdn && props.cdn != DEFAULT_CDN_URL ? `${props.cdn}/styles` : ''}/${props.library}/${
67
+ props.revision == 'published' ? '' : props.revision
68
+ }`.replace(/^\/|\/$|(?<!:)\/\//g, '')
69
+ }
70
+
71
+ export function parseStylesheetSource(source: string): FEAASCDNStylesheetParams {
72
+ const match = source.match(/^(?:(.*?)\/styles)?\/([^\/]+)\/([^\/]+)(?:.css?)?$/)
73
+ if (match) {
74
+ var [, cdn, library, revision] = match
75
+ return { cdn, library, revision: revision as any }
76
+ }
77
+ }
78
+ export function getComponentURL(props: FEAASCDNComponentProps) {
79
+ if ('src' in props) {
80
+ props = {
81
+ ...parseComponentSource(props.src)
82
+ }
83
+ }
84
+ return `${props.cdn || props.hostname || DEFAULT_CDN_URL}/components/${props.library}/${props.component}/${
85
+ props.version
86
+ }/${props.revision || 'published'}.html`
87
+ }
88
+
89
+ export function getStylesheetURL(props: FEAASCDNStylesheetProps) {
90
+ if ('src' in props) {
91
+ props = {
92
+ ...parseComponentSource(props.src)
93
+ }
94
+ }
95
+ return `${props.cdn || props.hostname || DEFAULT_CDN_URL}/styles/${props.library}/${
96
+ props.revision || 'published'
97
+ }.css`
98
+ }
99
+
100
+ export async function fetchAndRevalidateComponent(
101
+ props: FEAASCDNComponentProps,
102
+ callback?: (html: string) => void,
103
+ dontRevalidate = false
104
+ ) {
105
+ const onlyIfNotDeleted = (response: Response) => {
106
+ return response.headers.get('x-ms-meta-versionDeletedAt') == null
107
+ }
108
+ const parseResponse = (response: Response) => {
109
+ return response.clone().text().then(callback)
110
+ }
111
+
112
+ // Fetch two requests in parallel: One optimistically checking if component was forked,
113
+ // and another checking original definition of a component
114
+ const results = await Promise.all([
115
+ props.instance &&
116
+ fetchWithCacheAwareness({
117
+ options: {
118
+ headers: {
119
+ Accept: 'text/html'
120
+ }
121
+ },
122
+ url: getComponentURL({ ...props, version: props.instance }),
123
+ validator: onlyIfNotDeleted
124
+ }).catch((e) => e as Error),
125
+ fetchWithCacheAwareness({ url: getComponentURL(props), validator: onlyIfNotDeleted }).catch((e) => e as Error)
126
+ ])
127
+ const result = results.find((result) => result && 'isCached' in result && result)
128
+ if (!result || result instanceof Error) {
129
+ const error = results.find((result) => result && result instanceof Error)
130
+ throw error || new Error('Can not fetch component')
131
+ }
132
+ if (results[0] == result) {
133
+ console.log('Using fork!', results)
134
+ }
135
+ await parseResponse(result.response)
136
+ if (dontRevalidate) return result
137
+ return fetchToValidateCache({ ...result, callback: parseResponse }).catch((e) => {
138
+ if (results[0] == result) {
139
+ fetchBypassingCache({ ...result, callback: null, validator: null })
140
+ console.log('Fork was deleted')
141
+ // forked version was cached, but it's now deleted
142
+ if (!(results[1] instanceof Error)) {
143
+ parseResponse(results[1].response)
144
+ return fetchToValidateCache({ ...results[1], callback: parseResponse })
145
+ }
146
+ }
147
+ throw e
148
+ })
149
+ }
150
+
151
+ /** Fetches HTML of a component. */
152
+ export async function fetchComponent(props: FEAASCDNComponentProps | string) {
153
+ if (typeof props == 'string') props = { src: props }
154
+ var template: string
155
+ const { response } = await fetchAndRevalidateComponent(
156
+ props,
157
+ (result) => {
158
+ template = result
159
+ },
160
+ true
161
+ )
162
+ return { ...props, template, lastModified: response.headers.get('last-modified') }
163
+ }
164
+
165
+ export type ValidatableStylesheetCallback = (css: string, phase: ValidatableRequestPhase) => void
166
+ export async function fetchAndRevalidateStylesheet(
167
+ props: FEAASCDNStylesheetProps | string,
168
+ callback: ValidatableStylesheetCallback
169
+ ) {
170
+ if (typeof props == 'string') props = { src: props }
171
+ return fetchAndRevalidate(
172
+ getStylesheetURL(props),
173
+ {
174
+ headers: {
175
+ Accept: 'text/css'
176
+ }
177
+ },
178
+ (response) => {
179
+ return response.headers.get('x-ms-meta-deletedAt') == null
180
+ },
181
+ (response, phase) => {
182
+ return response
183
+ .clone()
184
+ .text()
185
+ .then((text) => callback(text, phase))
186
+ }
187
+ )
188
+ }
@@ -1,21 +1,47 @@
1
- import { JSXElementConstructor, ReactElement, ReactNode } from 'react'
1
+ import type { JSXElementConstructor, ReactElement, ReactNode } from 'react'
2
2
  import type ReactDOMClient from 'react-dom/client'
3
3
  import type ReactDOM from 'react-dom'
4
4
  import type React from 'react'
5
+ import { ControlledPromise } from '../utils/promise.js'
5
6
 
6
7
  export type OptionalExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>
7
8
 
8
9
  // Shim in case it's required in node.js environment
9
- var WebComponent = typeof HTMLElement == 'undefined' ? (class {} as unknown as typeof HTMLElement) : HTMLElement
10
- interface ReactPayload {
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 {
11
20
  React: typeof React
12
21
  ReactDOM: typeof ReactDOM
13
22
  ReactDOMClient: typeof ReactDOMClient
14
23
  }
15
-
24
+ /**
25
+ * FEAASBase is a superclass for FEAAS components with optional react support.
26
+ *
27
+ * Initialization lifecycle:
28
+ *
29
+ * - `set(props)`
30
+ * - `load() => payload`
31
+ * - `onLoad?(payload)`
32
+ * - `update()`
33
+ * - `render(payload, props)`
34
+ * - `whenRendered`
35
+ * - - / If needs to reload, e.g. bypass cache /
36
+ * - - `onLoad(newPayload) `
37
+ * - - `whenRendered`
38
+ * - `whenLoaded`
39
+ */
16
40
  export class FEAASBase<Props = any, Payload = any> extends WebComponent {
17
41
  static observedAttributes: string[]
18
42
 
43
+ defaultProps = {} as Partial<Props>
44
+
19
45
  /** Return attributes as parsed values */
20
46
  getProps(context?: HTMLElement): any {
21
47
  return {}
@@ -31,16 +57,19 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
31
57
  /** Is/was component in the dom? */
32
58
  connected = false
33
59
 
34
- /** Result of lazy loading */
35
- payload: Payload
36
-
37
60
  refs: Record<string, FEAASBase> = {}
38
61
  descendants: FEAASBase[] = []
39
62
 
63
+ /** Result of lazy loading */
64
+ payload: Payload
65
+ whenLoaded = ControlledPromise<Payload>((payload) => {
66
+ this.payload = payload
67
+ })
68
+
40
69
  constructor() {
41
70
  super()
42
- this.addEventListener('feaasConnected', this)
43
- this.addEventListener('feaasDisconnected', this)
71
+ this.addEventListener('feaasMount', this)
72
+ this.addEventListener('feaasUnmount', this)
44
73
  }
45
74
 
46
75
  /** Attaches shadow root if getShadowRootOptions any settings, or uses element itself */
@@ -55,7 +84,10 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
55
84
 
56
85
  /** Components parses its attributes when connected, and schedules rendeirng */
57
86
  connectedCallback() {
58
- this.initialize()
87
+ this.mount()
88
+ }
89
+ disconnectedCallback() {
90
+ this.unmount()
59
91
  }
60
92
 
61
93
  /**
@@ -63,20 +95,35 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
63
95
  * attempts to avoid this and inverts the initialization logic, so that children are initialized in the context of its
64
96
  * already-initialized parents.
65
97
  *
66
- * FEAAS components dispatch `feaasConnected` event that allows components to store references to each other. In
67
- * addition it allows external scripts to listen for initialization of FEAAS components
98
+ * FEAAS components dispatch `feaasMount` event that allows components to store references to each other. In addition
99
+ * it allows external scripts to listen for initialization of FEAAS components
68
100
  */
69
- initialize() {
70
- const parent = this.getParent() as FEAASBase
71
- // Allow parent component to initialize first
72
- if (parent && !parent.connected) return
73
- if (this.connected) return
74
- this.connected = true
75
- console.log('FEAAS: Connected', this.tagName, this)
76
- this.dispatchEvent(new Event('feaasConnected', { bubbles: true, composed: true }))
77
- this.getElements().map((element: FEAASBase) => element.initialize?.())
78
- this.set()
79
- return true
101
+ mount() {
102
+ try {
103
+ const parent = this.getParent() as FEAASBase
104
+ // Allow parent component to initialize first
105
+ if (parent && !parent.connected) return
106
+ if (this.connected) return
107
+ this.connected = true
108
+ this.log('FEAAS: Mount', this.tagName, this)
109
+ this.dispatchEvent(new Event('feaasMount', { bubbles: true, composed: true }))
110
+ this.getElements().map((element: FEAASBase) => element.mount?.())
111
+ this.set()
112
+ } catch (e) {
113
+ this.onError(e)
114
+ }
115
+ }
116
+
117
+ log(...args: any) {
118
+ console.log(...args)
119
+ }
120
+
121
+ /** Nothing special happens on unmount, except setting a flag */
122
+ unmount() {
123
+ if (!this.connected) return
124
+ this.log('FEAAS: Unmount', this)
125
+ this.dispatchEvent(new Event('feaasUnmount', { bubbles: true, composed: true }))
126
+ this.connected = false
80
127
  }
81
128
 
82
129
  getParent(onlyFeaas = true) {
@@ -102,22 +149,14 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
102
149
  .filter(Boolean)
103
150
  }
104
151
 
105
- /** Nothing special happens on disconnect, except setting a flag */
106
- disconnectedCallback() {
107
- if (!this.connected) return
108
- console.log('FEAAS: Disconnected', this)
109
- this.dispatchEvent(new Event('feaasDisconnected', { bubbles: true, composed: true }))
110
- this.connected = null
111
- }
112
-
113
152
  /** Catch-all event handler that dispatches events to callback */
114
153
  handleEvent(event: Event) {
115
154
  const target = event.composedPath()[0] as FEAASBase
116
155
  switch (event.type) {
117
- case 'feaasConnected':
156
+ case 'feaasMount':
118
157
  if (target != this) this.nestedCallback(target)
119
158
  break
120
- case 'feaasDisconnected':
159
+ case 'feaasUnmount':
121
160
  if (target != this) this.unnestedCallback(target)
122
161
  break
123
162
  }
@@ -127,7 +166,7 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
127
166
  unnestedCallback(target: FEAASBase) {
128
167
  const index = this.descendants.indexOf(target)
129
168
  if (index == -1) return
130
- console.log('FEAAS: Unnest', this, target)
169
+ this.log('FEAAS: Unnest', this, target)
131
170
  this.descendants.splice(index, -1)
132
171
  this.refs[target.getComponentName()] = null
133
172
  if (target.refs.parent == this) target.refs.parent = null
@@ -141,7 +180,7 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
141
180
  const index = this.descendants.indexOf(target)
142
181
  if (index != -1) return
143
182
  this.descendants.push(target)
144
- console.log('FEAAS: Nest', this, target)
183
+ this.log('FEAAS: Nest', this, target)
145
184
  this.refs[target.getComponentName()] = target
146
185
  target.refs.top = this
147
186
  target.refs.parent ||= this
@@ -167,13 +206,61 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
167
206
  return this.payload === undefined && this.getAttribute('hidden') == null
168
207
  }
169
208
 
209
+ /** Generic error handler */
210
+ onError(error: Error, where: string = 'uncaught', handled = false) {
211
+ const event = new CustomEvent('feaasError', { bubbles: true, composed: true, detail: error })
212
+ this.dispatchEvent(event)
213
+ this.whenLoaded.reject(error)
214
+ if (!event.defaultPrevented && !handled) {
215
+ this.whenRendered.reject(error)
216
+ try {
217
+ this.unmount()
218
+ } catch (e) {}
219
+ return true
220
+ }
221
+ return false
222
+ }
223
+
224
+ shouldUpdateOnLoad() {
225
+ return true
226
+ }
227
+
228
+ onLoad: (payload: Payload) => void
229
+
170
230
  /** Load lazy component & render */
171
- async reify() {
172
- if (this.payload !== undefined) {
231
+ async advance() {
232
+ try {
233
+ if (this.payload === undefined) {
234
+ if (!this.isReadyToLoad()) return
235
+ this.payload = null
236
+ this.dispatchEvent(new Event('feaasLoad', { bubbles: true, composed: true }))
237
+ const loaded = await this.load().catch((e) => {
238
+ this.payload = undefined
239
+ throw e
240
+ })
241
+
242
+ this.whenLoaded.resolve(loaded)
243
+
244
+ // Allow component to handle the post-loading
245
+ if (this.onLoad) {
246
+ return
247
+ }
248
+ } else if (this.payload == null) {
249
+ return
250
+ }
251
+ this.flush()
252
+ this.whenRendered.resolve(this)
253
+ } catch (e) {
254
+ this.onError(e)
255
+ }
256
+ }
257
+
258
+ flush() {
259
+ const flushSync = (this.payload as ReactPayload)?.ReactDOM?.flushSync
260
+ if (flushSync) {
261
+ flushSync(() => this.update())
262
+ } else {
173
263
  this.update()
174
- } else if (this.isReadyToLoad()) {
175
- this.payload = null
176
- this.load().then(this.onLoad)
177
264
  }
178
265
  }
179
266
 
@@ -182,31 +269,24 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
182
269
  return Promise.resolve(null)
183
270
  }
184
271
 
185
- /** Payload is inside onLoad callback. For web components that use react, it has to provide React & ReactDOM props */
186
- onLoad = (payload: Payload) => {
187
- if (payload == null) return
188
-
189
- this.payload = payload
190
- this.update()
191
- }
192
-
193
272
  /**
194
273
  * Component-specific logic that updates the DOM. Default render implementation assume react rendering, in that case
195
274
  * the component needs to provide React & ReactDOM references in its payload. Web component redefine its render to not
196
275
  * use react at all
197
276
  */
198
- update(nested = false): any {
199
- console.log('FEAAS: React', this.tagName, this.payload)
277
+ update(): any {
278
+ //this.log('FEAAS: React', this.tagName, this.payload)
200
279
  if (this.payload == null) return
280
+ if (!this.render) return
201
281
  const { React, ReactDOM, ReactDOMClient } = (this.payload as ReactPayload) || {}
202
282
  if (!React) throw new Error(`${this.tagName}: React is not present in payload`)
203
283
  if (!ReactDOM) throw new Error(`${this.tagName}: ReactDOM is not present in payload`)
204
284
  if (!this.refs.topReact && !this.forceUpdateReact) {
205
285
  if (ReactDOMClient) {
206
286
  this.reactRoot ||= ReactDOMClient.createRoot(this.getReactRootElement())
207
- this.reactRoot.render(React.createElement(this.getReactWrapper()))
287
+ this.reactRoot.render(this.getReactElement())
208
288
  } else {
209
- ReactDOM.render(React.createElement(this.getReactWrapper()), this.getReactRootElement())
289
+ ReactDOM.render(this.getReactElement(), this.getReactRootElement())
210
290
  }
211
291
  } else if (this.forceUpdateReact) {
212
292
  this.forceUpdateReact?.()
@@ -217,37 +297,88 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
217
297
 
218
298
  invokeReactRendering(): ReactNode {
219
299
  const { React, ReactDOM } = (this.payload as ReactPayload) || {}
300
+ if (!this.connected) return null
220
301
  const children = this.descendants
221
302
  .filter((r) => {
222
303
  return r.refs.parentReact == this && r.payload
223
304
  })
224
305
  .map((child: FEAASBase<any, any>, index) => {
225
- return React.createElement(this.getReactWrapper(child), {
226
- key: child.tagName + '-' + child.getUID(),
227
- slot: child.getAttribute('slot')
228
- })
306
+ return this.getReactElement(child)
229
307
  })
230
308
  return this.render({ ...this.props, children }, this.payload)
231
309
  }
232
310
 
233
- reactWrapper: (props?: any) => ReactNode
311
+ reactWrapper: React.FunctionComponent
312
+ reactErrorBoundary: new (...args: any) => React.Component<{ children: any; slot: string }, { error: Error }>
234
313
  reactRoot: ReactDOMClient.Root
314
+
315
+ getReactErrorBoundary(target: FEAASBase = this, displayName = target.tagName) {
316
+ if (this.reactErrorBoundary) return this.reactErrorBoundary
317
+
318
+ const { React, ReactDOM } = (target.payload as ReactPayload) || {}
319
+ return (this.reactErrorBoundary ||= class FEAASErrorBoundary extends React.Component<
320
+ { children: any; slot: string },
321
+ { error: Error }
322
+ > {
323
+ state = {
324
+ error: null as Error
325
+ }
326
+
327
+ static getDerivedStateFromError(error: Error) {
328
+ return { error: error }
329
+ }
330
+
331
+ componentDidCatch(error: Error, errorInfo: any) {
332
+ console.error(`FEAAS: ${target.tagName} Error:`, error, errorInfo)
333
+ target.onError(error, 'react')
334
+ //this.setState({ error: error })
335
+ }
336
+
337
+ render() {
338
+ if (this.state.error) {
339
+ // target.classList.add('feaas-error')
340
+ return
341
+ //return <h1>There seems to be a problem.</h1>
342
+ }
343
+ //if (target.classList.contains('feaas-error')) target.classList.remove('feaas-error')
344
+
345
+ return this.props.children
346
+ }
347
+ })
348
+ }
349
+
350
+ getReactElement(target: FEAASBase = this, props?: any) {
351
+ const { React, ReactDOM } = (target.payload as ReactPayload) || {}
352
+ const ErrorBoundary = this.getReactErrorBoundary()
353
+ const Wrapper = this.getReactWrapper(target)
354
+ return (
355
+ <ErrorBoundary key={target.tagName + '-' + target.getUID()} slot={target.getAttribute('slot')}>
356
+ <Wrapper />
357
+ </ErrorBoundary>
358
+ )
359
+ }
360
+
235
361
  getReactWrapper(target: FEAASBase = this, displayName = target.tagName) {
236
- const Wrapper = (target.reactWrapper ||= (props?: any) => {
362
+ if (target.reactWrapper) return target.reactWrapper
363
+
364
+ const { React, ReactDOM } = (target.payload as ReactPayload) || {}
365
+
366
+ target.reactWrapper = (props?: any) => {
237
367
  if (target.render) {
238
- const { React, ReactDOM } = (target.payload as ReactPayload) || {}
239
368
  const [value, forceUpdateReact] = React.useReducer((r) => r + 1, 0)
240
369
  target.forceUpdateReact = forceUpdateReact
241
370
  return ReactDOM.createPortal(<>{target.invokeReactRendering()}</>, target.getReactRootElement())
242
371
  } else {
243
372
  return target.update()
244
373
  }
245
- })
246
- Object.assign(Wrapper, {
247
- key: this.tagName + '-' + this.getUID(),
374
+ }
375
+
376
+ Object.assign(target.reactWrapper, {
377
+ key: target.tagName + '-' + target.getUID(),
248
378
  displayName: displayName
249
379
  })
250
- return Wrapper
380
+
381
+ return target.reactWrapper
251
382
  }
252
383
 
253
384
  uid: string
@@ -275,30 +406,50 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
275
406
 
276
407
  /** Set props and return values on new next tick */
277
408
  set(overrides?: Partial<Props>) {
278
- this.overrides = {
279
- ...this.overrides,
280
- ...overrides
281
- }
282
- this.props = {
283
- ...this.getProps(),
284
- ...this.overrides
285
- }
286
- for (var property in this.props) {
287
- const value = this.props[property as keyof typeof this.props]
288
- if (this.props[property] == null) {
289
- this.removeAttribute(property)
290
- } else if (typeof value == 'string' || typeof value == 'number') {
291
- if (this.getAttribute(property) != String(value)) this.setAttribute(property, String(value))
409
+ try {
410
+ this.scheduleRender()
411
+ this.overrides = {
412
+ ...this.overrides,
413
+ ...overrides
414
+ }
415
+ this.props = {
416
+ ...this.getProps(),
417
+ ...this.overrides
418
+ }
419
+ for (var property in this.props) {
420
+ const value = this.props[property]
421
+ const attribute = property.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
422
+ if (value == null || value === this.defaultProps[property as keyof typeof this.defaultProps]) {
423
+ this.removeAttribute(attribute)
424
+ } else if (typeof value == 'string' || typeof value == 'number' || typeof value == 'boolean') {
425
+ if (this.getAttribute(attribute) != String(value)) this.setAttribute(attribute, String(value))
426
+ }
292
427
  }
428
+ } catch (e) {
429
+ this.whenRendered.reject(e)
430
+ this.onError(e, 'set')
293
431
  }
294
- this.scheduleRender()
432
+ return this
433
+ }
434
+
435
+ import(path: string): Promise<Payload> {
436
+ return import(/* @vite-ignore */ /* webpackIgnore: true */ path)
295
437
  }
296
438
 
297
439
  /** Schedule rendering on next tick, to batch multiple attribute changes */
440
+ initialized: boolean
441
+ whenRendered = ControlledPromise<this>(
442
+ () => (this.initialized = true),
443
+ () => cancelAnimationFrame(this.nextRender)
444
+ )
298
445
  scheduleRender() {
446
+ if (this.nextRender == null) {
447
+ this.whenRendered = this.whenRendered.restart()
448
+ }
299
449
  cancelAnimationFrame(this.nextRender)
300
450
  this.nextRender = requestAnimationFrame(() => {
301
- this.reify()
451
+ this.nextRender = null
452
+ this.advance().catch((e) => {})
302
453
  })
303
454
  }
304
455
 
@@ -321,9 +472,10 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
321
472
  return this.getAttribute(name)
322
473
  }
323
474
 
324
- static register(tagName: string) {
325
- if (typeof window != 'undefined' && !window.customElements.get(tagName)) {
326
- window.customElements.define(tagName, this)
475
+ static register(tagName: string, win?: Window) {
476
+ if (win == null) win = typeof window != 'undefined' ? window : null
477
+ if (!win?.customElements.get(tagName)) {
478
+ win.customElements.define(tagName, this)
327
479
  }
328
480
  }
329
481
  }