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