@sitecore-feaas/clientside 0.2.2 → 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 (92) hide show
  1. package/README.md +1 -1
  2. package/build/components/FEAASComponent.js +3 -1
  3. package/build/components/FEAASComponent.js.map +1 -1
  4. package/build/components/FEAASElement.js +16 -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/FEAASExternal.js +15 -13
  12. package/build/ui/FEAASExternal.js.map +1 -1
  13. package/build/ui/FEAASPicker.js +11 -5
  14. package/build/ui/FEAASPicker.js.map +1 -1
  15. package/build/ui/react.js +23 -8
  16. package/build/ui/react.js.map +1 -1
  17. package/build/utils/datapath.js +15 -0
  18. package/build/utils/datapath.js.map +1 -1
  19. package/build/utils/fetch.js +16 -2
  20. package/build/utils/fetch.js.map +1 -1
  21. package/build/utils/settings.js +60 -0
  22. package/build/utils/settings.js.map +1 -0
  23. package/dist/browser/headless.cjs +8 -6
  24. package/dist/browser/headless.cjs.map +4 -4
  25. package/dist/browser/headless.esm.js +8 -6
  26. package/dist/browser/headless.esm.js.map +1 -1
  27. package/dist/browser/index.cjs +8 -6
  28. package/dist/browser/index.cjs.map +4 -4
  29. package/dist/browser/index.esm.js +8 -6
  30. package/dist/browser/index.esm.js.map +1 -1
  31. package/dist/browser/react.cjs +8 -6
  32. package/dist/browser/react.cjs.map +4 -4
  33. package/dist/browser/react.esm.js +8 -6
  34. package/dist/browser/react.esm.js.map +1 -1
  35. package/dist/node/headless.cjs +6 -4
  36. package/dist/node/headless.cjs.map +4 -4
  37. package/dist/node/headless.esm.js +6 -4
  38. package/dist/node/headless.esm.js.map +4 -4
  39. package/dist/node/index.cjs +7 -5
  40. package/dist/node/index.cjs.map +4 -4
  41. package/dist/node/index.esm.js +7 -5
  42. package/dist/node/index.esm.js.map +4 -4
  43. package/dist/node/react.cjs +6 -4
  44. package/dist/node/react.cjs.map +4 -4
  45. package/dist/node/react.esm.js +6 -4
  46. package/dist/node/react.esm.js.map +4 -4
  47. package/headless.cjs +6 -4
  48. package/headless.cjs.map +4 -4
  49. package/headless.js +6 -4
  50. package/headless.js.map +4 -4
  51. package/package.json +1 -1
  52. package/react.cjs +8 -6
  53. package/react.cjs.map +4 -4
  54. package/react.js +8 -6
  55. package/react.js.map +4 -4
  56. package/src/components/FEAASComponent.tsx +10 -3
  57. package/src/components/FEAASElement.tsx +17 -5
  58. package/src/dom/rendering.ts +1 -0
  59. package/src/dom/thumbnail.ts +29 -24
  60. package/src/headless.ts +5 -0
  61. package/src/ui/FEAASExternal.tsx +15 -13
  62. package/src/ui/FEAASPicker.tsx +15 -6
  63. package/src/ui/react.tsx +45 -16
  64. package/src/utils/datapath.ts +17 -0
  65. package/src/utils/fetch.ts +20 -4
  66. package/src/utils/settings.ts +69 -0
  67. package/test/FEAASComponent.react.test.tsx +1 -1
  68. package/test/ServerComponent.test.tsx +33 -0
  69. package/test/settings.test.ts +78 -0
  70. package/types/components/FEAASComponent.d.ts +3 -1
  71. package/types/components/FEAASComponent.d.ts.map +1 -1
  72. package/types/components/FEAASElement.d.ts +1 -0
  73. package/types/components/FEAASElement.d.ts.map +1 -1
  74. package/types/dom/rendering.d.ts.map +1 -1
  75. package/types/dom/thumbnail.d.ts +16 -16
  76. package/types/dom/thumbnail.d.ts.map +1 -1
  77. package/types/headless.d.ts +5 -0
  78. package/types/headless.d.ts.map +1 -1
  79. package/types/tsbuildinfo +1 -1
  80. package/types/ui/FEAASExternal.d.ts +15 -13
  81. package/types/ui/FEAASExternal.d.ts.map +1 -1
  82. package/types/ui/FEAASPicker.d.ts +9 -6
  83. package/types/ui/FEAASPicker.d.ts.map +1 -1
  84. package/types/ui/react.d.ts +7 -2
  85. package/types/ui/react.d.ts.map +1 -1
  86. package/types/ui/tsbuildinfo +1 -1
  87. package/types/utils/datapath.d.ts +2 -0
  88. package/types/utils/datapath.d.ts.map +1 -1
  89. package/types/utils/fetch.d.ts.map +1 -1
  90. package/types/utils/settings.d.ts +13 -0
  91. package/types/utils/settings.d.ts.map +1 -0
  92. /package/test/{FEAASComponent2.test.tsx → FEAASComponentData.test.tsx} +0 -0
@@ -13,12 +13,17 @@ export function FEAASComponentsProps(element: FEAASElement) {
13
13
  if (!parsed) throw new Error(`Could not parse FEAAS Component source: ${src}`)
14
14
  var { cdn, library, component, version, revision } = parsed
15
15
  }
16
+
16
17
  const dataValue = element.getAttribute('data')
18
+
17
19
  if (dataValue != null && dataValue != '') {
18
20
  try {
19
21
  data = typeof dataValue == 'string' ? JSON.parse(dataValue) : {}
20
- } catch (e) {}
22
+ } catch (e) {
23
+ console.error(e)
24
+ }
21
25
  }
26
+
22
27
  return {
23
28
  data: data || {},
24
29
  cdn: element.getContextAttribute('cdn') ?? cdn ?? DEFAULT_CDN_URL,
@@ -126,6 +131,7 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
126
131
  this.whenDataReady = this.whenDataReady.restart()
127
132
 
128
133
  const data = await fetchData(this.props.data)
134
+
129
135
  const alreadyRendered = this.alreadyRendered
130
136
 
131
137
  await this.whenDataReady.resolve(data)
@@ -229,7 +235,6 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
229
235
 
230
236
  update() {
231
237
  this.alreadyRendered = true
232
-
233
238
  if (Array.from(this.children).filter((c) => c.tagName != 'BR').length > 0 && !this.needsRefresh) {
234
239
  renderDOMElement(this, this.readyData)
235
240
  } else {
@@ -257,7 +262,9 @@ declare global {
257
262
  'with-stylesheet'?: 'false' | 'true' | ''
258
263
  } & (
259
264
  | {
260
- /** @deprecated Use cdn instead */
265
+ /**
266
+ * @deprecated Use cdn instead.
267
+ */
261
268
  hostname?: string
262
269
  cdn?: string
263
270
  library: string
@@ -118,7 +118,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
118
118
  if (this.connected) return
119
119
  this.connected = true
120
120
  this.log('FEAAS: Mount', this.tagName, this)
121
- this.dispatchEvent(new Event('feaasMount', { bubbles: true, composed: true }))
121
+ this.dispatchEventIndirectly(new Event('feaasMount', { bubbles: true, composed: true }))
122
122
  this.getElements().map((element: FEAASElement) => element.mount?.())
123
123
  this.set()
124
124
  } catch (e) {
@@ -126,6 +126,17 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
126
126
  }
127
127
  }
128
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
+
129
140
  log(...args: any) {
130
141
  if (typeof process != 'undefined' && process.env.TESTING) return
131
142
  console.log(...args)
@@ -135,7 +146,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
135
146
  unmount() {
136
147
  if (!this.connected) return
137
148
  this.log('FEAAS: Unmount', this)
138
- this.dispatchEvent(new Event('feaasUnmount', { bubbles: true, composed: true }))
149
+ this.dispatchEventIndirectly(new Event('feaasUnmount', { bubbles: true, composed: true }))
139
150
  this.connected = false
140
151
  }
141
152
 
@@ -182,7 +193,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
182
193
  const index = this.descendants.indexOf(target)
183
194
  if (index == -1) return
184
195
  this.log('FEAAS: Unnest', this, target)
185
- this.descendants.splice(index, -1)
196
+ this.descendants.splice(index, 1)
186
197
  this.refs[target.getComponentName()] = null
187
198
  if (target.refs.parent == this) target.refs.parent = null
188
199
  if (target.refs.top == this) {
@@ -224,7 +235,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
224
235
  /** Generic error handler */
225
236
  onError(error: Error, where: string = 'uncaught', handled = false) {
226
237
  const event = new CustomEvent('feaasError', { bubbles: true, composed: true, detail: error })
227
- this.dispatchEvent(event)
238
+ this.dispatchEventIndirectly(event)
228
239
  this.whenLoaded.reject(error)
229
240
  if (!event.defaultPrevented && !handled) {
230
241
  this.whenRendered.reject(error)
@@ -248,7 +259,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
248
259
  if (this.payload === undefined) {
249
260
  if (!this.isReadyToLoad()) return
250
261
  this.payload = null
251
- this.dispatchEvent(new Event('feaasLoad', { bubbles: true, composed: true }))
262
+ this.dispatchEventIndirectly(new Event('feaasLoad', { bubbles: true, composed: true }))
252
263
  const loaded = await this.load().catch((e) => {
253
264
  this.payload = undefined
254
265
  throw e
@@ -272,6 +283,7 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
272
283
  }
273
284
 
274
285
  flush() {
286
+ if (!this.connected) return
275
287
  const flushSync = (this.payload as ReactPayload)?.ReactDOM?.flushSync
276
288
  if (flushSync) {
277
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 {
@@ -122,10 +122,23 @@ export default class FEAASExternal extends FEAASElement<FEAASExternalProps, Reac
122
122
 
123
123
  /**
124
124
  * Register React component to be renderable as Sitecore component (in Components and Pages). Properties are defined
125
- * as JSON schema, from which a configuration form will be produced using RJSF library. The library allows passing a
126
- * UI schema that allows tight customization of form widgets, styles and its behavior.
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.
127
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
128
140
  * @example
141
+ *
129
142
  * FEAAS.External.registerComponent(MyComponent, {
130
143
  * // Name of a component as it will be presented to the user
131
144
  * name: 'My Component',
@@ -157,17 +170,6 @@ export default class FEAASExternal extends FEAASElement<FEAASExternalProps, Reac
157
170
  * required: ['color', 'text']
158
171
  * })
159
172
  *
160
- * @param {React.ComponentType} component React component that will be rendered.
161
- * @param {string} options.name Component name as presented to the user.
162
- * @param {string} options.title Component title to be used in the inputs form. Defaults to component name.
163
- * @param {JSONSchema['properties']} options.properties Object of properties.
164
- * @param {JSONSchema['type']} options.properties[].type JSON Schema type of a property.
165
- * @param {string} [options.properties[].title] Optional title displayed to the user.
166
- * @param {string} [options.group] Optional Image to be displayed in the UI.
167
- * @param {string} [options.thumbnail] Optional title displayed to the user.
168
- * @param {UISchema} [options.ui] Optional component groupping.
169
- * @see https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema
170
- * @see https://json-schema.org
171
173
  */
172
174
  static registerComponent(component: React.ComponentType, options: ExternalComponentOptions) {
173
175
  if (!options.name)
@@ -5,7 +5,7 @@ export function FEAASPickerProps(element: FEAASElement<any, any>) {
5
5
  return {
6
6
  part: element?.getAttribute('part') || 'picker',
7
7
  slot: element?.getAttribute('part') || 'picker',
8
- mode: element?.getAttribute('mode') as 'library' | 'element' | 'themes' | 'datasources',
8
+ mode: element?.getAttribute('mode') as FEAASPickerImports.PickerProps['mode'],
9
9
  placeholder: element?.getAttribute('placeholder') || 'No theme',
10
10
  frontend: element?.getContextAttribute('frontend') ?? 'https://components.sitecorecloud.io',
11
11
  src: element?.getAttribute('src') ?? '/picker.js',
@@ -13,7 +13,8 @@ export function FEAASPickerProps(element: FEAASElement<any, any>) {
13
13
  variant: element?.getAttribute('variant') as 'internal' | 'pages',
14
14
  context: undefined as FEAASPickerImports.PickerProps['context'],
15
15
  onClose: undefined as (...args: any[]) => void,
16
- onContextAttributeSet: undefined as (context: any, attribute: string, value: string) => void
16
+ setAttribute: undefined as FEAASPickerImports.AttributeSetter,
17
+ getAttribute: undefined as FEAASPickerImports.AttributeGetter
17
18
  }
18
19
  }
19
20
  export type FEAASPickerProps = ReturnType<typeof FEAASPickerProps>
@@ -40,21 +41,29 @@ export class FEAASPicker extends FEAASElement<FEAASPickerProps, typeof FEAASPick
40
41
  { placeholder, context, mode, children, variant }: React.PropsWithChildren<FEAASPickerProps>,
41
42
  { Picker, React, useLibrary }: typeof FEAASPickerImports
42
43
  ) {
44
+ const [renders, forceRender] = React.useReducer((c) => c + 1, 0)
43
45
  const collections = useLibrary('collections')
46
+ const components = useLibrary('components')
44
47
  const rules = useLibrary('stylesheet.styles')
48
+ const component = components.find((c) => c.id == context?.getAttribute('component'))
45
49
  return (
46
50
  <Picker
47
51
  rules={rules}
48
52
  collections={collections}
49
53
  themeContext={context}
50
54
  placeholder={placeholder}
51
- mode={mode || undefined}
52
- onModeChange={(mode) => {
53
- if (!mode) this.props.onClose(mode)
55
+ component={component}
56
+ versionId={context.getAttribute('version')}
57
+ mode={mode || 'component'}
58
+ setAttribute={(context, attribute, value) => {
59
+ this.props.setAttribute(context, attribute, value)
60
+ forceRender()
54
61
  }}
55
- onContextAttributeSet={this.props.onContextAttributeSet}
56
62
  context={context}
57
63
  variant={variant}
64
+ onModeChange={(mode) => {
65
+ this.set({ mode })
66
+ }}
58
67
  >
59
68
  {children}
60
69
  </Picker>
package/src/ui/react.tsx CHANGED
@@ -1,32 +1,61 @@
1
- import { DatasourceModel, SDK } from '@sitecore-feaas/sdk'
2
-
3
- export * from './index.js'
4
- import { FEAASComponentProps, parseComponentSource } from './index.js'
1
+ import {
2
+ FEAASComponentProps,
3
+ fetchComponent,
4
+ getComponentURL,
5
+ getStylesheetURL,
6
+ parseComponentSource
7
+ } from './index.js'
5
8
  import React from 'react'
6
9
  import { renderHTMLContent } from '../dom/html.js'
10
+ import { fetchData } from '../utils/fetch.js'
7
11
 
8
12
  export { renderHTMLContent }
9
13
  export { External } from './FEAASExternal.js'
14
+ export * from './index.js'
15
+ export * from '../headless.js'
16
+
17
+ export const ServerComponent = async (props: FEAASComponentProps<any>) => {
18
+ const fetchedData = await fetchData(props?.data || {})
19
+
20
+ const p = { ...props }
21
+
22
+ if (('src' in p || 'component' in p) && p?.template == null) {
23
+ const response = await fetchComponent(p)
24
+
25
+ p.template = response.template
26
+ // p.lastModified = response.lastModified
27
+ }
28
+
29
+ return FEAASEmbed({ ...p, data: fetchedData })
30
+ }
31
+
32
+ export function FEAASEmbed({ data, ...props }: FEAASComponentProps<any> & { src?: string; preload?: boolean }) {
33
+ let html = ''
34
+ let preload = null
10
35
 
11
- export function FEAASEmbed({ data, ...props }: FEAASComponentProps<any>) {
12
36
  if ('template' in props && props.template) {
13
- var html = renderHTMLContent(props.template, data || {})
37
+ html = renderHTMLContent(props.template, data || {})
14
38
  }
15
39
 
16
40
  if ('src' in props) {
17
- props = {
18
- ...parseComponentSource(props.src),
19
- ...props
20
- }
41
+ props = { ...parseComponentSource(props.src), ...props }
21
42
  }
22
43
 
23
44
  return (
24
- <feaas-component
25
- {...props}
26
- template={null}
27
- data={typeof data == 'string' ? data : data ? JSON.stringify(data) : null}
28
- dangerouslySetInnerHTML={{ __html: html || '' }}
29
- />
45
+ <>
46
+ {'preload' in props && preload === true && 'src' in props && (
47
+ <>
48
+ <link rel='preload' as='fetch' href={getComponentURL(props.src)} crossOrigin='anonymous' />
49
+ <link rel='preload' as='style' href={getStylesheetURL(props.src)} crossOrigin='anonymous' />
50
+ </>
51
+ )}
52
+ <feaas-component
53
+ {...props}
54
+ template={null}
55
+ data={typeof data == 'string' ? data : data ? JSON.stringify(data) : null}
56
+ dangerouslySetInnerHTML={{ __html: html || '' }}
57
+ />
58
+ </>
30
59
  )
31
60
  }
32
61
 
@@ -29,3 +29,20 @@ export function cleanCollectionBit(path: string) {
29
29
  export function normalizeCollectionScope(path: string) {
30
30
  return cleanCollectionBit(path) + '.*'
31
31
  }
32
+
33
+ export function getHumanizedLabel(path: string, maxBits = 2) {
34
+ var bits = cleanCollectionBit(path)
35
+ .split(/]\.|\.\*\./g)
36
+ .pop()
37
+ .split(/\./g)
38
+ .slice(-3)
39
+ bits = bits.filter((b) => isNaN(parseInt(b)))
40
+ return bits
41
+ .map((bit) => bit.charAt(0).toUpperCase() + bit.slice(1))
42
+ .slice(-maxBits)
43
+ .join(' ')
44
+ }
45
+
46
+ export function getLastPathBit(path: string) {
47
+ return cleanCollectionBit(path).split('.').pop()
48
+ }
@@ -2,8 +2,26 @@ type Data = object | any[]
2
2
 
3
3
  type DataSettings = { jsonpath: string; url: string }
4
4
 
5
- export const isDataSettings = (data: any): data is DataSettings =>
6
- typeof data === 'object' && 'jsonpath' in data && 'url' in data
5
+ export const isDataSettings = (data: any): data is DataSettings => {
6
+ if (typeof data.url != 'string') return false
7
+
8
+ for (var property in data) {
9
+ if (
10
+ property == 'body' ||
11
+ property == 'params' ||
12
+ property == 'headers' ||
13
+ property == 'url' ||
14
+ property === 'method' ||
15
+ (property == 'jsonpath' && typeof data[property] == 'string')
16
+ ) {
17
+ continue
18
+ }
19
+
20
+ return false
21
+ }
22
+
23
+ return true
24
+ }
7
25
 
8
26
  const performFetch = async (
9
27
  settings: Parameters<typeof fetch>[1] & {
@@ -24,8 +42,6 @@ const performFetch = async (
24
42
  export async function fetchData(data: DataSettings | Record<string, DataSettings | Data> | Data): Promise<Data> {
25
43
  if (!data) return data
26
44
 
27
- console.log(data)
28
-
29
45
  if (isDataSettings(data)) return performFetch(data)
30
46
 
31
47
  const keys = Object.keys(data)
@@ -0,0 +1,69 @@
1
+ export function clean(settings: any) {
2
+ let cleanSettings = {}
3
+
4
+ const hasNoBody =
5
+ settings?.method === 'GET' || settings?.method === 'HEAD' || Object.keys(settings?.body || {}).length === 0
6
+
7
+ const hasNoHeaders =
8
+ settings.headers &&
9
+ (Object.keys(settings.headers).length === 0 ||
10
+ (Object.keys(settings.headers).length === 1 &&
11
+ (settings.headers['Content-type'] || settings.headers['Content-Type']) &&
12
+ (settings.headers['Content-type'] === 'application/json' ||
13
+ settings.headers['Content-Type'] === 'application/json')))
14
+
15
+ const hasNoParams = settings.params || Object.keys(settings.params || {}).length === 0
16
+
17
+ if (settings.method && settings.method !== 'GET') cleanSettings = { ...cleanSettings, method: settings.method }
18
+ if (settings.body && !hasNoBody) cleanSettings = { ...cleanSettings, body: settings.body }
19
+ cleanSettings = {
20
+ ...cleanSettings,
21
+ headers: hasNoHeaders ? { 'Content-type': 'application/json' } : settings.headers
22
+ }
23
+ if (settings.params && !hasNoParams) cleanSettings = { ...cleanSettings, params: settings.params }
24
+
25
+ return { ...cleanSettings, url: settings.url || '' }
26
+ }
27
+
28
+ export function toFetchArguments(settings?: any): Parameters<typeof fetch> {
29
+ const normalized = normalize(settings)
30
+
31
+ let body = null
32
+
33
+ const contentType = normalized.headers['Content-Type'] || normalized.headers['Content-type']
34
+
35
+ const isJson = contentType === 'application/json'
36
+ const isForm = contentType === 'multipart/form-data'
37
+ const isUrlEncoded = contentType === 'application/x-www-form-urlencoded'
38
+
39
+ if (isJson) body = JSON.stringify(normalized.body)
40
+ if (isForm)
41
+ body = Object.keys(normalized.body).reduce((form: any, key) => {
42
+ form.append(key, normalized.body[key])
43
+ return form
44
+ }, new FormData())
45
+ if (isUrlEncoded) body = new URLSearchParams(normalized.body)
46
+
47
+ const params = Object.keys(normalized.params).length ? `?${new URLSearchParams(normalized.params)}` : ''
48
+
49
+ let url = `${normalized.url}${params}`
50
+
51
+ let options: any = {}
52
+
53
+ if (body) options.body = body
54
+ if (Object.keys(normalized.headers).length) options.headers = normalized.headers
55
+ if (normalized.method !== 'GET') options.method = normalized.method
56
+
57
+ return [url, options]
58
+ }
59
+
60
+ export function normalize(settings: any) {
61
+ return {
62
+ params: settings?.params || {},
63
+ headers: settings?.headers || {},
64
+ jsonpath: settings?.jsonpath || '$',
65
+ method: settings?.method || 'GET',
66
+ body: settings?.body || {},
67
+ url: settings?.url || ''
68
+ }
69
+ }
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
3
3
  import { describe, expect, it } from 'vitest'
4
4
  import { Component } from '../src/ui/react.js'
5
5
 
6
- async function test(react: any, html: string) {
6
+ export async function test(react: any, html: string) {
7
7
  const div = document.createElement('div')
8
8
  document.body.appendChild(div)
9
9
  const root = ReactDOM.render(react, div)
@@ -0,0 +1,33 @@
1
+ import React from 'react'
2
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { ServerComponent } from '../src/ui/react.js'
4
+ import { FEAASComponent } from '../src/components/FEAASComponent.js'
5
+ import {test} from './FEAASComponent.react.test.js'
6
+
7
+ const earliest = new Date(2000, 1, 1, 0, 0, 0)
8
+ const earlier = new Date(2000, 1, 2, 0, 0, 0)
9
+ const now = new Date(2000, 1, 2, 12, 0, 0)
10
+
11
+ describe('React Server', () => {
12
+ beforeAll(() => {
13
+ vi.setSystemTime(now)
14
+ FEAASComponent.register('feaas-component')
15
+ })
16
+
17
+ beforeEach(() => {
18
+ document.write()
19
+ })
20
+
21
+ it('render server component', async () => {
22
+ vi.spyOn(global, 'fetch').mockImplementation(
23
+ async (url: string, options: Parameters<typeof fetch>[1] = {}) => new Response('{"data": "1"}')
24
+ )
25
+
26
+ const Component = await ServerComponent({ src: 'library/test', data: { url: 'http://localhost:4000/api/data' } })
27
+
28
+ await test(
29
+ <>{Component}</>,
30
+ `<feaas-component library="library" component="test" src="library/test" data="{&quot;data&quot;:&quot;1&quot;}">{"data": "1"}</feaas-component>`
31
+ )
32
+ })
33
+ })