diff --git a/core/src/core-api/index.js b/core/src/core-api/index.js index 14e9ab048c..11095239c9 100644 --- a/core/src/core-api/index.js +++ b/core/src/core-api/index.js @@ -8,6 +8,7 @@ import { ux } from './ux'; import { globalSearch } from './globalsearch'; import { theming } from './theming'; import { featureToggles } from './featuretoggles'; +import { routing } from './routing'; export const LuigiConfig = config; export const LuigiAuth = auth; @@ -19,6 +20,7 @@ export const LuigiUX = ux; export const LuigiGlobalSearch = globalSearch; export const LuigiTheming = theming; export const LuigiFeatureToggles = featureToggles; +export const LuigiRouting = routing; // Expose it window for user app to call Luigi.setConfig() window.Luigi = config; @@ -31,3 +33,4 @@ window.Luigi.ux = () => ux; window.Luigi.globalSearch = () => globalSearch; window.Luigi.theming = () => theming; window.Luigi.featureToggles = () => featureToggles; +window.Luigi.routing = () => routing; diff --git a/core/src/core-api/routing.js b/core/src/core-api/routing.js new file mode 100644 index 0000000000..f24d84780d --- /dev/null +++ b/core/src/core-api/routing.js @@ -0,0 +1,78 @@ +import { LuigiConfig } from '.'; +import { Iframe } from '../services'; +import { GenericHelpers, RoutingHelpers } from '../utilities/helpers'; +/** + * @name Routing + */ +class LuigiRouting { + /** + * Use these functions for navigation-related features. + * @name Routing + */ + constructor() {} + + /** + * Get search parameter from URL as an object. + * @memberof Routing + * @since NEXTRELEASE + * @returns {Object} + * @example + * Luigi.routing().getSearchParams(); + */ + getSearchParams() { + const queryParams = {}; + const url = new URL(location); + if (LuigiConfig.getConfigValue('routing.useHashRouting')) { + for (const [key, value] of new URLSearchParams(url.hash.split('?')[1])) { + queryParams[key] = value; + } + } else { + for (const [key, value] of url.searchParams.entries()) { + queryParams[key] = value; + } + } + return queryParams; + } + + /** + * Add search parameters to the URL. + * If [hash routing](navigation-parameters-reference.md#usehashrouting) is enabled, the search parameters will be set after the hash. + * In order to delete a search query param you can set the value of the param to undefined. + * @memberof Routing + * @since NEXTRELEASE + * @param {Object} params + * @example + * Luigi.routing().addSearchParams({luigi:'rocks', mario:undefined}); + */ + addSearchParams(params) { + if (!GenericHelpers.isObject(params)) { + console.log('Params argument must be an object'); + return; + } + const url = new URL(location); + if (LuigiConfig.getConfigValue('routing.useHashRouting')) { + let [hashValue, givenQueryParamsString] = url.hash.split('?'); + let searchParams = new URLSearchParams(givenQueryParamsString); + this._modifySearchParam(params, searchParams); + url.hash = hashValue; + if (searchParams.toString() !== '') { + url.hash += `?${decodeURIComponent(searchParams.toString())}`; + } + } else { + this._modifySearchParam(params, url.searchParams); + } + window.history.pushState({}, '', url.href); + } + + //Adds and remove properties from searchParams + _modifySearchParam(params, searchParams) { + for (const [key, value] of Object.entries(params)) { + searchParams.set(key, value); + if (value === undefined) { + searchParams.delete(key); + } + } + } +} + +export const routing = new LuigiRouting(); diff --git a/core/src/utilities/helpers/routing-helpers.js b/core/src/utilities/helpers/routing-helpers.js index 2617df2d73..3d4755f1ba 100644 --- a/core/src/utilities/helpers/routing-helpers.js +++ b/core/src/utilities/helpers/routing-helpers.js @@ -1,6 +1,6 @@ // Helper methods for 'routing.js' file. They don't require any method from 'routing.js' but are required by them. // They are also rarely used directly from outside of 'routing.js' -import { LuigiConfig, LuigiFeatureToggles, LuigiI18N } from '../../core-api'; +import { LuigiConfig, LuigiFeatureToggles, LuigiI18N, LuigiRouting } from '../../core-api'; import { AsyncHelpers, EscapingHelpers, EventListenerHelpers, GenericHelpers } from './'; import { Routing } from '../../services/routing'; @@ -266,6 +266,7 @@ class RoutingHelpersClass { const contextVarPrefix = 'context.'; const nodeParamsVarPrefix = 'nodeParams.'; const i18n_currentLocale = '{i18n.currentLocale}'; + const searchQuery = 'routing.queryParams'; viewUrl = GenericHelpers.replaceVars(viewUrl, componentData.pathParams, ':', false); viewUrl = GenericHelpers.replaceVars(viewUrl, componentData.context, contextVarPrefix); @@ -275,6 +276,18 @@ class RoutingHelpersClass { viewUrl = viewUrl.replace(i18n_currentLocale, LuigiI18N.getCurrentLocale()); } + if (viewUrl.includes(searchQuery)) { + const viewUrlSearchParam = viewUrl.split('?')[1]; + if (viewUrlSearchParam) { + const key = viewUrlSearchParam.split('=')[0]; + if (LuigiRouting.getSearchParams()[key]) { + viewUrl = viewUrl.replace(`{${searchQuery}.${key}}`, LuigiRouting.getSearchParams()[key]); + } else { + viewUrl = viewUrl.replace(`?${key}={${searchQuery}.${key}}`, ''); + } + } + } + return viewUrl; } diff --git a/core/test/core-api/routing.spec.js b/core/test/core-api/routing.spec.js new file mode 100644 index 0000000000..f248a16151 --- /dev/null +++ b/core/test/core-api/routing.spec.js @@ -0,0 +1,129 @@ +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); + +import { afterEach } from 'mocha'; +import { LuigiRouting, LuigiConfig } from '../../src/core-api'; + +describe('Luigi routing', function() { + let globalLocationRef = global.location; + + beforeEach(() => { + window.history.pushState = sinon.spy(); + }); + afterEach(() => { + global.location = globalLocationRef; + }); + describe('SearchParams path routing', () => { + it('get searchparams', () => { + global.location = 'http://some.url.de?test=tets&luigi=rocks'; + assert.deepEqual(LuigiRouting.getSearchParams(), { test: 'tets', luigi: 'rocks' }); + }); + it('get searchparams', () => { + global.location = 'http://some.url.de/something?test=tets&luigi=rocks'; + assert.deepEqual(LuigiRouting.getSearchParams(), { test: 'tets', luigi: 'rocks' }); + }); + it('get searchparams when no query parameter', () => { + global.location = 'http://some.url.de'; + assert.deepEqual(LuigiRouting.getSearchParams(), {}); + }); + it('set searchparams', () => { + window.state = {}; + global.location = 'http://some.url.de'; + LuigiRouting.addSearchParams({ foo: 'bar' }); + sinon.assert.calledWithExactly(window.history.pushState, window.state, '', 'http://some.url.de/?foo=bar'); + }); + it('add search params to searchparams', () => { + window.state = {}; + global.location = 'http://some.url.de?test=tets'; + LuigiRouting.addSearchParams({ foo: 'bar' }); + sinon.assert.calledWithExactly( + window.history.pushState, + window.state, + '', + 'http://some.url.de/?test=tets&foo=bar' + ); + }); + it('call addSearchParams with wrong argument', () => { + console.log = sinon.spy(); + global.location = 'http://some.url.de'; + LuigiRouting.addSearchParams('bar'); + sinon.assert.calledWith(console.log, 'Params argument must be an object'); + }); + it('delete search params from url', () => { + window.state = {}; + global.location = 'http://some.url.de?luigi=rocks&mario=red'; + LuigiRouting.addSearchParams({ mario: undefined }); + sinon.assert.calledWithExactly(window.history.pushState, window.state, '', 'http://some.url.de/?luigi=rocks'); + }); + }); + describe('SearchParams hash routing', () => { + beforeEach(() => { + sinon + .stub(LuigiConfig, 'getConfigValue') + .withArgs('routing.useHashRouting') + .returns(true); + }); + afterEach(() => { + sinon.restore(); + }); + it('get searchparams hash routing', () => { + global.location = 'http://some.url.de/#/?test=tets&luigi=rocks'; + assert.deepEqual(LuigiRouting.getSearchParams(), { test: 'tets', luigi: 'rocks' }); + }); + it('get searchparams', () => { + global.location = 'http://some.url.de/#/something?test=tets&luigi=rocks'; + assert.deepEqual(LuigiRouting.getSearchParams(), { test: 'tets', luigi: 'rocks' }); + }); + it('get searchparams hash routing', () => { + global.location = 'http://some.url.de/#/'; + assert.deepEqual(LuigiRouting.getSearchParams(), {}); + }); + it('add searchparams hash routing', () => { + window.state = {}; + global.location = 'http://some.url.de/#/'; + LuigiRouting.addSearchParams({ foo: 'bar' }); + sinon.assert.calledWithExactly(window.history.pushState, window.state, '', 'http://some.url.de/#/?foo=bar'); + }); + it('add search params to hash routing', () => { + window.state = {}; + global.location = 'http://some.url.de/#/?test=tets'; + LuigiRouting.addSearchParams({ foo: 'bar' }); + sinon.assert.calledWithExactly( + window.history.pushState, + window.state, + '', + 'http://some.url.de/#/?test=tets&foo=bar' + ); + }); + it('add search params to hash routing', () => { + window.state = {}; + global.location = 'http://some.url.de/#/?~luigi=rocks'; + LuigiRouting.addSearchParams({ foo: 'bar' }); + sinon.assert.calledWithExactly( + window.history.pushState, + window.state, + '', + 'http://some.url.de/#/?~luigi=rocks&foo=bar' + ); + }); + it('call addSearchParams with wrong argument hash routing', () => { + console.log = sinon.spy(); + global.location = 'http://some.url.de/#/'; + LuigiRouting.addSearchParams('bar'); + sinon.assert.calledWith(console.log, 'Params argument must be an object'); + }); + it('delete search params from url', () => { + window.state = {}; + global.location = 'http://some.url.de/#/?luigi=rocks&mario=red'; + LuigiRouting.addSearchParams({ mario: undefined }); + sinon.assert.calledWithExactly(window.history.pushState, window.state, '', 'http://some.url.de/#/?luigi=rocks'); + }); + + it('_modifySearchParam', () => { + let searchParams = new URLSearchParams('mario=rocks'); + LuigiRouting._modifySearchParam({ test: 'tets', luigi: 'rocks', mario: undefined }, searchParams); + assert.equal(searchParams.toString(), 'test=tets&luigi=rocks'); + }); + }); +}); diff --git a/core/test/utilities/helpers/routing-helpers.spec.js b/core/test/utilities/helpers/routing-helpers.spec.js index 66ed987604..c414905935 100644 --- a/core/test/utilities/helpers/routing-helpers.spec.js +++ b/core/test/utilities/helpers/routing-helpers.spec.js @@ -3,7 +3,7 @@ const chai = require('chai'); const expect = chai.expect; const assert = chai.assert; import { GenericHelpers, RoutingHelpers } from '../../../src/utilities/helpers'; -import { LuigiConfig, LuigiFeatureToggles, LuigiI18N } from '../../../src/core-api'; +import { LuigiConfig, LuigiFeatureToggles, LuigiI18N, LuigiRouting } from '../../../src/core-api'; import { Routing } from '../../../src/services/routing'; import { config } from '../../../src/core-api/config'; @@ -97,6 +97,25 @@ describe('Routing-helpers', () => { expect(RoutingHelpers.substituteViewUrl(viewUrl, {})).to.equal(expected); }); }); + describe('substitute search query params', () => { + afterEach(() => { + sinon.restore(); + }); + it('substitutes search query parameter', () => { + sinon.stub(LuigiRouting, 'getSearchParams').returns({ luigi: 'rocks' }); + const viewUrl = '/microfrontend.html?luigi={routing.queryParams.luigi}'; + const expected = '/microfrontend.html?luigi=rocks'; + + expect(RoutingHelpers.substituteViewUrl(viewUrl, {})).to.equal(expected); + }); + it('substitutes search query parameter', () => { + sinon.stub(LuigiRouting, 'getSearchParams').returns({ mario: 'rocks' }); + const viewUrl = '/microfrontend.html?luigi={routing.queryParams.luigi}'; + const expected = '/microfrontend.html'; + + expect(RoutingHelpers.substituteViewUrl(viewUrl, {})).to.equal(expected); + }); + }); describe('defaultChildNodes', () => { let mockPathData; diff --git a/docs/luigi-core-api.md b/docs/luigi-core-api.md index ea0b264c34..02c7cbddbd 100644 --- a/docs/luigi-core-api.md +++ b/docs/luigi-core-api.md @@ -27,6 +27,7 @@ This document outlines the features provided by the Luigi Core API. It covers th - [Global search](#globalsearch) - functions related to Luigi's global search - [Theming](#theming) - functions related to Luigi theming capabilties - [Feature toggles](#featuretoggles) - functions related to Luigi's feature toggle mechanism +- [Routing](#routing) - functions to get and set search query parameters ## Luigi Config @@ -1285,3 +1286,45 @@ Returns **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Gl **Meta** - **since**: 1.4.0 + +## Luigi.routing() + + + +### Routing + +#### getSearchParams + +Get search parameter from URL as an object. + +##### Examples + +```javascript +Luigi.routing().getSearchParams(); +``` + +Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** + +**Meta** + +- **since**: NEXTRELEASE + +#### addSearchParams + +Add search parameters to the URL. +If [hash routing](navigation-parameters-reference.md#usehashrouting) is enabled, the search parameters will be set after the hash. +In order to delete a search query param you can set the value of the param to undefined. + +##### Parameters + +- `params` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** + +##### Examples + +```javascript +Luigi.routing().addSearchParams({luigi:'rocks', mario:undefined}); +``` + +**Meta** + +- **since**: NEXTRELEASE diff --git a/scripts/package.json b/scripts/package.json index 18af2ac147..3b85fd2936 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -8,8 +8,8 @@ "docu:client:generate:section": "documentation readme ../client/src/luigi-client.js -f md --readme-file=../docs/luigi-client-api.md --section='API Reference' --markdown-toc=false --quiet --github false", "docu:client:validate": "documentation lint ../client/src/luigi-client.js", "docu:core": "npm run docu:core:validate && npm run docu:core:generate:sections", - "docu:core:validate": "documentation lint --shallow ../core/src/core-api/config.js ../core/src/core-api/elements.js ../core/src/core-api/auth.js ../core/src/core-api/navigation.js ../core/src/core-api/i18n.js ../core/src/core-api/custom-messages.js ../core/src/core-api/ux.js ../core/src/core-api/globalsearch.js ../core/src/core-api/theming.js ../core/src/core-api/featuretoggles.js", - "docu:core:generate:sections": "npm run docu:core:generate:config && npm run docu:core:generate:dom-elements && npm run docu:core:generate:auth && npm run docu:core:generate:navigation && npm run docu:core:generate:i18n && npm run docu:core:generate:custom-messages && npm run docu:core:generate:ux && npm run docu:core:generate:globalsearch && npm run docu:core:generate:theming && npm run docu:core:generate:featuretoggles", + "docu:core:validate": "documentation lint --shallow ../core/src/core-api/config.js ../core/src/core-api/elements.js ../core/src/core-api/auth.js ../core/src/core-api/navigation.js ../core/src/core-api/i18n.js ../core/src/core-api/custom-messages.js ../core/src/core-api/ux.js ../core/src/core-api/globalsearch.js ../core/src/core-api/theming.js ../core/src/core-api/featuretoggles.js ../core/src/core-api/routing.js", + "docu:core:generate:sections": "npm run docu:core:generate:config && npm run docu:core:generate:dom-elements && npm run docu:core:generate:auth && npm run docu:core:generate:navigation && npm run docu:core:generate:i18n && npm run docu:core:generate:custom-messages && npm run docu:core:generate:ux && npm run docu:core:generate:globalsearch && npm run docu:core:generate:theming && npm run docu:core:generate:featuretoggles && npm run docu:core:generate:routing", "docu:core:generate:config": "documentation readme ../core/src/core-api/config.js --shallow -f md --section='Luigi Config' --readme-file=../docs/luigi-core-api.md --markdown-toc=false --github false --quiet", "docu:core:generate:dom-elements": "documentation readme ../core/src/core-api/dom-elements.js --shallow -f md --section='Luigi.elements()' --readme-file=../docs/luigi-core-api.md --markdown-toc=false --github false --quiet", "docu:core:generate:auth": "documentation readme ../core/src/core-api/auth.js --shallow -f md --section='Luigi.auth()' --readme-file=../docs/luigi-core-api.md --markdown-toc=false --github false --quiet", @@ -20,6 +20,7 @@ "docu:core:generate:globalsearch": "documentation readme ../core/src/core-api/globalSearch.js --shallow -f md --section='Luigi.globalSearch()' --readme-file=../docs/luigi-core-api.md --markdown-toc=false --github false --quiet", "docu:core:generate:theming": "documentation readme ../core/src/core-api/theming.js --shallow -f md --section='Luigi.theming()' --readme-file=../docs/luigi-core-api.md --markdown-toc=false --github false --quiet", "docu:core:generate:featuretoggles": "documentation readme ../core/src/core-api/featuretoggles.js --shallow -f md --section='Luigi.featureToggles()' --readme-file=../docs/luigi-core-api.md --markdown-toc=false --github false --quiet", + "docu:core:generate:routing": "documentation readme ../core/src/core-api/routing.js --shallow -f md --section='Luigi.routing()' --readme-file=../docs/luigi-core-api.md --markdown-toc=false --github false --quiet", "release": "babel-node tools/release-cli/release-cli.js", "release:watch": "nodemon --exec babel-node tools/release-cli/release-cli.js", "publish:nightly": "babel-node tools/publish-nightly.js"