Nested Templates in Angular 2 [duplicate] - javascript

This question already has an answer here:
Passing Components in Angular2
(1 answer)
Closed 6 years ago.
I have a component, <DropDown></DropDown> and I want to let the user pass in a template for the list items in the DropDown.
Assuming they want to make a custom list item that has an image and text they would do something like this:
<DropDown [data]="myData">
<template>
<span> <img src="..."> Some Text <span>
</template>
</DropDown>
Inside the HTML of my DropDown component I have:
<div>
<ul>
<DropDownList [data]="data">
</DropDownList>
</ul>
</div>
In the DropDownList component I have the following HTML:
<li *ngFor="let item of data
(click)="handleOnSelect(item)">
[class.selected]="selectedItems.includes(item)">
<template [ngWrapper]="itemWrapper>
</template>
</li>
(I am using the template wrapper method from this post:
Binding events when using a ngForTemplate in Angular 2)
This method works if I have the li elements within the DropDown component's HTML. However, I want to have wrap the li's into a DropDownList component and pass the template the user gave from DropDown into DropDownList.
Is it possible to do this?

You could try the following solution:
#Component({
selector: 'DropDownList',
template: `
<li *ngFor="let item of items" (click)="handleOnSelect(item)">
<template [ngTemplateOutlet]="itemWrapper" [ngOutletContext]="{ $implicit: item }">
</template>
</li>`
})
export class DropDownListComponent {
#Input() itemWrapper: TemplateRef<any>;
#Input() items: any;
handleOnSelect(item) {
console.log('clicked');
}
}
#Component({
selector: 'DropDown',
template: `
<div>
<ul>
<DropDownList [items]="items" [itemWrapper]="itemWrapper">
</DropDownList>
</ul>
</div>`
})
export class DropDownComponent {
#Input() items: string[];
#ContentChild(TemplateRef) itemWrapper: TemplateRef<any>;
}
#Component({
selector: 'my-app',
template: `
<DropDown [items]="items">
<template let-item>
<h1>item: {{item}}</h1>
</template>
</DropDown>
`
})
export class App {
items = ['this','is','a','test'];
}
Plunker Example
The ngTemplateOutlet(^2.0.0-rc.2) directive has the same functionality as your custom directive NgWrapper
See also related questions:
Creating a dynamic repeater with ng-content transclusion with Angular2
Switch html templates dynamically during runtime on user action in angular 2

Related

How to work with variables from a loop in HTML and with a component property?

The data I work with (bosses[]) has a boss object with contains the key-value email which is an string. I want to create the anchor with that string in the HTML. Also note that there's a loop in HTML that allows to access to each boss in bosses[].
So how can I access to create an anchor with boss.email which it only exists in the HTML loop?
I've tried <a [href]=`"mailto: + boss.email"></a> but doesn't work.
the html:
<div class="boss" *ngFor="let boss of bosses" >
<div class="boss-text">
<div class="boss-text-name">{{boss.name}} </div>
<div>{{boss.email}}</div>
<a [href]="mailto: + boss.email"></a>
</div>
</div>
The component:
import { Component, Input, OnInit } from '#angular/core';
import { boss} from 'interfaces'
#Component({
templateUrl: 'boss-cell.component.html',
selector: 'boss-cell',
})
export class BossCellComponent implements OnInit {
constructor() {}
bosses: any[] = [{
email: 'kennedy#gmail.com',
name: 'kennedy',
}]
}
You're close! I think this is what you're looking for:
<div class="boss" *ngFor="let boss of bosses" >
<div class="boss-text">
<div class="boss-text-name">{{boss.name}} </div>
<a [href]="'mailto:' + boss.email">{{ boss.email }}</a>
</div>
</div>
You can use interpolation as Suraj already mentionned in the comments, or bind to a function creating the string. Depending on weather you are going to link other elements to the mail, you should pick the cleanest option for you.
Template
<div class="boss" *ngFor="let boss of bosses; let i = index">
<div class="boss-text">
<div class="boss-text-name">{{ boss.name }}</div>
<a [href]="getMail(i)">{{ boss.email }}</a>
</div>
</div>
Script
bosses: any[] = [{
email: 'kennedy#gmail.com',
name: 'kennedy',
}]
getMail(index: number) {
return 'mailto:' + this.bosses[index].email
}
You need to update this line
<a [href]="'mailto:' + boss.email">{{ boss.email }}</a>

To insert dynamic elements with specific style attributes in *ngfor(Angular)

I have implemented drag and drop feature referring to a youtube tutorial
I have *ngFor which will generate divs from existing roomsFloorZone array, now my objective is to generate divs using style attribute and value received from backend response provided to the same *ngFor, as all the divs from existing roomsFloorZone or from backend response needs to be under same parent div
I have tried with already existing elements present in roomsFloorZone
import {Component, OnInit, AfterViewInit, Input, SimpleChange,
SimpleChanges} from '#angular/core';
#Component({
selector: 'floor-zone',
templateUrl: './floorzone.component.html',
styleUrls: ['./floorzone.component.scss']
})
export class FloorZoneComponent{
urlFloorZoneIn: any;
roomsFloorZoneIn: any;
#Input() urlFloorZone;
#Input() roomsFloorZone;
#Input() currentBoxFloorZone;
ngOnChanges(changes: SimpleChanges) {
if (changes.urlFloorZone && changes.urlFloorZone.currentValue) {
this.urlFloorZoneIn = changes.urlFloorZone.currentValue;
}
if (changes.roomsFloorZone && changes.roomsFloorZone.currentValue) {
this.roomsFloorZoneIn = changes.roomsFloorZone.currentValue
}
}
dropzone1 = [];
currentBox?: string = this.currentBoxFloorZone;
move(box: string, toList: string[]): void {
box = this.currentBoxFloorZone;
this.removeBox(box, this.roomsFloorZoneIn);
this.removeBox(box, this.dropzone1);
toList.push(box);
}
removeBox(item: string, list) {
if (list.indexOf(item) !== -1) {
list.splice(list.indexOf(item), 1);
}
}
}
I have an array existing roomsFloorZoneIn , from which I drag and drop the elements in the below html and the *ngfor works for that
html
<div id="toget" class="dropzone" [ngStyle]="{'width':'100%','background-
image': 'url('+urlFloorZoneIn+')','background-repeat': 'no-repeat',
'background-position': 'center', 'background-size': '100% 100%',
'border':'1px solid black', 'height':'340px'}" appMovableArea
appDropzone (drop)="move(currentBox, dropzone1)">
<div class="box" *ngFor="let box of dropzone1" appDroppable
(dragStart)="currentBox = box" appMovable>
{{ box.dis }}
</div>
</div>
My objective is to provide style(transform attribute) and (abc or def) for {{box.dis}} taken from below snippet
[I have style and node values handy]
to *ngFor for placing the divs
`<div xmlns="http://www.w3.org/1999/xhtml">
<!--bindings={
"ng-reflect-ng-for-of": ""
}-->
<div _ngcontent-c5="" appdroppable="" appmovable=""
class="box draggable movable ng-star-inserted" touch-action="none"
style="transform: translateX(183.2%) translateY(56%);"> abc
<span _ngcontent-c5="">X</span>
</div>
<div _ngcontent-c5="" appdroppable="" appmovable=""
class="box draggable movable ng-star-inserted" touch-action="none"
style="transform: translateX(183.2%) translateY(56%);"> def
<span _ngcontent-c5="">X</span>
</div>
</div>`
So this *ngFor for should work for existing elements(roomsFloorZoneIn) as well as I should be able to insert some elements received from backend(using their style attribute and node value)
Note: I already have the style and node value handy
If the two lists can come one after the other, you can use two separate <div>'s, both with *ngFor's using the two Arrays:
<div class="myEnclosingDiv">
<div class="firstList" *ngFor="let element of firstArray">
{{ element.stuff }}
</div>
<div class="secondList" *ngFor="let element of secondArray">
{{ element.otherStuff }}
</div>
</div>
On the other hand, if the two lists need to be mixed somehow, you simply create a new Array in your TypeScript code and use a single *ngFor.

Angular 8: detect if a ng-content has content in it (or exists)

I have a component whose template allows for 2 content areas: Text and "read more" text. If the consumer of the component adds the area for the "read more" text, I want to show the "read more" link the end-user would click to show the text. If they don't include/need any "read more" text I don't want to show the link.
How do I detect the presence of the template area, and act accordingly with an ngIf?
For example, the html might be:
<app-promohero-message-unit title="Title for messaging module">
<div description>
Include a short, informative description here.
</div>
<div readmoretext>
If you need to add more detail, include another sentence or two it in this section.
</div>
</app-promohero-message-unit>
Obviously, they might not need readmoretext, so if they've omitted it I should not show the readmore link.
The component code is, so far:
import { Component, Input } from '#angular/core';
#Component({
selector: 'app-promohero-message-unit',
template: `
<div>
<h3 class="text-white">{{ title }}</h3>
<p class="text-white">
<ng-content select="[description]"></ng-content>
</p>
<p class="text-white" *ngIf="readMore">
<ng-content select="[readmoretext]"></ng-content>
</p>
</div>
<p>
<a class="text-white" (click)="showReadMore()" *ngIf="something"><u>Read more</u></a>
</p>
`
})
export class PromoheroMessageUnitComponent {
#Input()
title: string;
readMore = false;
showReadMore() {
this.readMore = true;
}
}
In Angular 8 you dont have to use the ngAfterViewInit life cycle hook. You can use the ngOnInit as long as you set the "static" value of the viewchild to true.
import { Component, OnInit, ViewChild, TemplateRef, ElementRef } from '#angular/core';
#Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
#ViewChild('content', { read: ElementRef, static: true }) content: ElementRef;
constructor() { }
ngOnInit() {
console.log(!!this.content.nativeElement.innerHTML); // return true if there is a content
}
}
Note that you must wrap the ng-content directive with html tag (such as div, span etc) and to set the templateRef on this outer tag.
<div #content>
<ng-content></ng-content>
</div>
I putted it on stackblitz: https://stackblitz.com/edit/angular-8-communicating-between-components-mzneaa?file=app/app.component.html
You can get a reference to the ng-content (Template Variable) and then access that variable in your component to check the length on the content of that ng-content using ViewChild
Then you can use the ngAfterViewInit life cycle hook to check for ng-content length
Your code will be like this:
import { Component, Input, ViewChild, ElementRef } from '#angular/core';
#Component({
selector: 'app-promohero-message-unit',
template: `
<div>
<h3 class="text-white">{{ title }}</h3>
<p class="text-white">
<ng-content select="[description]"></ng-content>
</p>
<p class="text-white" *ngIf="readMore">
<ng-content #readMoreContent select="[readmoretext]"></ng-content>
</p>
</div>
<p>
<a class="text-white" (click)="showReadMore()" *ngIf="something"><u>Read more</u></a>
</p>
`
})
export class PromoheroMessageUnitComponent {
#Input()
title: string;
#ViewChild('readMoreContent') readMoreContent: ElementRef;
readMore = false;
ngAfterViewInit() {
if (this.readMoreContent.nativeElement.childNodes.length.value == 0){
this.readMore = false
}
}
showReadMore() {
this.readMore = true;
}
}
You can use the ContentChild decorator for this, but will need to use an ng-template with a defined id as your content:
<app-promohero-message-unit title="Title for messaging module">
<div description>
Include a short, informative description here.
</div>
<ng-template #readmoretext>
If you need to add more detail, include another sentence or two it in this section.
</ng-template>
</app-promohero-message-unit>
Then in your component, you can use the ContentChild annotation like this:
export class PromoheroMessageUnitComponent {
#ContentChild('readmoretext')
readMoreContent: TemplateRef<any>;
// ...snip
}
Then finally in the HTML for your component:
<!-- snip -->
<p class="text-white" *ngIf="readMoreContent">
<ng-container *ngTemplateOutlet="readMoreContent"></ng-container>
</p>

Angular 2 passing html to ng-content with bindings

I'm writing angular components for the foundation css framework. I am working on the tabs component, and want to be able to pass some HTML to the <ng-content> of this.
The problem is, I also need to pass html which a user can put bindings on, like this:
PARENT TEMPLATE
<tabs [data]='example'>
<div> Age <br> {{item.age}} </div>`
</tabs>
TABS COMPONENT
<ul class="tabs" #tabs>
<li *ngFor="let item of data | async" (click)="tabClick($event)">
<a>{{item.name}}</a>
</li>
</ul>
<div>
<ng-content></ng-content>
</div>
TABS TYPESCRIPT
#Component({
selector: 'tabs',
templateUrl: './tabs.component.html'
})
export class TabsComponent {
#Input('data') data:any;
#ViewChild('tabs') tabs: ElementRef;
}
Where item is a reference to an object in the example array.
However, I get this error:
Cannot read property 'name' of undefined
as item is being evaluated before it is inserted into the <ng-content> directive.
Is there a way to get around this limitation, or am I going about this the wrong way?
update Angular 5
ngOutletContext was renamed to ngTemplateOutletContext
See also https://github.com/angular/angular/blob/master/CHANGELOG.md#500-beta5-2017-08-29
original
ngTemplateOutlet or ngForTemplate can be used for that use case:
<tabs [data]='example'>
<ng-template let-item>
<div> Age <br> {{item.age}} </div>`
</ng-template>
</tabs>
#Component({
...
template: `
<ul class="tabs" #tabs>
<li *ngFor="let item of data | async" (click)="tabClick($event)">
<a>{{item.name}}</a>
</li>
</ul>
<div>
<ng-template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{$implicit: (data | async)}"></ng-template>
</div>
`
})
class TabsComponent {
#ContentChild(TemplateRef) templateRef:TemplateRef;
}
See also Angular 2 bind transcluded content to loop variable
You should be using this way instead,
<tabs [data]='example'>
<div> Age <br> {{item.age}} </div>`
</tabs>
Component typescript
#Component({
selector: 'tabs',
templateUrl: './tabs.component.html'
})
export class TabsComponent {
#Input() data:any;
item:any{};
}
In your content projection define a selector as
<div class="tabs-body">
<ng-content select=".tabs-body"> </ng-content>
</div>
As your passing with bindings
<tabs [data]='example'>
<div> Age <br> {{item.age}} </div>`
</tabs>
DEMO
You need to pass the item object to the ng-content component.
<ng-content [item]="selectedTab></ng-content>
I am not certain on what lies behind the tab click event but you can assign that item object to selectedTab which will be passed to the component.
The component that will control the tab view can have the following:
#Input() item: Item;
And this will pass that object when you click. I might be attacking this from the wrong angle but maybe it will help you in some way.

Manipulate position of an HTML element in angular 2

I have an angular component template like this.
<my-app>
<nav id="nav-bar">
<ul>
<li id="item-1"></li>
<li>
<div id="item-2"></div>
</li>
<li id="item-3"></li>
<div id="middle-div">
</div>
<div id="side-div">
</div>
</nav>
</my-app>
Now here I want to take the div with id, 'item-2' and append it to nav-bar so that it appears below the div with id "side-div".
I can easily do this with javascript's appendChild function like this.
var item = document.getElementById("item-2");
var nav = document.getElementById("nav-bar");
//change position of searchform in DOM
nav.appendChild(item);
However, I want to be able to do this in component's typescript file. Is ElementRef an ideal solution to this? or is there any other way to achieve this?
the component looks like this:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: '/app/Templates/nav.html',
providers: [MyService]
})
export class MyComponent implements OnInit {
constructor(){
}
ngOnInit(){
}
}
You can use appendChild using ElementRef. Here is an example
#Component({
selector: 'app',
template: `
<div>
<button type="button" (click)="appendToChild()">Append Item to Child</button>
<div #item>Item</div>
<div #appendToChildEl></div>
</div>
`
})
export class App {
#ViewChild('item') item: ElementRef;
#ViewChild('appendToChildEl') appendToChildEl: ElementRef;
appendToChild() {
this.appendToChildEl.nativeElement.appendChild(this.item.nativeElement);
}
}
A plunker example:
https://embed.plnkr.co/7DJ1nWN6sl563hcR71FV/
You could also do something like this if you don't want to use ElementRef
User a service or component variable to control the position:
<my-app>
<nav id="nav-bar">
<ul>
<li id="item-1"></li>
<li>
<div id="item-2" *ngIf="defaultPos"></div>
</li>
<li id="item-3"></li>
<div id="middle-div">
</div>
<div id="side-div">
</div>
<div id="item-2" *ngIf="newPos"></div>
</nav>
</my-app>

Categories

Resources