@sitecore-feaas/clientside 0.1.27 → 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.
- package/build/cdn.js +115 -0
- package/build/cdn.js.map +1 -0
- package/build/components/FEAASBase.js +212 -79
- package/build/components/FEAASBase.js.map +1 -1
- package/build/components/FEAASComponent.js +51 -17
- package/build/components/FEAASComponent.js.map +1 -1
- package/build/components/FEAASLoader.js +6 -0
- package/build/components/FEAASLoader.js.map +1 -1
- package/build/components/FEAASStylesheet.js +18 -13
- package/build/components/FEAASStylesheet.js.map +1 -1
- package/build/dom/component.js +34 -0
- package/build/dom/component.js.map +1 -0
- package/build/dom/data.js +105 -0
- package/build/dom/data.js.map +1 -0
- package/build/{dom.js → dom/rendering.js} +3 -194
- package/build/dom/rendering.js.map +1 -0
- package/build/dom/scripts.js +48 -0
- package/build/dom/scripts.js.map +1 -0
- package/build/dom/stylesheet.js +39 -0
- package/build/dom/stylesheet.js.map +1 -0
- package/build/fetch.js +72 -22
- package/build/fetch.js.map +1 -1
- package/build/{basic.js → headless.js} +11 -3
- package/build/headless.js.map +1 -0
- package/build/ui/FEAASContext.js +10 -3
- package/build/ui/FEAASContext.js.map +1 -1
- package/build/ui/FEAASEditor.js +5 -0
- package/build/ui/FEAASEditor.js.map +1 -1
- package/build/ui/FEAASPicker.js +7 -1
- package/build/ui/FEAASPicker.js.map +1 -1
- package/build/ui/index.js +1 -1
- package/build/ui/index.js.map +1 -1
- package/build/ui/react.js +6 -0
- package/build/ui/react.js.map +1 -0
- package/build/utils/promise.js +26 -0
- package/build/utils/promise.js.map +1 -0
- package/dist/browser/headless.esm.js +69 -0
- package/dist/browser/headless.esm.js.map +1 -0
- package/dist/browser/headless.js +69 -0
- package/dist/browser/headless.js.map +1 -0
- package/dist/browser/index.esm.js +3 -5
- package/dist/browser/index.esm.js.map +1 -1
- package/dist/browser/index.js +5 -5
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/react.js +67 -0
- package/dist/browser/react.js.map +1 -0
- package/dist/node/headless.esm.js +67 -0
- package/dist/node/headless.esm.js.map +7 -0
- package/dist/node/headless.js +67 -0
- package/dist/node/headless.js.map +7 -0
- package/dist/node/index.esm.js +3 -3
- package/dist/node/index.esm.js.map +4 -4
- package/dist/node/index.js +3 -3
- package/dist/node/index.js.map +4 -4
- package/dist/node/react.js +67 -0
- package/dist/node/react.js.map +7 -0
- package/examples/index.html +0 -5
- package/package.json +23 -12
- package/scripts/build.js +17 -6
- package/scripts/test-setup.ts +1 -0
- package/src/cdn.ts +188 -0
- package/src/components/FEAASBase.tsx +234 -82
- package/src/components/FEAASComponent.tsx +59 -21
- package/src/components/FEAASLoader.tsx +4 -0
- package/src/components/FEAASStylesheet.tsx +14 -13
- package/src/dom/component.ts +35 -0
- package/src/dom/data.ts +124 -0
- package/src/{dom.ts → dom/rendering.ts} +4 -218
- package/src/dom/scripts.ts +47 -0
- package/src/dom/stylesheet.ts +48 -0
- package/src/fetch.ts +79 -25
- package/src/{basic.ts → headless.ts} +11 -2
- package/src/ui/FEAASContext.tsx +12 -4
- package/src/ui/FEAASEditor.tsx +5 -0
- package/src/ui/FEAASPicker.tsx +6 -2
- package/src/ui/index.ts +1 -1
- package/src/ui/react.tsx +8 -0
- package/src/utils/promise.ts +34 -0
- package/test/FEAASBase.test.tsx +195 -0
- package/test/FEAASComponent.test.tsx +284 -0
- package/test/FEAASContext.test.tsx +76 -0
- package/test/FEAASPicker.test.tsx +40 -0
- package/test/FEAASStylesheet.test.tsx +159 -0
- package/test/cdn.ts +39 -0
- package/test/{render.ts → dom.test.ts} +12 -6
- package/test/extras.ts +1 -1
- package/test/fetch.ts +254 -0
- package/test/tsconfig.json +4 -1
- package/types/cdn.d.ts +57 -0
- package/types/cdn.d.ts.map +1 -0
- package/types/components/FEAASBase.d.ts +220 -16
- package/types/components/FEAASBase.d.ts.map +1 -1
- package/types/components/FEAASComponent.d.ts +34 -7
- package/types/components/FEAASComponent.d.ts.map +1 -1
- package/types/components/FEAASLoader.d.ts +4 -0
- package/types/components/FEAASLoader.d.ts.map +1 -1
- package/types/components/FEAASStylesheet.d.ts +5 -13
- package/types/components/FEAASStylesheet.d.ts.map +1 -1
- package/types/dom/component.d.ts +6 -0
- package/types/dom/component.d.ts.map +1 -0
- package/types/dom/data.d.ts +15 -0
- package/types/dom/data.d.ts.map +1 -0
- package/types/dom/rendering.d.ts +20 -0
- package/types/dom/rendering.d.ts.map +1 -0
- package/types/dom/scripts.d.ts +4 -0
- package/types/dom/scripts.d.ts.map +1 -0
- package/types/dom/stylesheet.d.ts +8 -0
- package/types/dom/stylesheet.d.ts.map +1 -0
- package/types/fetch.d.ts +25 -11
- package/types/fetch.d.ts.map +1 -1
- package/types/headless.d.ts +24 -0
- package/types/headless.d.ts.map +1 -0
- package/types/tsbuildinfo +1 -1
- package/types/tsbuildinfo_ui +1 -1
- package/types/ui/FEAASContext.d.ts +15 -5
- package/types/ui/FEAASContext.d.ts.map +1 -1
- package/types/ui/FEAASEditor.d.ts +18 -0
- package/types/ui/FEAASEditor.d.ts.map +1 -1
- package/types/ui/FEAASPicker.d.ts +15 -3
- package/types/ui/FEAASPicker.d.ts.map +1 -1
- package/types/ui/index.d.ts +1 -1
- package/types/ui/index.d.ts.map +1 -1
- package/types/ui/react.d.ts +7 -0
- package/types/ui/react.d.ts.map +1 -0
- package/types/utils/promise.d.ts +8 -0
- package/types/utils/promise.d.ts.map +1 -0
- package/build/basic.js.map +0 -1
- package/build/cloud.js +0 -71
- package/build/cloud.js.map +0 -1
- package/build/dom.js.map +0 -1
- package/build/node.js +0 -10
- package/build/node.js.map +0 -1
- package/build/ui/node.js +0 -3
- package/build/ui/node.js.map +0 -1
- package/dist/browser/basic.esm.js +0 -69
- package/dist/browser/basic.esm.js.map +0 -1
- package/dist/browser/basic.js +0 -69
- package/dist/browser/basic.js.map +0 -1
- package/dist/node/basic.esm.js +0 -67
- package/dist/node/basic.esm.js.map +0 -7
- package/dist/node/basic.js +0 -67
- package/dist/node/basic.js.map +0 -7
- package/src/cloud.ts +0 -118
- package/src/node.ts +0 -10
- package/src/ui/node.ts +0 -2
- package/test/components.ts +0 -31
- package/types/basic.d.ts +0 -16
- package/types/basic.d.ts.map +0 -1
- package/types/cloud.d.ts +0 -28
- package/types/cloud.d.ts.map +0 -1
- package/types/dom.d.ts +0 -41
- package/types/dom.d.ts.map +0 -1
- package/types/node.d.ts +0 -4
- package/types/node.d.ts.map +0 -1
- package/types/ui/node.d.ts +0 -3
- 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 =
|
|
10
|
-
|
|
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('
|
|
43
|
-
this.addEventListener('
|
|
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.
|
|
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 `
|
|
67
|
-
*
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 '
|
|
156
|
+
case 'feaasMount':
|
|
118
157
|
if (target != this) this.nestedCallback(target)
|
|
119
158
|
break
|
|
120
|
-
case '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
172
|
-
|
|
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(
|
|
199
|
-
|
|
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(
|
|
287
|
+
this.reactRoot.render(this.getReactElement())
|
|
208
288
|
} else {
|
|
209
|
-
ReactDOM.render(
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
Object.assign(target.reactWrapper, {
|
|
377
|
+
key: target.tagName + '-' + target.getUID(),
|
|
248
378
|
displayName: displayName
|
|
249
379
|
})
|
|
250
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this.
|
|
290
|
-
|
|
291
|
-
if (this.
|
|
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
|
|
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.
|
|
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'
|
|
326
|
-
|
|
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
|
}
|