Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Angular 2, creating a Component through ComponentFactory does not seem to render completely

I'm trying to use ag-grid in an Angular 2 application. Because I'm using infinite scrolling and server side filtering, I want basically all of my columns in the grid to have custom filters that I can then pass to the server where the filtering will actually be executed. While ag-grid has a relatively straight forward interface for setting up these custom filters, it is noted on the Angular 2 page in the ag-grid documentation that ag-grid made use of Angular 1 compiling and that since Angular 2 does not support compiling after application startup, none of the custom components in the grid (custom filters, cells, rows, etc.) will support any of the Angular 2 features (like two-way data binding, etc.).

So I've been searching for an Angular 2 way to dynamically load the component into the DOM element that ag-grid is inserting into its filter pop-up window. I've looked at both the DynamicComponentLoader (which is deprecated) and at a couple variations of using the ComponentResolver. Once I've got the ComponentResolver, I can call resolveComponent to get a ComponentFactory and then I can use @ViewChild to get ViewContainerRef and call createComponent on that ViewContainerRef to create my new component. However, that doesn't help me with the grid because @ViewChild won't find an element that's dynamically added to the DOM directly the way ag-grid is doing it.

Alternately, once I've got the ComponentResolver and call resolveComponent to get a ComponentFactory, I can call create on the componentFactory and pass it the injector from my ViewContainerRef and a string tag for the element I want the component inserted into and that "seems to" work, but the Component is not rendered correctly. I get similar behavior if I use DynamicComponentLoader (i.e. the component is not rendered as expected).

Is there some accepted way to load an Angular 2 Component within a particular element in the DOM?

Some code to illustrate the issue follows, which I based on the Angular 2 quickstart:

app.component.ts:

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

import { DynComponent } from './dyn.component';

@Component({
    selector: 'my-app',
    template: `
        <h1>My test Angular 2 App</h1>
        <input type="text"  class="form-control" required
          [(ngModel)]="name" >
          TODO: remove this: {{name}}
        <p> </p>
        <div #insertPoint>
            <button (click)="createDynamicComponent()">Create The Dynamic Component</button>
            Inserting a new component below this.
        </div>`
})
export class AppComponent {
    name: string;
    @ViewChild('insertPoint', {read: ViewContainerRef}) compInsertPoint: ViewContainerRef;

    constructor(private componentResolver: ComponentResolver){}

    createDynamicComponent(){
        console.log("Creating new Component.");

        //You're not supposed to manipulate the DOM like this in Angular 2,
        //but this is what ag-grid is doing
        var newTextSpan = document.createElement('span');
        newTextSpan.innerHTML = `<div id='dynCompDiv'> </div>`;
        this.compInsertPoint.element.nativeElement.appendChild(newTextSpan);

        this.componentResolver.resolveComponent(DynComponent)
            .then(cmpFactory => {
                const ctxInjector = this.compInsertPoint.injector;
                //The below commented out createComponent call will create the
                //component successfully, but I can't figure out how to do the
                //same thing and have the component created within the above
                //<div id='dynCompDiv'>
                // this.compInsertPoint.createComponent(cmpFactory, 0, ctxInjector);

                //This appears to try to create the component, but the component
                //is not expanded correctly (missing text, no two-way data
                //binding)
                cmpFactory.create(ctxInjector, null, '#dynCompDiv');
            })
    }
}

dyn.component.ts

import { Component } from '@angular/core';

@Component({
    selector: 'dyn-app',
    template: `
        <h1>My test Angular 2 App</h1>
        <input type="text"  class="form-control" required
          [(ngModel)]="name" >
          TODO: remove this: {{name}}`
})
export class DynComponent {
    name: string;
}

Resulting HTML after one click of the button in the app.component (note the mismatch in the input type="text" and the following plain text starting at "TODO:")

<my-app>
    <h1>My test Angular 2 App</h1>
    <input class="form-control ng-untouched ng-pristine ng-invalid" required="" type="text">
      TODO: remove this: 
    <p> </p>
    <div>
        <button>Create The Dynamic Component</button>
        Inserting a new component below this.
        <span>
            <div id="dynCompDiv">
                <h1>My test Angular 2 App</h1>
                <input class="form-control" required="" type="text">
            </div>
        </span>
    </div>
</my-app>

Update: Here's a Plunker with this example: Angular2 Dynamic Component

Update 2: Just a little more information about the above comment that I get similar behavior using the DynamicComponentLoader. If I read the Angular code correctly, the DynamicComponentLoader is basically doing the same thing I'm doing above. It's resolving the component to get a ComponentFactory and then using the create method on that factory. So the similar behavior makes perfect sense.

Update 3: Updated the Plunker to get it working again: Updated Plunker

like image 505
Mutmansky Avatar asked Nov 09 '22 11:11

Mutmansky


1 Answers

I have a work-around for this issue that is sufficient to get me moving again. As noted in the commented out code above, the createComponent call on a ViewContainerRef does correctly create a component. The only downside is you can only create it as a sibling to another element, not within another element. Since ag-grid's getGui call expects a return of html of a DOM element, I can create the component as a sibling and then return the element in the getGui call. This doesn't seem like the ideal solution to me and I still question whether the create call on ComponentFactory is working correctly, but the Angular folks don't see this as a bug: Angular 2 Issue 10523

So, I don't see any palatable alternatives other than to follow this workaround.

like image 194
Mutmansky Avatar answered Nov 15 '22 06:11

Mutmansky