diff --git a/src/app/dynamic-renderer/page-renderer/page-renderer.model.ts b/src/app/dynamic-renderer/page-renderer/page-renderer.model.ts new file mode 100644 index 0000000..b6fe6f0 --- /dev/null +++ b/src/app/dynamic-renderer/page-renderer/page-renderer.model.ts @@ -0,0 +1,11 @@ +import { PageConfig } from '../../shared/models/config'; +import { SectionRenderNode } from '../section-renderer/section-renderer.model'; + +export interface PageRenderModel { + id: string; + key: string; + title: string; + layout: string; + sections: SectionRenderNode[]; + source: PageConfig; +} diff --git a/src/app/dynamic-renderer/page-renderer/page-renderer.service.ts b/src/app/dynamic-renderer/page-renderer/page-renderer.service.ts new file mode 100644 index 0000000..90bf7b2 --- /dev/null +++ b/src/app/dynamic-renderer/page-renderer/page-renderer.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { PageConfig } from '../../shared/models/config'; +import { PageRenderModel } from './page-renderer.model'; +import { SectionRendererService } from '../section-renderer/section-renderer.service'; + +@Injectable({ providedIn: 'root' }) +export class PageRendererService { + constructor(private readonly sectionRenderer: SectionRendererService) {} + + toRenderModel(page: PageConfig): PageRenderModel { + const sections = (page.sections ?? []) + .filter(section => section.visible !== false) + .sort((a, b) => a.order - b.order) + .map(section => this.sectionRenderer.toRenderNode(section)); + + return { + id: page.id, + key: page.key, + title: page.title, + layout: page.layout, + sections, + source: page + }; + } +} diff --git a/src/app/dynamic-renderer/section-renderer/section-renderer.model.ts b/src/app/dynamic-renderer/section-renderer/section-renderer.model.ts new file mode 100644 index 0000000..3cad101 --- /dev/null +++ b/src/app/dynamic-renderer/section-renderer/section-renderer.model.ts @@ -0,0 +1,12 @@ +import { SectionConfig } from '../../shared/models/config'; +import { WidgetRenderNode } from '../widget-host/widget-host.model'; + +export interface SectionRenderNode { + id: string; + type: string; + order: number; + featureFlag?: string; + visible?: boolean; + widgets: WidgetRenderNode[]; + source: SectionConfig; +} diff --git a/src/app/dynamic-renderer/section-renderer/section-renderer.service.ts b/src/app/dynamic-renderer/section-renderer/section-renderer.service.ts new file mode 100644 index 0000000..5dcd2e6 --- /dev/null +++ b/src/app/dynamic-renderer/section-renderer/section-renderer.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { SectionConfig } from '../../shared/models/config'; +import { SectionRenderNode } from './section-renderer.model'; +import { WidgetRenderNode } from '../widget-host/widget-host.model'; + +@Injectable({ providedIn: 'root' }) +export class SectionRendererService { + toRenderNode(section: SectionConfig): SectionRenderNode { + const widgets = (section.widgets ?? []) + .filter(widget => widget.visible !== false) + .map(widget => ({ + id: widget.id, + type: widget.type, + version: widget.version, + props: widget.props ?? {}, + featureFlag: widget.featureFlag, + visible: widget.visible, + source: widget + })); + + return { + id: section.id, + type: section.type, + order: section.order, + featureFlag: section.featureFlag, + visible: section.visible, + widgets, + source: section + }; + } +} diff --git a/src/app/dynamic-renderer/widget-host/widget-host.model.ts b/src/app/dynamic-renderer/widget-host/widget-host.model.ts new file mode 100644 index 0000000..b9ff00d --- /dev/null +++ b/src/app/dynamic-renderer/widget-host/widget-host.model.ts @@ -0,0 +1,11 @@ +import { WidgetConfig } from '../../shared/models/config'; + +export interface WidgetRenderNode { + id: string; + type: string; + version: string; + props: Record; + featureFlag?: string; + visible?: boolean; + source: WidgetConfig; +} diff --git a/src/app/dynamic-renderer/widget-host/widget-host.service.ts b/src/app/dynamic-renderer/widget-host/widget-host.service.ts new file mode 100644 index 0000000..9b73899 --- /dev/null +++ b/src/app/dynamic-renderer/widget-host/widget-host.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { WidgetConfig } from '../../shared/models/config'; +import { WidgetRegistryService } from '../../widgets/registry/widget-registry.service'; +import { ResolvedWidget } from '../../widgets/contracts/widget-component.contract'; + +@Injectable({ providedIn: 'root' }) +export class WidgetHostService { + constructor(private readonly registry: WidgetRegistryService) {} + + resolveWidget(widget: WidgetConfig): ResolvedWidget | null { + const registered = this.registry.resolve(widget.type, widget.version); + if (!registered) { + return null; + } + + return { + type: registered.type, + version: registered.version, + component: registered.component, + props: widget.props ?? {} + }; + } +} diff --git a/src/app/widgets/contracts/widget-component.contract.ts b/src/app/widgets/contracts/widget-component.contract.ts new file mode 100644 index 0000000..e6e0d5d --- /dev/null +++ b/src/app/widgets/contracts/widget-component.contract.ts @@ -0,0 +1,20 @@ +import { Type } from '@angular/core'; + +export interface WidgetRenderContext { + pageId: string; + sectionId: string; + locale?: string; +} + +export interface RegisteredWidget { + type: string; + version: string; + component: Type; +} + +export interface ResolvedWidget { + type: string; + version: string; + component: Type; + props: Record; +} diff --git a/src/app/widgets/registry/widget-registry.service.ts b/src/app/widgets/registry/widget-registry.service.ts new file mode 100644 index 0000000..b943c28 --- /dev/null +++ b/src/app/widgets/registry/widget-registry.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { RegisteredWidget } from '../contracts/widget-component.contract'; + +@Injectable({ providedIn: 'root' }) +export class WidgetRegistryService { + private readonly widgets = new Map(); + + register(widget: RegisteredWidget): void { + this.widgets.set(this.toKey(widget.type, widget.version), widget); + } + + registerMany(widgets: RegisteredWidget[]): void { + for (const widget of widgets) { + this.register(widget); + } + } + + resolve(type: string, version: string): RegisteredWidget | null { + const exact = this.widgets.get(this.toKey(type, version)); + if (exact) { + return exact; + } + + const sameType = Array.from(this.widgets.values()) + .filter(widget => widget.type === type) + .sort((a, b) => b.version.localeCompare(a.version)); + + return sameType[0] ?? null; + } + + list(): RegisteredWidget[] { + return Array.from(this.widgets.values()); + } + + private toKey(type: string, version: string): string { + return `${type}@${version}`; + } +}