16

In creating dynamic components in Angular 2, I found out that this process requires ViewContainerRef in order to add newly created component to DOM.

And in passing @Input and @Outputto those dynamically created components, I found the answer in the second link above and here.

However, if I were to create a service named shape.service that contains functions returning different shape components with some @Input like bgColor, I don't know how this service will create a component without specifying DOM location, and how the container-component receives this returned component (probably its type will be ComponentRef) from the service and injects it to the DOM container-component specifies.

For example, a service contains a method:

getCircle(bgColor:string): ComponentRef<Circle> {
    let circleFactory = componentFactoryResolver.resolveComponentFactory(CircleComponent);
    let circleCompRef = this.viewContainerRef.createComponent(circleFactory);
    circleCompRef.instance.bgColor = bgColor;

    return circleCompRef;
}

First question rises here, how do I make this.viewContainerRef point to no where for the meantime? The reason why I'm importing ViewContainerRef is to create component dynamically.

Second question is after container-component receives input-specificcomponentRef from the service, how will it inject to its DOM?

UPDATE: I think my question above wasn't specific enough. I'm in a situation where:

  1. A parent component calls the service and gets the componentRef/s,
  2. Creates an object including componentRef/s together with some other data and stores those created object/s into array
  3. Passes it to its children as @Input,
  4. And let each child inject componentRef to its DOM and use rest of the data in the object in other way.

That means the service calling component has no idea where those componentRef will get injected. In short, I need independent component objects that can be injected to anywhere, anytime I want.

I'm reading the rumTimeCompiler solution several times already but I don't really get how that really works. It seems like there's too much work compared to component creation using viewContainerRef. I'll dig into that more if I find no other solution...

2

5 Answers 5

15

In case someone like me still looking for a simple and clear solution nowdays - here it is. I got it from @angular/cdk https://github.com/angular/components/tree/master/src/cdk and made a simple service.

import {
    Injectable,
    ApplicationRef,
    ComponentFactoryResolver,
    ComponentRef,
    Injector,
    EmbeddedViewRef
} from '@angular/core';

export type ComponentType<T> = new (...args: any[]) => T;

@Injectable({
    providedIn: 'root'
})
export class MyService {

    constructor(
        private _appRef: ApplicationRef,
        private _resolver: ComponentFactoryResolver,
        private _injector: Injector
    ) { }

    private _components: ComponentRef<any>[] = [];

    add<T>(
        component: ComponentType<T> | ComponentRef<T>,
        element?: Element | string
    ): ComponentRef<T> {
        const componentRef = component instanceof ComponentRef
            ? component
            : this._resolver.resolveComponentFactory(component).create(this._injector);
        this._appRef.attachView(componentRef.hostView);
        if (typeof element === 'string') {
            element = document.querySelector(element);
        }
        if (!element) {
            element = document.body;
        }
        element.appendChild(
            (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement
        );
        this._components.push(componentRef);
        return componentRef;
    }

    remove(dialog: number | ComponentRef<any>): boolean {
        let componentRef;
        if (typeof dialog === 'number' && this._components.length > dialog)  {
            componentRef = this._components.splice(dialog, 1)[0];
        }
        else {
            for (const cr of this._components) {
                if (cr === dialog) {
                    componentRef = cr;
                }
            }
        }
        if (componentRef) {
            this._remove(componentRef);
            return true;
        }
        return false;
    }

    private _remove(componentRef: ComponentRef<any>) {
        this._appRef.detachView(componentRef.hostView);
        componentRef.destroy();
    }

    clear() {
        while (this._components.length > 0) {
            this._remove(this._components.pop());
        }
    }

    getIndex(componentRef: ComponentRef<any>): number {
        return this._components.indexOf(componentRef);
    }

}

You can pass ComponentClass or ComponentRef to add and Element or any querySelector string pointing to any DOM element where you want to attach your component as second argument (or nothing, then it assumes you want to attach to body).

const cr = this._service.add(MyComponent); // will create MyComponent and attach it to document.body or
const cr = this._service.add(MyComponent, this.someElement); // to attach to Element stored in this.someElement or
const cr = this._service.add(MyComponent, 'body div.my-class > div.my-other-div'); // to search for that element and attach to it
const crIndex = this._service.getIndex(cr);
cr.instance.myInputProperty = 42;
cr.instance.myOutputEmitter.subscribe(
    () => {
        // do something then for example remove this component
        this._service.remove(cr);
    }
);
this._service.remove(crIndex); // remove by index or
this._service.remove(cr); // remove by reference or
this._service.clear(); // remove all dynamically created components

P.S. Don't forget to add your dynamic components to entryComponents: [] of @NgModule

Sign up to request clarification or add additional context in comments.

3 Comments

Works perfectly but I had to do some tweaking, btw I'm using Angular 9: * Injecting private appRef: ApplicationRef directly in the constructor caused a cyclic dependency error: Error: Cannot instantiate cyclic dependency! ApplicationRef, so I opted for the lazy loading of the object using the injector ``` this.appRef = this.injector.get(ApplicationRef); this.appRef.attachView(componentRef.hostView); ``` * Also when calling the add element you can attach it to a dom object identified by id using: this._service.add(MyComponent, '#my-element-id');
Yeah works nicely. Really many thanks! Not sure about the entryComponents[] since I could use this without setting it.
ComponentFactoryResolver is now deprecated :(
7

Maybe this plunker will help you: https://plnkr.co/edit/iTG7Ysjuv7oiDozuXwj6?p=preview

As far as i know, you will need the ViewContainerRef inside of your service. But the component calling your service can add it as an parameter, like this:

(just a the service.. see plunker for full working example)

import { Injectable, ViewContainerRef, ReflectiveInjector, ComponentFactoryResolver, ComponentRef } from '@angular/core';

import { HelloComponent, HelloModel } from './hello.component';

@Injectable()
export class DynamicCompService {

  constructor (private componentFactoryResolver: ComponentFactoryResolver) { }

  public createHelloComp (vCref: ViewContainerRef, modelInput: HelloModel): ComponentRef {

    let factory = this.componentFactoryResolver.resolveComponentFactory(HelloComponent);

    // vCref is needed cause of that injector..
    let injector = ReflectiveInjector.fromResolvedProviders([], vCref.parentInjector);

    // create component without adding it directly to the DOM
    let comp = factory.create(injector);

    // add inputs first !! otherwise component/template crashes ..
    comp.instance.model = modelInput;

    // all inputs set? add it to the DOM ..
    vCref.insert(comp.hostView);

    return comp;
  }
}

3 Comments

Oops, I pressed enter before finishing my sentence :( Sorry for removing the "green check" on your answer. I thought this was what I needed but I found out my question wasn't specific enough... Do you mind checking the update and giving another solution if possible? Sorry for the trouble
I dont know if its possible to create a component just like that way, cause the component needs to know which components, services and directives it could use..i am currently searching for a solution, too. maybe we will found something .. keep this thread up! :)
Thanks for your effort! I still have no idea why it is not possible to create component instance without specifying its viewContainer. If that's possible, I believe it'll be easier to move, delete, or add existing component instance easily from one container to another. It's somehow possible to do with componentFactory but it becomes hard to implement if that component requires input/output. I tried to look for why it is a "must" to specify viewcontainer in creating component instance, but I still don't know why. I hope someone would at least give an answer to that...
2

In my case I was looking for a solution without using ComponentFactoryResolver that is now deprecated:

Deprecated: Angular no longer requires Component factories. Please use other APIs where Component class can be used directly.

And I found this solution from a colleague of mine by using createComponent instead.

Comments

0

Here is an alternative approach to adding a component dynamically and managing communication between that component and its parent.

The specific example is for a dialog box with a form, which is a common use case.

Demo on Stackblitz.

Repo with code

  • dialog-wrapper is a dialog component
  • dialog-form is an example of a dynamic component being injected into dialog-wrapper, with the necessary supporting services.
  • name (Alice) is an example of arbitrary data being passed from the parent component to dialog-form via dialog-wrapper
  • An object with name and favouriteFood is then passed back to the parent when the form in dialog-form is submitted. This also tiggers the parent component to close dialog-wrapper.

I've tried to keep the code as straightforward as possible so it can be easily retasked. The dialog wrapper itself is fairly simple; most of the heavy lifting is in the injected component and the parent component.

This is not exactly the architecture outlined in the OP, but I believe it satisfies:

I need independent component objects that can be injected to anywhere, anytime I want.

Comments

0
    buildComponent = (
    componentType: Type<any>,
    inputs?: ComponentInputs
): ComponentRef<any> => {
    const appRef = this.injector.get(ApplicationRef);
    const component = createComponent(componentType, {
        environmentInjector: appRef.injector,
    });
    const componentInstance = component.instance;
    if (inputs) {
        Object.assign(componentInstance, inputs);
    }

    return componentInstance;
};

This works as intended. I guess ApplicationRef can't be casually injected in constructor because of Circular Dependency but this works properly. You might want to make appRef a private member of the service because it should be taking some resource to constantly get a reference. But if you only occasionally build components, this will work.

And an example is:

const component2 = this.dynamicComponentService.buildComponent(
                DateFormatterComponent,
                componentInputs
            );

Component Inputs are here to automatically set Inputs of the component.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.