Repository: SAP/luigi (js)

Issue:
Add new scope "navigation.context" to Luigi.configChanged()
When Luigi.configChanged('navigation.context') is called, it should trigger the invocation of the contextUpdateListeners (context setter for web components) at the micro frontends with the (potentially updated) context.

See: https://docs.luigi-project.io/docs/luigi-core-api/?section=configchanged

Pull request description:
Update context via config changed
Fixes #3685 

Patch:
diff --git a/core/src/core-api/config.js b/core/src/core-api/config.js
index 015b828a..1e0f12ec 100644
--- a/core/src/core-api/config.js
+++ b/core/src/core-api/config.js
@@ -1,4 +1,11 @@
-import { AsyncHelpers, EventListenerHelpers, GenericHelpers, StateHelpers } from '../utilities/helpers';
+import {
+  AsyncHelpers,
+  EventListenerHelpers,
+  GenericHelpers,
+  StateHelpers,
+  IframeHelpers,
+  RoutingHelpers
+} from '../utilities/helpers';
 import { LuigiAuth, LuigiElements } from '.';
 import { AuthLayerSvc, LifecycleHooks } from '../services';
 import { NodeDataManagementStorage } from '../services/node-data-management.js';
@@ -331,6 +338,40 @@ class LuigiConfig {
   getGlobalContext() {
     return this.config?.navigation?.globalContext || {};
   }
+
+  /**
+   * Updates the context values for visible iframes and LUI web components.
+   * Note: the updated context values are not persisted. The developers have to do it on their own.
+   * @param {Object} ctx - The context to be updated.
+   * @memberof Configuration
+   * @since NEXTRELEASE
+   */
+  updateContextValues(ctx) {
+    const visibleIframes = IframeHelpers.getMicrofrontendIframes();
+    if (visibleIframes && visibleIframes.length > 0) {
+      visibleIframes.forEach(iframe => {
+        // luigi configuration data about the mf which is rendered in the iframe
+        if (iframe.luigi) {
+          IframeHelpers.sendMessageToIframe(iframe, {
+            msg: 'luigi.navigate',
+            context: JSON.stringify(Object.assign({}, JSON.parse(iframe.luigi._lastUpdatedMessage.context), ctx)),
+            nodeParams: iframe.luigi._lastUpdatedMessage.nodeParams,
+            pathParams: JSON.stringify(Object.assign({}, iframe.luigi.pathParams)),
+            searchParams: JSON.stringify(
+              Object.assign({}, RoutingHelpers.prepareSearchParamsForClient(iframe.luigi.currentNode))
+            ),
+            internal: IframeHelpers.applyCoreStateData(JSON.parse(iframe.luigi._lastUpdatedMessage.internal))
+          });
+        }
+      });
+    }
+    if (document.querySelector('.wcContainer')) {
+      let luiWebComponents = document.querySelectorAll('[lui_web_component=true]');
+      luiWebComponents.forEach(luiWebComponent => {
+        luiWebComponent.context = Object.assign({}, luiWebComponent.context, ctx);
+      });
+    }
+  }
 }
 
 export const config = new LuigiConfig();
diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js
index 872c41dd..85a3e065 100644
--- a/core/src/services/web-components.js
+++ b/core/src/services/web-components.js
@@ -333,12 +333,14 @@ class WebComponentSvcClass {
           if (navNode?.webcomponent?.selfRegistered) {
             this.includeSelfRegisteredWCFromUrl(navNode, renderer.viewUrl, () => {
               const wc = document.createElement(wc_id);
+              wc.setAttribute('lui_web_component', true);
               this.initWC(wc, wc_id, wc, renderer.viewUrl, ctx, '_root');
               resolve(wc);
             });
           } else {
             this.registerWCFromUrl(renderer.viewUrl, wc_id).then(() => {
               const wc = document.createElement(wc_id);
+              wc.setAttribute('lui_web_component', true);
               this.initWC(wc, wc_id, wc, renderer.viewUrl, ctx, '_root');
               resolve(wc);
             });
diff --git a/docs/luigi-core-api.md b/docs/luigi-core-api.md
index 5ee24f08..8ff89e3c 100644
--- a/docs/luigi-core-api.md
+++ b/docs/luigi-core-api.md
@@ -264,6 +264,19 @@ Get the global context object.
 
 -   **since**: 2.5.0
 
+#### updateContextValues
+
+Updates the context values for visible iframes and LUI web components.
+Note: the updated context values are not persisted. The developers have to do it on their own.
+
+##### Parameters
+
+-   `ctx` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** The context to be updated.
+
+**Meta**
+
+-   **since**: NEXTRELEASE
+
 ## Luigi.elements()
 
 <!-- Generated by documentation.js. Update this documentation by updating the source code. -->


Test patch:
diff --git a/core/test/core-api/config.spec.js b/core/test/core-api/config.spec.js
new file mode 100644
index 00000000..4f382946
--- /dev/null
+++ b/core/test/core-api/config.spec.js
@@ -0,0 +1,95 @@
+import { IframeHelpers } from '../../src/utilities/helpers';
+import { LuigiConfig } from '../../src/core-api';
+const sinon = require('sinon');
+const chai = require('chai');
+const assert = chai.assert;
+
+describe('updateContextValues', () => {
+  let sendMessageToIframeStub;
+  let getMicrofrontendIframesStub;
+  let querySelectorStub;
+
+  beforeEach(() => {
+    sendMessageToIframeStub = sinon.stub(IframeHelpers, 'sendMessageToIframe');
+    getMicrofrontendIframesStub = sinon.stub(IframeHelpers, 'getMicrofrontendIframes');
+    querySelectorStub = sinon.stub(document, 'querySelector');
+  });
+
+  afterEach(() => {
+    sendMessageToIframeStub.restore();
+    getMicrofrontendIframesStub.restore();
+    querySelectorStub.restore();
+  });
+
+  it('should update context values correctly', () => {
+    sinon.stub(IframeHelpers, 'applyCoreStateData').returns('{}');
+    const mockIframe = {
+      luigi: {
+        _lastUpdatedMessage: {
+          context: '{"parentNavigationContexts":[],"initCtx":"init ctx","test":"context"}',
+          nodeParams: '{}',
+          internal: '{}'
+        }
+      }
+    };
+
+    const mockVisibleIframes = [mockIframe];
+    const mockCtx = { newContext: 'new context' };
+
+    getMicrofrontendIframesStub.returns(mockVisibleIframes);
+
+    LuigiConfig.updateContextValues(mockCtx);
+
+    sinon.assert.calledOnce(getMicrofrontendIframesStub);
+    sinon.assert.calledOnce(sendMessageToIframeStub);
+
+    const expectedMessage = {
+      msg: 'luigi.navigate',
+      context: '{"parentNavigationContexts":[],"initCtx":"init ctx","test":"context","newContext":"new context"}',
+      nodeParams: '{}',
+      pathParams: '{}',
+      searchParams: '{}',
+      internal: '{}'
+    };
+
+    sinon.assert.calledWith(sendMessageToIframeStub, sinon.match.same(mockIframe), expectedMessage);
+  });
+
+  it('should not update context if there are no visible iframes or LUI web components', () => {
+    getMicrofrontendIframesStub.returns([]);
+    querySelectorStub.returns(null);
+
+    const ctx = { test: 'context' };
+    LuigiConfig.updateContextValues(ctx);
+
+    sinon.assert.notCalled(sendMessageToIframeStub);
+  });
+
+  it('should update context property of luiWebComponents', () => {
+    const mockContainer = document.createElement('div');
+    mockContainer.className = 'wcContainer';
+
+    const mockLuiWebComponents = [
+      { context: { initialContext: 'initial' } },
+      { context: { initialContext: 'initial' } }
+    ];
+
+    querySelectorStub.withArgs('.wcContainer').returns(mockContainer);
+
+    sinon
+      .stub(document, 'querySelectorAll')
+      .withArgs('[lui_web_component=true]')
+      .returns(mockLuiWebComponents);
+
+    const newContext = { updatedContext: 'updated' };
+
+    LuigiConfig.updateContextValues(newContext);
+
+    sinon.assert.calledWith(querySelectorStub, '.wcContainer');
+    sinon.assert.calledWith(document.querySelectorAll, '[lui_web_component=true]');
+
+    mockLuiWebComponents.forEach(component => {
+      assert.deepEqual(component.context, { initialContext: 'initial', updatedContext: 'updated' });
+    });
+  });
+});