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