@sitecore-feaas/clientside 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +9 -0
- package/README.md +115 -0
- package/dist/browser/index.cjs +3 -0
- package/dist/browser/index.d.ts +25 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/react.cjs +3 -0
- package/dist/browser/react.d.ts +36 -0
- package/dist/browser/react.js +3 -0
- package/dist/browser/webcomponent-189a307d.d.ts +103 -0
- package/dist/node/index.cjs +1301 -0
- package/dist/node/index.d.ts +25 -0
- package/dist/node/index.js +1286 -0
- package/dist/node/react.cjs +1358 -0
- package/dist/node/react.d.ts +36 -0
- package/dist/node/react.js +1339 -0
- package/dist/node/webcomponent-189a307d.d.ts +103 -0
- package/examples/index.html +54 -0
- package/index.cjs +3 -0
- package/index.d.ts +25 -0
- package/index.js +3 -0
- package/package.json +64 -0
- package/react.cjs +3 -0
- package/react.d.ts +36 -0
- package/react.js +3 -0
- package/src/cloud.ts +58 -0
- package/src/dom.ts +321 -0
- package/src/fetch.ts +64 -0
- package/src/index.ts +11 -0
- package/src/react.ts +66 -0
- package/src/webcomponent.ts +169 -0
- package/test/render.ts +858 -0
- package/tsconfig.json +12 -0
- package/webcomponent-189a307d.d.ts +103 -0
package/src/dom.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { JSONPath } from 'jsonpath-plus'
|
|
2
|
+
import { CloudStylesheet, CloudTemplate, fetchComponent, fetchStylesheet } from './cloud.js'
|
|
3
|
+
|
|
4
|
+
export interface FEAASCustomizations {
|
|
5
|
+
processTemplate?: (element: HTMLElement, template: string, data: any) => string
|
|
6
|
+
processData?: (element: HTMLElement, data: any) => any
|
|
7
|
+
processValue?: (element: HTMLElement, property: string, value: any) => any
|
|
8
|
+
shouldRepeat?: (element: HTMLElement, value: any) => boolean
|
|
9
|
+
onRender?: (element: HTMLElement, data: any) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ExplicitTemplate {
|
|
13
|
+
template: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Datascopes = Record<string, any>
|
|
17
|
+
|
|
18
|
+
export type FEAASComponentProps = {
|
|
19
|
+
data?: Datascopes
|
|
20
|
+
} & (ExplicitTemplate | CloudTemplate) &
|
|
21
|
+
FEAASCustomizations
|
|
22
|
+
|
|
23
|
+
export function renderComponentElement(content: string, datasources?: Datascopes) {
|
|
24
|
+
const wrapper = document.createElement('div')
|
|
25
|
+
wrapper.innerHTML = content
|
|
26
|
+
const fragment = document.createDocumentFragment()
|
|
27
|
+
while (wrapper.firstChild) {
|
|
28
|
+
fragment.appendChild(wrapper.firstChild)
|
|
29
|
+
}
|
|
30
|
+
renderDOMElement(fragment, datasources)
|
|
31
|
+
return wrapper
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function cleanCollectionBit(path: string) {
|
|
35
|
+
return path.replace(/(\[[^\]\[]+\]|\.\*)$/g, '')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeCollectionScope(path: string) {
|
|
39
|
+
return cleanCollectionBit(path) + '.*'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getDOMCollectionIndex(element: HTMLElement) {
|
|
43
|
+
const datascope = element.getAttribute('data-path-scope')
|
|
44
|
+
var index = 0
|
|
45
|
+
for (var p = element; (p = <HTMLElement>p.previousElementSibling); ) {
|
|
46
|
+
if (p.getAttribute('data-path-scope') == datascope) {
|
|
47
|
+
index++
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return index
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function updateDOMClones(element: HTMLElement, targetCount: number, config?: FEAASCustomizations) {
|
|
54
|
+
const datascope = element.getAttribute('data-path-scope')
|
|
55
|
+
if (datascope == null) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
var index = getDOMCollectionIndex(element)
|
|
59
|
+
if (index != 0) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const parentElement = element.parentElement
|
|
64
|
+
if (parentElement == null) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*dont remove first element, just hide it*/
|
|
69
|
+
if (targetCount == 0) {
|
|
70
|
+
element.style.display = 'none'
|
|
71
|
+
} else if (element.style.display) {
|
|
72
|
+
element.style.display = ''
|
|
73
|
+
if (element.getAttribute('style') == '') element.removeAttribute('style')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var count = index + 1
|
|
77
|
+
var last = element
|
|
78
|
+
for (var n = element; (n = <HTMLElement>n.nextElementSibling); ) {
|
|
79
|
+
if (n.getAttribute('data-path-scope') == datascope) {
|
|
80
|
+
count++
|
|
81
|
+
last = n
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/* add missing clones */
|
|
85
|
+
for (var i = count; i < targetCount; i++) {
|
|
86
|
+
parentElement.insertBefore(last.cloneNode(true), last.nextElementSibling)
|
|
87
|
+
}
|
|
88
|
+
/* remove excessive clones fron previous renders */
|
|
89
|
+
for (var i = Math.max(1, targetCount); i < count; i++) {
|
|
90
|
+
last = <HTMLElement>last.previousElementSibling
|
|
91
|
+
parentElement.removeChild(<HTMLElement>last.nextElementSibling)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function queryScopes(scopes: any, datapath: string, singleValue = false, json: any = scopes) {
|
|
96
|
+
var path = datapath
|
|
97
|
+
/* find closest parent scope */
|
|
98
|
+
const keys = Object.keys(scopes).sort((b, a) => a.length - b.length)
|
|
99
|
+
for (const scope of keys) {
|
|
100
|
+
if (datapath.startsWith(scope) && 'ends with it') {
|
|
101
|
+
if (scope == datapath) {
|
|
102
|
+
if (singleValue) {
|
|
103
|
+
return scopes[scope]
|
|
104
|
+
} else {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
path = datapath.substring(scope.length + 1)
|
|
109
|
+
json = scopes[scope]
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const collection = JSONPath({
|
|
115
|
+
path: path,
|
|
116
|
+
json: json
|
|
117
|
+
})
|
|
118
|
+
if (singleValue && collection) {
|
|
119
|
+
return collection[0]
|
|
120
|
+
} else {
|
|
121
|
+
return collection
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function setDOMAttribute(element: HTMLElement, attribute: string, value: any, config?: FEAASCustomizations) {
|
|
126
|
+
if (!element) return
|
|
127
|
+
var target = element
|
|
128
|
+
if (config?.processValue) {
|
|
129
|
+
value = config?.processValue(element, attribute, value)
|
|
130
|
+
}
|
|
131
|
+
var prop = attribute.replace('data-path-', '')
|
|
132
|
+
if (attribute == 'data-path-src') {
|
|
133
|
+
target = element.tagName == 'IMG' ? element : <HTMLElement>element.querySelector('img, video')
|
|
134
|
+
} else if (attribute == 'data-path-href') {
|
|
135
|
+
target = element.querySelector('a') || element.closest('a')
|
|
136
|
+
} else if (attribute == 'data-path-hidden') {
|
|
137
|
+
value = !value ? true : null
|
|
138
|
+
} else if (attribute == 'data-path' || attribute == 'data-embed-html' || attribute == 'html') {
|
|
139
|
+
prop = 'innerHTML'
|
|
140
|
+
} else if (attribute == 'data-embed-src') {
|
|
141
|
+
loadScript(element, value)
|
|
142
|
+
} else if (attribute == 'data-path-attributes') {
|
|
143
|
+
const oldKeys = element.getAttribute('data-attributes-keys')?.split(',').filter(Boolean) || []
|
|
144
|
+
for (var property in value) {
|
|
145
|
+
var subvalue = value[property]
|
|
146
|
+
setDOMAttribute(
|
|
147
|
+
element,
|
|
148
|
+
property,
|
|
149
|
+
typeof subvalue == 'object' && subvalue ? JSON.stringify(subvalue) : subvalue,
|
|
150
|
+
config
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
for (var i = 0; i < oldKeys.length; i++) {
|
|
154
|
+
if (value == null || !(oldKeys[i] in value)) {
|
|
155
|
+
setDOMAttribute(element, oldKeys[i], null, config)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
setDOMAttribute(element, 'data-attributes-keys', Object.keys(value || {}).join(','))
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
if (typeof value == 'boolean') {
|
|
162
|
+
value = value ? '' : null
|
|
163
|
+
}
|
|
164
|
+
if (!target) return
|
|
165
|
+
|
|
166
|
+
var placeholder = String(element.getAttribute('data-path-placeholder') || target[prop as keyof HTMLElement])
|
|
167
|
+
|
|
168
|
+
if (value != null) {
|
|
169
|
+
if (prop == 'innerHTML') {
|
|
170
|
+
if (target.innerHTML != value) {
|
|
171
|
+
target.innerHTML = value
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
if (target.getAttribute(prop) != value) {
|
|
175
|
+
target.setAttribute(prop, value)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
if (prop == 'innerHTML') {
|
|
180
|
+
target.innerHTML = placeholder
|
|
181
|
+
} else {
|
|
182
|
+
target.removeAttribute(prop)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function observeDOMElement(element: HTMLElement) {
|
|
188
|
+
const observer = new element.ownerDocument.defaultView.MutationObserver((changes) => {
|
|
189
|
+
changes.forEach((change) => {
|
|
190
|
+
const target = change.target as HTMLElement
|
|
191
|
+
const src = target.getAttribute('data-embed-src')
|
|
192
|
+
if (src) {
|
|
193
|
+
loadScript(target, src)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (change.addedNodes) {
|
|
197
|
+
change.addedNodes.forEach((node) => {
|
|
198
|
+
if (node.nodeType == 1) autoloadScripts(node as HTMLElement, false)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
observer.observe(element, { attributes: true, subtree: true, childList: true })
|
|
204
|
+
return observer
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function autoloadScripts(element: HTMLElement, observe = true) {
|
|
208
|
+
const embeds = element.querySelectorAll('[data-embed-src]')
|
|
209
|
+
for (var i = 0; i < embeds.length; i++) {
|
|
210
|
+
const embed = embeds[i]
|
|
211
|
+
loadScript(embed as HTMLElement, embed.getAttribute('data-embed-src'))
|
|
212
|
+
}
|
|
213
|
+
if (observe) return observeDOMElement(element)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function loadScript(element: HTMLElement, src: string) {
|
|
217
|
+
for (var p = element; p; p = p.parentElement) {
|
|
218
|
+
if (!p.parentElement) {
|
|
219
|
+
const scripts = p.querySelectorAll('script')
|
|
220
|
+
for (var i = 0; i < scripts.length; i++) {
|
|
221
|
+
if (scripts[i].getAttribute('src') == src) {
|
|
222
|
+
return true
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (p.tagName == 'HTML') p = p.querySelector('head') || p
|
|
226
|
+
const script = p.ownerDocument.createElement('script')
|
|
227
|
+
script.type = 'module'
|
|
228
|
+
script.src = src
|
|
229
|
+
p.appendChild(script)
|
|
230
|
+
break
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// order in which attributes are proccesed. Rightmost are more important
|
|
236
|
+
var sortOrder = ['data-attributes-keys', 'data-path-attributes', 'data-path-scope']
|
|
237
|
+
export function renderDOMElement(
|
|
238
|
+
input: HTMLElement | DocumentFragment,
|
|
239
|
+
datascopes?: any,
|
|
240
|
+
config?: FEAASCustomizations,
|
|
241
|
+
isNested?: boolean
|
|
242
|
+
) {
|
|
243
|
+
const element = <HTMLElement>input
|
|
244
|
+
if (!isNested) {
|
|
245
|
+
datascopes = config?.processData?.(element, datascopes) || datascopes
|
|
246
|
+
}
|
|
247
|
+
if (element.nodeType == 1) {
|
|
248
|
+
const datascope = element.getAttribute('data-path-scope')
|
|
249
|
+
if (datascope) {
|
|
250
|
+
const index = getDOMCollectionIndex(element)
|
|
251
|
+
const collection = queryScopes(datascopes, datascope, false) || []
|
|
252
|
+
if (index == 0) {
|
|
253
|
+
updateDOMClones(element, config?.shouldRepeat?.(element, collection) === false ? 1 : collection.length, config)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
datascopes = {
|
|
257
|
+
...datascopes,
|
|
258
|
+
[normalizeCollectionScope(datascope)]: collection[index]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const attrs = Array.prototype.slice.call(element.attributes).sort((a, b) => {
|
|
262
|
+
return sortOrder.indexOf(a.name) - sortOrder.indexOf(b.name)
|
|
263
|
+
})
|
|
264
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
265
|
+
const { name, value: datapath } = attrs[i]
|
|
266
|
+
if (name.startsWith('data-path') && name != 'data-path-scope') {
|
|
267
|
+
setDOMAttribute(element, name, queryScopes(datascopes, datapath, true), config)
|
|
268
|
+
}
|
|
269
|
+
if (name == 'data-embed-src') {
|
|
270
|
+
setDOMAttribute(element, name, element.getAttribute(name), config)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (var i = 0; i < element.children.length; i++) {
|
|
275
|
+
renderDOMElement(<HTMLElement>element.children[i], datascopes, config, true)
|
|
276
|
+
}
|
|
277
|
+
if (!isNested) {
|
|
278
|
+
config?.onRender?.(element, datascopes)
|
|
279
|
+
}
|
|
280
|
+
return element
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function renderDOMContent(root: HTMLElement, template: string, data?: Datascopes, config?: FEAASCustomizations) {
|
|
284
|
+
const newTemplate = config?.processTemplate?.(root, template, data) || template
|
|
285
|
+
root.innerHTML = newTemplate
|
|
286
|
+
root.classList.add('-feaas')
|
|
287
|
+
return renderDOMElement(root, data, config)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function renderStylesheet(props: CloudStylesheet, style?: HTMLStyleElement) {
|
|
291
|
+
renderStylesheetPromise(props, (style ||= document.createElement('style')))
|
|
292
|
+
return style
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function renderStylesheetPromise(props: CloudStylesheet, style?: HTMLStyleElement) {
|
|
296
|
+
style ||= document.createElement('style')
|
|
297
|
+
return fetchStylesheet(props, (cssText) => {
|
|
298
|
+
style.textContent = cssText
|
|
299
|
+
}).then(() => style)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function renderComponent(props: FEAASComponentProps, root?: HTMLElement) {
|
|
303
|
+
renderComponentPromise(props, (root ||= document.createElement('div')))
|
|
304
|
+
return root
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function renderComponentPromise(props: FEAASComponentProps, root?: HTMLElement) {
|
|
308
|
+
root ||= document.createElement('div')
|
|
309
|
+
if (!root.childNodes.length) {
|
|
310
|
+
if ('template' in props) {
|
|
311
|
+
return renderDOMContent(root, props.template, props.data, props)
|
|
312
|
+
} else {
|
|
313
|
+
return fetchComponent(props, (template) => {
|
|
314
|
+
return renderDOMContent(root, template, props.data, props)
|
|
315
|
+
}).then(() => root)
|
|
316
|
+
}
|
|
317
|
+
/* update element with new data */
|
|
318
|
+
} else {
|
|
319
|
+
return renderDOMElement(root, props.data, props)
|
|
320
|
+
}
|
|
321
|
+
}
|
package/src/fetch.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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.
|
|
15
|
+
|
|
16
|
+
*/
|
|
17
|
+
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
|
|
22
|
+
) {
|
|
23
|
+
// 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')
|
|
30
|
+
|
|
31
|
+
// 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
|
+
})
|
|
39
|
+
|
|
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
|
+
}
|
|
47
|
+
|
|
48
|
+
// otherwise the new resource has to be re-fetched
|
|
49
|
+
response = await fetch(url, {
|
|
50
|
+
...options,
|
|
51
|
+
cache: 'reload'
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
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
|
|
64
|
+
}
|
package/src/index.ts
ADDED
package/src/react.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { CloudStylesheet, fetchComponent, fetchStylesheet } from './cloud.js'
|
|
3
|
+
import { FEAASComponentProps, renderDOMElement } from './dom.js'
|
|
4
|
+
export * from './dom.js'
|
|
5
|
+
export * from './cloud.js'
|
|
6
|
+
export * from './webcomponent.js'
|
|
7
|
+
import * as DOM from './dom.js'
|
|
8
|
+
import * as WEB from './webcomponent.js'
|
|
9
|
+
|
|
10
|
+
export function Component(props: FEAASComponentProps) {
|
|
11
|
+
const [template, setTemplate] = React.useState('template' in props ? props.template : null)
|
|
12
|
+
const data = props.data
|
|
13
|
+
const ref = React.useRef<HTMLElement>(null)
|
|
14
|
+
|
|
15
|
+
React.useEffect(
|
|
16
|
+
() => {
|
|
17
|
+
if (template == null && !('template' in props)) {
|
|
18
|
+
fetchComponent(props, setTemplate).catch(() => {
|
|
19
|
+
console.error(
|
|
20
|
+
`FEAAS: Could not fetch component: ${props.library}/${props.component}/${props.variant}/${props.version}`
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
'template' in props
|
|
26
|
+
? [props.template, null, null, null]
|
|
27
|
+
: [props.library, props.component, props.variant, props.version, props.hostname]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (ref.current && template) {
|
|
32
|
+
ref.current.innerHTML = template
|
|
33
|
+
}
|
|
34
|
+
}, [ref, template])
|
|
35
|
+
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
if (template && ref.current) {
|
|
38
|
+
renderDOMElement(ref.current, data || {}, props)
|
|
39
|
+
}
|
|
40
|
+
}, [ref, template, data])
|
|
41
|
+
|
|
42
|
+
return React.createElement('div', { ref, className: '-feaas' })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function Stylesheet(props: CloudStylesheet) {
|
|
46
|
+
const ref = React.useRef<HTMLStyleElement>(null)
|
|
47
|
+
const [css, setCss] = React.useState<string>('')
|
|
48
|
+
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
fetchStylesheet(props, setCss).catch((e) => {
|
|
51
|
+
console.error(`FEAAS: Could not fetch stylesheet for library ${props.library}`)
|
|
52
|
+
})
|
|
53
|
+
}, [props.hostname, props.library])
|
|
54
|
+
|
|
55
|
+
return React.createElement(
|
|
56
|
+
'style',
|
|
57
|
+
{ ref, rel: 'stylesheet', className: '-stylesheet', 'data-library-id': props.library },
|
|
58
|
+
[css]
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
export default {
|
|
62
|
+
...DOM,
|
|
63
|
+
...WEB,
|
|
64
|
+
Stylesheet,
|
|
65
|
+
Component
|
|
66
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { CloudTemplate, fetchComponent, fetchStylesheet } from './cloud.js'
|
|
2
|
+
import { Datascopes, renderDOMContent, renderDOMElement } from './dom.js'
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
namespace JSX {
|
|
6
|
+
interface IntrinsicElements {
|
|
7
|
+
'feaas-component': {
|
|
8
|
+
data?: any
|
|
9
|
+
library: string
|
|
10
|
+
component: string
|
|
11
|
+
variant: string
|
|
12
|
+
version?: string
|
|
13
|
+
hostname?: string
|
|
14
|
+
template?: string
|
|
15
|
+
children?: any
|
|
16
|
+
}
|
|
17
|
+
'feaas-stylesheet': {
|
|
18
|
+
library?: string
|
|
19
|
+
hostname?: string
|
|
20
|
+
children?: any
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Shim in case it's required in node.js environment
|
|
27
|
+
var WebComponent = typeof HTMLElement == 'undefined' ? (class {} as unknown as typeof HTMLElement) : HTMLElement
|
|
28
|
+
|
|
29
|
+
export class HTMLFeaasComponent extends WebComponent {
|
|
30
|
+
static get observedAttributes() {
|
|
31
|
+
return ['data', 'library', 'component', 'variant', 'version', 'hostname', 'template']
|
|
32
|
+
}
|
|
33
|
+
connected = false
|
|
34
|
+
|
|
35
|
+
template: string = null
|
|
36
|
+
data: any = undefined
|
|
37
|
+
isFetching: boolean = false
|
|
38
|
+
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
this.connected = true
|
|
41
|
+
this.render()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
disconnectedCallback() {}
|
|
45
|
+
|
|
46
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
47
|
+
if (!this.connected) return
|
|
48
|
+
if (name == 'template') {
|
|
49
|
+
this.template = newValue
|
|
50
|
+
}
|
|
51
|
+
if (name == 'data' && this.children.length > 0) {
|
|
52
|
+
this.update()
|
|
53
|
+
} else {
|
|
54
|
+
this.render()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
update() {
|
|
59
|
+
renderDOMElement(this, this.getProps().data)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setData(data: any) {
|
|
63
|
+
this.data = data
|
|
64
|
+
if (this.children.length > 0) {
|
|
65
|
+
this.update()
|
|
66
|
+
} else {
|
|
67
|
+
this.render()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getOriginal() {
|
|
72
|
+
const scope = this.getAttribute('data-path-scope')
|
|
73
|
+
if (!scope) return
|
|
74
|
+
for (var current = this as Element; (current = current.previousElementSibling); ) {
|
|
75
|
+
if (current.getAttribute('data-path-scope') == scope) {
|
|
76
|
+
var last = current
|
|
77
|
+
} else {
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return last as HTMLFeaasComponent
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getData(): any {
|
|
85
|
+
if (this.data) {
|
|
86
|
+
return this.data
|
|
87
|
+
}
|
|
88
|
+
if (this.getAttribute('data')) {
|
|
89
|
+
const dataValue = this.getAttribute('data')
|
|
90
|
+
return typeof dataValue == 'string' ? JSON.parse(dataValue) : {}
|
|
91
|
+
}
|
|
92
|
+
return this.getOriginal()?.getData() || {}
|
|
93
|
+
return {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
render() {
|
|
97
|
+
// defer rendering to original element
|
|
98
|
+
if (this.getOriginal()) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const newProps = this.getProps()
|
|
102
|
+
if (this.template == null) {
|
|
103
|
+
if (!this.isFetching) {
|
|
104
|
+
if (newProps.component != null && newProps.variant != null) {
|
|
105
|
+
this.isFetching = true
|
|
106
|
+
fetchComponent(newProps, (template) => {
|
|
107
|
+
this.template = template
|
|
108
|
+
this.isFetching = false
|
|
109
|
+
renderDOMContent(this, this.template, newProps.data)
|
|
110
|
+
}).catch(() => {
|
|
111
|
+
console.error(
|
|
112
|
+
`FEAAS: Could not fetch component: ${newProps.component}/${newProps.variant}/${newProps.version}`
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
renderDOMContent(this, this.template, newProps.data)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getProps(): CloudTemplate & { data: Datascopes } {
|
|
123
|
+
return {
|
|
124
|
+
data: this.getData(),
|
|
125
|
+
library: (this.getAttribute('library') || undefined) as string,
|
|
126
|
+
component: (this.getAttribute('component') || undefined) as string,
|
|
127
|
+
variant: (this.getAttribute('variant') || undefined) as string,
|
|
128
|
+
version: (this.getAttribute('version') || undefined) as string,
|
|
129
|
+
hostname: (this.getAttribute('hostname') || undefined) as string
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class HTMLFeaasStylesheet extends WebComponent {
|
|
135
|
+
static get observedAttributes() {
|
|
136
|
+
return ['library', 'version', 'hostname']
|
|
137
|
+
}
|
|
138
|
+
connected = false
|
|
139
|
+
connectedCallback() {
|
|
140
|
+
this.connected = true
|
|
141
|
+
this.render()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
attributeChangedCallback() {
|
|
145
|
+
if (this.connected) this.render()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
render() {
|
|
149
|
+
const props = this.getProps()
|
|
150
|
+
fetchStylesheet(props, (cssText) => {
|
|
151
|
+
this.innerHTML = '<style>' + cssText + '</style>'
|
|
152
|
+
}).catch((e) => {
|
|
153
|
+
console.error(`FEAAS: Could not fetch stylesheet for library ${props.library}`)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getProps() {
|
|
158
|
+
return {
|
|
159
|
+
library: (this.getAttribute('library') || undefined) as string,
|
|
160
|
+
version: (this.getAttribute('version') || undefined) as string,
|
|
161
|
+
hostname: (this.getAttribute('hostname') || undefined) as string
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (typeof window != 'undefined' && !window.customElements.get('feaas-stylesheet')) {
|
|
167
|
+
window.customElements.define('feaas-stylesheet', HTMLFeaasStylesheet)
|
|
168
|
+
window.customElements.define('feaas-component', HTMLFeaasComponent)
|
|
169
|
+
}
|