We are integrating an angular2 app into a cms (Sitecore) where content editors need the ability to add, remove, and rearrange components on a page, as well as include new components as needed.
This is achieved by having the cms generate script tags to load our components. An example of the generated html is:
<body>
<sales-root></sales-root>
<script type="text/html" id="generated-header">
<sales-welcome></sales-welcome>
</script>
<script type="text/html" id="generated-content">
<sales-personal-info></sales-personal-info>
<div><!-- regular html content --></div>
</script>
</body>
Within the sales-root
component, we have:
export class AppComponent extends Translation implements OnInit {
@ViewChild('header', { read: ViewContainerRef }) headerRef;
@ViewChild('content', { read: ViewContainerRef }) contentRef;
ngOnInit() {
this.loadSitecorePlaceholders(this.headerRef, 'generated-header');
this.loadSitecorePlaceholders(this.contentRef, 'generated-content');
// ...
}
private loadSitecorePlaceholders(containerRef: ViewContainerRef, placeholder: string) {
// get the generated components listed from the sitecore placeholder
const generatedContent = this.elementRef.nativeElement.parentNode.children[placeholder];
if (generatedContent === undefined) { return; }
if (containerRef === undefined) { return; }
this.createComponentService.createComponentsFromHtml(containerRef, generatedContent.innerText);
// we've finished creating all the components we need, remove the nodes created by sitecore.
this.elementRef.nativeElement.parentNode.removeChild(generatedContent);
}
}
In the CreateComponentService
, we initialize an array of allowable component factories and a generic htmlHost component factory:
this._selectableFactories = creatableComponents
.map((component: Type<any>) =>
this.componentFactoryResolver.resolveComponentFactory(component));
We extract selector information from the CMS-generated script tag, then either add the component or inject it into the generic html component:
private createAngularComponent(
container: ViewContainerRef,
factory: ComponentFactory<Type<any>>,
element: HTMLElement,
) {
const selector = factory.selector.toLocaleLowerCase();
container.createComponent(factory);
// ...
}
private createHtmlComponent(container: ViewContainerRef, element: HTMLElement) {
const component = container.createComponent(this.htmlHostFactory).instance;
component.content = element.outerHTML;
}
Although the system is currently functioning well, performance remains a concern. With numerous components being loaded and compiled, there is a noticeable delay (3 to 4 seconds on a fast machine). To address this, we are taking two approaches:
- Transitioning to AOT instead of JIT. This progress has been hindered by an internal library issue, but we are actively resolving it.
- Implementing lazy loading of components upon request. Since only a subset of components is required on any given page, this optimization should yield performance gains.
For #2, all creatable components must be listed in entryComponents
within the @NgModule
. Is it necessary for all components to exist at generation? Additionally, while pre-loading factories allows us to find selectors, could this list be constructed during development?
Ideally, we envision using a dictionary of selectors linked to lazy component factories. Upon first call, the respective component would be downloaded and injected, enhancing perceived load speed post ngInit.
Is it feasible to lazily load components without router implementation, considering the dynamic composition? How can this be achieved?