@sitecore-feaas/clientside 0.1.19 → 0.1.20

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 (79) hide show
  1. package/build/basic.js +5 -1
  2. package/build/basic.js.map +1 -1
  3. package/build/cloud.js +40 -7
  4. package/build/cloud.js.map +1 -1
  5. package/build/components/FEAASBase.js +60 -21
  6. package/build/components/FEAASBase.js.map +1 -1
  7. package/build/components/FEAASComponent.js +35 -1
  8. package/build/components/FEAASComponent.js.map +1 -1
  9. package/build/components/FEAASElement.js +14 -0
  10. package/build/components/FEAASElement.js.map +1 -0
  11. package/build/components/FEAASLoader.js +13 -1
  12. package/build/components/FEAASLoader.js.map +1 -1
  13. package/build/components/FEAASStylesheet.js.map +1 -1
  14. package/build/dom.js +6 -0
  15. package/build/dom.js.map +1 -1
  16. package/build/fetch.js +48 -45
  17. package/build/fetch.js.map +1 -1
  18. package/build/ui/FEAASContext.js +12 -13
  19. package/build/ui/FEAASContext.js.map +1 -1
  20. package/build/ui/FEAASEditor.js +448 -76
  21. package/build/ui/FEAASEditor.js.map +1 -1
  22. package/build/ui/FEAASPicker.js +9 -59
  23. package/build/ui/FEAASPicker.js.map +1 -1
  24. package/dist/browser/basic.esm.js +5 -5
  25. package/dist/browser/basic.esm.js.map +1 -1
  26. package/dist/browser/basic.js +5 -5
  27. package/dist/browser/basic.js.map +1 -1
  28. package/dist/browser/index.esm.js +5 -10
  29. package/dist/browser/index.esm.js.map +1 -1
  30. package/dist/browser/index.js +5 -10
  31. package/dist/browser/index.js.map +1 -1
  32. package/dist/node/basic.esm.js +3 -3
  33. package/dist/node/basic.esm.js.map +1 -1
  34. package/dist/node/basic.js +3 -3
  35. package/dist/node/basic.js.map +1 -1
  36. package/dist/node/index.esm.js +3 -8
  37. package/dist/node/index.esm.js.map +1 -1
  38. package/dist/node/index.js +3 -8
  39. package/dist/node/index.js.map +1 -1
  40. package/examples/index.html +8 -49
  41. package/package.json +1 -6
  42. package/src/basic.ts +8 -1
  43. package/src/cloud.ts +43 -13
  44. package/src/components/FEAASBase.tsx +73 -20
  45. package/src/components/{FEAASComponent.ts → FEAASComponent.tsx} +37 -1
  46. package/src/components/FEAASElement.tsx +21 -0
  47. package/src/components/{FEAASLoader.ts → FEAASLoader.tsx} +14 -1
  48. package/src/components/{FEAASStylesheet.ts → FEAASStylesheet.tsx} +0 -0
  49. package/src/dom.ts +5 -0
  50. package/src/fetch.ts +82 -51
  51. package/src/ui/FEAASContext.tsx +13 -14
  52. package/src/ui/FEAASEditor.tsx +598 -144
  53. package/src/ui/FEAASPicker.tsx +18 -65
  54. package/test/components.ts +19 -0
  55. package/types/basic.d.ts +3 -0
  56. package/types/basic.d.ts.map +1 -1
  57. package/types/cloud.d.ts +8 -1
  58. package/types/cloud.d.ts.map +1 -1
  59. package/types/components/FEAASBase.d.ts +18 -4
  60. package/types/components/FEAASBase.d.ts.map +1 -1
  61. package/types/components/FEAASComponent.d.ts +7 -0
  62. package/types/components/FEAASComponent.d.ts.map +1 -1
  63. package/types/components/FEAASElement.d.ts +12 -0
  64. package/types/components/FEAASElement.d.ts.map +1 -0
  65. package/types/components/FEAASLoader.d.ts +6 -0
  66. package/types/components/FEAASLoader.d.ts.map +1 -1
  67. package/types/components/FEAASStylesheet.d.ts +2 -2
  68. package/types/components/FEAASStylesheet.d.ts.map +1 -1
  69. package/types/dom.d.ts.map +1 -1
  70. package/types/fetch.d.ts +42 -1
  71. package/types/fetch.d.ts.map +1 -1
  72. package/types/tsbuildinfo +1 -1
  73. package/types/tsbuildinfo_ui +1 -1
  74. package/types/ui/FEAASContext.d.ts +4 -4
  75. package/types/ui/FEAASContext.d.ts.map +1 -1
  76. package/types/ui/FEAASEditor.d.ts +71 -5
  77. package/types/ui/FEAASEditor.d.ts.map +1 -1
  78. package/types/ui/FEAASPicker.d.ts +15 -34
  79. package/types/ui/FEAASPicker.d.ts.map +1 -1
@@ -1,54 +1,13 @@
1
1
  <script type="module">
2
- import FEAAS from '../dist/browser/index.js'
3
- const renderMyComponent = (element, data) =>
4
- FEAAS.renderComponent(
5
- {
6
- componentId: 'our-dominator1',
7
- versionId: 'dominator1-version2',
8
- revision: 'staged',
9
- hostname: 'https://feaascomponentsapidev.blob.core.windows.net',
10
- data: data
11
- },
12
- element
13
- )
2
+ import FEAASElement from '../dist/browser/basic.esm.js'
14
3
 
15
- const stylesheet = FEAAS.renderStylesheet({
16
- libraryId: 'our-wonderful-library'
17
- })
18
- document.head.appendChild(stylesheet)
19
4
 
20
- const element = renderMyComponent(null, {
21
- dominatorData: {
22
- name: 'YarikZ',
23
- goal: 'Explode the planet',
24
- status: 'progressing',
25
- items: [
26
- {
27
- 'title-text': 'Hello world Hello Thomas 2r23r',
28
- html: '<div class="-card--solid-color-cards -fill--light-blue"><button class="-button">Hola</button> <p><a href="#">Link</a> Text</div></p>',
29
- open: true
30
- },
31
- { 'title-text': 'Bye world', html: 'Mystical island' }
32
- ],
33
- items2: [
34
- {
35
- 'title-text': 'Cherry pop',
36
- html: '<div class="-card--solid-color-cards -fill--light-blue"><button class="-button">Hola</button> <p><a href="#">Link</a> Text</div></p>'
37
- },
38
- {
39
- 'title-text': 'Berry',
40
- html: '<div class="-card--solid-color-cards -fill--light-blue"><button class="-button">Hola</button> <p><a href="#">Link</a> Text</div></p>'
41
- },
42
- {
43
- 'title-text': 'Pear',
44
- html: '<div class="-card--solid-color-cards -fill--light-blue"><button class="-button">Hola</button> <a href="#">Link</a> Text</div>',
45
- open: true
46
- },
47
- { 'title-text': 'Coolio', html: 'Mystical island' }
48
- ]
5
+ export class Z extends FEAASElement<{}> {
6
+ renderReact(
7
+ props: { children?: ReactNode },
8
+ payload: { React: typeof import('react'); ReactDOM: typeof import('react-dom') }
9
+ ): ReactNode | ReactElement<any, string | JSXElementConstructor<any>> {
10
+ return
49
11
  }
50
- })
51
- document.body.appendChild(element)
52
-
53
- // renderMyComponent(element, {"user": {"name": "Bob"}})
12
+ }
54
13
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sitecore-feaas/clientside",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "scripts": {
5
5
  "start": "webpack serve --open",
6
6
  "build": "rm -rf dist;npm run build:types; npm run build:esbuild; npm run build:sourcemaps",
@@ -14,12 +14,7 @@
14
14
  "postpublish": "node ../cdn/upload-package.js clientside $npm_package_version"
15
15
  },
16
16
  "license": "ISC",
17
- "optionalDependencies": {
18
- "react": ">=16.8.0",
19
- "react-dom": ">=16.8.0"
20
- },
21
17
  "peerDependencies": {
22
- "react": ">=16.8.0",
23
18
  "react-dom": ">=16.8.0"
24
19
  },
25
20
  "main": "./dist/browser/index.esm.js",
package/src/basic.ts CHANGED
@@ -1,6 +1,9 @@
1
+ import FEAASElement from './components/FEAASElement.js'
2
+
1
3
  export * from './dom.js'
2
4
  export * from './fetch.js'
3
5
  export * from './components/FEAASBase.js'
6
+ export * from './components/FEAASElement.js'
4
7
  export * from './components/FEAASComponent.js'
5
8
  export * from './components/FEAASLoader.js'
6
9
  export * from './components/FEAASStylesheet.js'
@@ -8,7 +11,11 @@ export * from './components/FEAASStylesheet.js'
8
11
  declare global {
9
12
  interface Window {
10
13
  FEAASLoading: Promise<any>
14
+ FEAASElement: typeof FEAASElement
11
15
  }
12
16
  }
13
17
 
14
- if (typeof window != 'undefined') window.FEAASLoading ??= Promise.resolve()
18
+ if (typeof window != 'undefined') {
19
+ window.FEAASElement = FEAASElement
20
+ window.FEAASLoading ??= Promise.resolve()
21
+ }
package/src/cloud.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { fetchAndRevalidate } from './fetch.js'
1
+ import { fetchAndRevalidate, fetchAndValidate, invalidateRequest, refetchAndValidate } from './fetch.js'
2
2
 
3
3
  export interface CloudTemplate {
4
4
  cdn?: string
@@ -7,6 +7,7 @@ export interface CloudTemplate {
7
7
  component: string
8
8
  version: string
9
9
  revision?: string
10
+ instance?: string
10
11
  }
11
12
  export interface CloudStylesheet {
12
13
  cdn?: string
@@ -36,20 +37,49 @@ export function getStylesheetURL({
36
37
  }
37
38
 
38
39
  export async function fetchComponent(props: CloudTemplate, callback: (html: string) => void) {
39
- return fetchAndRevalidate(
40
- getComponentURL(props),
41
- {
42
- headers: {
43
- Accept: 'text/html'
40
+ const onlyIfNotDeleted = (response: Response) => {
41
+ return response.headers.get('x-ms-meta-versionDeletedAt') == null
42
+ }
43
+ const parseResponse = (response: Response) => {
44
+ response.text().then(callback)
45
+ }
46
+
47
+ // Fetch two requests in parallel: One optimistically checking if component was forked,
48
+ // and another checking original definition of a component
49
+ const results = await Promise.all([
50
+ props.instance &&
51
+ fetchAndValidate({
52
+ options: {
53
+ headers: {
54
+ Accept: 'text/html'
55
+ }
56
+ },
57
+ url: getComponentURL({ ...props, version: props.instance }),
58
+ validator: onlyIfNotDeleted
59
+ }).catch((e) => e as Error),
60
+ fetchAndValidate({ url: getComponentURL(props), validator: onlyIfNotDeleted }).catch((e) => e as Error)
61
+ ])
62
+ const result = results.find((result) => result && 'isCached' in result && result)
63
+ if (!result || result instanceof Error) {
64
+ const error = results.find((result) => result && result instanceof Error)
65
+ throw error || new Error('Can not fetch component')
66
+ }
67
+ if (results[0] == result) {
68
+ console.log('Using fork!', results)
69
+ }
70
+ parseResponse(result.response)
71
+ return refetchAndValidate({ ...result, callback: parseResponse }).catch((e) => {
72
+ if (results[0] == result) {
73
+ invalidateRequest({ ...result, callback: null, validator: null })
74
+ console.log('Fork was deleted')
75
+ // forked version was cached, but it's now deleted
76
+ if (!(results[1] instanceof Error)) {
77
+ parseResponse(results[1].response)
78
+ return refetchAndValidate({ ...results[1], callback: parseResponse })
44
79
  }
45
- },
46
- (response) => {
47
- return response.headers.get('x-ms-meta-versionDeletedAt') == null
48
- },
49
- (response) => {
50
- response.text().then(callback)
51
80
  }
52
- )
81
+ throw e
82
+ })
53
83
  }
54
84
 
55
85
  export async function fetchStylesheet(props: CloudStylesheet, callback: (css: string) => void) {
@@ -1,4 +1,5 @@
1
1
  import { JSXElementConstructor, ReactElement, ReactNode } from 'react'
2
+ import type ReactDOMClient from 'react-dom/client'
2
3
  import type ReactDOM from 'react-dom'
3
4
  import type React from 'react'
4
5
 
@@ -9,6 +10,7 @@ var WebComponent = typeof HTMLElement == 'undefined' ? (class {} as unknown as t
9
10
  interface ReactPayload {
10
11
  React: typeof React
11
12
  ReactDOM: typeof ReactDOM
13
+ ReactDOMClient: typeof ReactDOMClient
12
14
  }
13
15
 
14
16
  export class FEAASBase<Props = any, Payload = any> extends WebComponent {
@@ -27,7 +29,7 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
27
29
  nextRender: number
28
30
 
29
31
  /** Is/was component in the dom? */
30
- connected = null as Event
32
+ connected = false
31
33
 
32
34
  /** Result of lazy loading */
33
35
  payload: Payload
@@ -35,6 +37,12 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
35
37
  refs: Record<string, FEAASBase> = {}
36
38
  descendants: FEAASBase[] = []
37
39
 
40
+ constructor() {
41
+ super()
42
+ this.addEventListener('feaasConnected', this)
43
+ this.addEventListener('feaasDisconnected', this)
44
+ }
45
+
38
46
  /** Attaches shadow root if getShadowRootOptions any settings, or uses element itself */
39
47
  getRoot(): FEAASBase | HTMLElement | ShadowRoot {
40
48
  return this
@@ -47,38 +55,76 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
47
55
 
48
56
  /** Components parses its attributes when connected, and schedules rendeirng */
49
57
  connectedCallback() {
50
- console.log('FEAAS: Connected', this)
51
- this.connected = new Event('feaasConnected', { bubbles: true, composed: true })
52
- this.dispatchEvent(this.connected)
53
- this.addEventListener('feaasConnected', this)
54
- this.addEventListener('feaasDisconnected', this)
58
+ this.initialize()
59
+ }
60
+
61
+ /**
62
+ * Web components are initialized depth-first, making child elements render before its parents. This could here
63
+ * attempts to avoid this and inverts the initialization logic, so that children are initialized in the context of its
64
+ * already-initialized parents.
65
+ *
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
68
+ */
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())
55
78
  this.set()
79
+ return true
80
+ }
81
+
82
+ getParent(onlyFeaas = true) {
83
+ for (
84
+ var p: HTMLElement = this;
85
+ (p = p.parentElement || ((p.parentNode instanceof ShadowRoot ? p.parentNode.host : null) as HTMLElement));
86
+
87
+ ) {
88
+ if (!onlyFeaas || p.tagName.startsWith('FEAAS-')) return p
89
+ }
90
+ }
91
+
92
+ getElements(root: HTMLElement | ShadowRoot = this.getRoot(), onlyFeaas = true): HTMLElement[] {
93
+ return Array.from(root.querySelectorAll('*'))
94
+ .map(
95
+ (el) =>
96
+ [
97
+ !onlyFeaas || el.tagName.startsWith('FEAAS-') ? el : null,
98
+ el.shadowRoot ? this.getElements(el.shadowRoot, onlyFeaas) : null
99
+ ] as HTMLElement[]
100
+ )
101
+ .flat()
102
+ .filter(Boolean)
56
103
  }
57
104
 
58
105
  /** Nothing special happens on disconnect, except setting a flag */
59
106
  disconnectedCallback() {
107
+ if (!this.connected) return
60
108
  console.log('FEAAS: Disconnected', this)
61
- this.removeEventListener('feaasConnected', this)
62
- this.removeEventListener('feaasDisconnected', this)
63
109
  this.dispatchEvent(new Event('feaasDisconnected', { bubbles: true, composed: true }))
64
110
  this.connected = null
65
111
  }
66
112
 
67
113
  /** Catch-all event handler that dispatches events to callback */
68
114
  handleEvent(event: Event) {
115
+ const target = event.composedPath()[0] as FEAASBase
69
116
  switch (event.type) {
70
117
  case 'feaasConnected':
71
- this.nestedCallback(event)
118
+ if (target != this) this.nestedCallback(target)
72
119
  break
73
120
  case 'feaasDisconnected':
74
- this.unnestedCallback(event)
121
+ if (target != this) this.unnestedCallback(target)
75
122
  break
76
123
  }
77
124
  }
78
125
 
79
126
  /** Act on children element being removed */
80
- unnestedCallback(event: Event) {
81
- const target = event.composedPath()[0] as FEAASBase
127
+ unnestedCallback(target: FEAASBase) {
82
128
  const index = this.descendants.indexOf(target)
83
129
  if (index == -1) return
84
130
  console.log('FEAAS: Unnest', this, target)
@@ -91,8 +137,7 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
91
137
  }
92
138
 
93
139
  /** Act on children element being added */
94
- nestedCallback(event: Event): boolean | void {
95
- const target = event.composedPath()[0] as FEAASBase
140
+ nestedCallback(target: FEAASBase): boolean | void {
96
141
  const index = this.descendants.indexOf(target)
97
142
  if (index != -1) return
98
143
  this.descendants.push(target)
@@ -153,12 +198,12 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
153
198
  render(nested = false): any {
154
199
  console.log('FEAAS: React', this.tagName, this.payload)
155
200
  if (this.payload == null) return
156
- const { React, ReactDOM } = (this.payload as ReactPayload) || {}
201
+ const { React, ReactDOM, ReactDOMClient } = (this.payload as ReactPayload) || {}
157
202
  if (!React) throw new Error(`${this.tagName}: React is not present in payload`)
158
203
  if (!ReactDOM) throw new Error(`${this.tagName}: ReactDOM is not present in payload`)
159
-
160
204
  if (!this.refs.topReact && !this.forceUpdateReact) {
161
- ReactDOM.render(React.createElement(this.getReactWrapper()), this.getReactRoot())
205
+ this.reactRoot ||= ReactDOMClient.createRoot(this.getReactRootElement())
206
+ this.reactRoot.render(React.createElement(this.getReactWrapper(), { z: 'root' }))
162
207
  } else if (this.forceUpdateReact) {
163
208
  this.forceUpdateReact?.()
164
209
  } else {
@@ -182,13 +227,14 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
182
227
  }
183
228
 
184
229
  reactWrapper: (props?: any) => ReactNode
230
+ reactRoot: ReactDOMClient.Root
185
231
  getReactWrapper(target: FEAASBase = this, displayName = target.tagName) {
186
232
  const Wrapper = (target.reactWrapper ||= (props?: any) => {
187
233
  if (target.renderReact) {
188
234
  const { React, ReactDOM } = (target.payload as ReactPayload) || {}
189
235
  const [value, forceUpdateReact] = React.useReducer((r) => r + 1, 0)
190
236
  target.forceUpdateReact = forceUpdateReact
191
- return ReactDOM.createPortal(<>{target.invokeReactRendering()}</>, target.getReactRoot())
237
+ return ReactDOM.createPortal(<>{target.invokeReactRendering()}</>, target.getReactRootElement())
192
238
  } else {
193
239
  return target.render()
194
240
  }
@@ -214,7 +260,7 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
214
260
  payload: typeof this.payload
215
261
  ): ReactNode | ReactElement
216
262
 
217
- getReactRoot(): HTMLElement | ShadowRoot {
263
+ getReactRootElement(): HTMLElement | ShadowRoot {
218
264
  return this.getRoot()
219
265
  }
220
266
 
@@ -256,7 +302,14 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
256
302
  }
257
303
 
258
304
  getContextAttribute(name: string, element = this) {
259
- return element.closest(`[${name}]`)?.getAttribute(name) ?? this.getAttribute(name)
305
+ for (
306
+ var p: HTMLElement = this;
307
+ (p = p.parentElement || ((p.parentNode instanceof ShadowRoot ? p.parentNode.host : null) as HTMLElement));
308
+
309
+ ) {
310
+ if (p.getAttribute(name) != null) return p.getAttribute(name)
311
+ }
312
+ return this.getAttribute(name)
260
313
  }
261
314
 
262
315
  static register(tagName: string) {
@@ -1,6 +1,7 @@
1
1
  import { fetchComponent } from '../cloud.js'
2
2
  import { renderDOMContent, renderDOMElement } from '../dom.js'
3
3
  import { FEAASBase, OptionalExcept } from './FEAASBase.js'
4
+ import { FEAASLoader } from './FEAASLoader.js'
4
5
 
5
6
  export function FEAASComponentsProps(element: FEAASBase = this) {
6
7
  let data: any
@@ -19,12 +20,25 @@ export function FEAASComponentsProps(element: FEAASBase = this) {
19
20
  hostname: element.getAttribute('hostname'),
20
21
  cdn: element.getContextAttribute('cdn'),
21
22
  template: element.getAttribute('template'),
23
+ instance: element.getAttribute('instance'),
24
+ editable: element.getAttribute('editable') != null,
22
25
  suspended: element.getAttribute('suspended') != null
23
26
  }
24
27
  }
25
28
  export type FEAASComponentsProps = ReturnType<typeof FEAASComponentsProps>
26
29
  export class FEAASComponent extends FEAASBase<FEAASComponentsProps, string> {
27
- static observedAttributes = ['library', 'component', 'version', 'revision', 'hostname', 'cdn', 'template', 'data']
30
+ static observedAttributes = [
31
+ 'library',
32
+ 'component',
33
+ 'version',
34
+ 'revision',
35
+ 'hostname',
36
+ 'cdn',
37
+ 'template',
38
+ 'data',
39
+ 'instance',
40
+ 'editable'
41
+ ]
28
42
 
29
43
  needsRefresh: boolean
30
44
  setData(data: any) {
@@ -36,11 +50,33 @@ export class FEAASComponent extends FEAASBase<FEAASComponentsProps, string> {
36
50
  }
37
51
 
38
52
  attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
53
+ if (oldValue === newValue) return
39
54
  this.payload = undefined
40
55
  this.needsRefresh = true
41
56
  super.attributeChangedCallback(name, oldValue, newValue)
42
57
  }
43
58
 
59
+ getEditor(): any {
60
+ return (document.querySelector('feaas-context') as FEAASBase)?.refs.editor
61
+ }
62
+
63
+ constructor() {
64
+ super()
65
+ this.addEventListener('click', () => {
66
+ if (this.props.editable) {
67
+ this.edit()
68
+ }
69
+ })
70
+ }
71
+
72
+ edit() {
73
+ const editor = this.getEditor()
74
+ if (editor?.setTarget(this)) {
75
+ editor.open()
76
+ return true
77
+ }
78
+ }
79
+
44
80
  /* Check if component is a repeated clone, and return the original */
45
81
  getOriginal() {
46
82
  const scope = this.getAttribute('data-path-scope')
@@ -0,0 +1,21 @@
1
+ import { ReactNode, ReactElement, JSXElementConstructor } from 'react'
2
+ import { FEAASBase } from './FEAASBase.js'
3
+
4
+ export default class FEAASElement<P> extends FEAASBase<
5
+ P,
6
+ {
7
+ React: typeof React
8
+ ReactDOM: typeof ReactDOM
9
+ }
10
+ > {
11
+ load() {
12
+ return Promise.all([
13
+ // @ts-ignore
14
+ import('https://cdn.skypack.dev/react'),
15
+ // @ts-ignore
16
+ import('https://cdn.skypack.dev/react-dom')
17
+ ]).then(([React, ReactDOM]) => {
18
+ return { React, ReactDOM }
19
+ })
20
+ }
21
+ }
@@ -2,6 +2,7 @@ import { FEAASBase } from './FEAASBase.js'
2
2
 
3
3
  export function FEAASLoaderProps(element: HTMLElement = this) {
4
4
  return {
5
+ opacity: element?.getAttribute('opacity') ? parseFloat(element?.getAttribute('opacity')) : 1,
5
6
  hidden: element?.getAttribute('hidden') != null
6
7
  }
7
8
  }
@@ -11,9 +12,14 @@ export class FEAASLoader extends FEAASBase<FEAASLoaderProps, string> {
11
12
  stylesheet: HTMLStyleElement
12
13
  loader: HTMLDivElement
13
14
 
14
- static observedAttributes = ['hidden']
15
+ getProps() {
16
+ return FEAASLoaderProps(this)
17
+ }
18
+
19
+ static observedAttributes = ['hidden', 'opacity']
15
20
 
16
21
  attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
22
+ if (oldValue === newValue) return
17
23
  if (name == 'hidden') {
18
24
  this.style.setProperty('display', 'block', 'important')
19
25
  this.style.setProperty('transition', 'opacity 0.3s', 'important')
@@ -25,9 +31,12 @@ export class FEAASLoader extends FEAASBase<FEAASLoaderProps, string> {
25
31
  this.style.pointerEvents = 'all'
26
32
  }
27
33
  }
34
+ return super.attributeChangedCallback(name, oldValue, newValue)
28
35
  }
29
36
 
30
37
  connectedCallback() {
38
+ this.payload = null
39
+ super.connectedCallback()
31
40
  this.attributeChangedCallback('hidden', this.getAttribute('hidden'), this.getAttribute('hidden'))
32
41
  this.stylesheet = document.createElement('style')
33
42
  this.stylesheet.textContent = `
@@ -101,6 +110,10 @@ export class FEAASLoader extends FEAASBase<FEAASLoaderProps, string> {
101
110
  this.getRoot().appendChild(this.stylesheet)
102
111
  this.getRoot().appendChild(this.loader)
103
112
  }
113
+
114
+ render() {
115
+ this.loader.style.opacity = String(this.props.opacity)
116
+ }
104
117
  }
105
118
 
106
119
  declare global {
package/src/dom.ts CHANGED
@@ -310,6 +310,11 @@ export function renderDOMContent(root: HTMLElement, template: string, data?: Dat
310
310
  const newTemplate = config?.processTemplate?.(root, template, data) || template
311
311
  root.innerHTML = newTemplate
312
312
  root.classList.add('-feaas')
313
+ // fixme, jss needs to assign theme classes to component instead
314
+ const anchor = root.closest('.component.feaas') || root
315
+ if (anchor.className.includes('-theme--')) {
316
+ for (const child of root.children) child.className = child.className.replace(/(\s|^)-theme--[^\s]+\s*/g, '$1')
317
+ }
313
318
  return renderDOMElement(root, data, config)
314
319
  }
315
320
 
package/src/fetch.ts CHANGED
@@ -1,64 +1,95 @@
1
- /* Utility to cache-bust static files.
2
-
3
- It handles multiple situations:
4
-
5
- 1. Initial request, valid response
6
- 2. Initial request, invalid response
7
- 2. Initial request, missing response
8
- 3. Cached request, valid response (1 extra head request)
9
- 4. Cached request, invalid response, valid refetch (2 fetches + 1 head)
10
- 5. Cached request, invalid response, invalid refetch (2 fetches + 1 head)
11
-
12
- It works as promise, but also accepts callback to use cached blobs mid-flight and have access to headers.
13
- Any network errors will not caught.
14
- Function throws when unable to fetch valid response.
1
+ /**
2
+ * Utility to cache-bust static files. It relies on reading Date header to detect cached response, so it requires CORS
3
+ * settings to be set.
4
+ *
5
+ * It accepts validator function that determines if response is usable. If response was pulled from cache and is valid,
6
+ * the optional callback function is fired right away. Then HEAD request is made bypassing the cache, to see if the
7
+ * response has an newer Last-Modified header. In case it does and it passes validation, third request is fired that
8
+ * actually fetches the new content and callback is invoked the second time.
9
+ *
10
+ * It works as promise, but also accepts callback to use cached blobs mid-flight and have access to headers. Any network
11
+ * errors will not be caught. Function throws when unable to fetch valid response.
12
+ */
13
+ export interface ValidatableRequest {
14
+ url: Parameters<typeof fetch>[0]
15
+ options?: Parameters<typeof fetch>[1]
16
+ validator?: (resposne: Response) => boolean
17
+ callback?: (response: Response, status: 'initial' | 'updated' | 'check') => void
18
+ isCached?: boolean
19
+ response?: Response
20
+ }
15
21
 
16
- */
17
22
  export async function fetchAndRevalidate(
18
- url: Parameters<typeof fetch>[0],
19
- options: Parameters<typeof fetch>[1],
20
- validator: (resposne: Response) => boolean,
21
- callback: (response: Response, status: 'cached' | 'valid') => void
23
+ url: ValidatableRequest['url'],
24
+ options?: ValidatableRequest['options'],
25
+ validator?: ValidatableRequest['validator'],
26
+ callback?: ValidatableRequest['callback']
22
27
  ) {
23
28
  // make request as usual, see if there's cached version of an asset
24
- const date = new Date()
25
- const initialResponse = await fetch(url, options)
26
- if (!initialResponse.ok) throw new Error('Fetch failed')
27
- const responseDate = new Date(initialResponse.headers.get('date'))
28
- var response = initialResponse
29
- callback(initialResponse, 'cached')
29
+ return fetchAndValidate({ url, options, validator, callback })
30
+ .then(refetchAndValidate)
31
+ .then((result) => result.response)
32
+ }
30
33
 
34
+ export async function fetchAndValidate(
35
+ { url, options, validator, callback }: ValidatableRequest,
36
+ phase: 'initial' | 'updated' | 'check' = 'initial'
37
+ ) {
38
+ const date = new Date()
39
+ console.log('fetch', phase, url)
40
+ const response = await fetch(url, options)
41
+ if (!response.ok) throw new Error('Fetch failed')
42
+ if (validator?.(response) === false) throw new Error('Response is not valid')
31
43
  // see if response was pulled from cache
32
- if (responseDate < date) {
33
- // check if response was stale by sending quick HEAD reque st busting cache
34
- const freshHeadResponse = await fetch(url + '?' + Math.random(), {
35
- ...options,
36
- method: 'HEAD',
37
- cache: 'no-cache'
38
- })
44
+ const isCached = new Date(response.headers.get('date')) < date
45
+ callback?.(response, phase)
46
+ return {
47
+ url,
48
+ options,
49
+ isCached,
50
+ response,
51
+ validator
52
+ }
53
+ }
39
54
 
40
- // there's a newer version of the file that we may fetch
41
- if (freshHeadResponse.headers.get('last-modified') != initialResponse.headers.get('last-modified')) {
42
- // check if re-fetching is even necessary
43
- if (validator(freshHeadResponse) === false) {
44
- //callback('stale', freshHeadResponse)
45
- throw new Error('Response is stale')
46
- }
55
+ export async function refetchAndValidate(request: ValidatableRequest & Required<Pick<ValidatableRequest, 'response'>>) {
56
+ const { url, isCached, options, validator, response, callback } = request
57
+ if (isCached) {
58
+ // check if response was stale by sending quick HEAD request busting cache
59
+ const { response: head } = await fetchAndValidate(
60
+ {
61
+ url: url + '?' + Math.random(),
62
+ options: {
63
+ ...options,
64
+ method: 'HEAD',
65
+ cache: 'no-cache'
66
+ },
67
+ validator
68
+ },
69
+ 'check'
70
+ )
47
71
 
72
+ // there's a newer version of the file that we may fetch
73
+ if (head.headers.get('last-modified') != response.headers.get('last-modified')) {
48
74
  // otherwise the new resource has to be re-fetched
49
- response = await fetch(url, {
50
- ...options,
51
- cache: 'reload'
52
- })
75
+ return invalidateRequest(request)
53
76
  }
54
77
  }
78
+ return request
79
+ }
55
80
 
56
- if (validator(response) === false) {
57
- throw new Error('Response is not valid')
58
- }
59
- if (initialResponse !== response) {
60
- callback(response, 'valid')
61
- }
62
-
63
- return response
81
+ export async function invalidateRequest(request: ValidatableRequest) {
82
+ const { url, options, validator, callback } = request
83
+ return fetchAndValidate(
84
+ {
85
+ url,
86
+ options: {
87
+ ...options,
88
+ cache: 'reload'
89
+ },
90
+ validator,
91
+ callback
92
+ },
93
+ 'updated'
94
+ )
64
95
  }