Angular2, evaluate template from string inside a component - javascript

It's possible evaluate template from string in a variable?. I need place the string in the component instead of the expression,
e.g.
template: "<div>{{ template_string }}</div>"
template_string contains: <b>{{ name }}</b>
and all should be evaluated to <div><b>My Name</b></div>
but I see <div>{{ template_string }}</div>
I need something like {{ template_string | eval }} or something else to evaluate the content of the variable on current context.
It's possible? I need something to use this approach because template_string can be changed when the component is used.
Edit1:
Angular Version: 4.0.3
E.g.
#Component({
selector: 'product-item',
template: `
<div class="product">{{ template }}</div>`,
})
export class ProductItemComponent {
#Input() name: string;
#Input() price: number = 0;
#Input() template: string = `{{ name }} <b>{{ price | currency }}</b>`;
}
Usage:
<product-item [name]="product.name" [price]="product.price"></product-item>
Expected: Product Name USD3.00
Output: {{ name }} <b>{{ price | currency }}</b>

You can create your own directive that will do it:
compile.directive.ts
#Directive({
selector: '[compile]'
})
export class CompileDirective implements OnChanges {
#Input() compile: string;
#Input() compileContext: any;
compRef: ComponentRef<any>;
constructor(private vcRef: ViewContainerRef, private compiler: Compiler) {}
ngOnChanges() {
if(!this.compile) {
if(this.compRef) {
this.updateProperties();
return;
}
throw Error('You forgot to provide template');
}
this.vcRef.clear();
this.compRef = null;
const component = this.createDynamicComponent(this.compile);
const module = this.createDynamicModule(component);
this.compiler.compileModuleAndAllComponentsAsync(module)
.then((moduleWithFactories: ModuleWithComponentFactories<any>) => {
let compFactory = moduleWithFactories.componentFactories.find(x => x.componentType === component);
this.compRef = this.vcRef.createComponent(compFactory);
this.updateProperties();
})
.catch(error => {
console.log(error);
});
}
updateProperties() {
for(var prop in this.compileContext) {
this.compRef.instance[prop] = this.compileContext[prop];
}
}
private createDynamicComponent (template:string) {
#Component({
selector: 'custom-dynamic-component',
template: template,
})
class CustomDynamicComponent {}
return CustomDynamicComponent;
}
private createDynamicModule (component: Type<any>) {
#NgModule({
// You might need other modules, providers, etc...
// Note that whatever components you want to be able
// to render dynamically must be known to this module
imports: [CommonModule],
declarations: [component]
})
class DynamicModule {}
return DynamicModule;
}
}
Usage:
#Component({
selector: 'product-item',
template: `
<div class="product">
<ng-container *compile="template; context: this"></ng-container>
</div>
`,
})
export class ProductItemComponent {
#Input() name: string;
#Input() price: number = 0;
#Input() template: string = `{{ name }} <b>{{ price | currency }}</b>`;
}
Plunker Example
See also
Angular 2.1.0 create child component on the fly, dynamically

not sure how you're building the template string
import { ..., OnInit } from '#angular/core';
#Component({
selector: 'product-item',
template: `
<div class="product" [innerHtml]='template_string'>
</div>`,
})
export class ProductItemComponent implements OnInit {
#Input() name: string;
#Input() price: number = 0;
#Input() pre: string;
#Input() mid: string;
#Input() post: string;
template_string;
ngOnInit() {
// this is probably what you want
this.template_string = `${this.pre}${this.name}${this.mid}${this.price}${this.post}`
}
}
<product-item [name]="name" [price]="price" pre="<em>" mid="</em><b>" post="</b>"></product-item>
the string can be built from outside the component, would still recommend something like ngIf to control dynamic templates though.

In Angular double curly braces {{}} are used to evaluation an expression in a component's template. and not work on random strings or dynamically added DOM elements. So one way of doing this is to use typescript string interpolation using ${}. check the rest of code to understand
#Component({
selector: 'product-item',
template: `
<div class="product" [innerHTML]="template"></div>`,
})
export class ProductItemComponent {
#Input() name: string;
#Input() price: number = 0;
#Input() template: string = `${ this.name } <b>${ this.price }}</b>`;
}

Related

Angular 11: call function componet into html with generic pipe

i want made a generic pipe for call a component's function into component's html.
The wrong way is eg.:
{{ test('foo') }}
my idea is:
{{ 'foo' | fn:test }}
this is the pipe code
import { ChangeDetectorRef, EmbeddedViewRef, Type } from "#angular/core";
import { Pipe } from "#angular/core";
import { PipeTransform } from "#angular/core";
#Pipe({
name: "fn",
pure: true
})
export class FnPipe implements PipeTransform {
private context: any;
constructor(cdRef: ChangeDetectorRef) {
// retrive component instance (this is a workaround)
this.context = (cdRef as EmbeddedViewRef<Type<any>>).context;
}
public transform(
headArgument: any,
fnReference: Function,
...tailArguments: any[]
): any {
return fnReference.apply(this.context, [headArgument, ...tailArguments]);
}
}
and this is a component example
import { Component } from "#angular/core";
#Component({
selector: "my-app",
template: `
<!-- testFromPipe har a third parameter name for trigger pipe refresh -->
PIPE: {{ "arg1" | fn: testFromPipe:"arg2":name }}<br /><br />
<!-- wrong way for call a function nto html just for test the result -->
HTML: {{ testFromHtml("arg1", "arg2") }}<br /><br />
<button (click)="triggerCD()">test</button>
`
})
export class AppComponent {
name = null;
constructor() {
this.triggerCD();
}
test(a: string, b: string) {
// test multiple arguments anch this scope works
return `a:${a}; b:${b}; name:${this.name};`;
}
testFromHtml(a: string, b: string) {
console.log("FUNCTION");
return this.test(a, b);
}
testFromPipe(a: string, b: string) {
console.log("PIPE");
return this.test(a, b);
}
triggerCD() {
this.name = new Date().getMinutes();
}
}
this is a live example https://stackblitz.com/edit/angular-ivy-jvmcgz
the code seem works but is based on retrive the component instance into the pipe by private property context of ChangeDetectorRef.
constructor(cdRef: ChangeDetectorRef) {
this.context = (cdRef as EmbeddedViewRef<Type<any>>).context;
}
This is unsafe and future Angular update can break this trick.
There is a safe way to access to component instance into Pipe?

Angular - Structural directive with embedded view does not pass children to ng-template

I've got a structural directive that creates an embedded view by looking up a template ref using ng-template. My problem is that from this parent component (with structural directive), I cannot pass down children.
Parent component with structural directive
import { ViewChild, Component, OnInit, ElementRef } from "#angular/core";
import { TestJsonService } from "../../services/test-json.service";
#Component({
selector: "xfr-json-renderer",
template: `
<template-lookup></template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
<div>Why can't i pass this down to the child?</div>
</div>
`,
styleUrls: ["./json-renderer.component.css"],
})
export class JsonRendererComponent implements OnInit {
#ViewChild("childTemplate") childTemplate;
constructor(el: ElementRef, json: TestJsonService) {}
ngOnInit(): void {}
ngAfterViewInit() {}
}
Child component
import { Injectable, TemplateRef, Component, ViewChild } from "#angular/core";
#Injectable()
export class TemplateStore {
templates = new Map<string, TemplateRef<any>>();
}
#Component({
selector: "template-lookup",
template: `
<ng-template #flexRow></ng-template>
<ng-template #flexCol><xfr-flex-col>
// I want to pass the children into here
</xfr-flex-col></ng-template>
`,
})
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(private service: TemplateStore) {}
ngAfterViewInit() {
this.service.templates.set("flexRow", this.flexRowTemplate);
this.service.templates.set("flexCol", this.flexColTemplate);
}
}
Structural directive
import { ViewContainerRef } from "#angular/core";
import { TemplateStore } from "./../services/composite-template.service";
import { Directive, Input } from "#angular/core";
#Directive({
selector: "[replaceWith]",
})
export class CompositeTemplateDirective {
#Input() replaceWith: "flex-col" | "flex-row";
constructor(private service: TemplateStore, private view: ViewContainerRef) {}
ngAfterViewInit() {
this.view.createEmbeddedView(this.service.templates.get(this.replaceWith));
}
}
The problem is that you need to use internal API for that, what is not the best thing. I would use it until I stay with the same angular version and would test it before every update - then should work stable.
I was able to do the injection with Angular 9, quite sure a similar solution (but different internal API) can be applied for other angular versions.
The main thing for the injection - where to inject the content, in components we could use ng-content, but here it wouldn't work, because we have different component contexts. In this case we could use <ng-template [ngTemplateOutlet]></ng-template> to tell the script where we want the injection.
here you can find a live demo: https://codesandbox.io/s/nifty-wright-335bm?file=/src/app/json-renderer.component.ts
CompositeTemplateDirective
import {NgTemplateOutlet} from '#angular/common';
import {AfterViewInit, Directive, Input, TemplateRef, ViewContainerRef} from '#angular/core';
import {TemplateStore} from 'src/app/TemplateLookup/TemplateLookup';
#Directive({
selector: '[replaceWith]',
})
export class CompositeTemplateDirective implements AfterViewInit {
#Input() replaceWith: 'flex-col' | 'flex-row';
constructor(
private service: TemplateStore,
private view: ViewContainerRef,
private templateRef: TemplateRef<any>,
) {
}
public ngAfterViewInit(): void {
const wrapper = this.service.templates.get(this.replaceWith);
const source = this.templateRef;
const view: any = this.view.createEmbeddedView(wrapper);
let directive: NgTemplateOutlet;
const nodes: Array<any> = view._lView ? view._lView : view._view && view._view.nodes ? view._view.nodes : [];
for (const node of nodes) {
if (typeof node !== 'object') {
continue;
}
if (node instanceof NgTemplateOutlet) {
directive = node;
}
if (typeof node.instance === 'object' && node.instance instanceof NgTemplateOutlet) {
directive = node.instance;
}
}
if (directive) {
directive.ngTemplateOutlet = source;
directive.ngOnChanges({
ngTemplateOutlet: {
previousValue: null,
currentValue: source,
firstChange: true,
isFirstChange: () => true,
},
});
}
}
}
TemplateLookup
import {AfterViewInit, Component, Injectable, TemplateRef, ViewChild} from '#angular/core';
#Injectable()
export class TemplateStore {
templates = new Map<string, TemplateRef<any>>();
}
#Component({
selector: 'template-lookup',
template: `
<ng-template #flexRow>
<div>
flexRow template
</div>
</ng-template>
<ng-template #flexCol>
<div>
<div>wrap</div>
<ng-template [ngTemplateOutlet]></ng-template>
<div>wrap</div>
</div>
</ng-template>
`,
})
export class TemplateLookup implements AfterViewInit {
#ViewChild('flexRow', {static: false}) flexRowTemplate;
#ViewChild('flexCol', {static: false}) flexColTemplate;
constructor(
private service: TemplateStore,
) {
}
ngAfterViewInit() {
console.log('TemplateLookup:ngAfterViewInit');
this.service.templates.set('flexRow', this.flexRowTemplate);
this.service.templates.set('flexCol', this.flexColTemplate);
}
}
so the most pragmatic thing here seems to be to just put the child you want to pass as a child of the template-lookup component and use ng-content...
do this in the parent:
<template-lookup>
<div>I will pass to child</div>
</template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
</div>
and this in the child:
<ng-template #flexRow></ng-template>
<ng-template #flexCol>
<xfr-flex-col>
<ng-content></ng-content>
</xfr-flex-col>
</ng-template>
and that will solve your problem / fulfill the stated requirements.
You could also consider a rewrite to your service to solve timing problems between templates being set and gotten once and for all:
import { Injectable, TemplateRef } from "#angular/core";
import {ReplaySubject} from 'rxjs';
import {map, filter, distinctUntilChanged} from 'rxjs/operators';
#Injectable({providedIn: 'root'}) // provide appropriately, root for example
export class TemplateStore {
private templates = new Map<string, TemplateRef<any>>();
private tmpSource = new ReplaySubject<Map<string, TemplateRef<any>>>(1);
setTemplate(key: string, template: TemplateRef<any>) {
this.templates.set(key, template);
this.tmpSource.next(this.templates)
}
getTemplate(key: string) {
return this.tmpSource.pipe(
map(tmpMap => tmpMap.get(key)),
filter(tmp => !!tmp),
distinctUntilChanged()
)
}
}
and make the associated changes in the directive and child components...
export class CompositeTemplateDirective implements OnInit, OnDestroy {
#Input() replaceWith: "flex-col" | "flex-row";
private sub: Subscription;
constructor(private service: TemplateStore, private viewContainer: ViewContainerRef) { }
ngOnInit() {
this.sub = this.service.getTemplate(this.replaceWith).subscribe(t => {
this.viewContainer.clear()
this.viewContainer.createEmbeddedView(t)
})
}
ngOnDestroy() {
this.sub.unsubscribe()
}
}
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(private service: TemplateStore) {}
ngAfterViewInit() {
this.service.setTemplate("flexRow", this.flexRowTemplate);
this.service.setTemplate("flexCol", this.flexColTemplate);
}
}
functioning example: https://stackblitz.com/edit/angular-ygdveu
it's been pointed out that this doesn't support nesting... so make the following adjustments and you can nest. in template lookup, you'll need to use the SkipSelf modifier in your constructor, and also provide the TemplateStore... in the case of no nesting, this will have no effect, SkipSelf just tells the injector to start looking for the service at the parent rather than at the component:
#Component({
selector: "template-lookup",
template: `
<ng-template #flexRow>FLEX ROW</ng-template>
<ng-template #flexCol>
FLEX COL
<div class="flex-col">
<ng-content></ng-content>
</div>
</ng-template>
`,
providers: [TemplateStore]
})
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(#SkipSelf() private service: TemplateStore) {}
ngAfterViewInit() {
this.service.setTemplate("flexRow", this.flexRowTemplate);
this.service.setTemplate("flexCol", this.flexColTemplate);
}
}
then you can nest to your hearts content like so:
<template-lookup>
<div>I can pass this to the child!</div>
<template-lookup>NESTED</template-lookup>
<div class="nested-content" *replaceWith="'flexCol'"></div>
</template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
</div>
which is a little ugly, as you need to repeat the template-lookup component, but it does get the job done. This works by allowing the directive and template lookup to communicate with a different copy of the TemplateStore so you can nest different content.
working example of this variant: https://stackblitz.com/edit/angular-lpner2

angular 4+ assign #Input for ngComponentOutlet dynamically created component

In Angular 4 to dynamically create a component you can use ngComponentOutlet directive: https://angular.io/docs/ts/latest/api/common/index/NgComponentOutlet-directive.html
something like this:
Dynamic component
#Component({
selector: 'dynamic-component',
template: `
Dynamic component
`
})
export class DynamicComponent {
#Input() info: any;
}
App
#Component({
selector: 'my-app',
template: `
App<br>
<ng-container *ngComponentOutlet="component"></ng-container>
`
})
export class AppComponent {
this.component=DynamicComponent;
}
How do I pass #Input() info: any; information in this template <ng-container *ngComponentOutlet="component"></ng-container> ?
Such a feature was discussed in the pull request for ngComponentOutlet but was dropped for now.
Even the componentRef shown currently in https://angular.io/docs/ts/latest/api/common/index/NgComponentOutlet-directive.html is not public and therefore not available https://github.com/angular/angular/blob/3ef73c2b1945340ca6bd21f1790260c88698ae26/modules/%40angular/common/src/directives/ng_component_outlet.ts#L78
I'd suggest you create your own directive derived from https://github.com/angular/angular/blob/3ef73c2b1945340ca6bd21f1790260c88698ae26/modules/%40angular/common/src/directives/ng_component_outlet.ts#L72
and assign values to inputs like shown in Angular 2 dynamic tabs with user-click chosen components
this.compRef.instance.someProperty = 'someValue';
With the help of the post of #Günter Zöchbauer I solved a similar problem this way - I hope you can adapt it somehow.
First I defined some interfaces:
// all dynamically loaded components should implement this guy
export interface IDynamicComponent { Context: object; }
// data from parent to dynLoadedComponent
export interface IDynamicComponentData {
component: any;
context?: object;
caller?: any;
}
then I implemented them inside of the dynamically loaded component
dynamicLoadedComponentA.ts
// ...
export class DynamicLoadedComponentA implements IDynamicComponent {
// ...
// data from parent
public Context: object;
// ...
After that I built a new component which is responsible for the magic. Important here is that I had to register all dyn. loaded components as entryComponents.
dynamic.component.ts
#Component({
selector: 'ngc-dynamic-component',
template: ´<ng-template #dynamicContainer></ng-template>´,
entryComponents: [ DynamicLoadedComponentA ]
})
export class DynamicComponent implements OnInit, OnDestroy, OnChanges {
#ViewChild('dynamicContainer', { read: ViewContainerRef }) public dynamicContainer: ViewContainerRef;
#Input() public componentData: IDynamicComponentData;
private componentRef: ComponentRef<any>;
private componentInstance: IDynamicComponent;
constructor(private resolver: ComponentFactoryResolver) { }
public ngOnInit() {
this.createComponent();
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['componentData']) {
this.createComponent();
}
}
public ngOnDestroy() {
if (this.componentInstance) {
this.componentInstance = null;
}
if (this.componentRef) {
this.componentRef.destroy();
}
}
private createComponent() {
this.dynamicContainer.clear();
if (this.componentData && this.componentData.component) {
const factory: ComponentFactory<any> = this.resolver.resolveComponentFactory(this.componentData.component);
this.componentRef = this.dynamicContainer.createComponent(factory);
this.componentInstance = this.componentRef.instance as IDynamicComponent;
// fill context data
Object.assign(this.componentInstance.Context, this.componentData.context || {});
// register output events
// this.componentRef.instance.outputTrigger.subscribe(event => console.log(event));
}
}
}
here the usage of this shiny new stuff:
app.html
<!-- [...] -->
<div>
<ngc-dynamic-component [componentData]="_settingsData"></ngc-dynamic-component>
</div>
<!-- [...] -->
app.ts
// ...
private _settingsData: IDynamicComponent = {
component: DynamicLoadedComponentA,
context: { SomeValue: 42 },
caller: this
};
// ...
I think for now you can use
https://www.npmjs.com/package/ng-dynamic-component
It is made specifically for this issue

Angular 2 RC1: DataBinding and ComponentResolver

I've recently updated my project from beta.15 to RC1. Now, DynamicComponentLoader is deprecated. So I've re-writed my code using ComponentResolver.
My component is correctly loaded into another but I'm experiencing an issue : data-binding seems to doesn't works.
Here is my code :
#Component({
selector: 'browser-pane',
styleUrls: ['src/modules/component#browser/src/styles/pane.css'],
template: `
<li class="pane">
<ul class="nodes">
<li *ngFor="let node of nodes; let i = index" id="node-{{i}}"></li>
</ul>
</li>
`
})
export class PaneComponent implements OnInit {
#Input() nodeId: number;
#Input() nodes: any[];
#Input() fillerComponent: any;
#Input() mapComponentData: any;
constructor(private _injector: Injector, private _cr: ComponentResolver) {}
ngOnInit(): any {
this.nodes.forEach((node: any, i: number) => {
this._cr.resolveComponent(this.fillerComponent)
.then((factory: ComponentFactory) => factory.create(this._injector, null, `#node-${i}`))
.then((ref: ComponentRef<any>) => this.mapComponentData(ref.instance, node));
})
}
}
mapComponentData is just a function which map data to the component ref. In my case, the component that I'm creating dynamically needs an #Input() named 'media'. This function will do the following instruction: myComponentInstance.media = media;.
Here is the filler component (simplified) :
#Component({
selector: 'cmp-media-box',
styleUrls: ['src/modules/component#media-node/src/style.css'],
pipes: [ MediaCountPipe ],
template: `
<div class="media-node"
(click)="onClick()"
(dragover)="onDragover($event)"
(drop)="onDrop($event)"
[class.dropDisable]="!drop">
<span class="title">{{ media.title }}</span>
<div class="box" [class.empty]="isEmpty()">{{ media | mediaCount:'playlists':'channels' }}</div>
</div>
`
})
export class MediaNodeComponent {
#Input() media: Media;
private _OSelection:Observable<Media[]>;
private _selectionSubscription: Subscription;
private _removeAllFromSelection;
drop: boolean = false;
constructor(private _store:Store<AppStore>, private _mediaService:MediaService) {
setTimeout(() => this.initialize(), 0);
}
initialize():any {
if(this.media.type === MEDIA) {
this._OSelection = this._store.select(s => s['data#media'].selected);
this._removeAllFromSelection = mediaRemoveAll;
} else if(this.media.type === CHANNEL) {
this._OSelection = this._store.select(s => s['data#channel'].selected);
this._removeAllFromSelection = channelRemoveAll;
}
this.subscribeToSelection();
}
// ...
}
So what's the problem ? Inside thisfillerComponent, this.media is defined. If i put a console.log(this.media) inside initialize, I can see it. But I can't use it inside the template. I've tried many things :
use {{media?.title}}
use {{media.title | async}}
remove the #Input() in front of the media declaration
stop passing media and use a hard-coded variable (just in case)
use DynamicComponentLoader : same result. But I think that dcl uses ComponentResolver behind (not sure about that, I'm checking this point)
...
In other words : I can't use variables inside my template.
What am I doing wrong ?
Speaking about this code, there is another thing that I don't understand. I can't use OnInit on the filler component: ngOnInit() will never be triggered. That's why I'm using this horrible setTimeout().

Conditional styling on host element

I have a component that all it does is render , its something like this:
#Component({
selector: 'my-comp',
host: ???,
template: `
<ng-content></ng-content>
`
})
export default class MyComp {
#Input() title: string;
public isChanged: boolean;
}
The component has a isChanged property and I want to apply styling on the host element based on that isChanged property. Is this even possible?
You use the class and style prefix for this. Here is a sample:
#Component({
selector: 'my-comp',
host: {
'[class.className]': 'isChanged'
},
template: `
<ng-content></ng-content>
`
})
export default class MyComp {
#Input() title: string;
public isChanged: boolean;
}
See the Günter's answer for more details:
ngClass in host property of component decorator does not work
Solution using #HostBinder
The accepted solution is using the host metadata property which goes against the rules of TSLint:
TSLint: Use #HostBinding or #HostListener rather than the host
metadata property (https://angular.io/styleguide#style-06-03)
The same can be achieved using #HostBinding instead:
import { Component, HostBinding, Input } from '#angular/core';
#Component({
selector: 'my-comp',
template: `
<ng-content></ng-content>
`
})
export default class MyComp {
#Input() title: string;
public isChanged: boolean;
#HostBinding('class.className') get className() { return this.isChanged; }
}
Not sure what you're trying to do but something like this should suffice where you use ngAfterViewInit and ElementRef:
import {AfterViewInit, ElementRef} from '#angular/core';
#Component({
selector: 'my-comp',
host: ???,
template: `
<ng-content></ng-content>
`
})
export default class MyComp implements AfterViewInit {
#Input() title: string;
public isChanged: boolean;
constructor(private _ref: ElementRef) {}
ngAfterViewInit() {
var host = this._ref.nativeElement;
if (this.isChanged) {
host.style.width = '200px';
}
}
}
If you want to do some checking for isChanged every time it changes you could implement ngDoCheck instead/as well:
ngDoCheck() {
if (this.isChanged !== this.previousIsChanged) {
var host = this._ref.nativeElement;
if (this.isChanged) {
host.style.width = '200px';
}
}
}
I think you want to let your component fire an event that can be catched by the host (and possibly pass some data with it).
To do that you would have an #output property like:
#Output() isChanged: EventEmitter<any> = new EventEmitter()
then in your code you could do:
this.isChanged.emit(some value to pass)
And catch it like:
(isChanged)="doSomething($event)"

Categories

Resources