Atom
Molecule
Organism
Template
Page
Chromatic
export interface SearchRequest { query: string; searchConfig: SearchConfig; tab?: string; numberOfResults?: number; page?: number; } export function request({ ... }: SearchRequest): Promise<Results> { // ... return fetch( "https://content-search-service.apps.pcf-p02-we.rabobank.nl/internal/omni-search", { method: "POST", body: JSON.stringify(requestbody), headers: { "Content-Type": "application/json", }, } ) ... }
OmniSearch
OmniSearch
OmniSearch
// ... import { request, Results, SearchRequest } from "@global-sites-poc/shared/search"; @Injectable({ providedIn: "root", }) export class OmniSearchService { search$: Observable<Results>; private results = new Subject<Results>(); error$: Observable<unknown>; private error = new Subject<unknown>(); constructor() { this.search$ = this.results.asObservable(); this.error$ = this.error.asObservable(); } search(searchRequest: SearchRequest): void { request(searchRequest) .then((results) => this.results.next(results)) .catch((error) => this.error.next(error)); } }
// ... import { request, Results, SearchRequest } from "@global-sites-poc/shared/search"; function useOmniSearch( initialRequest: SearchRequest ): [ (request: SearchRequest | Setter<SearchRequest>) => void, Results | undefined ] { const [searchRequest, setSearchRequest] = useState<SearchRequest>(initialRequest); const [searchResponse, setSearchResponse] = useState<Results>(); useEffect(() => { if (searchRequest) { request(searchRequest).then(setSearchResponse); } }, [searchRequest.query]); return [setSearchRequest, searchResponse]; }
OmniSearch
OmniSearch
OmniSearch
@Component({ selector: "global-sites-poc-omni-search", templateUrl: "./omni-search.component.html", styleUrls: ["./omni-search.component.scss"], }) export class OmniSearchComponent implements OnInit { @Input() searchConfig: SearchConfig; search$: Observable<Results>; constructor(private omniSearch: OmniSearchService) { this.search$ = omniSearch.search$; } ngOnInit(): void { this.search(); } onSearch(event: CustomEvent<string>): void { this.search({ query: event.detail, }); } private search(request: Partial<SearchRequest> = {}) { this.omniSearch.search({ query: "", searchConfig: this.searchConfig, numberOfResults: 4, ...request, }); } }
<ng-container *ngIf="search$ | async as searchResponse"> <omni-search pageDetailsText="Pagina's" [searchResults]="searchResponse.results" [totalCount]="searchResponse.totalCount" ></omni-search> </ng-container>
export const OmniSearchContainer: React.FunctionComponent<{ slot: string }> = ({ slot, }) => { const [search, response] = useOmniSearch({ query: "", searchConfig: "rabobanknl_nl", numberOfResults: 4, }); const onSearch = (event: CustomEvent<string>) => search((request: SearchRequest) => ({ ...request, query: event.detail, })); return ( <OmniSearch slot={slot} searchResults={response?.results ?? []} onDoSearch={onSearch} pageDetailsText={"Pagina's"} /> ); };
CMS
export interface Page { id: string; title: string; // ... redirect: Page_Redirect; view: PageView; regions: Page_Region[]; // ... } export interface Page_Region { view: RegionView; entities: Page_Entity[]; regions: Page_Region[]; } export interface Page_Entity { type: EntityType; view: EntityView; content: any; id: Page_ComponentId; entityType: Page_Entity_Type; }
Template
Slot
Component
export interface Page { id: string; title: string; // ... redirect: Page_Redirect; view: PageView; regions: Page_Region[]; // ... } export interface Page_Region { view: RegionView; entities: Page_Entity[]; regions: Page_Region[]; } export interface Page_Entity { type: EntityType; view: EntityView; content: any; id: Page_ComponentId; entityType: Page_Entity_Type; }
export const ENTITY_CONFIG: EntityConfigMap< // eslint-disable-next-line React.FunctionComponent<any> | React.LazyExoticComponent<any> > = { [EntityType.GLOBALSITES__INTRO_VIDEO]: () => import("../components/organisms/video-banner").then((m) => ({ component: m.VideoBanner, mapper: m.mapper, })), /** * Configure component to type */ [EntityType.GLOBALSITES__NAVIGATION]: () => import("../components/organisms/app-header").then((m) => ({ component: m.AppHeader, mapper: m.mapper, })), [EntityType.GLOBALSITES__FOUNDATION_PROJECTS_OVERVIEW]: { /** * Configure lazy loaded component + lazy loaded mapper to type + view */ [FoundationProjectsOverviewView.FOUNDATION_PROJECTS_OVERVIEW]: () => import("../components/organisms/image-tiles-block").then((m) => ({ component: m.default, mapper: m.mapper, })), }, };
React/Angular
Type match
Type/view match
@Injectable({ providedIn: "root", }) export class CmsService { private readonly createPageAsync: CreatePageAsync<EntityConfig>; private readonly loadTemplateAsync: LoadTemplate; /** * Angular specific check to see if the template config is sync. */ private static isSyncTemplateConfig( template: TemplateConfig ): template is SyncTemplateConfig { return !!template.prototype && !!template.prototype.constructor.name; } constructor( @Inject(CMS_CONFIG) { entityConfigMap, templateConfigMap, defaultTemplate }: CmsConfig ) { this.createPageAsync = createPageFactory<EntityConfig>(entityConfigMap); this.loadTemplateAsync = (view: PageView) => { const templateConfig = templateConfigMap[view] ?? defaultTemplate; return CmsService.isSyncTemplateConfig(templateConfig) ? Promise.resolve(templateConfig) : templateConfig(); }; } loadTemplate(view: PageView): Observable<EntityConfig> { return from(this.loadTemplateAsync(view)); } createPage(page: Page): Observable<ParsedPage<EntityConfig>> { return from(this.createPageAsync(page)); } }
const createPage = createPageFactory<React.FunctionComponent>(ENTITY_CONFIG); const DEFAULT_TEMPLATE = FoundationAboutUsTemplate; type DynamicPage = { page: ParsedPage<React.FunctionComponent>; Template: React.FunctionComponent; }; export function useDynamicPage(pageResponse: Page): DynamicPage | undefined { const [data, setData] = useState<DynamicPage>(); useEffect(() => { (async () => { const page = await createPage(pageResponse); const Template = TEMPLATE_CONFIG[page.view] || DEFAULT_TEMPLATE; setData({ Template, page: page }); })(); }, [pageResponse]); return data; }
@Component({ selector: "global-sites-poc-dynamic-cms", templateUrl: "./dynamic-cms.component.html", styleUrls: ["./dynamic-cms.component.scss"], }) export class DynamicCmsComponent implements AfterViewInit { @ViewChild("slots", { read: ViewContainerRef }) slotsRef: ViewContainerRef; @ViewChild("template", { read: ViewContainerRef }) templateRef: ViewContainerRef; constructor(private cmsService: CmsService) {} ngAfterViewInit() { forkJoin([ this.cmsService.createPage(PAGE), this.cmsService.loadTemplate(PAGE.view), ]).subscribe(([{ regions }, template]) => { const { location: { nativeElement: templateElement }, } = this.templateRef.createComponent(template); regions.forEach(({ components, view }) => { components.forEach(async ({ component, data }) => { const { instance, location: { nativeElement }, } = this.slotsRef.createComponent(component); Object.keys(data).forEach((key) => (instance[key] = data[key])); nativeElement.slot = view; /** * DOM Hack that puts components inside the template. */ nativeElement.parentElement.removeChild(nativeElement); templateElement.appendChild(nativeElement); }); }); }); } }
export const DynamicCms = () => { const dynamicPage = useDynamicPage(FOUNDATION_ABOUT_US_PAGE); if (!dynamicPage) { return <div>Loading</div>; } const { Template, page: { regions }, } = dynamicPage; return ( <Template> {regions.map(({ view, components }) => components.map(({ component: Component, data }, i) => ( <Component {...data} slot={view} key={`${view}-${i}`} /> )) )} </Template> ); };