Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 68 additions & 20 deletions projects/demo/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ import { routes } from './app.routes';
import { loadingInterceptor } from './interceptors/loading.interceptor';
import { NavigationNode } from './model/navigation';
import { GraphqlService } from './services/graphql.service';
import { CustomTitleStrategy } from './strategies/custom-title.strategy';

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Title } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, Router, Route } from '@angular/router';
import { ActivatedRouteSnapshot, provideRouter, Router, Route, TitleStrategy } from '@angular/router';
import { ApolloClientOptions, InMemoryCache } from '@apollo/client/core';
import { IDS_ICON_DEFAULT_CONFIG, IdsIconDefaultConfig } from '@i-cell/ids-angular/icon';
import { provideTranslateService } from '@ngx-translate/core';
import { provideTranslateService, TranslateService } from '@ngx-translate/core';
import { provideTranslateHttpLoader } from '@ngx-translate/http-loader';
import { provideApollo } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { filter, firstValueFrom } from 'rxjs';

function extractGeneratedSlugs(nodes: NavigationNode[]): string[] {
const slugs: string[] = [];
interface GeneratedSlug {
slug: string;
title?: string;
}

function extractGeneratedSlugs(nodes: NavigationNode[]): GeneratedSlug[] {
const slugs: GeneratedSlug[] = [];
nodes.forEach((node) => {
if (node.page?.slug && node.page?.generated === true) {
slugs.push(node.page.slug);
slugs.push({ slug: node.page.slug, title: node.page.title ?? node.title });
}
if (node.children) {
slugs.push(...extractGeneratedSlugs(node.children));
Expand All @@ -28,26 +36,25 @@ function extractGeneratedSlugs(nodes: NavigationNode[]): string[] {
return slugs;
}

export function initializeDynamicRoutes(graphqlService: GraphqlService, router: Router): () => Promise<void> {
return async() => {
const result = await firstValueFrom(graphqlService
.getNavigation()
.pipe(filter((res): res is { loading?: boolean } =>
!(res as { loading?: boolean }).loading))) as { data?: { navs?: NavigationNode[] } };
export function initializeDynamicRoutes(graphqlService: GraphqlService, router: Router, titleService: Title): () => Promise<void> {
const buildAndApplyRoutes = async(): Promise<void> => {
const result = (await firstValueFrom(
graphqlService.getNavigation().pipe(filter((res): res is { loading?: boolean } => !(res as { loading?: boolean }).loading)),
)) as { data?: { navs?: NavigationNode[] } };

const navs = result.data?.navs || [];
const generatedSlugs: string[] = [];
const generatedSlugs: GeneratedSlug[] = [];

// iterate through the navigation tree and extract slugs from generated pages
navs.forEach((nav: NavigationNode) => {
if (nav.tree) {
generatedSlugs.push(...extractGeneratedSlugs(nav.tree));
}
});

const dynamicRoutes: Route[] = generatedSlugs.flatMap((slug) => [
const dynamicRoutes: Route[] = generatedSlugs.flatMap(({ slug, title }) => [
{
path: slug,
title,
loadComponent: () => import('./pages/list-page/list-page.component').then((module) => module.ListPageComponent),
data: {
collection: 'pages',
Expand All @@ -56,6 +63,7 @@ export function initializeDynamicRoutes(graphqlService: GraphqlService, router:
},
{
path: `${slug}/:slug`,
title: (routeSnapshot: ActivatedRouteSnapshot): string => (routeSnapshot.data['contentTitle'] as string | undefined) ?? title ?? '',
loadComponent: () => import('./pages/list-page/content-page/content-page.component').then((module) => module.ContentPageComponent),
data: {
collection: slug,
Expand All @@ -67,14 +75,52 @@ export function initializeDynamicRoutes(graphqlService: GraphqlService, router:
const langRoute = currentConfig.find((route) => route.path === ':lang');
if (langRoute?.children) {
const langChildren = langRoute.children;
const fallbackChild = langChildren.pop();
const staticPaths = new Set([
'components',
'home',
'support',
'',
]);
const staticChildren = langChildren.filter((route) => staticPaths.has(route.path ?? ''));
const fallbackChild = staticChildren.find((route) => route.path === '');
const nonFallbackStaticChildren = staticChildren.filter((route) => route.path !== '');

langChildren.push(...dynamicRoutes);
if (fallbackChild) {
langChildren.push(fallbackChild);
}
langRoute.children = [
...nonFallbackStaticChildren,
...dynamicRoutes,
...(fallbackChild ? [fallbackChild] : []),
];
}
router.resetConfig(currentConfig);

try {
const urlTree = router.parseUrl(router.url);
const primaryOutlet = urlTree.root.children['primary'];

if (primaryOutlet) {
const segments = primaryOutlet.segments;

if (segments.length >= 2) {
const currentSlug = segments[1].path;
const matchedSlug = generatedSlugs.find((slug) => slug.slug === currentSlug);

if (matchedSlug && matchedSlug.title) {
titleService.setTitle(`${matchedSlug.title} | i-Cell Design System`);
}
}
}
} catch(error) {
console.warn('Nem sikerült frissíteni a dinamikus route címét nyelvváltáskor', error);
}
};

return async() => {
const translate = inject(TranslateService);
translate.onLangChange.pipe(takeUntilDestroyed()).subscribe(() => {
void buildAndApplyRoutes();
});

await buildAndApplyRoutes();
};
}

Expand Down Expand Up @@ -105,10 +151,12 @@ export const appConfig: ApplicationConfig = {
}),
provideAnimations(),
{ provide: IDS_ICON_DEFAULT_CONFIG, useValue: iconDefaultConfig },
{ provide: TitleStrategy, useClass: CustomTitleStrategy },
provideAppInitializer(() => {
const graphqlService = inject(GraphqlService);
const router = inject(Router);
return initializeDynamicRoutes(graphqlService, router)();
const titleService = inject(Title);
return initializeDynamicRoutes(graphqlService, router, titleService)();
}),
],
};
78 changes: 41 additions & 37 deletions projects/demo/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,15 @@ const DEMO_IMPORTS: Record<string, () => Promise<Type<unknown>>> = {

export const CURRENT_DEMO_SERVICE = new InjectionToken<IComponentDemoService>('CurrentDemoService');

function buildComponentRoute(slug: string, demoService: Provider | EnvironmentProviders): Route {
function buildComponentRoute(slug: string, translationKey: string, demoService: Provider | EnvironmentProviders): Route {
const componentImport = DEMO_IMPORTS[slug];
if (!componentImport) {
throw new Error(`No component import found for slug: ${slug}`);
}
return {
path: slug,
component: ComponentDetailsComponent,
title: translationKey,
providers: [
demoService,
{ provide: CURRENT_DEMO_SERVICE, useExisting: demoService },
Expand Down Expand Up @@ -125,55 +126,58 @@ export const routes: Routes = [
children: [
{
path: 'components',
title: 'TITLES.COMPONENTS',
children: [
{
path: '',
loadComponent: () => import('./pages/components/components.component').then((module) => module.ComponentsComponent),
},
buildComponentRoute('accordion', AccordionDemoService),
buildComponentRoute('autocomplete', AutocompleteDemoService),
buildComponentRoute('avatar', AvatarDemoService),
buildComponentRoute('badge', BadgeDemoService),
buildComponentRoute('breadcrumb', BreadcrumbDemoService),
buildComponentRoute('button', ButtonDemoService),
buildComponentRoute('card', CardDemoService),
buildComponentRoute('checkbox', CheckboxDemoService),
buildComponentRoute('chip', ChipDemoService),
buildComponentRoute('date-picker', DatepickerDemoService),
buildComponentRoute('dialog', DialogDemoService),
buildComponentRoute('divider', DividerDemoService),
buildComponentRoute('fieldset', FieldsetDemoService),
buildComponentRoute('form-field', FormFieldDemoService),
buildComponentRoute('icon', IconDemoService),
buildComponentRoute('icon-button', IconButtonDemoService),
buildComponentRoute('menu-item', MenuItemDemoService),
buildComponentRoute('message', MessageDemoService),
buildComponentRoute('notification', NotificationDemoService),
buildComponentRoute('option', OptionDemoService),
buildComponentRoute('overlay-panel', OverlayPanelDemoService),
buildComponentRoute('paginator', PaginatorDemoService),
buildComponentRoute('radio', RadioDemoService),
buildComponentRoute('scrollbar', ScrollbarDemoService),
buildComponentRoute('segmented-control', SegmentedControlDemoService),
buildComponentRoute('segmented-control-toggle', SegmentedControlToggleDemoService),
buildComponentRoute('select', SelectDemoService),
buildComponentRoute('side-nav', SideNavDemoService),
buildComponentRoute('side-sheet', SideSheetDemoService),
buildComponentRoute('snackbar', SnackbarDemoService),
buildComponentRoute('spinner', SpinnerDemoService),
buildComponentRoute('switch', SwitchDemoService),
buildComponentRoute('tab', TabDemoService),
buildComponentRoute('table', TableDemoService),
buildComponentRoute('tag', TagDemoService),
buildComponentRoute('tooltip', TooltipDemoService),
buildComponentRoute('accordion', 'COMPONENTS.ACCORDION', AccordionDemoService),
buildComponentRoute('autocomplete', 'COMPONENTS.AUTOCOMPLETE', AutocompleteDemoService),
buildComponentRoute('avatar', 'COMPONENTS.AVATAR', AvatarDemoService),
buildComponentRoute('badge', 'COMPONENTS.BADGE', BadgeDemoService),
buildComponentRoute('breadcrumb', 'COMPONENTS.BREADCRUMB', BreadcrumbDemoService),
buildComponentRoute('button', 'COMPONENTS.BUTTON', ButtonDemoService),
buildComponentRoute('card', 'COMPONENTS.CARD', CardDemoService),
buildComponentRoute('checkbox', 'COMPONENTS.CHECKBOX', CheckboxDemoService),
buildComponentRoute('chip', 'COMPONENTS.CHIP', ChipDemoService),
buildComponentRoute('date-picker', 'COMPONENTS.DATE_PICKER', DatepickerDemoService),
buildComponentRoute('dialog', 'COMPONENTS.DIALOG', DialogDemoService),
buildComponentRoute('divider', 'COMPONENTS.DIVIDER', DividerDemoService),
buildComponentRoute('fieldset', 'COMPONENTS.FIELDSET', FieldsetDemoService),
buildComponentRoute('form-field', 'COMPONENTS.FORM_FIELD', FormFieldDemoService),
buildComponentRoute('icon', 'COMPONENTS.ICON', IconDemoService),
buildComponentRoute('icon-button', 'COMPONENTS.ICON_BUTTON', IconButtonDemoService),
buildComponentRoute('menu-item', 'COMPONENTS.MENU_ITEM', MenuItemDemoService),
buildComponentRoute('message', 'COMPONENTS.MESSAGE', MessageDemoService),
buildComponentRoute('notification', 'COMPONENTS.NOTIFICATION', NotificationDemoService),
buildComponentRoute('option', 'COMPONENTS.OPTION', OptionDemoService),
buildComponentRoute('overlay-panel', 'COMPONENTS.OVERLAY_PANEL', OverlayPanelDemoService),
buildComponentRoute('paginator', 'COMPONENTS.PAGINATOR', PaginatorDemoService),
buildComponentRoute('radio', 'COMPONENTS.RADIO', RadioDemoService),
buildComponentRoute('scrollbar', 'COMPONENTS.SCROLLBAR', ScrollbarDemoService),
buildComponentRoute('segmented-control', 'COMPONENTS.SEGMENTED_CONTROL', SegmentedControlDemoService),
buildComponentRoute('segmented-control-toggle', 'COMPONENTS.SEGMENTED_CONTROL_TOGGLE', SegmentedControlToggleDemoService),
buildComponentRoute('select', 'COMPONENTS.SELECT', SelectDemoService),
buildComponentRoute('side-nav', 'COMPONENTS.SIDE_NAV', SideNavDemoService),
buildComponentRoute('side-sheet', 'COMPONENTS.SIDE_SHEET', SideSheetDemoService),
buildComponentRoute('snackbar', 'COMPONENTS.SNACKBAR', SnackbarDemoService),
buildComponentRoute('spinner', 'COMPONENTS.SPINNER', SpinnerDemoService),
buildComponentRoute('switch', 'COMPONENTS.SWITCH', SwitchDemoService),
buildComponentRoute('tab', 'COMPONENTS.TAB', TabDemoService),
buildComponentRoute('table', 'COMPONENTS.TABLE', TableDemoService),
buildComponentRoute('tag', 'COMPONENTS.TAG', TagDemoService),
buildComponentRoute('tooltip', 'COMPONENTS.TOOLTIP', TooltipDemoService),
],
},
{
path: 'home',
title: 'TITLES.HOME',
loadComponent: () => import('./pages/index/index.component').then((module) => module.IndexComponent),
},
{
path: 'support',
title: 'TITLES.SUPPORT',
loadComponent: () => import('./pages/issue-report/issue-report.component').then((module) => module.IssueReportComponent),
},
{
Expand Down
40 changes: 40 additions & 0 deletions projects/demo/src/app/strategies/custom-title.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { inject, Injectable } from '@angular/core';
import { RouterStateSnapshot, TitleStrategy } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';

const APP_TITLE_SUFFIX = 'i-Cell Design System';

@Injectable({ providedIn: 'root' })
export class CustomTitleStrategy extends TitleStrategy {
private readonly _translate = inject(TranslateService);
private _lastRouterState?: RouterStateSnapshot;

constructor() {
super();
this._translate.onLangChange.subscribe(() => {
if (this._lastRouterState) {
this.updateTitle(this._lastRouterState);
}
});
}

public override updateTitle(routerState: RouterStateSnapshot): void {
this._lastRouterState = routerState;

let finalTitle: string | undefined = this.buildTitle(routerState);

if (finalTitle && this._isTranslationKey(finalTitle)) {
finalTitle = this._translate.instant(finalTitle);
}

if (finalTitle) {
document.title = `${finalTitle} | ${APP_TITLE_SUFFIX}`;
} else {
document.title = APP_TITLE_SUFFIX;
}
}

private _isTranslationKey(str: string): boolean {
return /^[A-Z_]+(\.[A-Z_]+)*$/.test(str);
}
}
7 changes: 7 additions & 0 deletions projects/demo/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"PATTERNS": "Patterns"
},
"RESOURCES": "Resources",
"TITLES": {
"HOME": "Home",
"SUPPORT": "Support",
"COMPONENTS": "Components"
},
"COMPONENTS": {
"ACCORDION": "Accordion",
"AUTOCOMPLETE": "Auto complete",
Expand All @@ -27,7 +32,9 @@
"ICON": "Icon",
"ICON_BUTTON": "Icon button",
"MENU_ITEM": "Menu item",
"MESSAGE": "Message",
"NOTIFICATION": "Notification",
"OPTION": "Option",
"OVERLAY_PANEL": "Overlay panel",
"PAGINATOR": "Paginator",
"RADIO": "Radio",
Expand Down
7 changes: 7 additions & 0 deletions projects/demo/src/assets/i18n/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"PATTERNS": "Minták"
},
"RESOURCES": "Forrásanyagok",
"TITLES": {
"HOME": "Kezdőlap",
"SUPPORT": "Támogatás",
"COMPONENTS": "Komponensek"
},
"COMPONENTS": {
"ACCORDION": "Accordion",
"AUTOCOMPLETE": "Auto complete",
Expand All @@ -27,7 +32,9 @@
"ICON": "Icon",
"ICON_BUTTON": "Icon button",
"MENU_ITEM": "Menu item",
"MESSAGE": "Message",
"NOTIFICATION": "Notification",
"OPTION": "Option",
"OVERLAY_PANEL": "Overlay panel",
"PAGINATOR": "Paginator",
"RADIO": "Radio",
Expand Down
Loading