Demo
Component library
Rabobank Foundation
Atomic Design
Example
Atom
Molecule
Organism
Template
Page
Layout composition
Template
Slots
Storybook
Visual Regression
Chromatic
See requirements change
Catch mistakes
Framework Agnostic
Omni Search
Omni Search
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",
},
}
)
...
}
App shell
OmniSearch
OmniSearch
OmniSearch
Angular
// ...
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));
}
}
React
// ...
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];
}
App shell
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,
});
}
}
Angular
<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"}
/>
);
};
React
Dynamic content
App shell
CMS
CMS Response
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
CMS Response
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;
}
Component config
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
CMS App shell (Angular)
@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;
}
CMS App shell (React)
@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);
});
});
});
}
}
CMS Render (Angular)
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>
);
};
CMS Render (React)
Development philosophy demo
By rachnerd
Development philosophy demo
- 255