Data-binding ng2 component's template only set OnInit - javascript

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...

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:
https://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 { }

I have not tested this. Try and let me know how it goes
import {Component, Output, Input,AfterViewInit} from '#angular/core';
export class codeStepComponent implements AfterViewInit {
ngAfterViewInit() {
this.content.innerHTML.replace('{{chapterURL}}', this.chapterURL);
}
}
This assumes you have one instance on {{ChapterUrl}} on your page and this placeholder will get replaced the old fashioned way after the view has been initialized.

Related

Angular 2+ : Get Reference of appcomponent div in another components

I have components called app.component which is the main component in the angular project.
Navigation to customer component is done by routing.
And
Folder structer
src\app
- app.component.html
- app.component.ts
and
src\app\components\customer
- customer.component.html
- customer.component.ts
In my app.component.html
<div class="top-container" #topContainerRef>
<router-outlet></router-outlet>
</div>
In my customer.component.ts
I want to get reference of the top most container div which is contained in app.components
I want to replace
document.getElementsByClassName('top-container')[0].scrollTop = some values
with something similar to
#ViewChild('topContainerRef', { read: ElementRef, static: false }) topContainerRef: ElementRef;
this.topContainerRef.nativeElement.scrollTop= "some value" //here the topContainerRef is undefined
Is there any way i can use elementRef instead of classname or Id's.
You cannot use ViewChild for the #topContainerRef to get a reference of this element, because it is not rendered by your CustomerComponent.
You either need to get the reference of this element inside the app component itself and find a way to pass it to all the other children that might need it (not recommended).
Or you can just build a service and use that to "request" the scrollTop change by whichever component has access to this element (in your case the app component).
I would do it something like this:
export class AppContainerService {
private scrollTopSource = new ReplaySubject<number>(1);
public scrollTop$ = this.scrollTopSource.asObservable();
public updateScrollTop(value: number) {
this.scrollTopSource.next(value);
}
}
Inside your CustomerComponent:
public class CustomerComponent implements OnInit {
// ...
constructor(private containerService: AppContainerService) {
}
ngOnInit() {
this.containerService.updateScrollTop(/* whatever value you need */);
}
// ...
}
And finally, the AppComponent that will react to the scrollTop changes:
export class AppComponent implements AfterViewInit {
#ViewChild('topContainerRef', { read: ElementRef, static: false }) topContainerRef: ElementRef;
private subscriptions = new Subscription();
constructor(private containerService: AppContainerService) {
}
ngAfterViewInit() {
this.subscriptions.add(this.containerService.scrollTop$.subscribe((value: number) => {
this.topContainerRef.nativeElement.scrollTop = value;
}));
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
}
Don't forget about unsubscribing inside ngOnDestroy. This is important so that you don't have memory leaks

Angular 2+: Child components ts variable changes but UI does not show changed value?

I have a child TestComponent component as follows:
import { Component, OnInit, Input } from '#angular/core';
import { ApiService } from '../../../api.service';
#Component({
selector: 'app-test',
templateUrl: './test.component.html'
})
export class TestComponent implements OnInit {
constructor(private apiService: ApiService) { }
testDisplayMessage = 'No data to show';
ngOnInit() {
}
getMessage(param: string) {
this.callingTest = true;
this.apiService.getTest( param ).subscribe( data => {
this.setTestDisplayMessage( data );
this.callingTest = false;
}, err => {
console.log( JSON.stringify( err ) );
this.setTestDisplayMessage( 'Failed to get data' );
this.callingTest = false;
} );
}
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
}
}
contents of test.component.html
<p style="padding: 10px;">{{ testDisplayMessage }}</p>
Use in parent componet :
Trigger JS Code in parent component on button click,
import { TestComponent } from './test/test.component';
....
.....
#Component({
providers: [ TestComponent ],
templateUrl: 'parent.component.html'
})
export class ParentComponent implements OnInit {
...
constructor(private testComponent: TestComponent) { }
...
// Button on parent template triggers this method
getMessage() {
this.testComponent.getMessage('Hello');
}
...
}
Html tag added in parent component,
<app-test></app-test>
When I debugged above code trigger point, call to setTestDisplayMessage() happens the field testDisplayMessage in TestComponent gets changed but UI shows the old message 'No data to show', why is the message on change does not reflect on UI template? Or this is not the way it is supposed to get used? Shall I use #Input
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
Use #ViewChild()
In html file:
<app-test #childComp></app-test>
In parent component.ts file
import { Component, OnInit, ViewChild } from '#angular/core';
....
.....
#Component( {
templateUrl: 'parent.component.html'
} )
export class ParentComponent implements OnInit {
#viewChild('childComp') childComp: any;
constructor() { }
...
// Button on parent template triggers this method
getMessage() {
this.childComp.getMessage('Hello');
}
...
}
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
definitely use #Input() but on set method
#Input()
set someProperty(value) {
// do some code
}
now every time you pass new value here, code will run
basically, your approach is wrong, please use Input() or Services to share data between components.
however, if you want to make ur code work, the below may work
import change detector
constructor(private cdRef: ChangeDetectorRef) {
}
note: import reference ->
import { ChangeDetectorRef } from '#angular/core';
execute detect change after the value is updated
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
this.cdRef.detectChanges();
}
I hope this helps

cannot read property of undefined angular 7

I am getting many errors at the dev tools console when adding a service into my component but the code still working but I want to get rid of from these errors
This's the service:
getPagesData(pageSlug: string): Observable<any[]> {
return this._http.get<any[]>(`${environment.apiUrl}wp/v2/pages/?slug=${pageSlug}`);
}
This is the component:
import { Component, OnInit } from '#angular/core';
import { DataService } from 'src/app/services/data.service';
#Component({
selector: 'app-membership',
templateUrl: './membership.page.html',
styleUrls: ['./membership.page.scss'],
})
export class MembershipPage implements OnInit {
public pageContent: any = {};
public content: string;
constructor(
private _data: DataService
) { }
ngOnInit() {
this._data.getPagesData('memberships')
.subscribe(
page => this.pageContent = page[0]
)
}
getContent(): string {
return this.pageContent.content.rendered.replace(/\[(.+?)\]/g, "");
}
}
What cause the errors is the getContent() method! it says that is the .rendered is an undefined property but it doses defined on the API!
I have searched on that problem and most of the solutions I found it's about using the symbol ? at HTML template but I can't use that in the component itself.
If you are calling getContent() in the HTML/template, you can most likely avoid this error by either:
Making pageContent initially null and using *ngIf to only display the content once it has asynchronously resolved:
Component:
public pageContent: any = null;
Template:
<div *ngIf="pageContent">{{getContent()}}</div>
Or you could instead RxJS operators such as map() and the async pipe:
Component:
import { Component, OnInit } from '#angular/core';
import { DataService } from 'src/app/services/data.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
#Component({
selector: 'app-membership',
templateUrl: './membership.page.html',
styleUrls: ['./membership.page.scss'],
})
export class MembershipPage implements OnInit {
public pageContent: Observable<string>;
public content: string;
constructor(private _data: DataService) { }
ngOnInit() {
this.pageContent = this._data.getPagesData('memberships')
.pipe(
map(page => page[0].content.rendered.replace(/\[(.+?)\]/g, ""))
);
}
}
Template:
<div>{{pageContent | async}}</div>
That being said, you should probably have additional checks to ensure each sub-property is available prior to accessing it, but usually this type of error is because you are attempting to access the contents before they have resolved.
Hopefully that helps!
Yes, you cannot use ? Elvis (Safe navigation) operator in the component itself because it is designed for view part only.
But you can add some check in the component too to avoid such errors like -
getContent(): string {
const dataToReturn = this.pageContent && this.pageContent.content && this.pageContent.content.rendered.replace(/\[(.+?)\]/g, "");
return dataToReturn
}
.rendered is an undefined property
Also, This error may produce you have defined pageContent = {} so on {} neither content nor rendered exist , may be that is also the reason to exist such errors.
Angular recommend to strongly typecast your data before use.

Angular 2 *ngFor doesn't display data

I'm working on an application using Ionic 2 together with Angular 2. Now I'm trying to the data from an API and display this on a page.
I can log the data and I think it's correct, but for some reason nothing is being displayed on the page itself:
The API where I'm receiving the data from is located here: http://peerligthart.com/grotekerk/v1/api.php/zerken?transform=1
*ngFor on my view
<ion-content padding>
<h1 *ngFor="let z of zerken">
{{ z.naam }}
</h1>
</ion-content>
Controller
import { Component } from '#angular/core';
import { NavController, PopoverController } from 'ionic-angular';
import { PopoverPage } from '../popover/popover';
import { ZerkenProvider } from '../../providers/zerken';
#Component({
selector: 'page-lijst',
templateUrl: 'lijst.html',
providers: [ZerkenProvider]
})
export class LijstPage {
zerken: Array<any>;
constructor(public navCtrl: NavController, public popoverCtrl: PopoverController, public zerkenProvider: ZerkenProvider) {
this.zerkenProvider.getZerken().subscribe(
data => {
console.log(data.zerken);
this.zerken = data.zerken.results;
}
)
}
openPopover(event) {
let popover = this.popoverCtrl.create(PopoverPage);
popover.present({
ev: event
});
}
}
And last, the provider
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Injectable()
export class ZerkenProvider {
static get parameters() {
return [[Http]];
}
constructor(public http: Http) {
}
getZerken() {
var url = "http://peerligthart.com/grotekerk/v1/api.php/zerken?transform=1";
var response = this.http.get(url).map(res => res.json());
    
return response;
}
}
So, what the page is displaying itself:
As you can see.. nothing. I hope someone has a solution, kind regards!
-------------EDIT-------------
I changed this.zerken = data.zerken.results to this.zerken. After doing this it's giving me an error:
Your zerken in your response doesn't seem to have an results object, so
this.zerken = data.zerken.results;
should be:
this.zerken = data.zerken;
Remember to initialize the array in your component:
zerken: Array<any> = [];
so that you won't get an error that zerken is undefined, since view is usually rendered before data has been received. Having it initialized will prevent that.
You need an *ngIf encapsulating the *ngFor since zerken is obtained at a later point of time.
Try:
<ion-content padding>
<div *ngIf="zerken">
<h1 *ngFor="let z of zerken">
{{ z.naam }}
</h1>
</div>
</ion-content>
Also you need to set zerken = data.zerken; as mentioned in the other answer by #AJT_82.

In angular2, are there any methods just like $compile()? [duplicate]

I want to manually compile some HTML containing directives. What is the equivalent of $compile in Angular 2?
For example, in Angular 1, I could dynamically compile a fragment of HTML and append it to the DOM:
var e = angular.element('<div directive></div>');
element.append(e);
$compile(e)($scope);
Angular 2.3.0 (2016-12-07)
To get all the details check:
How can I use/create dynamic template to compile dynamic Component with Angular 2.0?
To see that in action:
observe a working plunker (working with 2.3.0+)
The principals:
1) Create Template
2) Create Component
3) Create Module
4) Compile Module
5) Create (and cache) ComponentFactory
6) use Target to create an Instance of it
A quick overview how to create a Component
createNewComponent (tmpl:string) {
#Component({
selector: 'dynamic-component',
template: tmpl,
})
class CustomDynamicComponent implements IHaveDynamicData {
#Input() public entity: any;
};
// a component for this particular template
return CustomDynamicComponent;
}
A way how to inject component into NgModule
createComponentModule (componentType: any) {
#NgModule({
imports: [
PartsModule, // there are 'text-editor', 'string-editor'...
],
declarations: [
componentType
],
})
class RuntimeComponentModule
{
}
// a module for just this Type
return RuntimeComponentModule;
}
A code snippet how to create a ComponentFactory (and cache it)
public createComponentFactory(template: string)
: Promise<ComponentFactory<IHaveDynamicData>> {
let factory = this._cacheOfFactories[template];
if (factory) {
console.log("Module and Type are returned from cache")
return new Promise((resolve) => {
resolve(factory);
});
}
// unknown template ... let's create a Type for it
let type = this.createNewComponent(template);
let module = this.createComponentModule(type);
return new Promise((resolve) => {
this.compiler
.compileModuleAndAllComponentsAsync(module)
.then((moduleWithFactories) =>
{
factory = _.find(moduleWithFactories.componentFactories
, { componentType: type });
this._cacheOfFactories[template] = factory;
resolve(factory);
});
});
}
A code snippet how to use the above result
// here we get Factory (just compiled or from cache)
this.typeBuilder
.createComponentFactory(template)
.then((factory: ComponentFactory<IHaveDynamicData>) =>
{
// Target will instantiate and inject component (we'll keep reference to it)
this.componentRef = this
.dynamicComponentTarget
.createComponent(factory);
// let's inject #Inputs to component instance
let component = this.componentRef.instance;
component.entity = this.entity;
//...
});
The full description with all the details read here, or observe working example
.
.
OBSOLETE - Angular 2.0 RC5 related (RC5 only)
to see previous solutions for previous RC versions, please, search through the history of this post
Note: As #BennyBottema mentions in a comment, DynamicComponentLoader is now deprecated, hence so is this answer.
Angular2 doesn't have any $compile equivalent. You can use DynamicComoponentLoader and hack with ES6 classes to compile your code dynamically (see this plunk):
import {Component, DynamicComponentLoader, ElementRef, OnInit} from 'angular2/core'
function compileToComponent(template, directives) {
#Component({
selector: 'fake',
template , directives
})
class FakeComponent {};
return FakeComponent;
}
#Component({
selector: 'hello',
template: '<h1>Hello, Angular!</h1>'
})
class Hello {}
#Component({
selector: 'my-app',
template: '<div #container></div>',
})
export class App implements OnInit {
constructor(
private loader: DynamicComponentLoader,
private elementRef: ElementRef,
) {}
ngOnInit() {} {
const someDynamicHtml = `<hello></hello><h2>${Date.now()}</h2>`;
this.loader.loadIntoLocation(
compileToComponent(someDynamicHtml, [Hello])
this.elementRef,
'container'
);
}
}
But it will work only until html parser is inside angular2 core.
Angular Version I have Used - Angular 4.2.0
Angular 4 is came up with ComponentFactoryResolver to load components at runtime. This is a kind of same implementation of $compile in Angular 1.0 which serves your need
In this below example I am loading ImageWidget component dynamically in to a DashboardTileComponent
In order to load a component you need a directive that you can apply to ng-template which will helps to place the dynamic component
WidgetHostDirective
import { Directive, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[widget-host]',
})
export class DashboardTileWidgetHostDirective {
constructor(public viewContainerRef: ViewContainerRef) {
}
}
this directive injects ViewContainerRef to gain access to the view container of the element that will host the dynamically added component.
DashboardTileComponent(Place holder component to render the dynamic component)
This component accepts an input which is coming from a parent components or you can load from your service based on your implementation. This component is doing the major role to resolve the components at runtime. In this method you can also see a method named renderComponent() which ultimately loads the component name from a service and resolve with ComponentFactoryResolver and finally setting data to the dynamic component.
import { Component, Input, OnInit, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '#angular/core';
import { DashboardTileWidgetHostDirective } from './DashbardWidgetHost.Directive';
import { TileModel } from './Tile.Model';
import { WidgetComponentService } from "./WidgetComponent.Service";
#Component({
selector: 'dashboard-tile',
templateUrl: 'app/tile/DashboardTile.Template.html'
})
export class DashboardTileComponent implements OnInit {
#Input() tile: any;
#ViewChild(DashboardTileWidgetHostDirective) widgetHost: DashboardTileWidgetHostDirective;
constructor(private _componentFactoryResolver: ComponentFactoryResolver,private widgetComponentService:WidgetComponentService) {
}
ngOnInit() {
}
ngAfterViewInit() {
this.renderComponents();
}
renderComponents() {
let component=this.widgetComponentService.getComponent(this.tile.componentName);
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(component);
let viewContainerRef = this.widgetHost.viewContainerRef;
let componentRef = viewContainerRef.createComponent(componentFactory);
(<TileModel>componentRef.instance).data = this.tile;
}
}
DashboardTileComponent.html
<div class="col-md-2 col-lg-2 col-sm-2 col-default-margin col-default">
<ng-template widget-host></ng-template>
</div>
WidgetComponentService
This is a service factory to register all the components that you want to resolve dynamically
import { Injectable } from '#angular/core';
import { ImageTextWidgetComponent } from "../templates/ImageTextWidget.Component";
#Injectable()
export class WidgetComponentService {
getComponent(componentName:string) {
if(componentName==="ImageTextWidgetComponent"){
return ImageTextWidgetComponent
}
}
}
ImageTextWidgetComponent(component we are loading at runtime)
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'dashboard-imagetextwidget',
templateUrl: 'app/templates/ImageTextWidget.html'
})
export class ImageTextWidgetComponent implements OnInit {
#Input() data: any;
constructor() { }
ngOnInit() { }
}
Add Finally add this ImageTextWidgetComponent in to your app module as entryComponent
#NgModule({
imports: [BrowserModule],
providers: [WidgetComponentService],
declarations: [
MainApplicationComponent,
DashboardHostComponent,
DashboardGroupComponent,
DashboardTileComponent,
DashboardTileWidgetHostDirective,
ImageTextWidgetComponent
],
exports: [],
entryComponents: [ImageTextWidgetComponent],
bootstrap: [MainApplicationComponent]
})
export class DashboardModule {
constructor() {
}
}
TileModel
export interface TileModel {
data: any;
}
Orginal Reference from my blog
Official Documentation
Download Sample Source Code
this npm package made it easier for me:
https://www.npmjs.com/package/ngx-dynamic-template
usage:
<ng-template dynamic-template
[template]="'some value:{{param1}}, and some component <lazy-component></lazy-component>'"
[context]="{param1:'value1'}"
[extraModules]="[someDynamicModule]"></ng-template>
In order to dinamically create an instance of a component and attach it to your DOM you can use the following script and should work in Angular RC:
html template:
<div>
<div id="container"></div>
<button (click)="viewMeteo()">Meteo</button>
<button (click)="viewStats()">Stats</button>
</div>
Loader component
import { Component, DynamicComponentLoader, ElementRef, Injector } from '#angular/core';
import { WidgetMeteoComponent } from './widget-meteo';
import { WidgetStatComponent } from './widget-stat';
#Component({
moduleId: module.id,
selector: 'widget-loader',
templateUrl: 'widget-loader.html',
})
export class WidgetLoaderComponent {
constructor( elementRef: ElementRef,
public dcl:DynamicComponentLoader,
public injector: Injector) { }
viewMeteo() {
this.dcl.loadAsRoot(WidgetMeteoComponent, '#container', this.injector);
}
viewStats() {
this.dcl.loadAsRoot(WidgetStatComponent, '#container', this.injector);
}
}
Angular TypeScript/ES6 (Angular 2+)
Works with AOT + JIT at once together.
I created how to use it here:
https://github.com/patrikx3/angular-compile
npm install p3x-angular-compile
Component: Should have a context and some html data...
Html:
<div [p3x-compile]="data" [p3x-compile-context]="ctx">loading ...</div>
You can see the component, that allow to compile simple dynamic Angular components https://www.npmjs.com/package/#codehint-ng/html-compiler
I know this issue is old, but I spent weeks trying to figure out how to make this work with AOT enabled. I was able to compile an object but never able to execute existing components. Well I finally decided to change tact, as I was't looking to compile code so much as execute a custom template. My thought was to add the html which anyone can do and loop though the existing factories. In doing so I can search for the element/attribute/etc. names and execute the component on that HTMLElement. I was able to get it working and figured I should share this to save someone else the immense amount of time I wasted on it.
#Component({
selector: "compile",
template: "",
inputs: ["html"]
})
export class CompileHtmlComponent implements OnDestroy {
constructor(
private content: ViewContainerRef,
private injector: Injector,
private ngModRef: NgModuleRef<any>
) { }
ngOnDestroy() {
this.DestroyComponents();
}
private _ComponentRefCollection: any[] = null;
private _Html: string;
get Html(): string {
return this._Html;
}
#Input("html") set Html(val: string) {
// recompile when the html value is set
this._Html = (val || "") + "";
this.TemplateHTMLCompile(this._Html);
}
private DestroyComponents() { // we need to remove the components we compiled
if (this._ComponentRefCollection) {
this._ComponentRefCollection.forEach((c) => {
c.destroy();
});
}
this._ComponentRefCollection = new Array();
}
private TemplateHTMLCompile(html) {
this.DestroyComponents();
this.content.element.nativeElement.innerHTML = html;
var ref = this.content.element.nativeElement;
var factories = (this.ngModRef.componentFactoryResolver as any)._factories;
// here we loop though the factories, find the element based on the selector
factories.forEach((comp: ComponentFactory<unknown>) => {
var list = ref.querySelectorAll(comp.selector);
list.forEach((item) => {
var parent = item.parentNode;
var next = item.nextSibling;
var ngContentNodes: any[][] = new Array(); // this is for the viewchild/viewchildren of this object
comp.ngContentSelectors.forEach((sel) => {
var ngContentList: any[] = new Array();
if (sel == "*") // all children;
{
item.childNodes.forEach((c) => {
ngContentList.push(c);
});
}
else {
var selList = item.querySelectorAll(sel);
selList.forEach((l) => {
ngContentList.push(l);
});
}
ngContentNodes.push(ngContentList);
});
// here is where we compile the factory based on the node we have
let component = comp.create(this.injector, ngContentNodes, item, this.ngModRef);
this._ComponentRefCollection.push(component); // save for our destroy call
// we need to move the newly compiled element, as it was appended to this components html
if (next) parent.insertBefore(component.location.nativeElement, next);
else parent.appendChild(component.location.nativeElement);
component.hostView.detectChanges(); // tell the component to detectchanges
});
});
}
}
If you want to inject html code use directive
<div [innerHtml]="htmlVar"></div>
If you want to load whole component in some place, use DynamicComponentLoader:
https://angular.io/docs/ts/latest/api/core/DynamicComponentLoader-class.html

Categories

Resources