From 643983665f31bedbec87a1c66a8a48645df656ad Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Wed, 12 Aug 2020 01:34:32 +0200 Subject: [PATCH 01/23] grid definition --- core/src/services/routing.js | 20 +++++++++++++++++++- core/src/services/web-components.js | 29 ++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/core/src/services/routing.js b/core/src/services/routing.js index 6739c0bcbd..32f77ec22e 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -178,7 +178,7 @@ class RoutingClass { ); const viewUrl = nodeObject.viewUrl || ''; - if (!viewUrl) { + if (!viewUrl && !nodeObject.grid) { const defaultChildNode = await RoutingHelpers.getDefaultChildNode( pathData, async (node, ctx) => { @@ -327,6 +327,11 @@ class RoutingClass { iContainer.classList.add('lui-webComponent'); } this.navigateWebComponent(config, component, iframeElement, nodeObject, iContainer); + } else if (nodeObject.grid) { + if (iContainer) { + iContainer.classList.add('lui-webComponent'); + } + this.navigateWebComponentGrid(config, component, iframeElement, nodeObject, iContainer); } else { if (iContainer) { iContainer.classList.remove('lui-webComponent'); @@ -491,6 +496,19 @@ class RoutingClass { WebComponentService.renderWebComponent(componentData.viewUrl, wc_container, componentData.context); } + + + + navigateWebComponentGrid(config, component, node, navNode, iframeContainer) { + const componentData = component.get(); + const wc_container = document.querySelector('.wcContainer'); + + while (wc_container.lastChild) { + wc_container.lastChild.remove(); + } + + WebComponentService.renderWebComponentGrid(navNode, wc_container, componentData.context); + } } export const Routing = new RoutingClass(); diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index 0d9e8acdd1..a59ae67987 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -35,7 +35,9 @@ class WebComponentSvcClass { return new Promise((resolve, reject) => { this.dynamicImport(viewUrl).then(module => { try { - window.customElements.define(wc_id, module.default); + if(!window.customElements.get(wc_id)) { + window.customElements.define(wc_id, module.default); + } resolve(); } catch(e) { reject(e); @@ -65,6 +67,31 @@ class WebComponentSvcClass { } } } + + renderWebComponentGrid(navNode, wc_container, context) { + const containerClass = '__lui_grid_' + new Date().getTime(); + const gridCnt = document.createElement('div'); + gridCnt.classList.add(containerClass); + gridCnt.innerHTML = /*html*/` + + `; + navNode.grid.children.forEach(wc=>{ + const ctx = {...componentData.context, ...wc.context}; + const gridItemCnt = document.createElement('div'); + const grid = wc.grid || {}; + gridItemCnt.setAttribute('style', `grid-row: ${grid.row || 'auto'}; grid-column: ${grid.column || 'auto'}`); + gridCnt.appendChild(gridItemCnt); + WebComponentService.renderWebComponent(wc.viewUrl, gridItemCnt, ctx); + }); + wc_container.appendChild(gridCnt); + } } export const WebComponentService = new WebComponentSvcClass(); From 35f499e86a9ab4bdeea4c3c410d24bd24d23c63d Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Wed, 12 Aug 2020 13:51:03 +0200 Subject: [PATCH 02/23] context merge --- core/src/services/web-components.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index a59ae67987..e2eea04876 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -9,11 +9,13 @@ class WebComponentSvcClass { } /** Creates a web component with tagname wc_id and adds it to wc_container*/ - attachWC(wc_id, wc_container, ctx) { - const wc = document.createElement(wc_id); - wc.context = ctx; - wc.luigi = window.Luigi; - wc_container.appendChild(wc); + attachWC(wc_id, wcItemContainer, wc_container, ctx) { + if(wc_container && wc_container.contains(wcItemContainer)) { + const wc = document.createElement(wc_id); + wc.context = ctx; + wc.luigi = window.Luigi; + wcItemContainer.appendChild(wc); + } } /** Generates a unique web component id (tagname) based on the viewUrl @@ -51,18 +53,20 @@ class WebComponentSvcClass { */ renderWebComponent(viewUrl, wc_container, context) { const wc_id = this.generateWCId(viewUrl); + const wcItemCnt = document.createElement('div'); + wc_container.appendChild(wcItemCnt); if (window.customElements.get(wc_id)) { - this.attachWC(wc_id, wc_container, context); + this.attachWC(wc_id, wcItemCnt, wc_container, context); } else { /** Custom import function, if defined */ if(window.luigiWCFn) { - window.luigiWCFn(viewUrl, wc_id, wc_container, () => { - this.attachWC(wc_id, wc_container, context); + window.luigiWCFn(viewUrl, wc_id, wcItemCnt, () => { + this.attachWC(wc_id, wcItemCnt, wc_container, context); }); } else { this.registerWCFromUrl(viewUrl, wc_id).then(() => { - this.attachWC(wc_id, wc_container, context); + this.attachWC(wc_id, wcItemCnt, wc_container, context); }); } } @@ -83,7 +87,7 @@ class WebComponentSvcClass { `; navNode.grid.children.forEach(wc=>{ - const ctx = {...componentData.context, ...wc.context}; + const ctx = {...context, ...wc.context}; const gridItemCnt = document.createElement('div'); const grid = wc.grid || {}; gridItemCnt.setAttribute('style', `grid-row: ${grid.row || 'auto'}; grid-column: ${grid.column || 'auto'}`); From e315bdfffe8d9d32f69e24b890e690b9c30cc619 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 24 Aug 2020 13:06:43 +0200 Subject: [PATCH 03/23] eventing and responsiveness --- core/src/services/web-components.js | 96 ++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index e55520ad7e..983327b0f8 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -9,11 +9,24 @@ class WebComponentSvcClass { } /** Creates a web component with tagname wc_id and adds it to wcItemContainer, if attached to wc_container*/ - attachWC(wc_id, wcItemContainer, wc_container, ctx) { + attachWC(wc_id, wcItemContainer, wc_container, ctx, viewUrl, nodeId) { if(wc_container && wc_container.contains(wcItemContainer)) { const wc = document.createElement(wc_id); - wc.context = ctx; - wc.luigi = window.Luigi; + if(nodeId) { + wc.setAttribute('nodeId', nodeId); + } + const luigiObj = Object.assign({ + publishEvent : (ev) => { + wc_container.eventBus.onPublishEvent(ev, nodeId, wc_id); + } + }, window.Luigi); + if(wc.__postProcess) { + const url = new URL('./', viewUrl); + wc.__postProcess(ctx, luigiObj, url.origin + url.pathname); + } else { + wc.context = ctx; + wc.luigi = luigiObj; + } wcItemContainer.appendChild(wc); } } @@ -51,22 +64,22 @@ class WebComponentSvcClass { /** Adds a web component defined by viewUrl to the wc_container and sets the node context. * If the web component is not defined yet, it gets imported. */ - renderWebComponent(viewUrl, wc_container, context) { + renderWebComponent(viewUrl, wc_container, context, nodeId) { const wc_id = this.generateWCId(viewUrl); const wcItemCnt = document.createElement('div'); wc_container.appendChild(wcItemCnt); if (window.customElements.get(wc_id)) { - this.attachWC(wc_id, wcItemCnt, wc_container, context); + this.attachWC(wc_id, wcItemCnt, wc_container, context, viewUrl, nodeId); } else { /** Custom import function, if defined */ if(window.luigiWCFn) { window.luigiWCFn(viewUrl, wc_id, wcItemCnt, () => { - this.attachWC(wc_id, wcItemCnt, wc_container, context); + this.attachWC(wc_id, wcItemCnt, wc_container, context, viewUrl, nodeId); }); } else { this.registerWCFromUrl(viewUrl, wc_id).then(() => { - this.attachWC(wc_id, wcItemCnt, wc_container, context); + this.attachWC(wc_id, wcItemCnt, wc_container, context, viewUrl, nodeId); }); } } @@ -76,6 +89,32 @@ class WebComponentSvcClass { const containerClass = '__lui_grid_' + new Date().getTime(); const gridCnt = document.createElement('div'); gridCnt.classList.add(containerClass); + let mediaQueries = ''; + + if(navNode.grid.layouts) { + navNode.grid.layouts.forEach(el => { + if(el.minWidth || el.maxWidth) { + let mq = '@media only screen '; + if(el.minWidth) { + mq += `and (min-width: ${el.minWidth}px)` + } + if(el.maxWidth) { + mq += `and (max-width: ${el.maxWidth}px)` + } + + mq += `{ + .${containerClass} { + grid-template-columns: ${el.columns || 'auto'}; + grid-template-rows: ${el.rows || 'auto'}; + grid-gap: ${el.gap || '0'}; + } + } + `; + mediaQueries += mq; + } + }); + } + gridCnt.innerHTML = /*html*/` `; - navNode.grid.children.forEach(wc=>{ + const ebListeners = {}; + gridCnt.eventBus = { + listeners: ebListeners, + onPublishEvent: (event, srcNodeId, wcId) => { + //console.log(wcId, ':', srcNodeId, "has published", event); + const listeners = ebListeners[srcNodeId + '.' + event.type]; + //console.log('Searching for ', srcNodeId + '.' + event.type, 'in', ebListeners); + if(listeners) { + console.log("found listeners"); + listeners.forEach(listenerInfo => { + const target = gridCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); + target.dispatchEvent(new CustomEvent(listenerInfo.action, + { + detail: listenerInfo.converter ? listenerInfo.converter(event.detail) : event.detail + })); + }); + } + } + }; + navNode.grid.children.forEach((wc, index)=>{ const ctx = {...context, ...wc.context}; const gridItemCnt = document.createElement('div'); + gridItemCnt.eventBus = gridCnt.eventBus; const grid = wc.grid || {}; gridItemCnt.setAttribute('style', `grid-row: ${grid.row || 'auto'}; grid-column: ${grid.column || 'auto'}`); gridCnt.appendChild(gridItemCnt); - WebComponentService.renderWebComponent(wc.viewUrl, gridItemCnt, ctx); + const nodeId = wc.id ? wc.id : ('gen_' + index); + WebComponentService.renderWebComponent(wc.viewUrl, gridItemCnt, ctx, nodeId); + if(wc.eventListeners) { + wc.eventListeners.forEach(el => { + const evID = el.source + '.' + el.name; + const listenerList = ebListeners[evID]; + const listenerInfo = { + wcElementId: nodeId, + action: el.action, + converter: el.dataConverter + }; + if(listenerList) { + listenerList.push(listenerInfo); + } else { + ebListeners[evID] = [listenerInfo]; + } + }); + } }); wc_container.appendChild(gridCnt); } From b3f962928eb75b692d29b3493df0100ebd08e5b8 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 7 Sep 2020 09:40:59 +0200 Subject: [PATCH 04/23] compound renderers --- core/src/services/routing.js | 10 +- core/src/services/web-components.js | 104 ++++++--------- .../helpers/web-component-helpers.js | 126 ++++++++++++++++++ 3 files changed, 169 insertions(+), 71 deletions(-) create mode 100644 core/src/utilities/helpers/web-component-helpers.js diff --git a/core/src/services/routing.js b/core/src/services/routing.js index 32f77ec22e..23d4237a29 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -178,7 +178,7 @@ class RoutingClass { ); const viewUrl = nodeObject.viewUrl || ''; - if (!viewUrl && !nodeObject.grid) { + if (!viewUrl && !nodeObject.compound) { const defaultChildNode = await RoutingHelpers.getDefaultChildNode( pathData, async (node, ctx) => { @@ -327,11 +327,11 @@ class RoutingClass { iContainer.classList.add('lui-webComponent'); } this.navigateWebComponent(config, component, iframeElement, nodeObject, iContainer); - } else if (nodeObject.grid) { + } else if (nodeObject.compound) { if (iContainer) { iContainer.classList.add('lui-webComponent'); } - this.navigateWebComponentGrid(config, component, iframeElement, nodeObject, iContainer); + this.navigateWebComponentCompound(config, component, iframeElement, nodeObject, iContainer); } else { if (iContainer) { iContainer.classList.remove('lui-webComponent'); @@ -499,7 +499,7 @@ class RoutingClass { - navigateWebComponentGrid(config, component, node, navNode, iframeContainer) { + navigateWebComponentCompound(config, component, node, navNode, iframeContainer) { const componentData = component.get(); const wc_container = document.querySelector('.wcContainer'); @@ -507,7 +507,7 @@ class RoutingClass { wc_container.lastChild.remove(); } - WebComponentService.renderWebComponentGrid(navNode, wc_container, componentData.context); + WebComponentService.renderWebComponentCompound(navNode, wc_container, componentData.context); } } diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index 983327b0f8..df376869b5 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -1,3 +1,5 @@ +import {DefaultCompoundRenderer, resolveRenderer} from '../utilities/helpers/web-component-helpers'; + /** Methods for dealing with web components based micro frontend handling */ class WebComponentSvcClass { constructor() {} @@ -9,8 +11,8 @@ class WebComponentSvcClass { } /** Creates a web component with tagname wc_id and adds it to wcItemContainer, if attached to wc_container*/ - attachWC(wc_id, wcItemContainer, wc_container, ctx, viewUrl, nodeId) { - if(wc_container && wc_container.contains(wcItemContainer)) { + attachWC(wc_id, wcItemPlaceholder, wc_container, ctx, viewUrl, nodeId) { + if(wc_container && wc_container.contains(wcItemPlaceholder)) { const wc = document.createElement(wc_id); if(nodeId) { wc.setAttribute('nodeId', nodeId); @@ -27,7 +29,7 @@ class WebComponentSvcClass { wc.context = ctx; wc.luigi = luigiObj; } - wcItemContainer.appendChild(wc); + wc_container.replaceChild(wc, wcItemPlaceholder); } } @@ -66,95 +68,65 @@ class WebComponentSvcClass { */ renderWebComponent(viewUrl, wc_container, context, nodeId) { const wc_id = this.generateWCId(viewUrl); - const wcItemCnt = document.createElement('div'); - wc_container.appendChild(wcItemCnt); + const wcItemPlaceholder = document.createElement('div'); + wc_container.appendChild(wcItemPlaceholder); if (window.customElements.get(wc_id)) { - this.attachWC(wc_id, wcItemCnt, wc_container, context, viewUrl, nodeId); + this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); } else { /** Custom import function, if defined */ if(window.luigiWCFn) { window.luigiWCFn(viewUrl, wc_id, wcItemCnt, () => { - this.attachWC(wc_id, wcItemCnt, wc_container, context, viewUrl, nodeId); + this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); }); } else { this.registerWCFromUrl(viewUrl, wc_id).then(() => { - this.attachWC(wc_id, wcItemCnt, wc_container, context, viewUrl, nodeId); + this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); }); } } } - renderWebComponentGrid(navNode, wc_container, context) { - const containerClass = '__lui_grid_' + new Date().getTime(); - const gridCnt = document.createElement('div'); - gridCnt.classList.add(containerClass); - let mediaQueries = ''; - - if(navNode.grid.layouts) { - navNode.grid.layouts.forEach(el => { - if(el.minWidth || el.maxWidth) { - let mq = '@media only screen '; - if(el.minWidth) { - mq += `and (min-width: ${el.minWidth}px)` - } - if(el.maxWidth) { - mq += `and (max-width: ${el.maxWidth}px)` - } + renderWebComponentCompound(navNode, wc_container, context) { + let renderer; - mq += `{ - .${containerClass} { - grid-template-columns: ${el.columns || 'auto'}; - grid-template-rows: ${el.rows || 'auto'}; - grid-gap: ${el.gap || '0'}; - } - } - `; - mediaQueries += mq; - } - }); + if(navNode.compound.renderer) { + renderer = resolveRenderer(navNode.compound.renderer); } - gridCnt.innerHTML = /*html*/` - - `; + renderer = renderer || new DefaultCompoundRenderer(); + + const compoundCnt = renderer.createCompoundContainer(); + + const ebListeners = {}; - gridCnt.eventBus = { + compoundCnt.eventBus = { listeners: ebListeners, onPublishEvent: (event, srcNodeId, wcId) => { - //console.log(wcId, ':', srcNodeId, "has published", event); - const listeners = ebListeners[srcNodeId + '.' + event.type]; - //console.log('Searching for ', srcNodeId + '.' + event.type, 'in', ebListeners); - if(listeners) { - console.log("found listeners"); - listeners.forEach(listenerInfo => { - const target = gridCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); + const listeners = ebListeners[srcNodeId + '.' + event.type] || []; + listeners.push(...(ebListeners['*.' + event.type] || [])); + + listeners.forEach(listenerInfo => { + const target = compoundCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); + if(target) { target.dispatchEvent(new CustomEvent(listenerInfo.action, { detail: listenerInfo.converter ? listenerInfo.converter(event.detail) : event.detail })); - }); - } + } else { + console.debug("Could not find event target", listenerInfo); + } + }); } }; - navNode.grid.children.forEach((wc, index)=>{ + navNode.compound.children.forEach((wc, index)=>{ const ctx = {...context, ...wc.context}; - const gridItemCnt = document.createElement('div'); - gridItemCnt.eventBus = gridCnt.eventBus; - const grid = wc.grid || {}; - gridItemCnt.setAttribute('style', `grid-row: ${grid.row || 'auto'}; grid-column: ${grid.column || 'auto'}`); - gridCnt.appendChild(gridItemCnt); - const nodeId = wc.id ? wc.id : ('gen_' + index); - WebComponentService.renderWebComponent(wc.viewUrl, gridItemCnt, ctx, nodeId); + const compoundItemCnt = renderer.createCompoundItemContainer(wc.layoutConfig); + compoundItemCnt.eventBus = compoundCnt.eventBus; + renderer.attachCompoundItem(compoundCnt, compoundItemCnt); + + const nodeId = wc.id || ('gen_' + index); + WebComponentService.renderWebComponent(wc.viewUrl, compoundItemCnt, ctx, nodeId, true); if(wc.eventListeners) { wc.eventListeners.forEach(el => { const evID = el.source + '.' + el.name; @@ -172,7 +144,7 @@ class WebComponentSvcClass { }); } }); - wc_container.appendChild(gridCnt); + wc_container.appendChild(compoundCnt); } } diff --git a/core/src/utilities/helpers/web-component-helpers.js b/core/src/utilities/helpers/web-component-helpers.js new file mode 100644 index 0000000000..d9bda340a5 --- /dev/null +++ b/core/src/utilities/helpers/web-component-helpers.js @@ -0,0 +1,126 @@ +export class DefaultCompoundRenderer { + constructor(rendererObj) { + if(rendererObj) { + this.rendererObject = rendererObj; + this.config = rendererObj.config || {}; + } + } + + createCompoundContainer() { + return document.createElement('div'); + } + + createCompoundItemContainer() { + return document.createElement('div'); + } + + attachCompoundItem(compoundCnt, compoundItemCnt) { + compoundCnt.appendChild(compoundItemCnt); + } +} + +export class CustomCompoundRenderer extends DefaultCompoundRenderer { + constructor(rendererObj) { + super(rendererObj); + if(rendererObj.use && rendererObj.use.extends) { + this.superRenderer = resolveRenderer({ + use: rendererObj.use.extends, + config: rendererObj.config + }); + } + } + + createCompoundContainer() { + if(this.rendererObject.use.createCompoundContainer) { + return this.rendererObject.use.createCompoundContainer(this.config, this.superRenderer); + } else if (this.superRenderer) { + return this.superRenderer.createCompoundContainer(); + } + return super.createCompoundContainer(); + } + + createCompoundItemContainer(layoutConfig) { + if(this.rendererObject.use.createCompoundItemContainer) { + return this.rendererObject.use.createCompoundItemContainer(layoutConfig, this.config, this.superRenderer); + } else if (this.superRenderer) { + return this.superRenderer.createCompoundItemContainer(layoutConfig); + } + return super.createCompoundItemContainer(layoutConfig); + } + + attachCompoundItem(compoundCnt, compoundItemCnt) { + if(this.rendererObject.use.attachCompoundItem) { + this.rendererObject.use.attachCompoundItem(compoundCnt, compoundItemCnt, this.superRenderer); + } else if (this.superRenderer) { + this.superRenderer.attachCompoundItem(compoundCnt, compoundItemCnt); + } else { + super.attachCompoundItem(compoundCnt, compoundItemCnt); + } + } +} + +export class GridCompoundRenderer extends DefaultCompoundRenderer { + createCompoundContainer() { + const containerClass = '__lui_compound_' + new Date().getTime(); + const compoundCnt = document.createElement('div'); + compoundCnt.classList.add(containerClass); + let mediaQueries = ''; + + if(this.config.layouts) { + this.config.layouts.forEach(el => { + if(el.minWidth || el.maxWidth) { + let mq = '@media only screen '; + if(el.minWidth) { + mq += `and (min-width: ${el.minWidth}px)` + } + if(el.maxWidth) { + mq += `and (max-width: ${el.maxWidth}px)` + } + + mq += `{ + .${containerClass} { + grid-template-columns: ${el.columns || 'auto'}; + grid-template-rows: ${el.rows || 'auto'}; + grid-gap: ${el.gap || '0'}; + } + } + `; + mediaQueries += mq; + } + }); + } + + compoundCnt.innerHTML = /*html*/` + + `; + return compoundCnt; + } + + createCompoundItemContainer(layoutConfig) { + const config = layoutConfig || {}; + const compoundItemCnt = document.createElement('div'); + compoundItemCnt.setAttribute('style', `grid-row: ${config.row || 'auto'}; grid-column: ${config.column || 'auto'}`); + return compoundItemCnt; + } +} + +export const resolveRenderer = (renderer) => { + const rendererDef = renderer.use; + if(rendererDef === 'grid') { + return new GridCompoundRenderer(renderer); + } else if(rendererDef.createCompoundContainer + || rendererDef.createCompoundItemContainer + || rendererDef.attachCompoundItem) { + return new CustomCompoundRenderer(renderer); + } + return new DefaultCompoundRenderer(renderer); +}; From a0bb4146e13d43c6f6892bcb4a7463f07b2f3ebc Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 7 Sep 2020 09:44:58 +0200 Subject: [PATCH 05/23] luigielement enhancements --- client/src/luigi-element.js | 96 ++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/client/src/luigi-element.js b/client/src/luigi-element.js index 76683411ac..4fab8081b1 100644 --- a/client/src/luigi-element.js +++ b/client/src/luigi-element.js @@ -3,11 +3,75 @@ */ export class LuigiElement extends HTMLElement { constructor() { - super(); + super(); + this._shadowRoot = this.attachShadow({ mode: 'closed', delegatesFocus: false }); + this.__initialized = false; + } + + __postProcess(ctx, luigi, module_location_path) { + this.luigi = luigi; + this.context = ctx; const template = document.createElement('template'); - template.innerHTML = this.render ? this.render() : ''; - this._shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: false }); - this._shadowRoot.appendChild(template.content.cloneNode(true)); + template.innerHTML = this.render(ctx); + const attCnt = () => { + this._shadowRoot.appendChild(template.content.cloneNode(true)); + Reflect.ownKeys(Reflect.getPrototypeOf(this)).forEach(el=>{ + if(el.startsWith('$_')) { + console.log(el); + this._shadowRoot[el] = this[el].bind(this); + } + }); + const elementsWithIds = this._shadowRoot.querySelectorAll('[id]'); + if(elementsWithIds) { + elementsWithIds.forEach(el => { + this['$' + el.getAttribute('id')] = el; + }) + } + this.afterInit(ctx); + this.__initialized = true; + } + if(this.luigiConfig && this.luigiConfig.styleSources && this.luigiConfig.styleSources.length > 0) { + let nr_styles = this.luigiConfig.styleSources.length; + const loadStylesSync = this.luigiConfig.loadStylesSync; + const afterLoadOrError = () => { + nr_styles--; + if(nr_styles < 1) { + attCnt(); + } + }; + + this.luigiConfig.styleSources.forEach((element, index) => { + const link = document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('href', module_location_path + element); + if(loadStylesSync) { + link.addEventListener('load', afterLoadOrError); + link.addEventListener('error', afterLoadOrError); + } + this._shadowRoot.appendChild(link); + }); + if(!loadStylesSync) { + attCnt(); + } + } else { + attCnt(); + } + } + + afterInit(ctx) { + return; + } + + render(ctx) { + return ''; + } + + update() { + return; + } + + onContextUpdate(ctx) { + return; } /** @@ -25,10 +89,15 @@ export class LuigiElement extends HTMLElement { * @private */ set context(ctx) { - if(this.onContextUpdate) { + this.__lui_ctx = ctx; + if(this.__initialized) { this.onContextUpdate(ctx); + this.attributeChangedCallback(); } - this.attributeChangedCallback(); + } + + get context() { + return this.__lui_ctx; } /** @@ -37,9 +106,7 @@ export class LuigiElement extends HTMLElement { * @private */ attributeChangedCallback(name, oldVal, newVal) { - if (this.update) { - this.update(); - } + this.update(); } } @@ -50,6 +117,13 @@ export class LuigiElement extends HTMLElement { * @param {String} literal The literal to process. * @returns {String} Returns the processed literal. */ -export function html(literal) { - return literal; +export function html(literal, ...keys) { + let html = ''; + literal.forEach((el,index)=>{ + html += el; + if(index < keys.length && keys[index] !== undefined && keys[index] !== null) { + html += keys[index]; + } + }); + return html.replace(/\$\_/gi, 'this.getRootNode().$_'); } From d7d4af62d83391461e793cc9c47b3f52c70fb104 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Thu, 24 Sep 2020 13:34:14 +0200 Subject: [PATCH 06/23] nested --- core/src/services/routing.js | 8 +- core/src/services/web-components.js | 119 ++++++++++++++++------------ 2 files changed, 74 insertions(+), 53 deletions(-) diff --git a/core/src/services/routing.js b/core/src/services/routing.js index 06f41bb351..7d94897867 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -330,16 +330,16 @@ class RoutingClass { Navigation.onNodeChange(previousNode, currentNode); } } - if (nodeObject.webcomponent) { + if (nodeObject.compound) { if (iContainer) { iContainer.classList.add('lui-webComponent'); } - this.navigateWebComponent(config, component, iframeElement, nodeObject, iContainer); - } else if (nodeObject.compound) { + this.navigateWebComponentCompound(config, component, iframeElement, nodeObject, iContainer); + } else if (nodeObject.webcomponent) { if (iContainer) { iContainer.classList.add('lui-webComponent'); } - this.navigateWebComponentCompound(config, component, iframeElement, nodeObject, iContainer); + this.navigateWebComponent(config, component, iframeElement, nodeObject, iContainer); } else { if (iContainer) { iContainer.classList.remove('lui-webComponent'); diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index df376869b5..d0fccde03b 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -87,64 +87,85 @@ class WebComponentSvcClass { } } + createCompoundContainerAsync(renderer) { + return new Promise(resolve => { + if(renderer.viewUrl) { + const wc_id = this.generateWCId(renderer.viewUrl); + this.registerWCFromUrl(renderer.viewUrl, wc_id).then(()=>{ + resolve(document.createElement(wc_id)); + }); + } else { + resolve(renderer.createCompoundContainer()); + } + }); + } + renderWebComponentCompound(navNode, wc_container, context) { let renderer; - if(navNode.compound.renderer) { + if(navNode.webcomponent && navNode.viewUrl) { + renderer = new DefaultCompoundRenderer(); + renderer.viewUrl = navNode.viewUrl; + renderer.createCompoundItemContainer = (layoutConfig) => { + var cnt = document.createElement('div'); + cnt.setAttribute('slot', layoutConfig.slot); + return cnt; + }; + } else if(navNode.compound.renderer) { renderer = resolveRenderer(navNode.compound.renderer); } renderer = renderer || new DefaultCompoundRenderer(); - const compoundCnt = renderer.createCompoundContainer(); - - - const ebListeners = {}; - compoundCnt.eventBus = { - listeners: ebListeners, - onPublishEvent: (event, srcNodeId, wcId) => { - const listeners = ebListeners[srcNodeId + '.' + event.type] || []; - listeners.push(...(ebListeners['*.' + event.type] || [])); - - listeners.forEach(listenerInfo => { - const target = compoundCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); - if(target) { - target.dispatchEvent(new CustomEvent(listenerInfo.action, - { - detail: listenerInfo.converter ? listenerInfo.converter(event.detail) : event.detail - })); - } else { - console.debug("Could not find event target", listenerInfo); - } - }); - } - }; - navNode.compound.children.forEach((wc, index)=>{ - const ctx = {...context, ...wc.context}; - const compoundItemCnt = renderer.createCompoundItemContainer(wc.layoutConfig); - compoundItemCnt.eventBus = compoundCnt.eventBus; - renderer.attachCompoundItem(compoundCnt, compoundItemCnt); - - const nodeId = wc.id || ('gen_' + index); - WebComponentService.renderWebComponent(wc.viewUrl, compoundItemCnt, ctx, nodeId, true); - if(wc.eventListeners) { - wc.eventListeners.forEach(el => { - const evID = el.source + '.' + el.name; - const listenerList = ebListeners[evID]; - const listenerInfo = { - wcElementId: nodeId, - action: el.action, - converter: el.dataConverter - }; - if(listenerList) { - listenerList.push(listenerInfo); - } else { - ebListeners[evID] = [listenerInfo]; - } - }); - } + //const compoundCnt = renderer.createCompoundContainer(); + this.createCompoundContainerAsync(renderer).then(compoundCnt => { + const ebListeners = {}; + compoundCnt.eventBus = { + listeners: ebListeners, + onPublishEvent: (event, srcNodeId, wcId) => { + const listeners = ebListeners[srcNodeId + '.' + event.type] || []; + listeners.push(...(ebListeners['*.' + event.type] || [])); + + listeners.forEach(listenerInfo => { + const target = compoundCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); + if(target) { + target.dispatchEvent(new CustomEvent(listenerInfo.action, + { + detail: listenerInfo.converter ? listenerInfo.converter(event.detail) : event.detail + })); + } else { + console.debug("Could not find event target", listenerInfo); + } + }); + } + }; + navNode.compound.children.forEach((wc, index)=>{ + const ctx = {...context, ...wc.context}; + const compoundItemCnt = renderer.createCompoundItemContainer(wc.layoutConfig); + compoundItemCnt.eventBus = compoundCnt.eventBus; + renderer.attachCompoundItem(compoundCnt, compoundItemCnt); + + const nodeId = wc.id || ('gen_' + index); + WebComponentService.renderWebComponent(wc.viewUrl, compoundItemCnt, ctx, nodeId, true); + if(wc.eventListeners) { + wc.eventListeners.forEach(el => { + const evID = el.source + '.' + el.name; + const listenerList = ebListeners[evID]; + const listenerInfo = { + wcElementId: nodeId, + action: el.action, + converter: el.dataConverter + }; + if(listenerList) { + listenerList.push(listenerInfo); + } else { + ebListeners[evID] = [listenerInfo]; + } + }); + } + }); + wc_container.appendChild(compoundCnt); }); - wc_container.appendChild(compoundCnt); } } From b8bd5ac03fdfda5d8d0f847a36bb7466fc16c079 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Fri, 9 Oct 2020 16:30:55 +0200 Subject: [PATCH 07/23] update --- core/src/services/routing.js | 4 +++- core/src/services/web-components.js | 26 +++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/core/src/services/routing.js b/core/src/services/routing.js index 7d94897867..56be7ae546 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -504,7 +504,9 @@ class RoutingClass { WebComponentService.renderWebComponent( componentData.viewUrl, wc_container, - componentData.context + componentData.context, + undefined, + navNode ); } diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index d0fccde03b..86feed58c2 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -19,7 +19,9 @@ class WebComponentSvcClass { } const luigiObj = Object.assign({ publishEvent : (ev) => { - wc_container.eventBus.onPublishEvent(ev, nodeId, wc_id); + if(wc_container.eventBus) { + wc_container.eventBus.onPublishEvent(ev, nodeId, wc_id); + } } }, window.Luigi); if(wc.__postProcess) { @@ -66,8 +68,9 @@ class WebComponentSvcClass { /** Adds a web component defined by viewUrl to the wc_container and sets the node context. * If the web component is not defined yet, it gets imported. */ - renderWebComponent(viewUrl, wc_container, context, nodeId) { - const wc_id = this.generateWCId(viewUrl); + renderWebComponent(viewUrl, wc_container, context, nodeId, node) { + const wc_id = (node.webcomponent && node.webcomponent.tagName) ? + node.webcomponent.tagName : this.generateWCId(viewUrl); const wcItemPlaceholder = document.createElement('div'); wc_container.appendChild(wcItemPlaceholder); @@ -79,6 +82,17 @@ class WebComponentSvcClass { window.luigiWCFn(viewUrl, wc_id, wcItemCnt, () => { this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); }); + } else if (node.webcomponent && node.webcomponent.selfRegistered) { + let scriptTag = document.createElement('script'); + scriptTag.setAttribute('src', viewUrl); + if(node.webcomponent.type === 'module') { + scriptTag.setAttribute('type', 'module'); + } + scriptTag.setAttribute('defer', true); + scriptTag.addEventListener('load', ()=>{ + this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); + }); + document.body.appendChild(scriptTag); } else { this.registerWCFromUrl(viewUrl, wc_id).then(() => { this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); @@ -108,7 +122,9 @@ class WebComponentSvcClass { renderer.viewUrl = navNode.viewUrl; renderer.createCompoundItemContainer = (layoutConfig) => { var cnt = document.createElement('div'); - cnt.setAttribute('slot', layoutConfig.slot); + if(layoutConfig && layoutConfig.slot) { + cnt.setAttribute('slot', layoutConfig.slot); + } return cnt; }; } else if(navNode.compound.renderer) { @@ -146,7 +162,7 @@ class WebComponentSvcClass { renderer.attachCompoundItem(compoundCnt, compoundItemCnt); const nodeId = wc.id || ('gen_' + index); - WebComponentService.renderWebComponent(wc.viewUrl, compoundItemCnt, ctx, nodeId, true); + WebComponentService.renderWebComponent(wc.viewUrl, compoundItemCnt, ctx, nodeId, wc); if(wc.eventListeners) { wc.eventListeners.forEach(el => { const evID = el.source + '.' + el.name; From 45ef58b32fc089505a80a06656b0cef6017d047c Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 12 Oct 2020 10:42:28 +0200 Subject: [PATCH 08/23] added nesting wc to ev bus --- core/src/services/web-components.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index 86feed58c2..52f3cad8ad 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -133,7 +133,6 @@ class WebComponentSvcClass { renderer = renderer || new DefaultCompoundRenderer(); - //const compoundCnt = renderer.createCompoundContainer(); this.createCompoundContainerAsync(renderer).then(compoundCnt => { const ebListeners = {}; compoundCnt.eventBus = { @@ -143,7 +142,7 @@ class WebComponentSvcClass { listeners.push(...(ebListeners['*.' + event.type] || [])); listeners.forEach(listenerInfo => { - const target = compoundCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); + const target = listenerInfo.wcElement || compoundCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); if(target) { target.dispatchEvent(new CustomEvent(listenerInfo.action, { @@ -181,6 +180,24 @@ class WebComponentSvcClass { } }); wc_container.appendChild(compoundCnt); + + // listener for nesting wc + if(navNode.compound.eventListeners) { + navNode.compound.eventListeners.forEach(el => { + const evID = el.source + '.' + el.name; + const listenerList = ebListeners[evID]; + const listenerInfo = { + wcElement: compoundCnt, + action: el.action, + converter: el.dataConverter + }; + if(listenerList) { + listenerList.push(listenerInfo); + } else { + ebListeners[evID] = [listenerInfo]; + } + }); + } }); } } From 5c1ba3d06ab70ead17a3ad8be1abb8cb1b23f3b9 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Tue, 13 Oct 2020 14:22:39 +0200 Subject: [PATCH 09/23] added some comments to luigi element --- client/src/luigi-element.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/client/src/luigi-element.js b/client/src/luigi-element.js index 4fab8081b1..1350a680d3 100644 --- a/client/src/luigi-element.js +++ b/client/src/luigi-element.js @@ -8,6 +8,10 @@ export class LuigiElement extends HTMLElement { this.__initialized = false; } + /** + * Invoked by luigi core if present, internal, don't override. + * @private + */ __postProcess(ctx, luigi, module_location_path) { this.luigi = luigi; this.context = ctx; @@ -58,18 +62,37 @@ export class LuigiElement extends HTMLElement { } } + /** + * Override to execute logic after initialization of the web component, i.e. + * after internal rendering and all context data set. + * + * @param {*} ctx The context object passed by luigi core + */ afterInit(ctx) { return; } + /** + * Override to return the html template string defining the web component view. + * + * @param {*} ctx The context object passed by luigi core + */ render(ctx) { return ''; } + /** + * Override to execute logic after an attribute of this web component has changed. + */ update() { return; } + /** + * Override to execute logic when a new context object is set. + * + * @param {*} ctx The new context object passed by luigi core + */ onContextUpdate(ctx) { return; } From 044af010e8f0c44cd0aaf9db481342dde189a9e5 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Wed, 14 Oct 2020 11:54:50 +0200 Subject: [PATCH 10/23] check for allowed wc domains --- core/src/services/web-components.js | 83 +++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index 52f3cad8ad..7f5abefea9 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -1,4 +1,5 @@ import {DefaultCompoundRenderer, resolveRenderer} from '../utilities/helpers/web-component-helpers'; +import { LuigiConfig } from '../core-api'; /** Methods for dealing with web components based micro frontend handling */ class WebComponentSvcClass { @@ -52,19 +53,74 @@ class WebComponentSvcClass { * returns a promise that gets resolved after successfull import */ registerWCFromUrl(viewUrl, wc_id) { return new Promise((resolve, reject) => { - this.dynamicImport(viewUrl).then(module => { - try { - if(!window.customElements.get(wc_id)) { - window.customElements.define(wc_id, module.default); + if(this.checkWCUrl(viewUrl)) { + this.dynamicImport(viewUrl).then(module => { + try { + if(!window.customElements.get(wc_id)) { + window.customElements.define(wc_id, module.default); + } + resolve(); + } catch(e) { + reject(e); } - resolve(); - } catch(e) { - reject(e); - } - }).catch(err => reject(err)); + }).catch(err => reject(err)); + } else { + console.warn(`View URL '${viewUrl}' not allowed to be included`); + reject(`View URL '${viewUrl}' not allowed`); + } }); } + includeSelfRegisteredWCFromUrl(node, viewUrl, onload) { + if(this.checkWCUrl(viewUrl)) { + /** Append reg function to luigi object if not present */ + if(!window.Luigi._registerWebcomponent) { + window.Luigi._registerWebcomponent = (srcString, el) => { + window.customElements.define(this.generateWCId(srcString), el); + } + } + + let scriptTag = document.createElement('script'); + scriptTag.setAttribute('src', viewUrl); + if(node.webcomponent.type === 'module') { + scriptTag.setAttribute('type', 'module'); + } + scriptTag.setAttribute('defer', true); + scriptTag.addEventListener('load', ()=>{ + onload(); + }); + document.body.appendChild(scriptTag); + } else { + console.warn(`View URL '${viewUrl}' not allowed to be included`); + } + } + + checkWCUrl(url) { + if (url.indexOf('://') > 0 || url.trim().indexOf('//') === 0 ) { + const ur = new URL(url); + if(ur.host === window.location.host) { + return true; // same host is okay + } + + const valids = LuigiConfig.getConfigValue('navigation.validWebcomponentUrls'); + if(valids && valids.length > 0) { + for(let el of valids) { + try { + if(new RegExp(el).test(url)) { + return true; + } + + } catch (e) { + console.error(e); + } + }; + } + return false; + } + // relative URL is okay + return true; + } + /** Adds a web component defined by viewUrl to the wc_container and sets the node context. * If the web component is not defined yet, it gets imported. */ @@ -83,16 +139,9 @@ class WebComponentSvcClass { this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); }); } else if (node.webcomponent && node.webcomponent.selfRegistered) { - let scriptTag = document.createElement('script'); - scriptTag.setAttribute('src', viewUrl); - if(node.webcomponent.type === 'module') { - scriptTag.setAttribute('type', 'module'); - } - scriptTag.setAttribute('defer', true); - scriptTag.addEventListener('load', ()=>{ + this.includeSelfRegisteredWCFromUrl(node, viewUrl, ()=>{ this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); }); - document.body.appendChild(scriptTag); } else { this.registerWCFromUrl(viewUrl, wc_id).then(() => { this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); From 89034c977ece3f5fec897746932ac6b6717de386 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Wed, 14 Oct 2020 13:39:45 +0200 Subject: [PATCH 11/23] comments --- core/src/services/web-components.js | 29 +++++++++++++++++++ .../helpers/web-component-helpers.js | 25 ++++++++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index 7f5abefea9..ebd6cb2d47 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -71,6 +71,14 @@ class WebComponentSvcClass { }); } + /** + * Handles the import of self registered web component bundles, i.e. the web component + * is added to the customElements registry by the bundle code rather than by luigi. + * + * @param {*} node the corresponding navigation node + * @param {*} viewUrl the source of the wc bundle + * @param {*} onload callback function executed after script attached and loaded + */ includeSelfRegisteredWCFromUrl(node, viewUrl, onload) { if(this.checkWCUrl(viewUrl)) { /** Append reg function to luigi object if not present */ @@ -95,6 +103,13 @@ class WebComponentSvcClass { } } + + /** + * Checks if a url is allowed to be included, based on 'navigation.validWebcomponentUrls' in luigi config. + * Returns true, if allowed. + * + * @param {*} url the url string to check + */ checkWCUrl(url) { if (url.indexOf('://') > 0 || url.trim().indexOf('//') === 0 ) { const ur = new URL(url); @@ -150,6 +165,12 @@ class WebComponentSvcClass { } } + /** + * Creates a compound container according to the given renderer. + * Returns a promise that gets resolved with the created container DOM element. + * + * @param {DefaultCompoundRenderer} renderer + */ createCompoundContainerAsync(renderer) { return new Promise(resolve => { if(renderer.viewUrl) { @@ -163,6 +184,14 @@ class WebComponentSvcClass { }); } + /** + * Responsible for rendering web component compounds based on a renderer or a nesting + * micro frontend. + * + * @param {*} navNode the navigation node defining the compound + * @param {*} wc_container the web component container dom element + * @param {*} context the luigi node context + */ renderWebComponentCompound(navNode, wc_container, context) { let renderer; diff --git a/core/src/utilities/helpers/web-component-helpers.js b/core/src/utilities/helpers/web-component-helpers.js index d9bda340a5..782000ec40 100644 --- a/core/src/utilities/helpers/web-component-helpers.js +++ b/core/src/utilities/helpers/web-component-helpers.js @@ -1,3 +1,6 @@ +/** + * Default compound renderer. + */ export class DefaultCompoundRenderer { constructor(rendererObj) { if(rendererObj) { @@ -19,6 +22,9 @@ export class DefaultCompoundRenderer { } } +/** + * Compound Renderer for custom rendering as defined in luigi config. + */ export class CustomCompoundRenderer extends DefaultCompoundRenderer { constructor(rendererObj) { super(rendererObj); @@ -59,6 +65,9 @@ export class CustomCompoundRenderer extends DefaultCompoundRenderer { } } +/** + * Compound Renderer for a css grid compound view. + */ export class GridCompoundRenderer extends DefaultCompoundRenderer { createCompoundContainer() { const containerClass = '__lui_compound_' + new Date().getTime(); @@ -113,14 +122,20 @@ export class GridCompoundRenderer extends DefaultCompoundRenderer { } } -export const resolveRenderer = (renderer) => { - const rendererDef = renderer.use; +/** + * Returns the compound renderer class for a given config. + * If no specific one is found, {DefaultCompoundRenderer} is returned. + * + * @param {*} rendererConfig the renderer config object defined in luigi config + */ +export const resolveRenderer = (rendererConfig) => { + const rendererDef = rendererConfig.use; if(rendererDef === 'grid') { - return new GridCompoundRenderer(renderer); + return new GridCompoundRenderer(rendererConfig); } else if(rendererDef.createCompoundContainer || rendererDef.createCompoundItemContainer || rendererDef.attachCompoundItem) { - return new CustomCompoundRenderer(renderer); + return new CustomCompoundRenderer(rendererConfig); } - return new DefaultCompoundRenderer(renderer); + return new DefaultCompoundRenderer(rendererConfig); }; From d9e91d51b9a7541fa03ed2753eb303796b53bd92 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Fri, 16 Oct 2020 00:55:04 +0200 Subject: [PATCH 12/23] tests --- core/src/services/routing.js | 1 - core/src/services/web-components.js | 107 +++---- .../helpers/web-component-helpers.js | 30 ++ core/test/services/web-components.spec.js | 262 +++++++++++++++++- 4 files changed, 319 insertions(+), 81 deletions(-) diff --git a/core/src/services/routing.js b/core/src/services/routing.js index c0a97bc063..1ba3f300b9 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -505,7 +505,6 @@ class RoutingClass { componentData.viewUrl, wc_container, componentData.context, - undefined, navNode ); } diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index ebd6cb2d47..65c49419a5 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -1,4 +1,4 @@ -import {DefaultCompoundRenderer, resolveRenderer} from '../utilities/helpers/web-component-helpers'; +import {DefaultCompoundRenderer, resolveRenderer, registerEventListeners} from '../utilities/helpers/web-component-helpers'; import { LuigiConfig } from '../core-api'; /** Methods for dealing with web components based micro frontend handling */ @@ -11,7 +11,9 @@ class WebComponentSvcClass { return __luigi_dyn_import(viewUrl); } - /** Creates a web component with tagname wc_id and adds it to wcItemContainer, if attached to wc_container*/ + /** Creates a web component with tagname wc_id and adds it to wcItemContainer, + * if attached to wc_container + */ attachWC(wc_id, wcItemPlaceholder, wc_container, ctx, viewUrl, nodeId) { if(wc_container && wc_container.contains(wcItemPlaceholder)) { const wc = document.createElement(wc_id); @@ -139,7 +141,7 @@ class WebComponentSvcClass { /** Adds a web component defined by viewUrl to the wc_container and sets the node context. * If the web component is not defined yet, it gets imported. */ - renderWebComponent(viewUrl, wc_container, context, nodeId, node) { + renderWebComponent(viewUrl, wc_container, context, node, nodeId) { const wc_id = (node.webcomponent && node.webcomponent.tagName) ? node.webcomponent.tagName : this.generateWCId(viewUrl); const wcItemPlaceholder = document.createElement('div'); @@ -150,7 +152,7 @@ class WebComponentSvcClass { } else { /** Custom import function, if defined */ if(window.luigiWCFn) { - window.luigiWCFn(viewUrl, wc_id, wcItemCnt, () => { + window.luigiWCFn(viewUrl, wc_id, wcItemPlaceholder, () => { this.attachWC(wc_id, wcItemPlaceholder, wc_container, context, viewUrl, nodeId); }); } else if (node.webcomponent && node.webcomponent.selfRegistered) { @@ -211,71 +213,44 @@ class WebComponentSvcClass { renderer = renderer || new DefaultCompoundRenderer(); - this.createCompoundContainerAsync(renderer).then(compoundCnt => { - const ebListeners = {}; - compoundCnt.eventBus = { - listeners: ebListeners, - onPublishEvent: (event, srcNodeId, wcId) => { - const listeners = ebListeners[srcNodeId + '.' + event.type] || []; - listeners.push(...(ebListeners['*.' + event.type] || [])); - - listeners.forEach(listenerInfo => { - const target = listenerInfo.wcElement || compoundCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); - if(target) { - target.dispatchEvent(new CustomEvent(listenerInfo.action, - { - detail: listenerInfo.converter ? listenerInfo.converter(event.detail) : event.detail - })); - } else { - console.debug("Could not find event target", listenerInfo); - } - }); - } - }; - navNode.compound.children.forEach((wc, index)=>{ - const ctx = {...context, ...wc.context}; - const compoundItemCnt = renderer.createCompoundItemContainer(wc.layoutConfig); - compoundItemCnt.eventBus = compoundCnt.eventBus; - renderer.attachCompoundItem(compoundCnt, compoundItemCnt); - - const nodeId = wc.id || ('gen_' + index); - WebComponentService.renderWebComponent(wc.viewUrl, compoundItemCnt, ctx, nodeId, wc); - if(wc.eventListeners) { - wc.eventListeners.forEach(el => { - const evID = el.source + '.' + el.name; - const listenerList = ebListeners[evID]; - const listenerInfo = { - wcElementId: nodeId, - action: el.action, - converter: el.dataConverter - }; - if(listenerList) { - listenerList.push(listenerInfo); - } else { - ebListeners[evID] = [listenerInfo]; - } - }); - } - }); - wc_container.appendChild(compoundCnt); + return new Promise((resolve) => { + this.createCompoundContainerAsync(renderer).then(compoundCnt => { + const ebListeners = {}; + compoundCnt.eventBus = { + listeners: ebListeners, + onPublishEvent: (event, srcNodeId, wcId) => { + const listeners = ebListeners[srcNodeId + '.' + event.type] || []; + listeners.push(...(ebListeners['*.' + event.type] || [])); - // listener for nesting wc - if(navNode.compound.eventListeners) { - navNode.compound.eventListeners.forEach(el => { - const evID = el.source + '.' + el.name; - const listenerList = ebListeners[evID]; - const listenerInfo = { - wcElement: compoundCnt, - action: el.action, - converter: el.dataConverter - }; - if(listenerList) { - listenerList.push(listenerInfo); - } else { - ebListeners[evID] = [listenerInfo]; + listeners.forEach(listenerInfo => { + const target = listenerInfo.wcElement || compoundCnt.querySelector('[nodeId=' + listenerInfo.wcElementId + ']'); + if(target) { + target.dispatchEvent(new CustomEvent(listenerInfo.action, + { + detail: listenerInfo.converter ? listenerInfo.converter(event.detail) : event.detail + })); + } else { + console.debug("Could not find event target", listenerInfo); + } + }); } + }; + navNode.compound.children.forEach((wc, index)=>{ + const ctx = {...context, ...wc.context}; + const compoundItemCnt = renderer.createCompoundItemContainer(wc.layoutConfig); + compoundItemCnt.eventBus = compoundCnt.eventBus; + renderer.attachCompoundItem(compoundCnt, compoundItemCnt); + + const nodeId = wc.id || ('gen_' + index); + this.renderWebComponent(wc.viewUrl, compoundItemCnt, ctx, wc, nodeId); + registerEventListeners(ebListeners, wc, nodeId); }); - } + wc_container.appendChild(compoundCnt); + + // listener for nesting wc + registerEventListeners(ebListeners, navNode.compound, undefined, compoundCnt); + resolve(compoundCnt); + }); }); } } diff --git a/core/src/utilities/helpers/web-component-helpers.js b/core/src/utilities/helpers/web-component-helpers.js index 782000ec40..c3dc826873 100644 --- a/core/src/utilities/helpers/web-component-helpers.js +++ b/core/src/utilities/helpers/web-component-helpers.js @@ -139,3 +139,33 @@ export const resolveRenderer = (rendererConfig) => { } return new DefaultCompoundRenderer(rendererConfig); }; + + +/** + * Registers event listeners defined at the navNode. + * + * @param {*} eventbusListeners a map of event listener arrays with event id as key + * @param {*} navNode the web component node configuration object + * @param {*} nodeId the web component node id + * @param {*} wcElement the web component element - optional + */ +export const registerEventListeners = (eventbusListeners, navNode, nodeId, wcElement) => { + if(navNode.eventListeners) { + navNode.eventListeners.forEach(el => { + const evID = el.source + '.' + el.name; + const listenerList = eventbusListeners[evID]; + const listenerInfo = { + wcElementId: nodeId, + wcElement: wcElement, + action: el.action, + converter: el.dataConverter + }; + + if(listenerList) { + listenerList.push(listenerInfo); + } else { + eventbusListeners[evID] = [listenerInfo]; + } + }); + } +} diff --git a/core/test/services/web-components.spec.js b/core/test/services/web-components.spec.js index 8259c7ee46..b2b4d925b5 100644 --- a/core/test/services/web-components.spec.js +++ b/core/test/services/web-components.spec.js @@ -4,8 +4,10 @@ const expect = chai.expect; const assert = chai.assert; import { WebComponentService } from '../../src/services/web-components'; +import { LuigiConfig } from '../../src/core-api'; +import { DefaultCompoundRenderer } from '../../src/utilities/helpers/web-component-helpers'; -describe('WebComponentService', function() { +describe.only('WebComponentService', function() { describe('generate web component id', function() { const someRandomString = 'dsfgljhbakjdfngb,mdcn vkjrzwero78to4 wfoasb f,asndbf'; @@ -24,7 +26,7 @@ describe('WebComponentService', function() { describe('attach web component', function() { const container = document.createElement('div'); - const itemContainer = document.createElement('div'); + const itemPlaceholder = document.createElement('div'); const ctx = { someValue: true}; before(()=>{ @@ -36,18 +38,18 @@ describe('WebComponentService', function() { }); it('check dom injection abort if container not attached', () => { - WebComponentService.attachWC('div', itemContainer, container, ctx); + WebComponentService.attachWC('div', itemPlaceholder, container, ctx); - expect(itemContainer.children.length).to.equal(0); + expect(container.children.length).to.equal(0); }); it('check dom injection', () => { - container.appendChild(itemContainer); - WebComponentService.attachWC('div', itemContainer, container, ctx); + container.appendChild(itemPlaceholder); + WebComponentService.attachWC('div', itemPlaceholder, container, ctx); - const expectedCmp = itemContainer.children[0]; + const expectedCmp = container.children[0]; expect(expectedCmp.context).to.equal(ctx); - expect(expectedCmp.luigi).to.equal(window.Luigi); + expect(expectedCmp.luigi.mario).to.equal('luigi'); }); }); @@ -63,9 +65,14 @@ describe('WebComponentService', function() { sb.stub(WebComponentService, 'dynamicImport').returns(new Promise((resolve, reject) => { resolve({ default: {} }); })); - window.customElements = { define: (id, clazz)=>{ - definedId = id; - }}; + window.customElements = { + define: (id, clazz)=>{ + definedId = id; + }, + get: (id) => { + return undefined; + } + }; WebComponentService.registerWCFromUrl('url', 'id').then(()=>{ expect(definedId).to.equal('id'); @@ -90,6 +97,15 @@ describe('WebComponentService', function() { done(); }); }); + + it('check reject due to not-allowed url', (done) => { + WebComponentService.registerWCFromUrl('http://luigi-project.io/mfe.js', 'id').then(()=>{ + assert(false, "should not be here"); + done(); + }).catch(err=>{ + done(); + }); + }); }); describe('render web component', function() { @@ -97,6 +113,7 @@ describe('WebComponentService', function() { const ctx = { someValue: true}; const viewUrl = 'someurl'; const sb = sinon.createSandbox(); + const node = {}; before(()=>{ window.Luigi = { mario: 'luigi', luigi: window.luigi }; @@ -114,6 +131,7 @@ describe('WebComponentService', function() { afterEach(()=>{ sb.restore(); + delete window.luigiWCFn; }); @@ -137,7 +155,7 @@ describe('WebComponentService', function() { done(); }); - WebComponentService.renderWebComponent(viewUrl, container, ctx); + WebComponentService.renderWebComponent(viewUrl, container, ctx, node); }); it('check invocation of custom function', (done) => { @@ -166,7 +184,7 @@ describe('WebComponentService', function() { cb(); } - WebComponentService.renderWebComponent(viewUrl, container, ctx); + WebComponentService.renderWebComponent(viewUrl, container, ctx, node); }); it('check creation and attachment of new wc', (done) => { @@ -193,7 +211,223 @@ describe('WebComponentService', function() { done(); }); - WebComponentService.renderWebComponent(viewUrl, container, ctx); + WebComponentService.renderWebComponent(viewUrl, container, ctx, node); + }); + }); + + describe('check valid wc url', function() { + const sb = sinon.createSandbox(); + + afterEach(()=>{ + sb.restore(); + }); + + it('check permission for relative and absolute urls from same domain', () => { + let relative1 = WebComponentService.checkWCUrl('/folder/sth.js'); + expect(relative1).to.be.true; + let relative2 = WebComponentService.checkWCUrl('folder/sth.js'); + expect(relative2).to.be.true; + let relative3 = WebComponentService.checkWCUrl('./folder/sth.js'); + expect(relative3).to.be.true; + + let absolute = WebComponentService.checkWCUrl(window.location.href + '/folder/sth.js'); + expect(absolute).to.be.true; + }); + + it('check permission and denial for urls based on config', () => { + sb.stub(LuigiConfig, 'getConfigValue').returns([ + 'https\:\/\/fiddle\.luigi\-project\.io\/.?', + 'https\:\/\/docs\.luigi\-project\.io\/.?' + ]); + + let valid1 = WebComponentService.checkWCUrl('https://fiddle.luigi-project.io/folder/sth.js'); + expect(valid1).to.be.true; + let valid2 = WebComponentService.checkWCUrl('https://docs.luigi-project.io/folder/sth.js'); + expect(valid2).to.be.true; + + let invalid1 = WebComponentService.checkWCUrl('http://fiddle.luigi-project.io/folder/sth.js'); + expect(invalid1).to.be.false; + let invalid2 = WebComponentService.checkWCUrl('https://slack.luigi-project.io/folder/sth.js'); + expect(invalid2).to.be.false; + }); + }); + + describe('check includeSelfRegisteredWCFromUrl', function() { + const sb = sinon.createSandbox(); + const node = { + webcomponent: { + selfRegistered: true + } + } + + before(()=>{ + window.Luigi = { mario: 'luigi', luigi: window.luigi }; + }); + + after(()=>{ + window.Luigi = window.Luigi.luigi; + }); + + afterEach(()=>{ + sb.restore(); + }); + + it('check if script tag is added', () => { + let element; + sb.stub(document.body, 'appendChild').callsFake((el) => { + element = el; + }); + + WebComponentService.includeSelfRegisteredWCFromUrl(node, '/mfe.js', () => {}); + expect(element.getAttribute('src')).to.equal('/mfe.js'); + }); + + it('check if script tag is not added for untrusted url', () => { + sb.spy(document.body, 'appendChild'); + WebComponentService.includeSelfRegisteredWCFromUrl(node, 'https://luigi-project.io/mfe.js', () => {}); + assert(document.body.appendChild.notCalled); + }); + }); + + describe('check createCompoundContainerAsync', function() { + const sb = sinon.createSandbox(); + + afterEach(()=>{ + sb.restore(); + }); + + it('check compound container created', (done) => { + let renderer = new DefaultCompoundRenderer(); + sb.spy(renderer); + WebComponentService.createCompoundContainerAsync(renderer).then(()=>{ + assert(renderer.createCompoundContainer.calledOnce, 'createCompoundContainer called once'); + done(); + }, e => { + assert(false, "should not be here"); + done(); + }); + }); + + it('check nesting mfe created', (done) => { + let renderer = new DefaultCompoundRenderer(); + renderer.viewUrl = 'mfe.js'; + sb.stub(WebComponentService, 'registerWCFromUrl').resolves(); + sb.spy(renderer); + WebComponentService.createCompoundContainerAsync(renderer).then(()=>{ + assert(renderer.createCompoundContainer.notCalled, 'createCompoundContainer should not be called'); + assert(WebComponentService.registerWCFromUrl.calledOnce, 'registerWCFromUrl called once'); + done(); + }, e => { + assert(false, "should not be here"); + done(); + }); + }); + }); + + describe('check renderWebComponentCompound', function() { + const sb = sinon.createSandbox(); + + const context = { key: 'value', mario: 'luigi' }; + + const eventEmitter = "emitterId"; + const eventName = "emitterId"; + + const navNode = { + compound: { + eventListeners: [{ + source: '*', + name: eventName, + action: 'update', + dataConverter: (data) => { + return 'new text: ' + data; + } + }], + children: [{ + viewUrl: 'mfe1.js', + context: { + title: 'My Awesome Grid' + }, + layoutConfig: { + row: "1", + column: "1 / -1" + }, + eventListeners: [{ + source: eventEmitter, + name: eventName, + action: 'update', + dataConverter: (data) => { + return 'new text: ' + data; + } + }] + },{ + id: eventEmitter, + viewUrl: 'mfe2.js', + context: { + title: 'Some input', + instant: true + } + } + ] + } + }; + + afterEach(()=>{ + sb.restore(); + }); + + it('render flat compound', (done) => { + const wc_container = document.createElement('div'); + + sb.spy(WebComponentService, 'renderWebComponent'); + sb.stub(WebComponentService, 'registerWCFromUrl').resolves(); + + WebComponentService.renderWebComponentCompound(navNode, wc_container, context).then((compoundCnt) => { + expect(wc_container.children.length).to.equal(1); + + // eventbus test + let evBus = compoundCnt.eventBus; + const listeners = evBus.listeners[eventEmitter + '.' + eventName]; + expect(listeners.length).to.equal(1); + const target = compoundCnt.querySelector('[nodeId=' + listeners[0].wcElementId + ']'); + sb.spy(target, 'dispatchEvent'); + evBus.onPublishEvent(new CustomEvent(eventName), eventEmitter); + assert(target.dispatchEvent.calledOnce); + + // Check if renderWebComponent is called for each child + assert(WebComponentService.renderWebComponent.calledTwice); + + done(); + }); + }); + + it('render nested compound', (done) => { + const wc_container = document.createElement('div'); + const compoundCnt = document.createElement('div'); + const node = JSON.parse(JSON.stringify(navNode)); + node.viewUrl = 'mfe.js' + node.webcomponent = true; + window.customElements = { + get: () => { + return false; + } + }; + + sb.stub(WebComponentService, 'registerWCFromUrl').resolves(); + + WebComponentService.renderWebComponentCompound(node, wc_container, context).then((compoundCnt) => { + expect(WebComponentService.registerWCFromUrl.callCount).to.equal(3); + + // eventbus test + let evBus = compoundCnt.eventBus; + sb.spy(compoundCnt, 'dispatchEvent'); + evBus.onPublishEvent(new CustomEvent(eventName), eventEmitter); + assert(compoundCnt.dispatchEvent.calledOnce); + + done(); + }, () => { + assert(false, 'should not be here'); + done(); + }); }); }); }); From 9f5dbd8c8a617eeff2db773e3094394a8e34c574 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Fri, 16 Oct 2020 00:56:58 +0200 Subject: [PATCH 13/23] tests --- core/test/services/web-components.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/test/services/web-components.spec.js b/core/test/services/web-components.spec.js index b2b4d925b5..8b680bbb33 100644 --- a/core/test/services/web-components.spec.js +++ b/core/test/services/web-components.spec.js @@ -7,7 +7,7 @@ import { WebComponentService } from '../../src/services/web-components'; import { LuigiConfig } from '../../src/core-api'; import { DefaultCompoundRenderer } from '../../src/utilities/helpers/web-component-helpers'; -describe.only('WebComponentService', function() { +describe('WebComponentService', function() { describe('generate web component id', function() { const someRandomString = 'dsfgljhbakjdfngb,mdcn vkjrzwero78to4 wfoasb f,asndbf'; From 57e8f805a3710ac0270804d67f6faa63b6e8f041 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 19 Oct 2020 15:18:16 +0200 Subject: [PATCH 14/23] tests --- .../helpers/web-component-helpers.js | 19 +- .../helpers/web-component-helpers.spec.js | 272 ++++++++++++++++++ 2 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 core/test/utilities/helpers/web-component-helpers.spec.js diff --git a/core/src/utilities/helpers/web-component-helpers.js b/core/src/utilities/helpers/web-component-helpers.js index c3dc826873..863cde176e 100644 --- a/core/src/utilities/helpers/web-component-helpers.js +++ b/core/src/utilities/helpers/web-component-helpers.js @@ -6,6 +6,8 @@ export class DefaultCompoundRenderer { if(rendererObj) { this.rendererObject = rendererObj; this.config = rendererObj.config || {}; + } else { + this.config = {}; } } @@ -27,8 +29,8 @@ export class DefaultCompoundRenderer { */ export class CustomCompoundRenderer extends DefaultCompoundRenderer { constructor(rendererObj) { - super(rendererObj); - if(rendererObj.use && rendererObj.use.extends) { + super(rendererObj || { use: {} }); + if(rendererObj && rendererObj.use && rendererObj.use.extends) { this.superRenderer = resolveRenderer({ use: rendererObj.use.extends, config: rendererObj.config @@ -79,11 +81,11 @@ export class GridCompoundRenderer extends DefaultCompoundRenderer { this.config.layouts.forEach(el => { if(el.minWidth || el.maxWidth) { let mq = '@media only screen '; - if(el.minWidth) { - mq += `and (min-width: ${el.minWidth}px)` + if(el.minWidth != null) { + mq += `and (min-width: ${el.minWidth}px) ` } - if(el.maxWidth) { - mq += `and (max-width: ${el.maxWidth}px)` + if(el.maxWidth != null) { + mq += `and (max-width: ${el.maxWidth}px) ` } mq += `{ @@ -130,7 +132,10 @@ export class GridCompoundRenderer extends DefaultCompoundRenderer { */ export const resolveRenderer = (rendererConfig) => { const rendererDef = rendererConfig.use; - if(rendererDef === 'grid') { + if(!rendererDef) { + return new DefaultCompoundRenderer(rendererConfig); + } + else if(rendererDef === 'grid') { return new GridCompoundRenderer(rendererConfig); } else if(rendererDef.createCompoundContainer || rendererDef.createCompoundItemContainer diff --git a/core/test/utilities/helpers/web-component-helpers.spec.js b/core/test/utilities/helpers/web-component-helpers.spec.js new file mode 100644 index 0000000000..667a111388 --- /dev/null +++ b/core/test/utilities/helpers/web-component-helpers.spec.js @@ -0,0 +1,272 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const expect = chai.expect; +const assert = chai.assert; + +import { + DefaultCompoundRenderer, + CustomCompoundRenderer, + GridCompoundRenderer, + resolveRenderer, + registerEventListeners + } from '../../../src/utilities/helpers/web-component-helpers'; + +describe('WebComponentHelpers', function() { + describe('check DefaultCompoundRenderer', function() { + it('check default constructor and methods', () => { + const dcr = new DefaultCompoundRenderer(); + + const compoundContainer = dcr.createCompoundContainer(); + expect(compoundContainer.tagName).to.equal('DIV'); + + const compoundItemContainer = dcr.createCompoundItemContainer(); + expect(compoundItemContainer.tagName).to.equal('DIV'); + + dcr.attachCompoundItem(compoundContainer, compoundItemContainer); + expect(compoundContainer.firstChild).to.equal(compoundItemContainer); + }); + + it('check constructor with renderer object', () => { + const rendererObject = { + config: { + key: 'value' + } + }; + const dcr = new DefaultCompoundRenderer(rendererObject); + expect(dcr.config.key).to.equal('value'); + }); + }); + + describe('check CustomCompoundRenderer', function() { + const sb = sinon.createSandbox(); + + afterEach(() => { + sb.restore(); + }) + + it('check default constructor and methods', () => { + const ccr = new CustomCompoundRenderer(); + + const compoundContainer = ccr.createCompoundContainer(); + expect(compoundContainer.tagName).to.equal('DIV'); + + const compoundItemContainer = ccr.createCompoundItemContainer(); + expect(compoundItemContainer.tagName).to.equal('DIV'); + + ccr.attachCompoundItem(compoundContainer, compoundItemContainer); + expect(compoundContainer.firstChild).to.equal(compoundItemContainer); + }); + + it('check constructor with custom renderer object', () => { + const rendererObject = { + config: { + key: 'value', + tag: 'span' + }, + use: { + createCompoundContainer: (config, superRenderer)=>{ + expect(superRenderer).to.be.undefined; + return document.createElement(config.tag); + }, + createCompoundItemContainer: (layoutConfig, config, superRenderer) => { + expect(superRenderer).to.be.undefined; + return document.createElement(config.tag); + }, + attachCompoundItem: (compoundCnt, compoundItemCnt, superRenderer) => { + expect(superRenderer).to.be.undefined; + const wrapper = document.createElement('div'); + compoundCnt.appendChild(wrapper); + wrapper.appendChild(compoundItemCnt); + } + } + }; + const ccr = new CustomCompoundRenderer(rendererObject); + expect(ccr.config.key).to.equal('value'); + expect(ccr.rendererObject).to.equal(rendererObject); + + const compoundContainer = ccr.createCompoundContainer(); + expect(compoundContainer.tagName).to.equal('SPAN'); + + const compoundItemContainer = ccr.createCompoundItemContainer(); + expect(compoundItemContainer.tagName).to.equal('SPAN'); + + ccr.attachCompoundItem(compoundContainer, compoundItemContainer); + expect(compoundContainer.firstChild.tagName).to.equal('DIV'); + expect(compoundContainer.firstChild.firstChild).to.equal(compoundItemContainer); + }); + + it('check extending existing renderer', () => { + const rendererObject = { + config: { + key: 'value', + tag: 'span' + }, + use: { + extends: 'sth' + } + }; + + const ccr = new CustomCompoundRenderer(rendererObject); + const superRenderer = ccr.superRenderer; + expect(superRenderer).to.not.be.undefined; + sb.spy(superRenderer); + + const compoundContainer = ccr.createCompoundContainer(); + assert(superRenderer.createCompoundContainer.calledOnce, 'superrenderer should be called'); + + const compoundItemContainer = ccr.createCompoundItemContainer(); + assert(superRenderer.createCompoundContainer.calledOnce, 'superrenderer should be called'); + + ccr.attachCompoundItem(compoundContainer, compoundItemContainer); + assert(superRenderer.attachCompoundItem.calledOnce, 'superrenderer should be called'); + }); + }); + + describe('check GridCompoundRenderer', function() { + it('check default constructor and methods', () => { + const gcr = new GridCompoundRenderer(); + + const compoundContainer = gcr.createCompoundContainer(); + const cnt = compoundContainer.innerHTML.trim(); + assert(cnt.indexOf(' 1, 'should contain display grid'); + assert(cnt.indexOf('grid-template-columns: auto') > 1, 'should contain default grid-template-columns'); + assert(cnt.indexOf('grid-template-rows: auto;') > 1, 'should contain default grid-template-rows'); + assert(cnt.indexOf('grid-gap: 0;') > 1, 'should contain default grid-gap'); + assert(cnt.indexOf('min-height: auto;') > 1, 'should contain default min-height'); + + const compoundItemContainer = gcr.createCompoundItemContainer(); + assert(compoundItemContainer.getAttribute('style').indexOf('grid-row:') >= 0, + 'style attribute should contain grid-row'); + assert(compoundItemContainer.getAttribute('style').indexOf('grid-column:') >= 0, + 'style attribute should contain grid-column'); + + const layoutConfig = { + row: 'myrowconfigvalue', + column: 'mycolumnconfigvalue' + }; + const compoundItemContainer2 = gcr.createCompoundItemContainer(layoutConfig); + assert(compoundItemContainer2.getAttribute('style').indexOf('myrowconfigvalue') >= 0, + 'style attribute should contain grid-row'); + assert(compoundItemContainer2.getAttribute('style').indexOf('mycolumnconfigvalue') >= 0, + 'style attribute should contain grid-column'); + + gcr.attachCompoundItem(compoundContainer, compoundItemContainer); + expect(compoundContainer.children[1]).to.equal(compoundItemContainer); + }); + + it('check layout config', () => { + const rendererObject = { + config: { + columns: '10', + rows: '10', + gap: '20', + minHeight: '10vh', + layouts: [{ + minWidth: 0, + maxWidth: 50, + columns: 4, + rows: 4, + gap: 10 + },{ + minWidth: 51, + maxWidth: 100, + columns: 40, + rows: 40, + gap: 100 + }] + } + }; + + const gcr = new GridCompoundRenderer(rendererObject); + const compoundContainer = gcr.createCompoundContainer(); + const cnt = compoundContainer.innerHTML.trim(); + assert(cnt.indexOf('display: grid;') > 1, 'should contain display grid'); + assert(cnt.indexOf('grid-template-columns: 10') > 1, 'should contain configured grid-template-columns'); + assert(cnt.indexOf('grid-template-rows: 10;') > 1, 'should contain configured grid-template-rows'); + assert(cnt.indexOf('grid-gap: 20;') > 1, 'should contain configured grid-gap'); + assert(cnt.indexOf('min-height: 10vh;') > 1, 'should contain configured min-height'); + + const mqIndex = cnt.indexOf('@media only screen and (min-width: 0px) and (max-width: 50px)'); + assert( mqIndex > 1, 'should contain proper media query'); + assert(cnt.indexOf('grid-template-columns: 4') > mqIndex, 'should contain configured mq grid-template-columns'); + assert(cnt.indexOf('grid-template-rows: 4;') > mqIndex, 'should contain configured mq grid-template-rows'); + assert(cnt.indexOf('grid-gap: 10;') > mqIndex, 'should contain configured mq grid-gap'); + + const mqIndex2 = cnt.indexOf('@media only screen and (min-width: 51px) and (max-width: 100px)'); + assert( mqIndex2 > mqIndex, 'should contain proper media query'); + assert(cnt.indexOf('grid-template-columns: 40') > mqIndex2, 'should contain configured mq grid-template-columns'); + assert(cnt.indexOf('grid-template-rows: 40;') > mqIndex2, 'should contain configured mq grid-template-rows'); + assert(cnt.indexOf('grid-gap: 100;') > mqIndex2, 'should contain configured mq grid-gap'); + }); + }); + + + describe('check resolveRenderer function', function() { + it('check gridRenderer resolution', () => { + const rendererInstance = resolveRenderer({ use: 'grid' }); + expect(typeof rendererInstance === typeof new GridCompoundRenderer()); + }); + + it('check customRenderer resolution', () => { + let rendererInstance = resolveRenderer({ + use: { createCompoundContainer: () => {} } + }); + expect(typeof rendererInstance === typeof new CustomCompoundRenderer()); + + rendererInstance = resolveRenderer({ + use: { createCompoundItemContainer: () => {} } + }); + expect(typeof rendererInstance === typeof new CustomCompoundRenderer()); + + rendererInstance = resolveRenderer({ + use: { attachCompoundItem: () => {} } + }); + expect(typeof rendererInstance === typeof new CustomCompoundRenderer()); + }); + + it('check fallback to default', () => { + let rendererInstance = resolveRenderer({}); + expect(typeof rendererInstance === typeof new DefaultCompoundRenderer()); + + rendererInstance = resolveRenderer({ use: 'unknownRenderer'}); + expect(typeof rendererInstance === typeof new DefaultCompoundRenderer()); + }); + }); + + describe('check registerEventListeners', function() { + it('check resolve', () => { + const navNode = { + eventListeners: [{ + source: 'evSrc', + name: 'someEvent', + action: 'handler', + dataConverter: (data) => { return data + 'tada'; } + },{ + source: '*', + name: 'update', + action: 'doSth' + }] + }; + const eventbusListeners = { + '*.update': ['listenerMock'], + 'src.someEvent' : ['listenerMock'] + }; + const nodeId = 'somerandomid'; + const wcElement = { elementmock : 1 }; + + registerEventListeners(eventbusListeners, navNode, nodeId, wcElement); + + expect(Object.keys(eventbusListeners).length).to.equal(3); + expect(eventbusListeners['evSrc.someEvent'].length).to.equal(1); + expect(eventbusListeners['*.update'].length).to.equal(2); + + const listenerInfo = eventbusListeners['evSrc.someEvent'][0]; + expect(listenerInfo.wcElementId).to.equal(nodeId); + expect(listenerInfo.wcElement).to.equal(wcElement); + expect(listenerInfo.action).to.equal('handler'); + expect(listenerInfo.converter('data')).to.equal('datatada'); + }); + }); +}); + From c8325e3c230eb571613824767482aa246b780f18 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 19 Oct 2020 15:19:51 +0200 Subject: [PATCH 15/23] vscode debug config --- .vscode/launch.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..7bd5cb3f26 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "test core", + "request": "launch", + "runtimeArgs": [ + "run-script", + "test" + ], + "cwd": "${workspaceFolder}/core", + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "node" + },{ + "name": "test core -watch", + "request": "launch", + "runtimeArgs": [ + "run-script", + "test:watch" + ], + "cwd": "${workspaceFolder}/core", + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "node" + }] +} \ No newline at end of file From 9dc1bd6b63e17ee9df62120049ae99d7d795fe7d Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 19 Oct 2020 20:21:46 +0200 Subject: [PATCH 16/23] element postProcess test --- core/package-lock.json | 6 ++++ core/package.json | 3 +- core/test/services/web-components.spec.js | 36 +++++++++++++++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index e6385b1bd8..80fbf14a12 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -3279,6 +3279,12 @@ "estraverse": "^4.1.1" } }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true + }, "esprima": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", diff --git a/core/package.json b/core/package.json index b642967a08..0b0953eca7 100644 --- a/core/package.json +++ b/core/package.json @@ -21,6 +21,7 @@ "copy-webpack-plugin": "^5.1.1", "core-js": "^3.2.1", "css-loader": "^3.0.0", + "esm": "^3.2.25", "file-loader": "^2.0.0", "fs-extra": "9.0.0", "fundamental-styles": "^0.11.0", @@ -60,7 +61,7 @@ "bundle-develop": "npm run bundle-develop-evergreen", "bundle-develop-evergreen": "npm run bundle-evergreen -- -d --watch", "bundle-develop-ie11": "MINIFY=false webpack --display-error-details --config webpack-ie11.config.js --debug --devtool cheap-source-map --output-pathinfo --watch", - "test": "babel-node ./node_modules/nyc/bin/nyc.js mocha -- --recursive test", + "test": "babel-node ./node_modules/nyc/bin/nyc.js mocha -- --require esm --recursive test", "test:watch": "npm run test -- --watch", "bundlesize": "npm run bundle && bundlesize", "bundlesizeOnly": "bundlesize", diff --git a/core/test/services/web-components.spec.js b/core/test/services/web-components.spec.js index 8b680bbb33..0dd61b21de 100644 --- a/core/test/services/web-components.spec.js +++ b/core/test/services/web-components.spec.js @@ -6,6 +6,7 @@ const assert = chai.assert; import { WebComponentService } from '../../src/services/web-components'; import { LuigiConfig } from '../../src/core-api'; import { DefaultCompoundRenderer } from '../../src/utilities/helpers/web-component-helpers'; +import { LuigiElement } from '../../../client/src/luigi-element'; describe('WebComponentService', function() { describe('generate web component id', function() { @@ -25,8 +26,9 @@ describe('WebComponentService', function() { }); describe('attach web component', function() { - const container = document.createElement('div'); - const itemPlaceholder = document.createElement('div'); + const sb = sinon.createSandbox(); + let container; + let itemPlaceholder; const ctx = { someValue: true}; before(()=>{ @@ -37,6 +39,15 @@ describe('WebComponentService', function() { window.Luigi = window.Luigi.luigi; }); + afterEach(()=>{ + sb.restore(); + }); + + beforeEach(()=>{ + container = document.createElement('div'); + itemPlaceholder = document.createElement('div'); + }); + it('check dom injection abort if container not attached', () => { WebComponentService.attachWC('div', itemPlaceholder, container, ctx); @@ -51,6 +62,27 @@ describe('WebComponentService', function() { expect(expectedCmp.context).to.equal(ctx); expect(expectedCmp.luigi.mario).to.equal('luigi'); }); + + it('check post-processing', () => { + const wc_id = 'my-wc'; + var MyLuigiElement = class extends LuigiElement { + render(ctx) { + return '
'; + } + }; + + var myEl = Object.create(MyLuigiElement.prototype, {}); + sb.stub(myEl, '__postProcess').callsFake(() => {}); + sb.stub(document, 'createElement').callThrough().withArgs('my-wc').callsFake(() => { + return myEl; + }); + sb.stub(container, 'replaceChild').callsFake(() => {}); + + container.appendChild(itemPlaceholder); + WebComponentService.attachWC(wc_id, itemPlaceholder, container, ctx, 'http://localhost:8080/'); + + assert(myEl.__postProcess.calledOnce, '__postProcess should be called'); + }); }); describe('register web component from url', function() { From 6f2215dab29656bf39de3409438966ea91450668 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 26 Oct 2020 15:19:59 +0100 Subject: [PATCH 17/23] allow non-default exports --- core/src/services/web-components.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index 65c49419a5..7f6d95fef7 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -51,15 +51,26 @@ class WebComponentSvcClass { } /** Does a module import from viewUrl and defines a new web component - * with the default export of the module. - * returns a promise that gets resolved after successfull import */ + * with the default export of the module or the first export extending HTMLElement if no default is + * specified. + * @returns a promise that gets resolved after successfull import */ registerWCFromUrl(viewUrl, wc_id) { return new Promise((resolve, reject) => { if(this.checkWCUrl(viewUrl)) { this.dynamicImport(viewUrl).then(module => { try { if(!window.customElements.get(wc_id)) { - window.customElements.define(wc_id, module.default); + let cmpClazz = module.default; + if(!(HTMLElement.isPrototypeOf(cmpClazz))) { + let props = Object.keys(module); + for(let i = 0; i < props.length; i++) { + cmpClazz = module[props[i]]; + if(HTMLElement.isPrototypeOf(cmpClazz)) { + break; + } + } + } + window.customElements.define(wc_id, cmpClazz); } resolve(); } catch(e) { From 60bace4aac4d5b393df43aba738c3978b10490a9 Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 26 Oct 2020 15:42:13 +0100 Subject: [PATCH 18/23] mark webcomponents as experimental --- core/src/services/routing.js | 4 ++-- core/src/utilities/helpers/generic-helpers.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/core/src/services/routing.js b/core/src/services/routing.js index 1ba3f300b9..27a5fdf8c0 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -330,12 +330,12 @@ class RoutingClass { Navigation.onNodeChange(previousNode, currentNode); } } - if (nodeObject.compound) { + if (nodeObject.compound && GenericHelpers.requestExperimentalFeature('webcomponents', true)) { if (iContainer) { iContainer.classList.add('lui-webComponent'); } this.navigateWebComponentCompound(config, component, iframeElement, nodeObject, iContainer); - } else if (nodeObject.webcomponent) { + } else if (nodeObject.webcomponent && GenericHelpers.requestExperimentalFeature('webcomponents', true)) { if (iContainer) { iContainer.classList.add('lui-webComponent'); } diff --git a/core/src/utilities/helpers/generic-helpers.js b/core/src/utilities/helpers/generic-helpers.js index 49d412d50b..b860ebfcb4 100644 --- a/core/src/utilities/helpers/generic-helpers.js +++ b/core/src/utilities/helpers/generic-helpers.js @@ -1,5 +1,6 @@ // Standalone or partly-standalone methods that are used widely through the whole app and are synchronous. import { LuigiElements } from '../../core-api'; +import { LuigiConfig } from '../../core-api'; class GenericHelpersClass { /** @@ -319,6 +320,22 @@ class GenericHelpersClass { } return 0; } + + /** + * Checks, if an experimental feature is enabled under settings.experminental + * + * @param {*} expFeatureName the feature name to check for + * @param {*} showWarn if true, prints a warning on js console that feature is not enabled + * + * @returns true, if feature enabled, false otherwise. + */ + requestExperimentalFeature(expFeatureName, showWarn) { + var val = LuigiConfig.getConfigValue('settings.experimental.' + expFeatureName); + if(showWarn && !val) { + console.warn("Experimental feature not enabled: ", expFeatureName); + } + return val; + } } export const GenericHelpers = new GenericHelpersClass(); From d25d20cc7cb5341650f75feb8e6cd7ac8909ccec Mon Sep 17 00:00:00 2001 From: Philipp Pracht Date: Mon, 9 Nov 2020 12:31:11 +0100 Subject: [PATCH 19/23] increased ie11 bundle size limit --- core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/package.json b/core/package.json index 0b0953eca7..afe0ed4228 100644 --- a/core/package.json +++ b/core/package.json @@ -81,7 +81,7 @@ }, { "path": "./public-ie11/luigi-ie11.js", - "maxSize": "580 kB", + "maxSize": "590 kB", "compression": "none" }, { From 06b10c09a98d2653c11ab9a4d88b573171a91607 Mon Sep 17 00:00:00 2001 From: JohannesDoberer Date: Wed, 11 Nov 2020 13:51:08 +0100 Subject: [PATCH 20/23] modal support for wc fix --- core/src/Modal.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/Modal.html b/core/src/Modal.html index c168ee7cb8..f30d4c0a0e 100644 --- a/core/src/Modal.html +++ b/core/src/Modal.html @@ -103,7 +103,8 @@

{modalSettings.title}

WebComponentService.renderWebComponent( nodeObject.viewUrl, document.querySelector('.iframeModalCtn'), - pathData.context + pathData.context, + nodeObject ); dispatch('wcCreated', { modalWC: document.querySelector('.iframeModalCtn'), From 723af291098c72b1e28accd47ff9f9b165074464 Mon Sep 17 00:00:00 2001 From: JohannesDoberer Date: Wed, 11 Nov 2020 15:07:56 +0100 Subject: [PATCH 21/23] fix wc support for split-view --- core/src/services/split-view.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/services/split-view.js b/core/src/services/split-view.js index 81e271596a..c6e31e3161 100644 --- a/core/src/services/split-view.js +++ b/core/src/services/split-view.js @@ -93,7 +93,8 @@ class SplitViewSvcClass { WebComponentService.renderWebComponent( lastNode.viewUrl, document.querySelector('.iframeSplitViewCnt'), - pathData.context + pathData.context, + lastNode ); const wcInfo = { splitViewWC: document.querySelector('.iframeSplitViewCnt'), From 924b920247bffbd2e9512608ad818edcaa289d45 Mon Sep 17 00:00:00 2001 From: JohannesDoberer Date: Fri, 20 Nov 2020 13:32:36 +0100 Subject: [PATCH 22/23] no message --- core/src/App.html | 1 + core/src/Modal.html | 10 +++++----- .../src/luigi-config/extended/projectDetailNav.js | 5 ----- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/core/src/App.html b/core/src/App.html index 5a99c5ce73..729fc8f1b0 100644 --- a/core/src/App.html +++ b/core/src/App.html @@ -9,6 +9,7 @@ nodepath="{mfModal.nodepath}" on:close="{closeModal}" on:iframeCreated="{modalIframeCreated}" + on:wcCreated="{modalWCCreated}" > {/if} {#if mfDrawer.displayed && mfDrawer.settings.isDrawer} diff --git a/core/src/Modal.html b/core/src/Modal.html index eb15031450..73c7d0b322 100644 --- a/core/src/Modal.html +++ b/core/src/Modal.html @@ -161,16 +161,16 @@

{settings.title}

const setModalSize = () => { const elem = document.getElementsByClassName('lui-modal-mf'); let modalSize = '80%'; - if (modalSettings.size) { - if (modalSettings.size === 'l') { + if (settings.size) { + if (settings.size === 'l') { modalSize = '80%'; - } else if (modalSettings.size === 'm') { + } else if (settings.size === 'm') { modalSize = '60%'; - } else if (modalSettings.size === 's') { + } else if (settings.size === 's') { modalSize = '40%'; - elem[0].setAttribute('style', `width:${modalSize};height:${modalSize}`); }; } + elem[0].setAttribute('style', `width:${modalSize};height:${modalSize}`); } diff --git a/test/e2e-test-application/src/luigi-config/extended/projectDetailNav.js b/test/e2e-test-application/src/luigi-config/extended/projectDetailNav.js index 591d904056..c7b10ff186 100644 --- a/test/e2e-test-application/src/luigi-config/extended/projectDetailNav.js +++ b/test/e2e-test-application/src/luigi-config/extended/projectDetailNav.js @@ -424,12 +424,7 @@ export const projectDetailNavStructure = projectId => [ pathSegment: 'users', label: 'Users and Groups', viewUrl: '/sampleapp.html#/projects/' + projectId + '/users', -<<<<<<< HEAD icon: 'group' -======= - icon: 'group', - ->>>>>>> master }, { category: 'User Management', From 112ba27de9d63f95954048695b3dbf450b3ef214 Mon Sep 17 00:00:00 2001 From: Ndricim Rrapi Date: Tue, 24 Nov 2020 13:47:31 +0100 Subject: [PATCH 23/23] fix iframcnt e2e bug --- .../tests/1-angular/luigi-client-link-manager-features.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e-test-application/e2e/tests/1-angular/luigi-client-link-manager-features.spec.js b/test/e2e-test-application/e2e/tests/1-angular/luigi-client-link-manager-features.spec.js index a7e86d0213..6606cac9cf 100644 --- a/test/e2e-test-application/e2e/tests/1-angular/luigi-client-link-manager-features.spec.js +++ b/test/e2e-test-application/e2e/tests/1-angular/luigi-client-link-manager-features.spec.js @@ -86,7 +86,7 @@ describe('Luigi client linkManager', () => { cy.wrap($iframeBody) .contains('Open webcomponent in splitView') .click(); - cy.get('.iframeSplitViewCnt>div>').then(container => { + cy.get('.iframeSplitViewCnt>').then(container => { const root = container.children().prevObject[0].shadowRoot; const wcContent = root.querySelector('p').innerText; expect(wcContent).to.equal('Hello WebComponent!');