depending on the value of a (boolean) class variable I would like my ng-content to either be wrapped in a div or to not be wrapped in div (I.e. the div should not even be in the DOM) ... Whats the best way to go about this ? I have a Plunker that tries to do this, in what I assumed was the most obvious way, using ngIf .. but it's not working... It displays content only for one of the boolean values but not the other
kindly assist
Thank you!
http://plnkr.co/edit/omqLK0mKUIzqkkR3lQh8
#Component({
selector: 'my-component',
template: `
<div *ngIf="insideRedDiv" style="display: inline; border: 1px red solid">
<ng-content *ngIf="insideRedDiv" ></ng-content>
</div>
<ng-content *ngIf="!insideRedDiv"></ng-content>
`,
})
export class MyComponent {
insideRedDiv: boolean = true;
}
#Component({
template: `
<my-component> ... "Here is the Content" ... </my-component>
`
})
export class App {}
Angular ^4
As workaround i can offer you the following solution:
<div *ngIf="insideRedDiv; else elseTpl" style="display: inline; border: 1px red solid">
<ng-container *ngTemplateOutlet="elseTpl"></ng-container>
</div>
<ng-template #elseTpl><ng-content></ng-content> </ng-template>
Plunker Example angular v4
Angular < 4
Here you can create dedicated directive that will do the same things:
<div *ngIf="insideRedDiv; else elseTpl" style="display: inline; border: 1px red solid">
<ng-container *ngTemplateOutlet="elseTpl"></ng-container>
</div>
<template #elseTpl><ng-content></ng-content></template>
Plunker Example
ngIf4.ts
class NgIfContext { public $implicit: any = null; }
#Directive({ selector: '[ngIf4]' })
export class NgIf4 {
private context: NgIfContext = new NgIfContext();
private elseTemplateRef: TemplateRef<NgIfContext>;
private elseViewRef: EmbeddedViewRef<NgIfContext>;
private viewRef: EmbeddedViewRef<NgIfContext>;
constructor(private viewContainer: ViewContainerRef, private templateRef: TemplateRef<NgIfContext>) { }
#Input()
set ngIf4(condition: any) {
this.context.$implicit = condition;
this._updateView();
}
#Input()
set ngIf4Else(templateRef: TemplateRef<NgIfContext>) {
this.elseTemplateRef = templateRef;
this.elseViewRef = null;
this._updateView();
}
private _updateView() {
if (this.context.$implicit) {
this.viewContainer.clear();
this.elseViewRef = null;
if (this.templateRef) {
this.viewRef = this.viewContainer.createEmbeddedView(this.templateRef, this.context);
}
} else {
if (this.elseViewRef) return;
this.viewContainer.clear();
this.viewRef = null;
if (this.elseTemplateRef) {
this.elseViewRef = this.viewContainer.createEmbeddedView(this.elseTemplateRef, this.context);
}
}
}
}
Remember that you can put all this logic in separate component! (based on yurzui answer):
import { Component, Input } from '#angular/core';
#Component({
selector: 'div-wrapper',
template: `
<div *ngIf="wrap; else unwrapped">
<ng-content *ngTemplateOutlet="unwrapped">
</ng-content>
</div>
<ng-template #unwrapped>
<ng-content>
</ng-content>
</ng-template>
`,
})
export class ConditionalDivComponent {
#Input()
public wrap = false;
}
You can then use it like this:
<div-wrapper [wrap]="'true'">
Hello world!
</div-wrapper>
I checked into this and found an open issue on the subject of multiple transclusions with the tag. This prevents you from defining multiple tags in a single template file.
This explains why the content is displayed correctly only when the other tag is removed in your plunker example.
You can see the open issue here:
https://github.com/angular/angular/issues/7795
Related
I am trying to make a popover with angular. Which currently has two components and one directive.
When I click on my button, I get an error saying
ERROR TypeError: Cannot read property 'openPopover' of undefined
How can I get a reference to the parent from the directive and then get the child from that parent?
So, the click path would look like so: [open-popover] / <map-popover> / <map-popover-content>
app.component.html
<mat-popover>
<mat-popover-content>
<p>Hello World</p>
</mat-popover-content>
<button mat-flat-button open-popover color="primary">Add Debt</button>
</mat-popover>
popover.component.ts
#Component({
selector: 'mat-popover',
template: '<ng-content></ng-content>',
styleUrls: ['./popover.component.scss']
})
export class MatPopoverComponent {
#ViewChild(MatPopoverContentComponent)
public content: MatPopoverContentComponent
public open() {
this.content.openPopover()
}
}
content.component.ts
#Component({
selector: 'mat-popover-content',
template: `<ng-template #popoverContent>
<ng-content></ng-content>
</ng-template>`,
styleUrls: ['./content.component.scss']
})
export class MatPopoverContentComponent {
#ViewChild('popoverContent')
public template: TemplateRef<any>
public constructor(public dialog: MatDialog) { }
openPopover(): void {
this.dialog.open(this.template, {
hasBackdrop: false
})
}
}
open.directive.ts
#Directive({
selector: '[open-popover]'
})
export class OpenPopoverDirective {
public constructor(#Host() private popover: MatPopoverComponent) { }
#HostListener('click')
public onClick() {
this.popover.open()
}
}
Replace ContentChild decorator with ViewChild here #ViewChild(MatPopoverContentComponent) and it should work.
Explanation
You should distinguish Light DOM and Shadow DOM:
component
#Component({
selector: 'mat-popover',
template: `<ng-content></ng-content>`, <--- Shadow DOM
styleUrls: ['./popover.component.scss']
})
export class MatPopoverComponent {}
consumer
<mat-popover>
<!-- Light DOM starts -->
<mat-popover-content>
<p>Hello World</p>
</mat-popover-content>
<button mat-flat-button open-popover color="primary">Add Debt</button>
<!-- Light DOM ends-->
</mat-popover>
So in Angular we query elements in Shadow DOM by using ViewChild/ren and elements in Light DOM by using ContentChild/ren
Here I want to add CSS classes dynamically to the element using ngClass directive in angular.js.
<p [ngClass]="addClasses()">testing content</p>
and below is my ts code:
isbold: boolean = false;
isItalic: boolean = true;
addClasses() {
let classes = {
boldClass: this.isbold,
italicsClass: this.isItalic
};
return classes;
}
boldClass and italicsClass are two classes in my css file, these classes should apply depending on the value in the corresponding boolean variables.
But I'm getting the error as Can not find name for both boldClass and italicsClass. Any help appreciated.
The issue is with your JSON, when you declare a JSON, the properties of it shouldn't be with the "=", try to use something like:
addClasses(){
let classes={
boldClass: this.isbold,
italicsClass: this.isItalic
};
return classes;
}
There was a pending "," after this.isbold, and also, you had this.Italic, and it should be this.isItalic.
This is one way you can add dynamic classes
<p [class.boldClass]="isbold"
[class.italicsClass]="isItalic">testing content </p>
Try using Angular's Renderer2
#ViewChild('el') element: ElementRef
constructor(private renderer: Renderer2) {
}
addClasses(classes: Array<string>){
classes.forEach(class => this.renderer.addClass(this.element.nativeElement, 'someClass'))
}
<p #el>testing content </p>
ts:
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
isbold: boolean = false;
isItalic: boolean = true;
addClasses() {
let classes = {
boldClass: this.isbold,
italicsClass: this.isItalic
};
return classes;
}
}
template:
<p [ngClass]="addClasses()">testing content </p>
css:
.boldClass {
font-weight: bold;
}
.italicsClass {
font-style: italic;
}
Demo
[ngClass]="{'italicsClass':isItalic,'boldClass':isbold}"
plnkr demo here
#Component({
selector: 'my-demo',
template: `This is <ng-content select=".content"></ng-content>`
})
export class DemoComponent { name = 'Angular'; }
#Component({
selector: 'my-app',
template: `<h1>Hello {{name}}</h1>
<my-demo><div class="content">In Content</div></my-demo>
`
})
export class AppComponent { name = 'Angular'; }
I want to conditional ng-content, like
<ng-template *ngIf='hasContent(".content"); else noContent'>
This is <ng-content select=".content"></ng-content>
</ng-template>
<ng-template #noContent>No content</ng-template>
Is there possible in angular2 ?
Günter Zöchbauer's solution is acceptable, doesn't affect the useage, let the component detect this, I also found a easier way to do this without any json by using the :empty
<!-- must no content: https://css-tricks.com/almanac/selectors/e/empty/ -->
<!--#formatter:off-->
<div class="title"><ng-content select=".nav-title"></ng-content></div>
<!--#formatter:on-->
.title:empty {
display: none;
}
Works for any html+css.
template: `This is <span #wrapper>
<ng-content select=".content"></ng-content>
</span>`
#ViewChild('wrapper') wrapper:ElementRef;
ngAfterContentInit() {
console.log(this.wrapper.innerHTML); // or `wrapper.text`
}
See also
Get component child as string
Access transcluded content
if you want to use conditional statement with *ngIf and else the yes it's possible
#Component({
selector: 'my-demo',
template: `<div *ngIf='content; else noContent'>
This is <ng-content select=".content"></ng-content>
</div>
<ng-template #noContent>No content</ng-template>`
})
export class DemoComponent { name = 'Angular'; content = true}
Demo
I have created dynamic component instances by selecting pre-existing components. For example,
#Component({
selector: 'dynamic-component',
template: `<div #container><ng-content></ng-content></div>`
})
export class DynamicComponent {
#ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
public addComponent(ngItem: Type<WidgetComponent>,selectedPlugin:Plugin): WidgetComponent {
let factory = this.compFactoryResolver.resolveComponentFactory(ngItem);
const ref = this.container.createComponent(factory);
const newItem: WidgetComponent = ref.instance;
newItem.pluginId = Math.random() + '';
newItem.plugin = selectedPlugin;
this._elements.push(newItem);
return newItem;
}
}
My pre-existed components are ChartWidget and PatientWidget which extended the class WidgetComponent that I wanted to add in the container. For example,
#Component({
selector: 'chart-widget',
templateUrl: 'chart-widget.component.html',
providers: [{provide: WidgetComponent, useExisting: forwardRef(() => ChartWidget) }]
})
export class ChartWidget extends WidgetComponent implements OnInit {
constructor(ngEl: ElementRef, renderer: Renderer) {
super(ngEl, renderer);
}
ngOnInit() {}
close(){
console.log('close');
}
refresh(){
console.log('refresh');
}
...
}
chart-widget.compoment.html (using primeng Panel)
<p-panel [style]="{'margin-bottom':'20px'}">
<p-header>
<div class="ui-helper-clearfix">
<span class="ui-panel-title" style="font-size:14px;display:inline-block;margin-top:2px">Chart Widget</span>
<div class="ui-toolbar-group-right">
<button pButton type="button" icon="fa-window-minimize" (click)="minimize()"</button>
<button pButton type="button" icon="fa-refresh" (click)="refresh()"></button>
<button pButton type="button" icon="fa-expand" (click)="expand()" ></button>
<button pButton type="button" (click)="close()" icon="fa-window-close"></button>
</div>
</div>
</p-header>
some data
</p-panel>
data-widget.compoment.html (same as chart-widget using primeng Panel)
#Component({
selector: 'data-widget',
templateUrl: 'data-widget.component.html',
providers: [{provide: WidgetComponent, useExisting: forwardRef(() =>DataWidget) }]
})
export class DataWidget extends WidgetComponent implements OnInit {
constructor(ngEl: ElementRef, renderer: Renderer) {
super(ngEl, renderer);
}
ngOnInit() {}
close(){
console.log('close');
}
refresh(){
console.log('refresh');
}
...
}
WidgetComponent.ts
#Component({
selector: 'widget',
template: '<ng-content></ng-content>'
})
export class WidgetComponent{
}
Now I added the components by selecting a component from the existed components (e.g. chart-widget and data-widget) in the following way and stored the instances into an array.
#Component({
templateUrl: 'main.component.html',
entryComponents: [ChartWidget, DataWidget],
})
export class MainComponent implements OnInit {
private elements: Array<WidgetComponent>=[];
private WidgetClasses = {
'ChartWidget': ChartWidget,
'DataWidget': DataWidget
}
#ViewChild(DynamicComponent) dynamicComponent: DynamicComponent;
addComponent(): void{
let ref= this.dynamicComponent.addComponent(this.WidgetClasses[this.selectedComponent], this.selectedComponent);
this.elements.push(ref);
this.dynamicComponent.resetContainer();
}
}
Now, I am facing problem to render the components using innerHtml in main.component.html. It render the html but I am not able to use button click event or other event on it. I have also tried to render chart using primeng but its also not working.
main.component.html
<dynamic-component [hidden]="true" ></dynamic-component>
<widget *ngFor="let item of elements">
<div [innerHTML]="item._ngEl.nativeElement.innerHTML | sanitizeHtml">
</div>
</widget>
I have also implemented a sanitizeHtml Pipe but its giving still same result. So, as I understand innerHTML is only showing the html data but I can't use any button event as well as the js chart. I have also tried to show the items like this {{item}} under tag. But it display like a text [object object]. So, could anyone give a solution for it? How can I render the components allowing the button events and js chart? Thanks.
EDIT: See my Plunker here https://plnkr.co/edit/lugU2pPsSBd3XhPHiUP1?p=preview
You can see here, it is possible to add chart or data widget dynamically and I am showing it using innerHTML. So, the button events are not working here. If I coding like {{item}} then it shows [object object] text. You can also see in console the component array data. The main Question is, How can I active the button events on it (e.g. if i click close or refresh button then it will call the related functions)?
I would create structural directive like:
view.directive.ts
import { ViewRef, Directive, Input, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[view]'
})
export class ViewDirective {
constructor(private vcRef: ViewContainerRef) {}
#Input()
set view(view: ViewRef) {
this.vcRef.clear();
this.vcRef.insert(view);
}
ngOnDestroy() {
this.vcRef.clear()
}
}
then
app.component.ts
private elements: Array<{ view: ViewRef, component: WidgetComponent}> = [];
...
addComponent(widget: string ): void{
let component = this.dynamicComponent.addComponent(this.WidgetClasses[widget]);
let view: ViewRef = this.dynamicComponent.container.detach(0);
this.elements.push({view,component});
this.dynamicComponent.resetContainer();
}
and
app.component.html
<widget *ngFor="let item of elements">
<ng-container *view="item.view"></ng-container>
</widget>
So i have just moved view from dynamic component container to desired place.
Plunker Example
I know the textbook rules on that <div *ngFor="let foo of foobars">{{foo.stuff}}</div> turns into <template ngFor let-foo="$implicit" [ngForOf]="foobars"><div>...</div></template>. My question is two-fold:
HOW?
What do I need to do to leverage this mechanism ("microsyntax") myself?
Ie turn <div *myDirective="item">{{item.stuff}}</div> into <template myDirective let-item="$implicit"><div>{{item.stuff}}</div></template>?
Since I read ngFor's source code top to bottom, I can only assume this dark magic is in the compiler somewhere, I've been up and down the angular github, but I can't put my finger on it. Help!
Yes, all magic happens in the compiler.
Let's take this template:
<div *ngFor="let foo of foobars">{{foo}}</div>
First it will be transformed to the following:
<div template="ngFor let foo of foobars>{{foo}}</div>
And then:
<template ngFor let-foo [ngForOf]="foobars"><div>{{foo}}</div></template>
In Angular2 rc.4 it looks like this
First is generated ast tree node (Abstract Syntax Tree node) and then all magic happens in the TemplateParseVisitor.visitElement(https://github.com/angular/angular/blob/2.0.0-rc.4/modules/%40angular/compiler/src/template_parser.ts#L284) specifically at the bottom (https://github.com/angular/angular/blob/2.0.0-rc.4/modules/%40angular/compiler/src/template_parser.ts#L394)
if (hasInlineTemplates) {
var templateCssSelector = createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
var templateDirectiveMetas = this._parseDirectives(this.selectorMatcher, templateCssSelector);
var templateDirectiveAsts = this._createDirectiveAsts(
true, element.name, templateDirectiveMetas, templateElementOrDirectiveProps, [],
element.sourceSpan, []);
var templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(
element.name, templateElementOrDirectiveProps, templateDirectiveAsts);
this._assertNoComponentsNorElementBindingsOnTemplate(
templateDirectiveAsts, templateElementProps, element.sourceSpan);
var templateProviderContext = new ProviderElementContext(
this.providerViewContext, parent.providerContext, parent.isTemplateElement,
templateDirectiveAsts, [], [], element.sourceSpan);
templateProviderContext.afterElement();
parsedElement = new EmbeddedTemplateAst(
[], [], [], templateElementVars, templateProviderContext.transformedDirectiveAsts,
templateProviderContext.transformProviders,
templateProviderContext.transformedHasViewContainer, [parsedElement], ngContentIndex,
element.sourceSpan);
}
return parsedElement;
This method returns EmbeddedTemplateAst. It's the same as:
<template ngFor let-foo [ngForOf]="foobars"><div>{{foo}}</div></template>
If you want to turn:
<div *myDirective="item">{{item.stuff}}</div>
into
<template myDirective let-item><div>{{item.stuff}}</div></template>
then you need to use the following syntax:
<div *myDirective="let item">{{item.stuff}}</div>
But in this case you won't pass context.
Your custom structural directive might look like this:
#Directive({
selector: '[myDirective]'
})
export class MyDirective {
constructor(
private _viewContainer: ViewContainerRef,
private _templateRef: TemplateRef<any>) {}
#Input() set myDirective(prop: Object) {
this._viewContainer.clear();
this._viewContainer.createEmbeddedView(this._templateRef, prop); <== pass context
}
}
And you can use it like:
<div *myDirective="item">{{item.stuff}}</div>
||
\/
<div template="myDirective:item">{{item.stuff}}</div>
||
\/
<template [myDirective]="item">
<div>{{item.stuff}}</div>
</template>
I hope this will help you understand how structural directives work.
Update:
Let's see how it works (plunker)
*dir="let foo v foobars" => [dirV]="foobars"
So you can write the following directive:
#Directive({
selector: '[dir]'
})
export class MyDirective {
#Input()
dirV: any;
#Input()
dirK: any;
ngAfterViewInit() {
console.log(this.dirV, this.dirK);
}
}
#Component({
selector: 'my-app',
template: `<h1>Angular 2 Systemjs start</h1>
<div *dir="let foo v foobars k arr">{ foo }</div>
`,
directives: [MyDirective]
})
export class AppComponent {
foobars = [1, 2, 3];
arr = [3,4,5]
}
Here is the corresponding Plunker
See also
https://angular.io/docs/ts/latest/guide/structural-directives.html#!#the-asterisk-effect
https://teropa.info/blog/2016/03/06/writing-an-angular-2-template-directive.html
https://www.bennadel.com/blog/3076-creating-an-index-loop-structural-directive-in-angular-2-beta-14.htm
https://egghead.io/lessons/angular-2-write-a-structural-directive-in-angular-2
Live example you can find here https://alexzuza.github.io/enjoy-ng-parser/
*ngFor, *ngIf, ... are structural directives.
Either apply it on a <template> element or prefix it with a *
https://angular.io/docs/ts/latest/guide/structural-directives.html#!#unless
import { Directive, Input } from '#angular/core';
import { TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({ selector: '[myUnless]' })
export class UnlessDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) { }
#Input() set myUnless(condition: boolean) {
if (!condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}