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>
);
};