Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Insert dynamic Angular 4.x content with known/declared components

I have a (what I think would be a fairly common) problem that I cannot find a good way to solve with the current Angular 4.x architecture. Maybe there is a method that I haven't found yet, but I have searched pretty extensively.

I would like to insert some dynamic, user generated HTML content into an Angular app. This HTML content my also include some known(included in the module declaration) Angular components that should be rendered. Below is some puesdo-app HTML might help me explain:

<app-component>

    <!-- standard angular header and router-outlet -->
    <app-header>
        <app-nav>
            <ul>
                <li routerLink="/page-one">First Page</li>
                <li routerLink="/page-two">Second Page</li>
                <li routerLink="/page-three">Third Page</li>
            </ul>
        </app-nav>
    </app-header>
    <router-outlet></router-outlet>

    <!--
        the dynamic content would be inserted into the <main> element as HTML with angular <app-... tags
        the content in the HTML content inserted main tag could contain anything
        the angular components within the content should be initalized as normal
    -->
    <main>
        <h1>Title</h1>
        <app-component>
            <p>this component and its content should be initalized when HTML is inserted into main</p>
        </app-component>
        <app-component>
            <app-sub-component>
                angular components here might also contain other angular components
            </app-sub-component>
        </app-component>
        <p>more regular HTML content</p>
    </main>

    <!-- more traditional Angular components could come after -->
    <app-footer></app-footer>

</app-component>

I have tried two methods of achieving this, neither of which truly work.

1. Using the Dynamic Template/Component pattern with the JIT Compiler.

TLDR; It doesn't work with AOT. The AOT is all or nothing.

I've used a dynamic component pattern like this one. This is very simple to implement. Using the JIT Compiler, make a new Component and Module out of the HTML content, and insert an instance of the new component. This works as long as the project is not compiled AOT. Since Angular 4.x the JitCompilerFactory cannot be included in an AOT compiled build. It seems like it might make sense to allow the JIT Complier into AOT compiled builds so known components can get the performance boost, and use the JIT Compiler just for dynamic components. It would be the best of both worlds. BUT, I don't know the inner workings of Angular and I assume there is a good reason not to allow the JIT Compiler's inclusion.

2. Using the Dynamic Component Loader Pattern pattern

TLDR; Can't get an entry point to load the components.

After my issues with the JIT Compiler in an AOT build, I thought I could maybe use the ComponentFactoryResolver. Since I'm only using components already declared in the module, I could just create a component in the place of where it should be in the HTML content. This idea is a little crazy, but I thought it might work. My approach would look like this:

  1. Insert the dynamic HTML content
    • get dynamic content as HTML
    • don't remove Angular components using the DomSanatizer.bypassSecurityTrustHtml(),
    • and insert it into the DOM
  2. parse the inserted HTML for components
    • using an {<app-selector-string> : ComponentClass} json table
    • find all components as DOM Nodes and match them to their corresponding ComponentClass
  3. create a view ViewContainerRef in the spot I would like to insert the component (this part is not possible)
  4. using ViewContainerRef.createComponent(), create a new instance of the proper component
    • insert it in place of the old element
    • give the component projectableNodes that is the content of the old html component
  5. give the new component its inputs using a reference to it.

This does not work because dynamically creating a ViewContainerRef in step 3 is not possible.(It may also not be possible to do anything after step 3. IDK. I didn't get that far)

Is there any way to do this? Am I over thinking it?

Thanks!

like image 393
James Bradford Avatar asked May 26 '17 04:05

James Bradford


1 Answers

This is very possible, actually...

Angular's ApplicationRef has an attachView() method that will keep track of a new component reference. That component reference can then be inserted anywhere just like a regular component.

Here's a pseudo-code example:

// create a componentRef
const componentFactory = ComponentFactoryResolver.resolveComponentFactory(AngularComponent);
const componentRef = componentFactory.create(Injector);

// attach the componentRef to the angular ApplicationRef
ApplicationRef.attachView(componentRef.hostView);

// insert the root element of the new componentRef into any DOM node
const componentRoot: HTMLElement = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0];
const node = document.getElementById('insertionPoint'); 
Renderer2.insertBefore(node.parentNode, componentRoot, node);

// don't forget to destroy the component when you're done with it
ApplicationRef.detachView(componentRef.hostView);
componentRef.destroy();

Better explanation here

like image 88
James Bradford Avatar answered Nov 17 '22 23:11

James Bradford