phase-6: add registry-driven dynamic page-section-widget rendering engine
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<WidgetRenderNode>(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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/app/dynamic-renderer/widget-host/widget-host.model.ts
Normal file
11
src/app/dynamic-renderer/widget-host/widget-host.model.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { WidgetConfig } from '../../shared/models/config';
|
||||||
|
|
||||||
|
export interface WidgetRenderNode {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
props: Record<string, unknown>;
|
||||||
|
featureFlag?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
source: WidgetConfig;
|
||||||
|
}
|
||||||
23
src/app/dynamic-renderer/widget-host/widget-host.service.ts
Normal file
23
src/app/dynamic-renderer/widget-host/widget-host.service.ts
Normal file
@@ -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 ?? {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/app/widgets/contracts/widget-component.contract.ts
Normal file
20
src/app/widgets/contracts/widget-component.contract.ts
Normal file
@@ -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<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedWidget {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
component: Type<unknown>;
|
||||||
|
props: Record<string, unknown>;
|
||||||
|
}
|
||||||
38
src/app/widgets/registry/widget-registry.service.ts
Normal file
38
src/app/widgets/registry/widget-registry.service.ts
Normal file
@@ -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<string, RegisteredWidget>();
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user