3

DynamicComponentLoader has been deprecated. I am trying to build a parent component that renders child components inside of the parent component based on an array of child components, meaning that each user has a settings file that holds the child components that need to be in the parent. Right now the parent looks something like this:

import { Component, OnInit, Input, ComponentResolver, ViewContainerRef } from '@angular/core';

import { Child1Component } from '../children/child1.component'
import { Child2Component } from '../children/child2.component'
import { Child3Component } from '../children/child3.component'

import { ParentService } from './parent.service'

@Component({
  selector: 'parent',
  directives: [Child1Component, Child2Component, Child3Component],
  providers: [ParentService],
  template: `
    <style>
      .parent{
        background-image: linear-gradient(141deg, #8ecb45 0%, #97cd76 71%, #96d885 100%);
        width: 100%;
        height: 200px;
      }
    </style>
    <div class="parent"></div>
  `
})
export class ParentComponent {
  constructor(private parentService:ParentService, private children: any, viewContainer: ViewContainerRef, private componentResolver: ComponentResolver) {

    this.children = parentService.getChildren();

    for(var i = 0; i < children.length; i++) {
      this.componentResolver.resolveComponent(children[i].component)
        .then(componentFactory => {
          const ctxInjector = viewContainer.injector;
          return viewContainer.createComponent(componentFactory, 0, ctxInjector);
        })
    }

  }
}

Right now, call the service in the ParentComponent constructor seems to be causing some issues. But before I was working on looping over the componentResolver, I was able to attach a component underneath then parent node, but not within it.

Does anyone know a better way to build a dynamic parent component? This is to manage a dashboard type of a layout. Most examples are explicit about how many components will be loaded. This wont work for me. I have seen a few posts about how to make this work, but so far I have not seen anything running on with the RC with a config file.

This comes closest to what I am trying to do: http://plnkr.co/edit/jAmMZKz2VqponmFtPpes?p=preview but it uses the dcl...

Currently I am getting an error that I have not figured out when launching the app: TypeError: Cannot read property 'query' of null

Thanks for the help!

Here is a link if you feel like downloading it and giving it a stab:

https://github.com/weswhite/ng2-layout

EDIT/UPDATE: It may have to do with the children: any I am injecting. I dont think I am doing that correctly, still figuring that bit out...

WesW
  • 128
  • 1
  • 9
  • You don't need to list components in `directives: [...]` when they are only added by `viewContainerRef.createComponent()`. http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components/36325468#36325468 is my answer to a similar question. – Günter Zöchbauer May 22 '16 at 11:13
  • What is `private children: any,` supposed to get passed in? – Günter Zöchbauer May 22 '16 at 11:17
  • I realised that right now the private any is not correct. Do you have an idea as to how to inject some object that represents an array of components? Right now I am trying to do something more like creating a child class and having a children: Child[] property on the component. Have not figured that out quite yet – WesW May 22 '16 at 15:08
  • Not sure what `children: Child[]` is supposed to get passed. Where should the array and its content come from? – Günter Zöchbauer May 22 '16 at 15:13
  • parentService.getChildren(); This returns the list of components in the main component – WesW May 22 '16 at 15:15
  • If you already inject `parentService` just call ` parentService.getChildren()` in the constructor of `ParentComponent` (as you already do). There is no need to list `children` in the constructor. If you get an error message that `children` doesn't exist just declare it outside the constructor: `export class ParentComponent { private children: any; constructor(private parentService:ParentService, viewContainer: ViewContainerRef, private componentResolver: ComponentResolver) {` – Günter Zöchbauer May 22 '16 at 15:25
  • I dont think it likes the fact that I am trying to reference this.children[i].component as a component when it is actually just a string property in an object getting returned by a service... The createComponent works when I just reference a single component, but when I wrap it in a for loop and try to trick it into creating a bunch of components from a json file, the createComponent logic seems to be failing. But the Angular errors are so useless it is tough to debug. – WesW May 22 '16 at 17:22
  • I'd suggest using `ngFor` over an array of types using a component like `` like shown in the question I linked in my first comment. Your Plunker uses quite an outdated Angular2 version. `DynamicComponentLoader` is about to be deprecated in favor of `ViewContainerRef.createComponent()` (demonstrated in the my answer to the linked question. In recent Angular2 versions `DynamicComponentLoader` can't be used in the `constructor`. You can't use a string instead of a component type. – Günter Zöchbauer May 22 '16 at 17:42
  • Trying to figure out your Plunker, any reason to make the loader a component? Reuse, or does it have to in order to work? Sill learning Angular... – WesW May 22 '16 at 18:11
  • It's not necessary but it allows to make use of it declaratively and also for example with `*ngFor` – Günter Zöchbauer May 22 '16 at 18:13
  • I was able to implement your plunker. Post an answer with the link to your plunk and I will mark it as the solution so others can know too! Thanks – WesW May 22 '16 at 21:56
  • I updated my answer. – Günter Zöchbauer May 23 '16 at 04:17

2 Answers2

3

I'm pretty sure the error message you mentioned in your question is caused by this parameter

private children: any,

Angular can't inject dependencies where no concrete type is provided and also no @Inject(...) annotation.

Update

Full example for a wrapper class to be able to add dynamic components declaratively

@Component({
  selector: 'dcl-wrapper',
  template: `<div #target></div>`
})
export class DclWrapper {
  @ViewChild('target', {read: ViewContainerRef}) target;
  @Input() type;
  cmpRef:ComponentRef;
  private isViewInitialized:boolean = false;

  constructor(private resolver: ComponentResolver) {}

  updateComponent() {
    if(!this.isViewInitialized) {
      return;
    }
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }
   this.resolver.resolveComponent(this.type).then((factory:ComponentFactory<any>) => {
      this.cmpRef = this.target.createComponent(factory)
    });
  }

  ngOnChanges() {
    this.updateComponent();
  }

  ngAfterViewInit() {
    this.isViewInitialized = true;
    this.updateComponent();  
  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }    
  }
}

From https://stackoverflow.com/a/36325468/217408

Plunker example RC.1

Plunker example beta.17

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • This ended up working perfectly fine, but I went teleaziz's answer because it was more inline with what I was thinking. Thanks for all your help in getting me towards a solution! – WesW May 24 '16 at 17:45
3

You can use ComponentResolver with a child ViewContainerRef to create the component dynamically and load them in the parent component.

@Component({
  selector: 'parent',
  providers: [ParentService],
  template: `
    <style>
      .parent{
        background-image: linear-gradient(141deg, #8ecb45 0%, #97cd76 71%, #96d885 100%);
        width: 100%;
        height: 200px;
      }
    </style>
    <div #content></div>
   `
  })

export class ParentComponent {
  @ViewChild('content', { read: ViewContainerRef }) contentContainer: ViewContainerRef;

  children: Component[];

  constructor(
    private parentService:ParentService,
    private resolver: ComponentResolver) {
    this.children = parentService.getChildren();
  }

  ngOnInit() {
    this.children.forEach( (component, index) => this.loadComponent(component, index));
  }

  private loadComponent(component: Component, index: number) {
    return this
      .resolver
      .resolveComponent(component)
      .then( factory => 
        this.contentContainer.createComponent(factory, index, this.contentContainer.injector))
  }
}

I also upgraded the provided plunker to use latest @angular and the ComponentResolver instead of the DynamicComponentLoader: Plunker: http://plnkr.co/edit/Z6bXwBcwAnf4DSpNlU8H?p=preview

Using DCL with loadNextTolocation: http://plnkr.co/edit/NYgJzz9UjrtGsNnO5WXS?p=info

P.S: You were getting TypeError: Cannot read property 'query' of null, because of the way you were injecting children, you have to specify an injectable Type.

See also Angular 2 dynamic tabs with user-click chosen components

Community
  • 1
  • 1
teleaziz
  • 2,220
  • 1
  • 19
  • 25
  • 1
    Looks great! I went a little bit of a different route, but this too would work! I like how you followed my original intent without adding a wrapper component. – WesW May 24 '16 at 17:35
  • Is it possible to update this to RC5 where the ComponentResolver is no longer in use? I tried myself and I get a "No component factory found for ...: error in the console. – David Chen Sep 01 '16 at 21:24