From 73a7f72426dfbf0a21209e11c43acfcca9f64641 Mon Sep 17 00:00:00 2001 From: markuczy Date: Tue, 25 Jun 2024 12:33:48 +0200 Subject: [PATCH 1/6] feat: breadcrumb service for matcher routes --- .../src/lib/services/breadcrumb.service.ts | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts b/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts index 75c02bda..f25cb622 100644 --- a/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts +++ b/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts @@ -13,41 +13,62 @@ export class BreadcrumbService { itemsHandler = this.itemsSource.asObservable() constructor(private router: Router, private activeRoute: ActivatedRoute, private translateService: TranslateService) { - const breadcrumbs: MenuItem[] = [] - this.addBreadcrumb(this.activeRoute.snapshot, [], breadcrumbs) - this.generatedItemsSource.next(breadcrumbs) + this.generateBreadcrumbs(this.activeRoute.snapshot) this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe(() => { const root = this.router.routerState.snapshot.root - const breadcrumbs: MenuItem[] = [] - this.addBreadcrumb(root, [], breadcrumbs) - - this.generatedItemsSource.next(breadcrumbs) + this.generateBreadcrumbs(root) }) } + private generateBreadcrumbs(route: ActivatedRouteSnapshot | null) { + if (route && route.url) { + if (route.data['mfeInfo']) { + const breadcrumbs: MenuItem[] = [ + { + label: route.data['mfeInfo'].productName, + routerLink: route.data['mfeInfo'].baseHref, + }, + ] + const baseUrl: string[] = (route.data['mfeInfo'].baseHref as string).split('/').filter((value) => value) + const parentUrl: string[] = route.url.map((url) => url.path) + if (!parentUrl.every((item) => baseUrl.includes(item))) { + this.createBreadcrumb(route, parentUrl, breadcrumbs) + } + this.addBreadcrumb(route.firstChild, parentUrl, breadcrumbs) + this.generatedItemsSource.next(breadcrumbs) + } else { + this.generateBreadcrumbs(route.firstChild) + } + } + } + private addBreadcrumb(route: ActivatedRouteSnapshot | null, parentUrl: string[], breadcrumbs: MenuItem[]) { if (route && route.url) { const routeUrl = parentUrl.concat(route.url.map((url) => url.path)) if (route.routeConfig?.path) { - if (route.data['breadcrumb']) { - const breadcrumb: MenuItem = { - label: this.getLabel(route.data, route.paramMap), - routerLink: '/' + routeUrl.join('/'), - } - breadcrumbs.push(breadcrumb) - } else { - const breadcrumb: MenuItem = { - label: 'NA', - routerLink: '/' + routeUrl.join('/'), - } - breadcrumbs.push(breadcrumb) - } + this.createBreadcrumb(route, routeUrl, breadcrumbs) } this.addBreadcrumb(route.firstChild, routeUrl, breadcrumbs) } } + private createBreadcrumb(route: ActivatedRouteSnapshot, routeUrl: string[], breadcrumbs: MenuItem[]) { + if (route.data['breadcrumb']) { + const breadcrumb: MenuItem = { + label: this.getLabel(route.data, route.paramMap), + routerLink: '/' + routeUrl.join('/'), + } + breadcrumbs.push(breadcrumb) + } else { + const breadcrumb: MenuItem = { + label: 'NA', + routerLink: '/' + routeUrl.join('/'), + } + breadcrumbs.push(breadcrumb) + } + } + private getLabel(data: Data, params: ParamMap) { if (typeof data['breadcrumbFn'] === 'function') { return data['breadcrumbFn'](data, params) From 01be280ce90e523ea7b158a9f071134dddc42f3b Mon Sep 17 00:00:00 2001 From: markuczy Date: Tue, 25 Jun 2024 16:10:28 +0200 Subject: [PATCH 2/6] feat: unsubscribe on destroy --- .../src/lib/services/breadcrumb.service.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts b/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts index f25cb622..b51b4e84 100644 --- a/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts +++ b/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core' +import { Injectable, OnDestroy } from '@angular/core' import { ActivatedRoute, ActivatedRouteSnapshot, Data, NavigationEnd, ParamMap, Router } from '@angular/router' import { TranslateService } from '@ngx-translate/core' import { BehaviorSubject, filter } from 'rxjs' @@ -6,7 +6,7 @@ import { MenuItem } from 'primeng/api' import { BreadCrumbMenuItem } from '../model/breadcrumb-menu-item.model' @Injectable({ providedIn: 'any' }) -export class BreadcrumbService { +export class BreadcrumbService implements OnDestroy { private itemsSource = new BehaviorSubject([]) generatedItemsSource = new BehaviorSubject([]) @@ -19,6 +19,10 @@ export class BreadcrumbService { this.generateBreadcrumbs(root) }) } + ngOnDestroy(): void { + this.itemsSource.unsubscribe() + this.generatedItemsSource.unsubscribe() + } private generateBreadcrumbs(route: ActivatedRouteSnapshot | null) { if (route && route.url) { @@ -31,7 +35,8 @@ export class BreadcrumbService { ] const baseUrl: string[] = (route.data['mfeInfo'].baseHref as string).split('/').filter((value) => value) const parentUrl: string[] = route.url.map((url) => url.path) - if (!parentUrl.every((item) => baseUrl.includes(item))) { + const isUsingMatcher = !parentUrl.every((item) => baseUrl.includes(item)) + if (isUsingMatcher) { this.createBreadcrumb(route, parentUrl, breadcrumbs) } this.addBreadcrumb(route.firstChild, parentUrl, breadcrumbs) From e82dd3e86699d3e205b896be1e402dced4e5c9dd Mon Sep 17 00:00:00 2001 From: markuczy Date: Wed, 26 Jun 2024 10:37:47 +0200 Subject: [PATCH 3/6] feat: utils for webcomponents --- libs/angular-webcomponents/package.json | 6 +++ libs/angular-webcomponents/src/index.ts | 3 ++ .../lib/utils/webcomponent-bootstrap.utils.ts | 37 +++++++++++++++++++ .../webcomponent-router-initializer.utils.ts | 21 +++++++++++ .../lib/utils/webcomponent-router.utils.ts | 17 +++++++++ package-lock.json | 23 ++++++++++-- package.json | 1 + 7 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 libs/angular-webcomponents/src/lib/utils/webcomponent-bootstrap.utils.ts create mode 100644 libs/angular-webcomponents/src/lib/utils/webcomponent-router-initializer.utils.ts create mode 100644 libs/angular-webcomponents/src/lib/utils/webcomponent-router.utils.ts diff --git a/libs/angular-webcomponents/package.json b/libs/angular-webcomponents/package.json index 7d94c298..26bc55f3 100644 --- a/libs/angular-webcomponents/package.json +++ b/libs/angular-webcomponents/package.json @@ -2,6 +2,12 @@ "name": "@onecx/angular-webcomponents", "version": "4.33.0", "peerDependencies": { + "@angular/core": "^15.2.7 || ^16.0.0 || ^17.0.0", + "@angular/platform-browser": "^15.2.7 || ^16.0.0 || ^17.0.0", + "@angular/elements": "^15.2.7 || ^16.0.0 || ^17.0.0", + "@angular/router": "^15.2.7 || ^16.0.0 || ^17.0.0", + "@onecx/portal-integration-angular": "^4", + "rxjs": "~7.8.0" }, "dependencies": {}, "publishConfig": { diff --git a/libs/angular-webcomponents/src/index.ts b/libs/angular-webcomponents/src/index.ts index 70a66cd7..f8fdabc2 100644 --- a/libs/angular-webcomponents/src/index.ts +++ b/libs/angular-webcomponents/src/index.ts @@ -1 +1,4 @@ export * from './lib/model/remote-webcomponent' +export * from './lib/utils/webcomponent-bootstrap.utils' +export * from './lib/utils/webcomponent-router-initializer.utils' +export * from './lib/utils/webcomponent-router.utils' diff --git a/libs/angular-webcomponents/src/lib/utils/webcomponent-bootstrap.utils.ts b/libs/angular-webcomponents/src/lib/utils/webcomponent-bootstrap.utils.ts new file mode 100644 index 00000000..e8e28ca3 --- /dev/null +++ b/libs/angular-webcomponents/src/lib/utils/webcomponent-bootstrap.utils.ts @@ -0,0 +1,37 @@ +import { createCustomElement } from '@angular/elements' +import { createApplication } from '@angular/platform-browser' +import { EnvironmentProviders, NgZone, PlatformRef, Provider, Type, VERSION, Version, getPlatform } from '@angular/core' + +export async function bootstrapRemoteComponent( + component: Type, + elementName: string, + providers: (Provider | EnvironmentProviders)[] +): Promise { + const app = await createApplication({ + providers: [ + (window as any)['@angular-architects/module-federation-tools'].ngZone + ? { + provide: NgZone, + useValue: (window as any)['@angular-architects/module-federation-tools'].ngZone, + } + : [], + ...providers, + ], + }) + + const platform = getPlatform() + let platformCache: Map = (window as any)['@angular-architects/module-federation-tools'] + .platformCache + if (!platformCache) { + platformCache = new Map() + ;(window as any)['@angular-architects/module-federation-tools'].platformCache = platformCache + } + const version = VERSION + platform && platformCache.set(version, platform) + + const myRemoteComponentAsWebComponent = createCustomElement(component, { + injector: app.injector, + }) + + customElements.define(elementName, myRemoteComponentAsWebComponent) +} diff --git a/libs/angular-webcomponents/src/lib/utils/webcomponent-router-initializer.utils.ts b/libs/angular-webcomponents/src/lib/utils/webcomponent-router-initializer.utils.ts new file mode 100644 index 00000000..72bbd13d --- /dev/null +++ b/libs/angular-webcomponents/src/lib/utils/webcomponent-router-initializer.utils.ts @@ -0,0 +1,21 @@ +import { Router } from '@angular/router' +import { AppStateService } from '@onecx/portal-integration-angular' +import { firstValueFrom, map } from 'rxjs' + +export function initializeRouter(router: Router, appStateService: AppStateService) { + return () => + firstValueFrom( + appStateService.currentMfe$.asObservable().pipe( + map((mfeInfo) => { + const routes = router.config + routes.forEach((route) => { + route.data = { + ...route.data, + mfeInfo: mfeInfo, + } + }) + router.resetConfig(routes) + }) + ) + ) +} diff --git a/libs/angular-webcomponents/src/lib/utils/webcomponent-router.utils.ts b/libs/angular-webcomponents/src/lib/utils/webcomponent-router.utils.ts new file mode 100644 index 00000000..e8f41592 --- /dev/null +++ b/libs/angular-webcomponents/src/lib/utils/webcomponent-router.utils.ts @@ -0,0 +1,17 @@ +import { Route, UrlMatcher, UrlSegment, UrlSegmentGroup } from '@angular/router' + +export function match(prefix: string): UrlMatcher { + return (url: UrlSegment[], UrlSegmentGroup: UrlSegmentGroup, route: Route) => { + const mfeBaseHref: string = route?.data?.['mfeInfo']?.baseHref + if (!mfeBaseHref) { + console.warn('mfeInfo was not provided for route') + } + const baseHrefSegmentAmount = mfeBaseHref.split('/').filter((value) => value).length + const urlWithoutBaseHref = url.slice(baseHrefSegmentAmount) + const index = prefix === '' ? 0 : urlWithoutBaseHref.findIndex((u) => u.path === prefix) + if (index >= 0) { + return { consumed: url.slice(0, index + baseHrefSegmentAmount) } + } + return null + } +} diff --git a/package-lock.json b/package-lock.json index ea7ebadb..04b3daa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@onecx/onecx-portal-ui-libs", - "version": "4.30.1", + "version": "4.33.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@onecx/onecx-portal-ui-libs", - "version": "4.30.1", + "version": "4.33.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -16,6 +16,7 @@ "@angular/common": "^15.2.7", "@angular/compiler": "^15.2.7", "@angular/core": "^15.2.7", + "@angular/elements": "^15.2.7", "@angular/forms": "^15.2.7", "@angular/platform-browser": "^15.2.7", "@angular/platform-browser-dynamic": "^15.2.7", @@ -111,7 +112,8 @@ } }, "libs/portal-layout-styles": { - "version": "4.30.1", + "name": "@onecx/portal-layout-styles", + "version": "4.33.0", "peerDependencies": { "tslib": "^2.5.0" } @@ -877,6 +879,21 @@ "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" } }, + "node_modules/@angular/elements": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-15.2.10.tgz", + "integrity": "sha512-/yZwLsumcFIN0POJ58kfrYcjBnm6dsQGddA8gbaD6JeIkny0sAsZZuKSavs9c4YWn2JHBH/N3L9BToRTOrQX9A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/forms": { "version": "15.2.10", "license": "MIT", diff --git a/package.json b/package.json index 9822f2c4..89f88348 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@angular/common": "^15.2.7", "@angular/compiler": "^15.2.7", "@angular/core": "^15.2.7", + "@angular/elements": "^15.2.7", "@angular/forms": "^15.2.7", "@angular/platform-browser": "^15.2.7", "@angular/platform-browser-dynamic": "^15.2.7", From 7160f3db9951be110eb5988e64694963672e42ce Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 1 Jul 2024 09:09:48 +0200 Subject: [PATCH 4/6] fix: removed duplicate method --- .../src/lib/services/breadcrumb.service.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts b/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts index 5606af75..9df161b6 100644 --- a/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts +++ b/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts @@ -51,33 +51,6 @@ export class BreadcrumbService { this.generateBreadcrumbs(route.firstChild) } } - ngOnDestroy(): void { - this.itemsSource.unsubscribe() - this.generatedItemsSource.unsubscribe() - } - - private generateBreadcrumbs(route: ActivatedRouteSnapshot | null) { - if (route && route.url) { - if (route.data['mfeInfo']) { - const breadcrumbs: MenuItem[] = [ - { - label: route.data['mfeInfo'].productName, - routerLink: route.data['mfeInfo'].baseHref, - }, - ] - const baseUrl: string[] = (route.data['mfeInfo'].baseHref as string).split('/').filter((value) => value) - const parentUrl: string[] = route.url.map((url) => url.path) - const isUsingMatcher = !parentUrl.every((item) => baseUrl.includes(item)) - if (isUsingMatcher) { - this.createBreadcrumb(route, parentUrl, breadcrumbs) - } - this.addBreadcrumb(route.firstChild, parentUrl, breadcrumbs) - this.generatedItemsSource.next(breadcrumbs) - } else { - this.generateBreadcrumbs(route.firstChild) - } - } - } private addBreadcrumb(route: ActivatedRouteSnapshot | null, parentUrl: string[], breadcrumbs: MenuItem[]) { if (route && route.url) { From 8a4fc7b48a515fcd127c0bef8017aa6dbb484eae Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 1 Jul 2024 09:10:15 +0200 Subject: [PATCH 5/6] fix: removed redundant import --- libs/angular-accelerator/src/lib/services/breadcrumb.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts b/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts index 9df161b6..c2d17267 100644 --- a/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts +++ b/libs/angular-accelerator/src/lib/services/breadcrumb.service.ts @@ -1,4 +1,4 @@ -import { Injectable, OnDestroy } from '@angular/core' +import { Injectable } from '@angular/core' import { ActivatedRoute, ActivatedRouteSnapshot, Data, NavigationEnd, ParamMap, Router } from '@angular/router' import { TranslateService } from '@ngx-translate/core' import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' From b28d76269fb58719479badba65677aa7866a837d Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 1 Jul 2024 09:26:59 +0200 Subject: [PATCH 6/6] feat: matcher improvements --- .../lib/utils/webcomponent-router.utils.ts | 32 +++++++++++++------ package-lock.json | 6 ++-- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/libs/angular-webcomponents/src/lib/utils/webcomponent-router.utils.ts b/libs/angular-webcomponents/src/lib/utils/webcomponent-router.utils.ts index e8f41592..6cf9c1d7 100644 --- a/libs/angular-webcomponents/src/lib/utils/webcomponent-router.utils.ts +++ b/libs/angular-webcomponents/src/lib/utils/webcomponent-router.utils.ts @@ -1,17 +1,29 @@ import { Route, UrlMatcher, UrlSegment, UrlSegmentGroup } from '@angular/router' -export function match(prefix: string): UrlMatcher { +export function startsWith(prefix: string): UrlMatcher { return (url: UrlSegment[], UrlSegmentGroup: UrlSegmentGroup, route: Route) => { - const mfeBaseHref: string = route?.data?.['mfeInfo']?.baseHref - if (!mfeBaseHref) { - console.warn('mfeInfo was not provided for route') - } - const baseHrefSegmentAmount = mfeBaseHref.split('/').filter((value) => value).length - const urlWithoutBaseHref = url.slice(baseHrefSegmentAmount) - const index = prefix === '' ? 0 : urlWithoutBaseHref.findIndex((u) => u.path === prefix) - if (index >= 0) { - return { consumed: url.slice(0, index + baseHrefSegmentAmount) } + const urlWithoutBaseHref = sliceBaseHref(route, url) + + const fullUrl = urlWithoutBaseHref.map((u) => u.path).join('/') + if (fullUrl.startsWith(prefix)) { + const prefixLength = prefix.split('/').filter((value) => value).length + return { consumed: url.slice(0, baseHrefSegmentAmount(url, urlWithoutBaseHref) + prefixLength) } } return null } } + +export function sliceBaseHref(route: Route, url: UrlSegment[]): UrlSegment[] { + const mfeBaseHref: string = route?.data?.['mfeInfo']?.baseHref + if (!mfeBaseHref) { + console.warn( + 'mfeInfo was not provided for route. initializeRouter function is required to be registered as app initializer.' + ) + } + const baseHrefSegmentAmount = mfeBaseHref.split('/').filter((value) => value).length + return url.slice(baseHrefSegmentAmount) +} + +export function baseHrefSegmentAmount(url: UrlSegment[], urlWithoutBaseHref: UrlSegment[]) { + return url.length - urlWithoutBaseHref.length +} diff --git a/package-lock.json b/package-lock.json index a48b932f..dfc4b433 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@onecx/onecx-portal-ui-libs", - "version": "4.33.3", + "version": "4.36.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@onecx/onecx-portal-ui-libs", - "version": "4.33.3", + "version": "4.36.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -113,7 +113,7 @@ }, "libs/portal-layout-styles": { "name": "@onecx/portal-layout-styles", - "version": "4.33.3", + "version": "4.36.0", "peerDependencies": { "tslib": "^2.5.0" }