@sitecore-feaas/clientside 0.2.1 → 0.3.0

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 (113) hide show
  1. package/README.md +7 -7
  2. package/build/components/FEAASComponent.js +72 -10
  3. package/build/components/FEAASComponent.js.map +1 -1
  4. package/build/components/FEAASElement.js +19 -5
  5. package/build/components/FEAASElement.js.map +1 -1
  6. package/build/dom/rendering.js.map +1 -1
  7. package/build/dom/thumbnail.js +29 -24
  8. package/build/dom/thumbnail.js.map +1 -1
  9. package/build/headless.js +5 -0
  10. package/build/headless.js.map +1 -1
  11. package/build/ui/FEAAS.js +5 -0
  12. package/build/ui/FEAAS.js.map +1 -0
  13. package/build/ui/FEAASContext.js +9 -6
  14. package/build/ui/FEAASContext.js.map +1 -1
  15. package/build/ui/FEAASExternal.js +68 -4
  16. package/build/ui/FEAASExternal.js.map +1 -1
  17. package/build/ui/FEAASPicker.js +15 -5
  18. package/build/ui/FEAASPicker.js.map +1 -1
  19. package/build/ui/index.js +3 -4
  20. package/build/ui/index.js.map +1 -1
  21. package/build/ui/react.js +23 -8
  22. package/build/ui/react.js.map +1 -1
  23. package/build/utils/datapath.js +15 -0
  24. package/build/utils/datapath.js.map +1 -1
  25. package/build/utils/fetch.js +40 -0
  26. package/build/utils/fetch.js.map +1 -0
  27. package/build/utils/settings.js +60 -0
  28. package/build/utils/settings.js.map +1 -0
  29. package/dist/browser/headless.cjs +8 -6
  30. package/dist/browser/headless.cjs.map +4 -4
  31. package/dist/browser/headless.esm.js +8 -6
  32. package/dist/browser/headless.esm.js.map +1 -1
  33. package/dist/browser/index.cjs +8 -6
  34. package/dist/browser/index.cjs.map +4 -4
  35. package/dist/browser/index.esm.js +8 -6
  36. package/dist/browser/index.esm.js.map +1 -1
  37. package/dist/browser/react.cjs +8 -6
  38. package/dist/browser/react.cjs.map +4 -4
  39. package/dist/browser/react.esm.js +8 -6
  40. package/dist/browser/react.esm.js.map +1 -1
  41. package/dist/node/headless.cjs +6 -4
  42. package/dist/node/headless.cjs.map +4 -4
  43. package/dist/node/headless.esm.js +6 -4
  44. package/dist/node/headless.esm.js.map +4 -4
  45. package/dist/node/index.cjs +7 -5
  46. package/dist/node/index.cjs.map +4 -4
  47. package/dist/node/index.esm.js +7 -5
  48. package/dist/node/index.esm.js.map +4 -4
  49. package/dist/node/react.cjs +6 -4
  50. package/dist/node/react.cjs.map +4 -4
  51. package/dist/node/react.esm.js +6 -4
  52. package/dist/node/react.esm.js.map +4 -4
  53. package/headless.cjs +6 -4
  54. package/headless.cjs.map +4 -4
  55. package/headless.js +6 -4
  56. package/headless.js.map +4 -4
  57. package/package.json +2 -2
  58. package/react.cjs +8 -6
  59. package/react.cjs.map +4 -4
  60. package/react.js +8 -6
  61. package/react.js.map +4 -4
  62. package/scripts/build.js +1 -1
  63. package/src/components/FEAASComponent.tsx +101 -10
  64. package/src/components/FEAASElement.tsx +30 -5
  65. package/src/dom/rendering.ts +1 -0
  66. package/src/dom/thumbnail.ts +29 -24
  67. package/src/headless.ts +5 -0
  68. package/src/ui/FEAAS.tsx +4 -0
  69. package/src/ui/FEAASContext.tsx +10 -1
  70. package/src/ui/FEAASExternal.tsx +75 -16
  71. package/src/ui/FEAASPicker.tsx +19 -7
  72. package/src/ui/index.ts +4 -4
  73. package/src/ui/react.tsx +48 -14
  74. package/src/utils/datapath.ts +17 -0
  75. package/src/utils/fetch.ts +60 -0
  76. package/src/utils/settings.ts +69 -0
  77. package/test/FEAASComponent.react.test.tsx +1 -1
  78. package/test/FEAASComponent.test.tsx +1 -0
  79. package/test/FEAASComponentData.test.tsx +215 -0
  80. package/test/FEAASContext.test.tsx +3 -3
  81. package/test/FEAASExternal.test.tsx +1 -8
  82. package/test/ServerComponent.test.tsx +33 -0
  83. package/test/fetch-data.ts +55 -0
  84. package/test/settings.test.ts +78 -0
  85. package/types/components/FEAASComponent.d.ts +33 -1
  86. package/types/components/FEAASComponent.d.ts.map +1 -1
  87. package/types/components/FEAASElement.d.ts +10 -1
  88. package/types/components/FEAASElement.d.ts.map +1 -1
  89. package/types/dom/rendering.d.ts.map +1 -1
  90. package/types/dom/thumbnail.d.ts +16 -16
  91. package/types/dom/thumbnail.d.ts.map +1 -1
  92. package/types/headless.d.ts +5 -0
  93. package/types/headless.d.ts.map +1 -1
  94. package/types/tsbuildinfo +1 -1
  95. package/types/ui/FEAAS.d.ts +5 -0
  96. package/types/ui/FEAAS.d.ts.map +1 -0
  97. package/types/ui/FEAASContext.d.ts +8 -0
  98. package/types/ui/FEAASContext.d.ts.map +1 -1
  99. package/types/ui/FEAASExternal.d.ts +56 -3
  100. package/types/ui/FEAASExternal.d.ts.map +1 -1
  101. package/types/ui/FEAASPicker.d.ts +10 -6
  102. package/types/ui/FEAASPicker.d.ts.map +1 -1
  103. package/types/ui/index.d.ts +1 -4
  104. package/types/ui/index.d.ts.map +1 -1
  105. package/types/ui/react.d.ts +7 -2
  106. package/types/ui/react.d.ts.map +1 -1
  107. package/types/ui/tsbuildinfo +1 -1
  108. package/types/utils/datapath.d.ts +2 -0
  109. package/types/utils/datapath.d.ts.map +1 -1
  110. package/types/utils/fetch.d.ts +9 -0
  111. package/types/utils/fetch.d.ts.map +1 -0
  112. package/types/utils/settings.d.ts +13 -0
  113. package/types/utils/settings.d.ts.map +1 -0
package/scripts/build.js CHANGED
@@ -3,6 +3,7 @@ import * as esbuild from 'esbuild'
3
3
  import replace from 'esbuild-plugin-replace-regex'
4
4
  import * as url from 'url'
5
5
  import fs from 'fs'
6
+
6
7
  const __filename = url.fileURLToPath(import.meta.url)
7
8
  const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
8
9
 
@@ -17,7 +18,6 @@ function compile({ input, output, ...step }) {
17
18
  bundle: true,
18
19
  write: false,
19
20
  external: ['node:crypto', 'node-fetch', '_MAGIC_IMPORT_COMMENTS_ESBUILD_WORKAROUND__', 'react', 'react-dom', 'vm'],
20
-
21
21
  ...step,
22
22
 
23
23
  minify: true,
@@ -1,6 +1,8 @@
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 { ControlledPromise } from '../utils/promise.js'
5
+ import { fetchData } from '../utils/fetch.js'
4
6
  import { FEAASElement } from './FEAASElement.js'
5
7
 
6
8
  export function FEAASComponentsProps(element: FEAASElement) {
@@ -11,12 +13,17 @@ export function FEAASComponentsProps(element: FEAASElement) {
11
13
  if (!parsed) throw new Error(`Could not parse FEAAS Component source: ${src}`)
12
14
  var { cdn, library, component, version, revision } = parsed
13
15
  }
16
+
14
17
  const dataValue = element.getAttribute('data')
18
+
15
19
  if (dataValue != null && dataValue != '') {
16
20
  try {
17
21
  data = typeof dataValue == 'string' ? JSON.parse(dataValue) : {}
18
- } catch (e) {}
22
+ } catch (e) {
23
+ console.error(e)
24
+ }
19
25
  }
26
+
20
27
  return {
21
28
  data: data || {},
22
29
  cdn: element.getContextAttribute('cdn') ?? cdn ?? DEFAULT_CDN_URL,
@@ -33,8 +40,40 @@ export function FEAASComponentsProps(element: FEAASElement) {
33
40
  withStylesheet: element.getAttribute('with-stylesheet') != 'false'
34
41
  }
35
42
  }
43
+
36
44
  export type FEAASComponentsProps = ReturnType<typeof FEAASComponentsProps>
45
+
46
+ /**
47
+ * - Initialization lifecycle:
48
+ * - - `set(props)`
49
+ * - - `scheduleRender()` - any attribute change causes render to schedule to next frame
50
+ * - - `whenRendered.restart()` - rendering promise is reset and will resolve after render
51
+ * - - `advance()` - called on next frame
52
+ * - - `if (isReadyToLoad()) => boolean` - check if component has all necessary properties to load
53
+ * - - `load() => payload` => component is loaded
54
+ * - - `onLoad?(payload)` => invoke loaded callback for cached version of a component
55
+ * - - `advance()` => going further in state machine
56
+ * - - `flush()` => flushes all react changes if needed
57
+ * - - `update()` => component-specific method (or react) to render changes to dom
58
+ * - - `render(payload, props)` => render means react
59
+ * - - `whenRendered.resolve()` - rendering promise is resolved after rendering
60
+ * - - IF CACHE WAS STALE - FETCH AND RENDER AGAIN
61
+ * - - `fetchByPassingCache()` - load up-to-date version of a component
62
+ * - - `onLoad(newPayload) `
63
+ * - - `advance()` => going further in state machine
64
+ * - - `flush()` => flushes all react changes if needed
65
+ * - - `update()` => component-specific method (or react) to render changes to dom
66
+ * - - `render(payload, props)` => render means react
67
+ * - - `whenRendered.resolve()` - rendering promise is resolved after rendering
68
+ * - - `whenLoaded.resolved()` - resolved after loading of up to date component
69
+ */
37
70
  export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
71
+ readyData: any = {}
72
+ whenDataReady = ControlledPromise<any>(
73
+ (data) => (this.readyData = data),
74
+ (e) => this.onError(e)
75
+ )
76
+
38
77
  static observedAttributes = [
39
78
  'library',
40
79
  'component',
@@ -59,6 +98,7 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
59
98
  } as Partial<FEAASComponentsProps>
60
99
 
61
100
  needsRefresh: boolean
101
+
62
102
  setData(data: any) {
63
103
  this.set({ data })
64
104
  }
@@ -69,12 +109,35 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
69
109
 
70
110
  attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
71
111
  if (oldValue === newValue) return
112
+
72
113
  // force component to be refreshed
73
114
  if (name == 'library' || name == 'component' || name == 'revision' || name == 'version') {
74
115
  this.payload = undefined
75
116
  }
117
+
76
118
  this.needsRefresh = true
119
+
77
120
  super.attributeChangedCallback(name, oldValue, newValue)
121
+
122
+ if (name === 'data') {
123
+ this.fetchData()
124
+ }
125
+ }
126
+
127
+ // Reset readyData and perform a new fetch.
128
+ async fetchData() {
129
+ this.readyData = undefined
130
+
131
+ this.whenDataReady = this.whenDataReady.restart()
132
+
133
+ const data = await fetchData(this.props.data)
134
+
135
+ const alreadyRendered = this.alreadyRendered
136
+
137
+ await this.whenDataReady.resolve(data)
138
+ if (alreadyRendered) {
139
+ this.advance()
140
+ }
78
141
  }
79
142
 
80
143
  getEditor(): any {
@@ -83,6 +146,11 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
83
146
 
84
147
  constructor() {
85
148
  super()
149
+
150
+ if (!this.getAttribute('data')) {
151
+ this.whenDataReady.resolve({})
152
+ }
153
+
86
154
  this.addEventListener('click', () => {
87
155
  if (this.props.editable) {
88
156
  this.edit()
@@ -125,36 +193,57 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
125
193
 
126
194
  async load() {
127
195
  // defer loading to original element
128
- if (this.getOriginal()) {
129
- return
196
+ if (this.getOriginal()) return
197
+
198
+ const advanceIfEverythingIsReady = () => {
199
+ // html is not loaded
200
+ if (!this.payload) return
201
+
202
+ // shared stylesheet is still being loaded
203
+ if (stylesheetPromise) return
204
+
205
+ // data is still loading
206
+ if (!this.readyData) return
207
+
208
+ this.onLoad(this.payload)
130
209
  }
210
+
131
211
  if (this.props.withStylesheet) {
132
212
  var stylesheetPromise = loadStylesheetAllowStale({ ...this.props, revision: 'published' }).then(() => {
133
213
  stylesheetPromise = null
134
- if (this.payload) {
135
- this.onLoad(this.payload)
136
- }
214
+ advanceIfEverythingIsReady()
137
215
  })
138
216
  }
217
+
139
218
  return Promise.all([
140
219
  stylesheetPromise,
220
+ this.whenDataReady.then(() => advanceIfEverythingIsReady()),
141
221
  fetchAndRevalidateComponent(this.props, (html) => {
142
222
  this.payload = html
143
- if (!stylesheetPromise) this.onLoad(html)
223
+ advanceIfEverythingIsReady()
144
224
  })
145
225
  ]).then(() => this.payload)
146
226
  }
147
227
 
228
+ alreadyRendered: boolean
229
+
230
+ flush() {
231
+ if (!this.readyData) return
232
+
233
+ super.flush()
234
+ }
235
+
148
236
  update() {
237
+ this.alreadyRendered = true
149
238
  if (Array.from(this.children).filter((c) => c.tagName != 'BR').length > 0 && !this.needsRefresh) {
150
- renderDOMElement(this, this.props.data)
239
+ renderDOMElement(this, this.readyData)
151
240
  } else {
152
241
  this.needsRefresh = true
153
242
  // defer rendering to original element
154
243
  if (this.getOriginal()) {
155
244
  return
156
245
  }
157
- renderDOMContent(this, this.payload, this.props.data)
246
+ renderDOMContent(this, this.payload, this.readyData)
158
247
  }
159
248
  }
160
249
  }
@@ -173,7 +262,9 @@ declare global {
173
262
  'with-stylesheet'?: 'false' | 'true' | ''
174
263
  } & (
175
264
  | {
176
- /** @deprecated Use cdn instead */
265
+ /**
266
+ * @deprecated Use cdn instead.
267
+ */
177
268
  hostname?: string
178
269
  cdn?: string
179
270
  library: string
@@ -21,6 +21,16 @@ export interface ReactPayload {
21
21
  ReactDOM: typeof ReactDOM
22
22
  ReactDOMClient: typeof ReactDOMClient
23
23
  }
24
+
25
+ declare global {
26
+ interface DocumentEventMap {
27
+ feaasMount: Event
28
+ feaasUnmount: Event
29
+ feaasLoad: Event
30
+ feaasError: CustomEvent<Error>
31
+ }
32
+ }
33
+
24
34
  /**
25
35
  * FEAASElement is a superclass for FEAAS components with optional react support.
26
36
  *
@@ -108,7 +118,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
108
118
  if (this.connected) return
109
119
  this.connected = true
110
120
  this.log('FEAAS: Mount', this.tagName, this)
111
- this.dispatchEvent(new Event('feaasMount', { bubbles: true, composed: true }))
121
+ this.dispatchEventIndirectly(new Event('feaasMount', { bubbles: true, composed: true }))
112
122
  this.getElements().map((element: FEAASElement) => element.mount?.())
113
123
  this.set()
114
124
  } catch (e) {
@@ -116,6 +126,17 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
116
126
  }
117
127
  }
118
128
 
129
+ dispatchEventIndirectly(event: Event) {
130
+ this.dispatchEvent(event)
131
+ const parent = this.getParent() as FEAASElement
132
+
133
+ if (parent && !parent.contains(this)) {
134
+ Object.defineProperty(event, 'target', { value: this, writable: false })
135
+ Object.defineProperty(event, 'composedPath', { value: () => [this], writable: false })
136
+ parent.handleEvent(event)
137
+ }
138
+ }
139
+
119
140
  log(...args: any) {
120
141
  if (typeof process != 'undefined' && process.env.TESTING) return
121
142
  console.log(...args)
@@ -125,7 +146,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
125
146
  unmount() {
126
147
  if (!this.connected) return
127
148
  this.log('FEAAS: Unmount', this)
128
- this.dispatchEvent(new Event('feaasUnmount', { bubbles: true, composed: true }))
149
+ this.dispatchEventIndirectly(new Event('feaasUnmount', { bubbles: true, composed: true }))
129
150
  this.connected = false
130
151
  }
131
152
 
@@ -137,6 +158,8 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
137
158
  ) {
138
159
  if (!onlyFeaas || p.tagName.startsWith('FEAAS-')) return p
139
160
  }
161
+ // feaas context anywhere in the dom acts as a root element
162
+ if (this.tagName != 'FEAAS-CONTEXT') return document.querySelector('feaas-context')
140
163
  }
141
164
 
142
165
  getElements(root: HTMLElement | ShadowRoot = this.getRoot(), onlyFeaas = true): HTMLElement[] {
@@ -170,7 +193,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
170
193
  const index = this.descendants.indexOf(target)
171
194
  if (index == -1) return
172
195
  this.log('FEAAS: Unnest', this, target)
173
- this.descendants.splice(index, -1)
196
+ this.descendants.splice(index, 1)
174
197
  this.refs[target.getComponentName()] = null
175
198
  if (target.refs.parent == this) target.refs.parent = null
176
199
  if (target.refs.top == this) {
@@ -212,7 +235,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
212
235
  /** Generic error handler */
213
236
  onError(error: Error, where: string = 'uncaught', handled = false) {
214
237
  const event = new CustomEvent('feaasError', { bubbles: true, composed: true, detail: error })
215
- this.dispatchEvent(event)
238
+ this.dispatchEventIndirectly(event)
216
239
  this.whenLoaded.reject(error)
217
240
  if (!event.defaultPrevented && !handled) {
218
241
  this.whenRendered.reject(error)
@@ -236,7 +259,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
236
259
  if (this.payload === undefined) {
237
260
  if (!this.isReadyToLoad()) return
238
261
  this.payload = null
239
- this.dispatchEvent(new Event('feaasLoad', { bubbles: true, composed: true }))
262
+ this.dispatchEventIndirectly(new Event('feaasLoad', { bubbles: true, composed: true }))
240
263
  const loaded = await this.load().catch((e) => {
241
264
  this.payload = undefined
242
265
  throw e
@@ -251,6 +274,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
251
274
  } else if (this.payload == null) {
252
275
  return
253
276
  }
277
+
254
278
  this.flush()
255
279
  this.whenRendered.resolve(this)
256
280
  } catch (e) {
@@ -259,6 +283,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
259
283
  }
260
284
 
261
285
  flush() {
286
+ if (!this.connected) return
262
287
  const flushSync = (this.payload as ReactPayload)?.ReactDOM?.flushSync
263
288
  if (flushSync) {
264
289
  flushSync(() => this.update())
@@ -192,6 +192,7 @@ export function renderDOMElement(
192
192
  }
193
193
  return element
194
194
  }
195
+
195
196
  function isElement(node: Node): node is HTMLElement {
196
197
  return node.nodeType == 1
197
198
  }
@@ -25,9 +25,9 @@ export const responseToImage = (response: Response): Promise<HTMLImageElement> =
25
25
  * The function ensures that the version data for the component is available and selects the one that is most suitable
26
26
  * for a display width of 800 pixels.
27
27
  *
28
- * @param {ComponentModel} component - The component for which the version needs to be obtained.
28
+ * @param {ComponentModel} component - The component for which the version needs to be obtained.
29
29
  * @returns {Promise<VersionModel>} - A promise that resolves to the most suitable version for a display width of 800
30
- * pixels.
30
+ * pixels.
31
31
  */
32
32
  export async function getVersionForThumbnail(component: ComponentModel): Promise<VersionModel> {
33
33
  if (!component.versions.length) await component.versions.fetch()
@@ -45,9 +45,9 @@ export async function getVersionForThumbnail(component: ComponentModel): Promise
45
45
  * 1. The first call can happen with the stale cached or outdated image.
46
46
  * 2. The second call occurs with the up-to-date loaded or freshly generated generated image.
47
47
  *
48
- * @param {ComponentModel} component - Relevant component.
49
- * @param {VersionModel} [version] - Relevant version (optional, falls back to responsive).
50
- * @param {Callback} [callback] - Callback to handle the loaded or generated image (optional).
48
+ * @param {ComponentModel} component - Relevant component.
49
+ * @param {VersionModel} [version] - Relevant version (optional, falls back to responsive).
50
+ * @param {Callback} [callback] - Callback to handle the loaded or generated image (optional).
51
51
  * @returns {Promise<HTMLImageElement | null>} - The final thumbnail image (either loaded or freshly generated).
52
52
  */
53
53
  export async function get(component: ComponentModel, version?: VersionModel, callback?: Callback) {
@@ -83,16 +83,16 @@ export async function get(component: ComponentModel, version?: VersionModel, cal
83
83
  *
84
84
  * 1. The first call occurs if the image was previously cached by the browser, but the cache is stale.
85
85
  * 2. The second call happens when the image is updated from a fresh download. By this design, the user gets to see a
86
- * cached image immediately while the updated image is being downloaded in the background.
86
+ * cached image immediately while the updated image is being downloaded in the background.
87
87
  *
88
88
  * If the version of a component is known to be newer than remote file, the function will throw `InvalidResponse` error.
89
89
  * As a way to handle that error, the `generate` function may be used to produce new thumbnail (@see render).
90
90
  *
91
- * @param {ComponentModel} component - The component to load a thumbnail for.
92
- * @param {VersionModel} [version] - The version of the component to load a thumbnail for. If not provided, a version
93
- * suitable for a responsive component is selected.
94
- * @param {Callback} [callback] - A callback to handle the loaded image. This callback may be invoked twice for the
95
- * reasons described above.
91
+ * @param {ComponentModel} component - The component to load a thumbnail for.
92
+ * @param {VersionModel} [version] - The version of the component to load a thumbnail for. If not provided, a
93
+ * version suitable for a responsive component is selected.
94
+ * @param {Callback} [callback] - A callback to handle the loaded image. This callback may be invoked twice for
95
+ * the reasons described above.
96
96
  * @returns {Promise<HTMLImageElement>} - The thumbnail image for the component/version combination.
97
97
  */
98
98
  export async function load(
@@ -143,8 +143,8 @@ export async function load(
143
143
  * thumbnail. It then leverages `render` to generate an image with provided parameters and finally, if the component
144
144
  * isn't new, uploads the generated as a Blob onto cdn for future retrieval.
145
145
  *
146
- * @param {ComponentModel} component - Component instance.
147
- * @param {VersionModel} [version] - Version instance.
146
+ * @param {ComponentModel} component - Component instance.
147
+ * @param {VersionModel} [version] - Version instance.
148
148
  * @returns {Promise<HTMLImageElement | null>} Thumbnail image or null if generation isn't possible.
149
149
  */
150
150
  export async function generate(component: ComponentModel, version?: VersionModel) {
@@ -174,15 +174,15 @@ export async function generate(component: ComponentModel, version?: VersionModel
174
174
  * injected into the iframe to capture the rendered component as an image. The image is created respecting the provided
175
175
  * design breakpoint, ensuring an accurate visual representation.
176
176
  *
177
- * @param {string[]} classList - Array of class names applied to the rendered component.
178
- * @param {Style.Rule<'breakpoint'>} breakpoint - Breakpoint style rule that dictates the viewport dimensions for
179
- * rendering.
180
- * @param {string} template - String template of the component to render.
181
- * @param {any} data - Data object to be used when rendering the component.
182
- * @param {string} cssText - CSS rules to be applied during component rendering.
183
- * @param {EnvironmentVariables} env - Environment variables used to set frontend and backend endpoint URLs.
177
+ * @param {string[]} classList - Array of class names applied to the rendered component.
178
+ * @param {Style.Rule<'breakpoint'>} breakpoint - Breakpoint style rule that dictates the viewport dimensions for
179
+ * rendering.
180
+ * @param {string} template - String template of the component to render.
181
+ * @param {any} data - Data object to be used when rendering the component.
182
+ * @param {string} cssText - CSS rules to be applied during component rendering.
183
+ * @param {EnvironmentVariables} env - Environment variables used to set frontend and backend endpoint URLs.
184
184
  * @returns {Promise<BasicImage>} - Promise that resolves to an object containing the src, width, and height of the
185
- * created image.
185
+ * created image.
186
186
  */
187
187
  function render(
188
188
  classList: string[],
@@ -239,8 +239,13 @@ function render(
239
239
  ${cssText}
240
240
  body, html { padding: 0; margin: 0; overflow: hidden }
241
241
  `
242
+ const stylesheet2 = iframe.contentWindow.document.createElement('style')
243
+ stylesheet2.textContent = `
244
+ body, html { padding: 0; margin: 0; overflow: hidden }
245
+ `
242
246
 
243
247
  iframe.contentWindow.document.head.appendChild(stylesheet)
248
+ iframe.contentWindow.document.head.appendChild(stylesheet2)
244
249
  const root = renderComponent({ template, data })
245
250
  root.classList.add(...classList, '-feaas--preview', '-emulate--' + breakpoint.details.slug)
246
251
  iframe.contentWindow.document.body.appendChild(root)
@@ -254,9 +259,9 @@ function render(
254
259
  * several metadata points, including identifiers and timestamps. Upon successful upload, it sends a no-cache fetch
255
260
  * request to the constructed URL to bust the cache.
256
261
  *
257
- * @param {ComponentModel} component - Component instance.
258
- * @param {VersionModel} version - Version instance.
259
- * @param {Blob} blob - Blob instance to upload.
262
+ * @param {ComponentModel} component - Component instance.
263
+ * @param {VersionModel} version - Version instance.
264
+ * @param {Blob} blob - Blob instance to upload.
260
265
  * @returns {Promise<string>} URL of the uploaded Blob.
261
266
  */
262
267
  export async function upload(component: ComponentModel, version: VersionModel, blob: Blob): Promise<string> {
package/src/headless.ts CHANGED
@@ -19,6 +19,11 @@ export * from './components/FEAASComponent.js'
19
19
  export * from './components/FEAASLoader.js'
20
20
  export * from './components/FEAASStylesheet.js'
21
21
  export * from './utils/datapath.js'
22
+ export * as DOM from './dom/rendering.js'
23
+ export * as Datascope from './dom/data.js'
24
+ export * as Datapath from './utils/datapath.js'
25
+ export * from './utils/fetch.js'
26
+ export * as DataSettings from './utils/settings.js'
22
27
  export * as Thumbnail from './dom/thumbnail.js'
23
28
 
24
29
  declare global {
@@ -0,0 +1,4 @@
1
+ export * from '../headless.js'
2
+ export * from './FEAASEditor.js'
3
+ export * from './FEAASPicker.js'
4
+ export * from './FEAASContext.js'
@@ -2,11 +2,14 @@ import type * as FEAASContextImports from '@sitecore-feaas/frontend/context'
2
2
  import type { AuthModel } from '@sitecore-feaas/sdk'
3
3
  import { FEAASElement, OptionalExcept } from '../components/FEAASElement.js'
4
4
  import { ControlledPromise } from '../utils/promise.js'
5
+ import type * as FEAAS from './index.js'
5
6
 
6
7
  export function FEAASContextProps(element: HTMLElement) {
7
8
  return {
8
9
  library: element?.getAttribute('library') as string,
9
10
  src: element?.getAttribute('src') ?? '/context.js',
11
+ // override environment
12
+ environment: element?.getAttribute('environment') ?? undefined,
10
13
  frontend: element?.getAttribute('frontend') ?? 'https://components.sitecorecloud.io',
11
14
  backend: element?.getAttribute('backend') ?? 'https://components.sitecorecloud.io/api',
12
15
  cdn: element?.getAttribute('cdn') ?? 'https://feaas.blob.core.windows.net',
@@ -60,6 +63,7 @@ export class FEAASContext extends FEAASElement<FEAASContextProps, typeof FEAASCo
60
63
 
61
64
  /** Pull in children FEAAS react elements into the shadow DOM so they receieve the styles */
62
65
  connectedCallback() {
66
+ this.setAttribute('feaas-role', 'ui')
63
67
  //if (this.props.isolate)
64
68
  Array.from(this.children).forEach((child) => {
65
69
  this.getRoot().appendChild(child)
@@ -75,6 +79,7 @@ export class FEAASContext extends FEAASElement<FEAASContextProps, typeof FEAASCo
75
79
  }
76
80
 
77
81
  sdk: FEAASContextImports.SDK
82
+ clientside: typeof FEAAS
78
83
  onStatusChange: FEAASContextImports.SDKStatusCallback = (status, sdk) => {
79
84
  if (status == 'ready') {
80
85
  this.whenSDKReady.resolve(sdk)
@@ -99,6 +104,7 @@ export class FEAASContext extends FEAASElement<FEAASContextProps, typeof FEAASCo
99
104
  }
100
105
  return (
101
106
  <AppContext
107
+ environment={this.props.environment as any}
102
108
  backend={this.props.backend}
103
109
  libraryId={this.props.library}
104
110
  container={this.getRoot()}
@@ -109,7 +115,7 @@ export class FEAASContext extends FEAASElement<FEAASContextProps, typeof FEAASCo
109
115
  portalProps
110
116
  }}
111
117
  chakraProps={{
112
- resetScope: '.feaas-reset',
118
+ resetScope: ':where([feaas-role="ui"])',
113
119
  cssVarsRoot: ':host',
114
120
  resetCSS: true,
115
121
  toastOptions: {
@@ -129,6 +135,9 @@ declare global {
129
135
  'feaas-context': OptionalExcept<FEAASContextProps, 'library'>
130
136
  }
131
137
  }
138
+ interface DocumentEventMap {
139
+ feaasReady: CustomEvent<FEAASContextImports.SDK>
140
+ }
132
141
  }
133
142
 
134
143
  export { FEAASContext as Context }
@@ -4,7 +4,7 @@ import { FEAASElement } from '../components/FEAASElement.js'
4
4
  import { ReactJSX, ReactModules } from '../components/FEAASReactElement.js'
5
5
  import { kebabCase, reverseKebabCase } from '../utils/string.js'
6
6
  import { Component, FEAASComponentProps } from './react.js'
7
- import type { ExternalComponent, ExternalComponentOptions } from '@sitecore-feaas/sdk'
7
+ import type { ExternalComponent, ExternalComponentOptions, JSONSchema, UISchema } from '@sitecore-feaas/sdk'
8
8
 
9
9
  /** Checks if title exists in a property and if not, it generates it from (kebab-cased) property key */
10
10
  const addTitles = (properties: { [key: string]: any }) =>
@@ -21,13 +21,14 @@ const addTitles = (properties: { [key: string]: any }) =>
21
21
  return acc
22
22
  }, {})
23
23
 
24
+ /** Converts the keys of an object to kebab case. */
24
25
  const objectKeysToKebabCase = (object: { [key: string]: any } = {}) =>
25
26
  Object.keys(object).reduce((acc: { [key: string]: any }, key) => {
26
27
  acc[kebabCase(key)] = object[key]
27
28
  return acc
28
29
  }, {})
29
30
 
30
- const transformSchema = (schema: ExternalComponent['schema']) => {
31
+ const transformSchema = (schema: JSONSchema) => {
31
32
  if (!schema) return
32
33
  if (!schema.properties) return schema
33
34
  let properties, required
@@ -44,9 +45,9 @@ const transformSchema = (schema: ExternalComponent['schema']) => {
44
45
  return { ...schema, properties, required, title }
45
46
  }
46
47
 
47
- const transformUiSchema = (uiSchema: ExternalComponent['uiSchema'], properties: { [key: string]: any }) => {
48
+ const transformUiSchema = (uiSchema: UISchema, properties: { [key: string]: any }) => {
48
49
  if (!uiSchema || !properties) return
49
- let transformed: ExternalComponent['uiSchema']
50
+ let transformed: UISchema
50
51
 
51
52
  /* kebab-case keys */
52
53
  transformed = objectKeysToKebabCase(uiSchema)
@@ -60,12 +61,8 @@ const transformUiSchema = (uiSchema: ExternalComponent['uiSchema'], properties:
60
61
 
61
62
  return transformed
62
63
  }
63
-
64
- export const parseProps = (
65
- propName: string,
66
- propValue: string,
67
- properties: ExternalComponent['schema']['properties']
68
- ) => {
64
+ /** Parses a property value based on its property name and type defined in the JSON properties object. */
65
+ export const parseProps = (propName: string, propValue: string, properties: JSONSchema['properties']) => {
69
66
  //@ts-ignore
70
67
  switch (properties?.[propName]?.type) {
71
68
  case 'string':
@@ -88,7 +85,7 @@ export const parseProps = (
88
85
  export function FEAASExternalProps(element: FEAASElement) {
89
86
  const attributes = element.getAttributeNames()
90
87
  const id = element.getAttribute('data-external-id')
91
- const schema: ExternalComponent['schema'] = FEAASExternal.registered[id]?.schema
88
+ const schema: JSONSchema = FEAASExternal.registered[id]?.schema
92
89
  const props = attributes.reduce((prev, curr) => {
93
90
  if (!curr.startsWith('data-attribute') && !['class', 'id', 'contenteditable'].includes(curr)) {
94
91
  const value = element.getAttribute(curr)
@@ -123,11 +120,72 @@ export default class FEAASExternal extends FEAASElement<FEAASExternalProps, Reac
123
120
  return FEAASExternalProps(this)
124
121
  }
125
122
 
126
- static registerComponent(component: React.FC, options: ExternalComponentOptions = {}) {
127
- const id = options.name || component.name
123
+ /**
124
+ * Register React component to be renderable as Sitecore component (in Components and Pages). Properties are defined
125
+ * as {@link https://json-schema.org JSON Schema}, from which a configuration form will be produced using
126
+ * {@link https://rjsf-team.github.io/react-jsonschema-form/ RJSF library}. The library allows passing a
127
+ * {@link https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema UI Schema} that allows tight
128
+ * customization of form widgets, styles and behavior.
129
+ *
130
+ * @param {React.ComponentType} component React component that will be rendered.
131
+ * @param {string} options.title Component name as presented to the user.
132
+ * @param {JSONSchema['properties']} options.properties Object of properties.
133
+ * @param {JSONSchema['type']} options.properties[].type JSON Schema type of a property.
134
+ * @param {string} [options.properties[].title] Optional title displayed to the user.
135
+ * @param {string} [options.group] Optional Image to be displayed in the UI.
136
+ * @param {string} [options.thumbnail] Optional title displayed to the user.
137
+ * @param {UISchema} [options.ui] Optional component groupping.
138
+ * @see https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema
139
+ * @see https://json-schema.org
140
+ * @example
141
+ *
142
+ * FEAAS.External.registerComponent(MyComponent, {
143
+ * // Name of a component as it will be presented to the user
144
+ * name: 'My Component',
145
+ * // Title of a component as it will be presented in the inputs form
146
+ * title: 'My Component',
147
+ * // Optional: Thumbnail image for component to be displayed in UI
148
+ * thumbnail: 'http://examaple.com/component.jpg',
149
+ * // Optional: Group components by name
150
+ * group: 'Assorted components',
151
+ * // Definitions of three configurable properties that MyComponent accepts
152
+ * properties: {
153
+ * // Color is a string, but only Red/Green/Blue are allowed
154
+ * color: { type: 'string', enum: ['red', 'green', 'blue'] },
155
+ * // Text is a property labelled as "Text summary"
156
+ * text: { type: 'string', title: 'Text summary' },
157
+ * // Count is a number with 1 as default value
158
+ * count: { type: 'number', default: 1 }
159
+ * },
160
+ * // Optional UI schema customization
161
+ * ui: {
162
+ * // Present color choices as radio buttons
163
+ * color: { 'ui:widget': 'radio' },
164
+ * // Text is a textarea with 5 rows
165
+ * text: { 'ui:widget': 'textarea', 'ui:options': { rows: 5 } },
166
+ * // Count is a number field with spinner button
167
+ * count: { 'ui:widget': 'updown' }
168
+ * },
169
+ * // Color and text area required properties
170
+ * required: ['color', 'text']
171
+ * })
172
+ *
173
+ */
174
+ static registerComponent(component: React.ComponentType, options: ExternalComponentOptions) {
175
+ if (!options.name)
176
+ throw new Error('Could not register external component. Please make sure you provide a name in the options')
177
+ const id = options.name
128
178
  const { thumbnail, group, ui: uiSchema, ...schemaOptions } = options
129
- const schema = { ...schemaOptions, type: 'object' } as ExternalComponent['schema']
130
- FEAASExternal.registered[id] = { component, schema, uiSchema, thumbnail, group, title: schema?.title || id }
179
+ const schema = { ...schemaOptions, type: 'object' } as JSONSchema
180
+ FEAASExternal.registered[id] = {
181
+ component,
182
+ name: id,
183
+ schema,
184
+ uiSchema,
185
+ thumbnail,
186
+ group,
187
+ title: schema?.title || id
188
+ }
131
189
  FEAASExternal.setRegistrationCallback()
132
190
  }
133
191
 
@@ -161,7 +219,7 @@ export default class FEAASExternal extends FEAASElement<FEAASExternalProps, Reac
161
219
 
162
220
  /*static loadComponents() {
163
221
  const components = JSON.parse(sessionStorage.getItem('feaas-external-components'))
164
- components.forEach((c: { name: string; schema: any; component: React.FC }) => {
222
+ components.forEach((c: { name: string; schema: any; component: React.ComponentType }) => {
165
223
  const name = c.name
166
224
  const schema = c.schema
167
225
  const component = eval('(' + c.component + ')')
@@ -192,4 +250,5 @@ declare global {
192
250
  FEAASExternal.register('feaas-external')
193
251
  FEAASExternal.setRegistrationCallback()
194
252
 
253
+ export const registerComponent = FEAASExternal.registerComponent
195
254
  export { FEAASExternal as External }