George Edwards George Edwards - 3 months ago 28
HTTP Question

Data-binding ng2 component's template only set OnInit

I have an angular 2 (RC5) component which makes an HTTP call and sets the result as the template of the component. I want to inject a value into the HTML that is returned by the HTTP call. so for example, one of the lines in the returned HTML is:

<a class="d2h-file-name" href="{{chapterURL}}">app/views/login/login.xml</a>


However, that is rendered exactly as is, without having the chapterURL injected. Presumably, this is because the template isn't set during the initialization process? If so, How should I inject these dynamic values into the templates?

Here's the component.

@Component({
selector: 'codestep',
template: `<div class="codestep" [innerHTML]="content"></div>`
})
export class codeStepComponent {
@Input() step: string;
private content: string = '';
private chapterURL;

constructor(private route: ActivatedRoute, private http: Http) { }

ngOnInit() {
this.chapterURL = './diff/' + this.step + '.html';
this.getChapter()
.subscribe(
chapterContent => this.content = chapterContent,
error => this.errorMessage = <any>error);
}

getChapter(): Observable<any> {
return this.http.get(this.chapterURL)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Res) {
let body = res._body;
return body;
}
//Error handling function here...
}


Edit:



I have changed the source html file which is returned by the http call, to:

<a class="d2h-file-name" href={{chapterURL}}>app/views/login/login.xml</a>


and then changed the component's template to:

template: `<div class="codestep" [innerHTML]="content|rawHtml"></div>`


where
rawHtml
is a pipe that sanitises the content with the
bypassSecurityTrustHtml()
function on the
DomSanitizationService
however, I still get the same result, the rendered result is:

<a class="d2h-file-name" href="gitURL">app/views/login/login.xml</a>


if I do
ng.probe($0)
with the component selected in the browser, then the returned resultant object has properties, but the only property listed is
innerHTML
, nothing else...

Answer

2 Methods

Method 1 - search and replace

This is simple and easy, if the data only need to be updated once during initialization.

ngOnInit() {
    this.chapterURL = './diff/' + this.step + '.html';
    this.getChapter()
        .subscribe(
        chapterContent:string => {

            // Pre-process the content
            processedContent = chapterContent.replace('{{chapterURL}}',this.chapterURL);

            this.content = processedContent;
        },
        error => this.errorMessage = <any>error);
}

Method 2 - dynamic component

Angular 2 does not support component template run time update.

innerHTML will not meet your requirement as Angular2 will not parse the content of it. So data binding within innerHTML will not work.

To archive run time template update, or more precisely, run time template generation is using dynamic component.

There is a detail answer with example here by Radim Köhler: http://stackoverflow.com/a/38888009/1810391

http://plnkr.co/edit/iXckLz?p=preview

Following is a very minimalistic example I put together:

cf.com.ts

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

import { RuntimeCompiler } from '@angular/compiler';

import { CfModule } from './cf.module';

@Component({
    selector: 'cf-com',
    template: `
        <h1>{{title}}</h1>
        <button (click)="template1()">Template 1</button>
        <button (click)="template2()">Template 2</button>
        <button (click)="moreChild()">More Child</button>
        <template [ngIf]="childRef" #child></template>`
})
export class CfCom {
    title = 'Component Factory Test';

    // reference for html element with #child tag
    @ViewChild('child', { read: ViewContainerRef }) protected childComTarget: ViewContainerRef;
    // Child component reference
    protected childRef: ComponentRef<any>;

    constructor(private compiler: RuntimeCompiler) { }

    // Child Input. Use object, not basic type
    childInput = { counter: 0 };

    // Click to get more children
    moreChild() {
        this.childInput.counter++;
    }

    // Click to use template 1
    template1() {
        let t = 'Child:{{j.counter}}';
        this.createChild(t);
    }

    // Click to use template 1
    template2() {
        let t = 'Children:{{j.counter}}';
        this.createChild(t);
    }

    createChild(t: string) {
        // Destroy child if exist
        if (this.childRef) {
            this.childRef.destroy();
            this.childRef = null;
        }

        // cf-child class
        @Component({
            selector: 'cf-child',
            template: t // template from parameter t
        })
        class CfChildCom {
            j; // will be bind with parent childInput, see below
        }

        this.compiler.compileComponentAsync<any>(CfChildCom, CfModule)
            .then(factory => {
                this.childRef = this.childComTarget.createComponent(factory, 0);

                // This is how parent variable bind with child variable
                this.childRef.instance.j = this.childInput;
            });
    }
}

cf.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { COMPILER_PROVIDERS } from '@angular/compiler';

import { CfCom } from './cf.com';

@NgModule({
    imports: [BrowserModule],
    exports: [CfCom],
    providers: [COMPILER_PROVIDERS],
    declarations: [CfCom]
})
export class CfModule { }