I have a dynamic component loader, and I need to pass in data through a service. I can get the data to display if I fire the function on click
for example, but not OnInit
.
Update: Working StackBlitz
app.component.html
<app-answer-set></app-answer-set>
header.component.html
<ng-template appHeaderHost></ng-template>
header.component.ts
export class HeaderComponent implements OnInit {
@Input() component;
@ViewChild(HeaderHostDirective) headerHost: HeaderHostDirective;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngOnInit() {
this.loadComponent();
}
loadComponent() {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.component);
const viewContainerRef = this.headerHost.viewContainerRef;
viewContainerRef.createComponent(componentFactory);
}
}
header-host.directive.ts
export class HeaderHostDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
header-data.service.ts
export class HeaderDataService {
private headerDataSource = new Subject<any>();
headerData$ = this.headerDataSource.asObservable();
constructor() { }
setHeaderData(data: any) {
this.headerDataSource.next(data);
}
}
answer-set.component.html
<app-header [component]="component"></app-header>
<button (click)="setHeaderData()">click</button>
answer-set.component.ts
export class AnswerSetComponent implements OnInit {
component: any = AnswerSetHeaderDetailsComponent;
constructor(private headerDataService: HeaderDataService) { }
ngOnInit() {
this.setHeaderData();
}
setHeaderData() {
const data = [{name: 'Header stuff'}];
this.headerDataService.setHeaderData(data);
}
}
answer-set-header-details.html
<dt>First:</dt>
<dd>Description</dd>
<dt>Second</dt>
<dd>Description</dd>
<p>
data will show on click of button but not <code>onInit</code>:
</p>
<p>
{{headerData}}
</p>
answer-set-header-details.component.ts
export class AnswerSetHeaderDetailsComponent implements OnInit {
constructor(private headerDataService: HeaderDataService) { }
headerData: any;
ngOnInit() {
this.headerDataService.headerData$
.subscribe(data => {
this.headerData = data[0].name;
});
}
}
After running through the Angular documentation I found that I needed to pass the data to the componentRef instance, something like this:
(<IHeader>componentRef.instance).data = headerItems.data;
After a little refactoring I ended up passing both the component and the data through a service, see the updated StackBlitz for a working example.
Eventually I'll look to pass in multiple components, but for now this works.
header.component.html
<ng-template appHeaderHost></ng-template>
header.component.ts
/* ... */
export class HeaderComponent implements OnInit {
@ViewChild(HeaderHostDirective) headerHost: HeaderHostDirective;
subscriptionManager = new Subscription();
constructor(
private headerService: HeaderService,
private componentFactoryResolver: ComponentFactoryResolver
) {
const headerConfigSubscription = this.headerService.headerConfig$
.subscribe((headerItems: HeaderItem) => {
this.loadComponent(headerItems);
});
this.subscriptionManager
.add(headerConfigSubscription);
}
ngOnInit() {
}
loadComponent(headerItems: HeaderItem) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(headerItems.component);
const viewContainerRef = this.headerHost.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
(<IHeader>componentRef.instance).data = headerItems.data ? headerItems.data : null;
}
ngOnDestroy() {
/**
* @description Unsubscribe from all subscriptions
* to prevent memory leaks
*/
this.subscriptionManager.unsubscribe();
}
}
header.service.ts
/* ... */
export class HeaderService {
private headerConfigSource = new Subject<any>();
headerConfig$ = this.headerConfigSource.asObservable();
constructor() { }
configureHeaderItems(headerItems: HeaderItem) {
this.headerConfigSource.next(headerItems);
}
}
header.ts
/* ... */
export interface IHeader {
data: any;
}
export interface IHeaderItem {
component: Type<any>;
data?: any;
}
export class HeaderItem implements IHeaderItem {
constructor(
public component: Type<any>,
public data?: any
) {
this.component = component;
this.data = data;
}
}
main.component.html
<app-header></app-header>
main.component.ts
export class MainComponent implements OnInit {
headerItems: HeaderItem;
constructor(private headerService: HeaderService) { }
ngOnInit() {
this.configureHeaderItems();
}
configureHeaderItems() {
this.headerItems = new HeaderItem(MainHeaderDetailsComponent, {});
this.getHeaderItemData();
}
/**
* @description This is where we would make the API call and get the data
* but we can mock it for now
*/
getHeaderItemData() {
const data = {
name: 'I am loaded dynamically!'
};
this.headerItems.data = data;
this.headerService.configureHeaderItems(this.headerItems);
}
}
main.module.ts
@NgModule({
/* ... */
// Need to add to entryComponents
entryComponents: [MainHeaderDetailsComponent]
})
export class AnswerSetModule { }
main-header-details.component.html
<p>
Header content. {{data.name}}
</p>
main-header-details.component.ts
/* ... */
export class MainHeaderDetailsComponent implements OnInit {
constructor() { }
@Input() data: any;
ngOnInit() {
}
}