querySelector does not seem to find elements inside ng-container - html

I am developing a web application using angular dart.
I am trying to find a 'div' element inside the component using document.querySelector() and I am trying to modify(add some content to) its body.
But it doesn't seem to find the 'div' element.
Here is my html:
<ng-container *ngFor="let item of list">
<ng-container *ngIf="item.canShowChart">
<div [id]="item.elementID" class="chart"></div>
</ng-container>
</ng-container>
Here is my component method which tries to modify the 'div':
void drawChart() {
for (final item in list) {
if (!item.canShowChart) {
continue;
}
final DivElement _container = document.querySelector('#' + item.elementID);
print(_container);
}
}
It always prints the '_container' as 'null'
I tried removing the ng-container and having only the 'div' in the page like below and it seems to work!.
<div [id]="item.elementID" class="chart"></div>
What is the problem?
TIA.

It is not working because as at the time you used 'querySelectorAll', angular had not loaded ng-container to the DOM yet. You should put your code in the 'AfterViewChecked' lifecycle hook.
export class ImageModalComponent implements OnInit, AfterViewChecked{
//AfterViewChecked
ngAfterViewChecked {
void drawChart() {
for (final item in list) {
if (!item.canShowChart) {
continue;
}
final DivElement _container = document.querySelector('#' + item.elementID);
print(_container);
}
}
}
}
Make sure to import 'AfterViewChecked' like so;
import { Component, OnInit, AfterViewChecked } from '#angular/core';

You can make it a separate component, let's call it app-chart:
<ng-container *ngFor="let item of list">
<app-chart *ngIf="item.canShowChart" [item]="item">
</app-chart>
</ng-container>
In the AppChartComponent declare necessary input(s), and inject ElementRef in the constructor:
#Input() item: any;
constructor(private ref: ElementRef) {}
this.ref.nativeElement is how you can access the DOM element from inside.

Never use querySelector to find elements in your template. Angular and DOM are two seperate paradigms and you should not mix them.
To find an element in your template, use a reference to an element.
<div #chartContainer class="chart"></div>
Then you can reference the div from your code.
See https://itnext.io/working-with-angular-5-template-reference-variable-e5aa59fb9af for an explanation.

AfterViewChecked not worked. Use AfterViewInit

Related

Angular 10 scrollTo from parent component to an element in child component html

parent html:
<div>
<button type="button" (click)="scroll(childelementTwo)">TO CHILD</button>
</div>
<div>
<app-child>
</div>
child html:
<div>
<div #childelementOne>
lot of stuff
</div>
<div #childelementTwo>
another lot of stuff
</div>
</div>
if all this html code were in the "same" component.html I would use this function, but not:
scroll(el: HTMLElement) {
el.scrollIntoView();
}
So: How can I scroll to an html element in child component ?
You can use #ViewChildren for this.
List-Item:
#Component({
selector: 'app-list-item',
templateUrl: './list-item.component.html',
styleUrls: ['./list-item.component.css']
})
export class ListItemComponent implements OnInit {
#Input() list;
constructor(private elRef: ElementRef) { }
ngOnInit() {
}
scrollIntoView() {
this.elRef.nativeElement.scrollIntoView();
}
}
List-Component:
#ViewChildren(ListItemComponent) viewChildren!: QueryList<ListItemComponent>;
list = new Array(1000).fill(true)
scrollTo() {
this.viewChildren.toArray()[100].scrollIntoView()
}
HTML:
<button (click)="scrollTo()">scroll to 100</button>
<app-list-item *ngFor="let item of list">
list works!
</app-list-item>
stackblitz: https://stackblitz.com/edit/angular-ivy-6ccaav?file=src%2Fapp%2Fapp.component.html
Mat, your "elements" are in child and you want control in parent. So, first make access to the elements in child using ViewChild
//in your child.component
#ViewChild("childelementOne") childelementOne;
#ViewChild("childelementTwo") childelementTwo;
Then in parent you can do
<div>
<button type="button" (click)="scroll(childComponent.childelementTwo)">
TO CHILD
</button>
</div>
<div>
<!--see that use a template reference variable to the childComponent-->
<app-child #childComponent></app-child>
</div>
scroll(el: ElementRef) {
el.nativeElement.scrollIntoView();
}
See how, in the .html we are using childComponent.childelementTwo. childComponentis the own component app-child, childComponent.childelementTwo is the "variable" that we get in the #ViewChild. By defect is an ElementRef. You get to the HTMLElement using el.nativeElement. Yes, using a template reference we can access to all the public variables and public function of your child.component
I create a stackblitz that is looks like the stackblitz in enno's answer, but see that is complety different
NOTE. You can also use the same referenceVariable in the child.component and use ViewChildren, so you can pass to the function the QueryList and the index

How to Show Placeholder if *ngFor Let Object of Objects from HTML Binding returns an empty array

I display data from a template that I create in another component (team-announcements.component)... in the main team.component.ts I use the selector for team-announcements, and add the [announcement]="announcements" and ngFor="let announcement of announcements" to load the data. If the array returns no data IE there are no announcements, how can I display a placeholder like "No Announcements"?
This is where I load the announcements in team.component.html. The data is served through an API service and is retrieved in "team.component.ts", the HTML for the objects in question is below.
team.component.ts (get announcement functions):
getAnnouncements() {
this.teamsService.getTeamAnnouncements(this.team.slug)
.subscribe(announcements => this.announcements = announcements);
console.log("announcements", this.announcements);
}
team.component.html
<div class="team-announcement">
<div class="announcement-title">Message of the Day</div>
<app-team-announcements
[announcement]="announcement"
*ngFor="let announcement of announcements">
</app-team-announcements>
</div>
This is how "app-team-announcements" above is templated in a separate file, "team-announcement.component.html" and is exported, and then used in the above code...
team-announcements.component.ts
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '#angular/core';
import { Team, Announcement, User, UserService } from '../core';
import { Subscription } from 'rxjs';
#Component({
selector: 'app-team-announcements',
templateUrl: './team-announcement.component.html'
})
export class TeamAnnouncementComponent implements OnInit, OnDestroy {
constructor(
private userService: UserService
) {}
private subscription: Subscription;
#Input() announcement: Announcement;
#Output() deleteAnnouncement = new EventEmitter<boolean>();
canModify: boolean;
ngOnInit() {
// Load the current user's data
this.subscription = this.userService.currentUser.subscribe(
(userData: User) => {
this.canModify = (userData.username === this.announcement.author.username);
}
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
team-announcements.component.html
<div class="announcement-text">
{{announcement.body}}
</div>
I am unsure of how or where to "If" check the array length to display a placeholder. Can anyone help?
If you want to hide it and display something else instead you can use the else property from *ngIf:
<div class="team-announcement">
<div class="announcement-title">Message of the Day</div>
<ng-container *ngIf="announcements.length != 0; else emptyArray">
<app-team-announcements
[announcement]="announcement"
*ngFor="let announcement of announcements">
</app-team-announcements>
</ng-container>
</div>
<ng-template #emptyArray>No announcements...</ng-template>
When you want an element with *ngFor to depend on a condition (*ngIf), a good alternative is to nest the element with an *ngFor in a <ng-container> with an *ngIf. A good thing about <ng-container> is that it wont actually be part of the DOM but will obey the *ngIf.
You could insert a div wich is only displayed when your array is empty:
<div class="team-announcement">
<div class="announcement-title">Message of the Day</div>
<app-team-announcements
[announcement]="announcement"
*ngFor="let announcement of announcements">
</app-team-announcements>
<div *ngIf="announcements.length===0"> No announcements </div>
</div>
Edit: Corrected the errors

#ContentChildren not picking up items inside custom component

I'm trying to use #ContentChildren to pick up all items with the #buttonItem tag.
#ContentChildren('buttonItem', { descendants: true })
This works when we have the ref item directly in the parent component.
<!-- #ContentChildren returns child item -->
<parent-component>
<button #buttonItem></button>
<parent-component>
But, if the element with the #buttonItem ref is wrapped in a custom component, that does not get picked by the #ContentChildren even when I set the {descendants: true} option.
<!-- #ContentChildren returns empty -->
<parent-component>
<child-component-with-button-ref></child-component-with-button-ref>
<parent-component>
I have created a simple StackBlitz example demonstrating this.
Doesn't appear to be a timeline for a resolution of this item via github... I also found a comment stating you cannot query across an ng-content boundary.
https://github.com/angular/angular/issues/14320#issuecomment-278228336
Below is possible workaround to get the elements to bubble up from the OptionPickerComponent.
in OptionPickerComponent count #listItem there and emit the array AfterContentInit
#Output() grandchildElements = new EventEmitter();
#ViewChildren('listItem') _items
ngAfterContentInit() {
setTimeout(() => {
this.grandchildElements.emit(this._items)
})
}
Set template reference #picker, register to (grandchildElements) event and set the $event to picker.grandchildElements
<app-option-picker #picker [optionList]="[1, 2, 3]" (grandchildElements)="picker.grandchildElements = $event" popup-content>
Create Input on PopupComponent to accept values from picker.grandchildElements
#Input('grandchildElements') grandchildElements: any
In app.component.html accept picker.grandchildElements to the input
<app-popup [grandchildElements]="picker.grandchildElements">
popup.component set console.log for open and close
open() {
if (this.grandchildElements) {
console.log(this.grandchildElements);
}
else {
console.log(this.childItems);
}
close() {
if (this.grandchildElements) {
console.log(this.grandchildElements);
}
else {
console.log(this.childItems);
}
popup.component change your ContentChildren back to listItem
#ContentChildren('listItem', { descendants: true }) childItems: Element;
popup.component.html set header expression
<h3>Child Items: {{grandchildElements ? grandchildElements.length : childItems.length}}</h3>
Stackblitz
https://stackblitz.com/edit/angular-popup-child-selection-issue-bjhjds?embed=1&file=src/app/option-picker/option-picker.component.ts
I had the same issue. We are using Kendo Components for angular. It is required to define Columns as ContentChilds of the Grid component. When I wanted to wrap it into a custom component and tried to provide additional columns via ng-content it simply didn't work.
I managed to get it working by resetting the QueryList of the grid component AfterViewInit of the custom wrapping component.
#ViewChild(GridComponent, { static: true })
public grid: GridComponent;
#ContentChildren(ColumnBase)
columns: QueryList<ColumnBase>;
ngAfterViewInit(): void {
this.grid.columns.reset([...this.grid.columns.toArray(), ...this.columns.toArray()]);
this.grid.columnList = new ColumnList(this.grid.columns);
}
One option is re-binding to the content child.
In the template where you are adding the content child you want picked up:
<outer-component>
<content-child [someBinding]="true" (onEvent)="someMethod($event)">
e.g. inner text content
</content-child>
</outer-component>
And inside of the example fictional <outer-component>:
#Component()
class OuterComponent {
#ContentChildren(ContentChild) contentChildren: QueryList<ContentChild>;
}
and the template for <outer-component> adding the <content-child> component, re-binding to it:
<inner-template>
<content-child
*ngFor="let child of contentChildren?.toArray()"
[someBinding]="child.someBinding"
(onEvent)="child.onEvent.emit($event)"
>
<!--Here you'll have to get the inner text somehow-->
</content-child>
</inner-template>
Getting that inner text could be impossible depending on your case. If you have full control over the fictional <content-child> component you could expose access to the element ref:
#Component()
class ContentChildComponent {
constructor(public element: ElementRef<HTMLElement>)
}
And then when you're rebinding to it, you can add the [innerHTML] binding:
<content-child
*ngFor="let child of contentChildren?.toArray()"
[someBinding]="child.someBinding"
(onEvent)="child.onEvent.emit($event)"
[innerHTML]="child.element.nativeElement.innerHTML"
></content-child>
You may have to sanitize the input to [innerHTML] however.

Dynamically ADDING and REMOVING Components in Angular

The current official docs only shows how to dynamically change components within an <ng-template> tag. https://angular.io/guide/dynamic-component-loader
What I want to achieve is, let's say I have 3 components: header, section, and footer with the following selectors:
<app-header>
<app-section>
<app-footer>
And then there are 6 buttons that will add or remove each component: Add Header, Add Section, and Add Footer
and when I click Add Header, the page will add <app-header> to the page that renders it, so the page will contain:
<app-header>
And then if I click Add Section twice, the page will now contain:
<app-header>
<app-section>
<app-section>
And if I click Add Footer, the page will now contain all these components:
<app-header>
<app-section>
<app-section>
<app-footer>
Is it possible to achieve this in Angular? Note that ngFor is not the solution I'm looking for, as it only allows to add the same components, not different components to a page.
EDIT: ngIf and ngFor is not the solution I'm looking for as the templates are already predetermined. What I am looking for is something like a stack of components or an array of components where we can add, remove, and change any index of the array easily.
EDIT 2: To make it more clear, let's have another example of why ngFor does not work. Let's say we have the following components:
<app-header>
<app-introduction>
<app-camera>
<app-editor>
<app-footer>
Now here comes a new component, <app-description>, which the user wants to insert in between and <app-editor>. ngFor works only if there is one same component that I want to loop over and over. But for different components, ngFor fails here.
What you're trying to achieve can be done by creating components dynamically using the ComponentFactoryResolver and then injecting them into a ViewContainerRef. One way to do this dynamically is by passing the class of the component as an argument of your function that will create and inject the component.
See example below:
import {
Component,
ComponentFactoryResolver, Type,
ViewChild,
ViewContainerRef
} from '#angular/core';
// Example component (can be any component e.g. app-header app-section)
import { DraggableComponent } from './components/draggable/draggable.component';
#Component({
selector: 'app-root',
template: `
<!-- Pass the component class as an argument to add and remove based on the component class -->
<button (click)="addComponent(draggableComponentClass)">Add</button>
<button (click)="removeComponent(draggableComponentClass)">Remove</button>
<div>
<!-- Use ng-template to ensure that the generated components end up in the right place -->
<ng-template #container>
</ng-template>
</div>
`
})
export class AppComponent {
#ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
// Keep track of list of generated components for removal purposes
components = [];
// Expose class so that it can be used in the template
draggableComponentClass = DraggableComponent;
constructor(private componentFactoryResolver: ComponentFactoryResolver) {
}
addComponent(componentClass: Type<any>) {
// Create component dynamically inside the ng-template
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
const component = this.container.createComponent(componentFactory);
// Push the component so that we can keep track of which components are created
this.components.push(component);
}
removeComponent(componentClass: Type<any>) {
// Find the component
const component = this.components.find((component) => component.instance instanceof componentClass);
const componentIndex = this.components.indexOf(component);
if (componentIndex !== -1) {
// Remove component from both view and array
this.container.remove(this.container.indexOf(component));
this.components.splice(componentIndex, 1);
}
}
}
Notes:
If you want to make it easier to remove the components later on, you can keep track of them in a local variable, see this.components. Alternatively you can loop over all the elements inside the ViewContainerRef.
You have to register your component as an entry component. In your module definition register your component as an entryComponent (entryComponents: [DraggableComponent]).
Running example:
https://plnkr.co/edit/mrXtE1ICw5yeIUke7wl5
For more information:
https://angular.io/guide/dynamic-component-loader
Angular v13 or above - simple way to add dynamic components to DOM
parent.component.html
<ng-template #viewContainerRef></ng-template>
parent.component.ts
#ViewChild("viewContainerRef", { read: ViewContainerRef }) vcr!: ViewContainerRef;
ref!: ComponentRef<YourChildComponent>
addChild() {
this.ref = this.vcr.createComponent(YourChildComponent)
}
removeChild() {
const index = this.vcr.indexOf(this.ref.hostView)
if (index != -1) this.vcr.remove(index)
}
Angular v12 or below
I have created a demo to show the dynamic add and remove process.
The parent component creates the child components dynamically and removes them.
Click for demo
Parent Component
// .ts
export class ParentComponent {
#ViewChild("viewContainerRef", { read: ViewContainerRef })
VCR: ViewContainerRef;
child_unique_key: number = 0;
componentsReferences = Array<ComponentRef<ChildComponent>>()
constructor(private CFR: ComponentFactoryResolver) {}
createComponent() {
let componentFactory = this.CFR.resolveComponentFactory(ChildComponent);
let childComponentRef = this.VCR.createComponent(componentFactory);
let childComponent = childComponentRef.instance;
childComponent.unique_key = ++this.child_unique_key;
childComponent.parentRef = this;
// add reference for newly created component
this.componentsReferences.push(childComponentRef);
}
remove(key: number) {
if (this.VCR.length < 1) return;
let componentRef = this.componentsReferences.filter(
x => x.instance.unique_key == key
)[0];
let vcrIndex: number = this.VCR.indexOf(componentRef as any);
// removing component from container
this.VCR.remove(vcrIndex);
// removing component from the list
this.componentsReferences = this.componentsReferences.filter(
x => x.instance.unique_key !== key
);
}
}
// .html
<button type="button" (click)="createComponent()">
I am Parent, Create Child
</button>
<div>
<ng-template #viewContainerRef></ng-template>
</div>
Child Component
// .ts
export class ChildComponent {
public unique_key: number;
public parentRef: ParentComponent;
constructor() {
}
remove_me() {
console.log(this.unique_key)
this.parentRef.remove(this.unique_key)
}
}
// .html
<button (click)="remove_me()">I am a Child {{unique_key}}, click to Remove</button>

How to declare a variable in a template in Angular

I have the following template :
<div>
<span>{{aVariable}}</span>
</div>
and would like to end up with :
<div "let a = aVariable">
<span>{{a}}</span>
</div>
Is there a way to do it ?
Update
We can just create directive like *ngIf and call it *ngVar
ng-var.directive.ts
#Directive({
selector: '[ngVar]',
})
export class VarDirective {
#Input()
set ngVar(context: unknown) {
this.context.$implicit = this.context.ngVar = context;
if (!this.hasView) {
this.vcRef.createEmbeddedView(this.templateRef, this.context);
this.hasView = true;
}
}
private context: {
$implicit: unknown;
ngVar: unknown;
} = {
$implicit: null,
ngVar: null,
};
private hasView: boolean = false;
constructor(
private templateRef: TemplateRef<any>,
private vcRef: ViewContainerRef
) {}
}
with this *ngVar directive we can use the following
<div *ngVar="false as variable">
<span>{{variable | json}}</span>
</div>
or
<div *ngVar="false; let variable">
<span>{{variable | json}}</span>
</div>
or
<div *ngVar="45 as variable">
<span>{{variable | json}}</span>
</div>
or
<div *ngVar="{ x: 4 } as variable">
<span>{{variable | json}}</span>
</div>
Plunker Example Angular4 ngVar
See also
Where does Angular 4 define "as local-var" behavior for *ngIf?
Original answer
Angular v4
div + ngIf + let
{{variable.a}}
{{variable.b}}
div + ngIf + as
view
<div *ngIf="{ a: 1, b: 2, c: 3 + x } as variable">
<span>{{variable.a}}</span>
<span>{{variable.b}}</span>
<span>{{variable.c}}</span>
</div>
component.ts
export class AppComponent {
x = 5;
}
If you don't want to create wrapper like div you can use ng-container
view
<ng-container *ngIf="{ a: 1, b: 2, c: 3 + x } as variable">
<span>{{variable.a}}</span>
<span>{{variable.b}}</span>
<span>{{variable.c}}</span>
</ng-container>
As #Keith mentioned in comments
this will work in most cases but it is not a general solution since it
relies on variable being truthy
See update for another approach.
You can declare variables in html code by using a template element in Angular 2 or ng-template in Angular 4+.
Templates have a context object whose properties can be assigned to variables using let binding syntax. Note that you must specify an outlet for the template, but it can be a reference to itself.
<ng-template #selfie [ngTemplateOutlet]="selfie"
let-a="aVariable" [ngTemplateOutletContext]="{ aVariable: 123 }">
<div>
<span>{{a}}</span>
</div>
</ng-template>
<!-- Output
<div>
<span>123</span>
</div>
-->
You can reduce the amount of code by using the $implicit property of the context object instead of a custom property.
<ng-template #t [ngTemplateOutlet]="t"
let-a [ngTemplateOutletContext]="{ $implicit: 123 }">
<div>
<span>{{a}}</span>
</div>
</ng-template>
The context object can be a literal object or any other binding expression. Other valid examples:
<!-- Use arbitrary binding expressions -->
<ng-template let-sum [ngTemplateOutletContext]="{ $implicit: 1 + 1 }">
<!-- Use pipes -->
<ng-template let-formatPi [ngTemplateOutletContext]="{ $implicit: 3.141592 | number:'3.1-5' }">
<!-- Use the result of a public method of your component -->
<ng-template let-root [ngTemplateOutletContext]="{ $implicit: sqrt(2116) }">
<!--
You can create an alias for a public property of your component:
anotherVariable: number = 123;
-->
<ng-template let-aliased [ngTemplateOutletContext]="{ $implicit: anotherVariable }">
<!--
The entire context object can be bound from a public property:
ctx: { first: number, second: string } = { first: 123, second: "etc" }
-->
<ng-template let-a="first" let-b="second" [ngTemplateOutletContext]="ctx">
Ugly, but:
<div *ngFor="let a of [aVariable]">
<span>{{a}}</span>
</div>
When used with async pipe:
<div *ngFor="let a of [aVariable | async]">
<span>{{a.prop1}}</span>
<span>{{a.prop2}}</span>
</div>
update 3
Issue https://github.com/angular/angular/issues/2451 is fixed in Angular 4.0.0
See also
https://github.com/angular/angular/pull/13297
https://github.com/angular/angular/commit/b4db73d
https://github.com/angular/angular/issues/13061
update 2
This isn't supported.
There are template variables but it's not supported to assign arbitrary values. They can only be used to refer to the elements they are applied to, exported names of directives or components and scope variables for structural directives like ngFor,
See also https://github.com/angular/angular/issues/2451
Update 1
#Directive({
selector: '[var]',
exportAs: 'var'
})
class VarDirective {
#Input() var:any;
}
and initialize it like
<div #aVariable="var" var="abc"></div>
or
<div #aVariable="var" [var]="'abc'"></div>
and use the variable like
<div>{{aVariable.var}}</div>
(not tested)
#aVariable creates a reference to the VarDirective (exportAs: 'var')
var="abc" instantiates the VarDirective and passes the string value "abc" to it's value input.
aVariable.var reads the value assigned to the var directives var input.
I would suggest this: https://medium.com/#AustinMatherne/angular-let-directive-a168d4248138
This directive allow you to write something like:
<div *ngLet="'myVal' as myVar">
<span> {{ myVar }} </span>
</div>
Here is a directive I wrote that expands on the use of the exportAs decorator parameter, and allows you to use a dictionary as a local variable.
import { Directive, Input } from "#angular/core";
#Directive({
selector:"[localVariables]",
exportAs:"localVariables"
})
export class LocalVariables {
#Input("localVariables") set localVariables( struct: any ) {
if ( typeof struct === "object" ) {
for( var variableName in struct ) {
this[variableName] = struct[variableName];
}
}
}
constructor( ) {
}
}
You can use it as follows in a template:
<div #local="localVariables" [localVariables]="{a: 1, b: 2, c: 3+2}">
<span>a = {{local.a}}</span>
<span>b = {{local.b}}</span>
<span>c = {{local.c}}</span>
</div>
Of course #local can be any valid local variable name.
In case if you want to get the response of a function and set it into a variable, you can use it like the following in the template, using ng-container to avoid modifying the template.
<ng-container *ngIf="methodName(parameters) as respObject">
{{respObject.name}}
</ng-container>
And the method in the component can be something like
methodName(parameters: any): any {
return {name: 'Test name'};
}
If you need autocomplete support from within in your templates from the Angular Language Service:
Synchronous:
myVar = { hello: '' };
<ng-container *ngIf="myVar; let var;">
{{var.hello}}
</ng-container>
Using async pipe:
myVar$ = of({ hello: '' });
<ng-container *ngIf="myVar$ | async; let var;">
{{var.hello}}
</ng-container>
A simple solution that worked for my requirement is:
<ng-container *ngIf="lineItem.productType as variable">
{{variable}}
</ng-container>
OR
<ng-container *ngIf="'ANY VALUE' as variable">
{{variable}}
</ng-container>
I am using Angular version: 12. It seems it may work with other version as well.
I liked the approach of creating a directive to do this (good call #yurzui).
I ended up finding a Medium article Angular "let" Directive which explains this problem nicely and proposes a custom let directive which ended up working great for my use case with minimal code changes.
Here's the gist (at the time of posting) with my modifications:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core'
interface LetContext <T> {
appLet: T | null
}
#Directive({
selector: '[appLet]',
})
export class LetDirective <T> {
private _context: LetContext <T> = { appLet: null }
constructor(_viewContainer: ViewContainerRef, _templateRef: TemplateRef <LetContext <T> >) {
_viewContainer.createEmbeddedView(_templateRef, this._context)
}
#Input()
set appLet(value: T) {
this._context.appLet = value
}
}
My main changes were:
changing the prefix from 'ng' to 'app' (you should use whatever your app's custom prefix is)
changing appLet: T to appLet: T | null
Not sure why the Angular team hasn't just made an official ngLet directive but whatevs.
Original source code credit goes to #AustinMatherne
For those who decided to use a structural directive as a replacement of *ngIf, keep in mind that the directive context isn't type checked by default. To create a type safe directive ngTemplateContextGuard property should be added, see Typing the directive's context. For example:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({
// don't use 'ng' prefix since it's reserved for Angular
selector: '[appVar]',
})
export class VarDirective<T = unknown> {
// https://angular.io/guide/structural-directives#typing-the-directives-context
static ngTemplateContextGuard<T>(dir: VarDirective<T>, ctx: any): ctx is Context<T> {
return true;
}
private context?: Context<T>;
constructor(
private vcRef: ViewContainerRef,
private templateRef: TemplateRef<Context<T>>
) {}
#Input()
set appVar(value: T) {
if (this.context) {
this.context.appVar = value;
} else {
this.context = { appVar: value };
this.vcRef.createEmbeddedView(this.templateRef, this.context);
}
}
}
interface Context<T> {
appVar: T;
}
The directive can be used just like *ngIf, except that it can store false values:
<ng-container *appVar="false as value">{{value}}</ng-container>
<!-- error: User doesn't have `nam` property-->
<ng-container *appVar="user as user">{{user.nam}}</ng-container>
<ng-container *appVar="user$ | async as user">{{user.name}}</ng-container>
The only drawback compared to *ngIf is that Angular Language Service cannot figure out the variable type so there is no code completion in templates. I hope it will be fixed soon.
With Angular 12 :
<div *ngIf="error$ | async as error">
<span class="text-warn">{{error.message}}</span>
</div>
I am using angular 6x and I've ended up by using below snippet.
I've a scenerio where I've to find user from a task object. it contains array of users but I've to pick assigned user.
<ng-container *ngTemplateOutlet="memberTemplate; context:{o: getAssignee(task) }">
</ng-container>
<ng-template #memberTemplate let-user="o">
<ng-container *ngIf="user">
<div class="d-flex flex-row-reverse">
<span class="image-block">
<ngx-avatar placement="left" ngbTooltip="{{user.firstName}} {{user.lastName}}" class="task-assigned" value="28%" [src]="user.googleId" size="32"></ngx-avatar>
</span>
</div>
</ng-container>
</ng-template>
I was trying to do something similar and it looks like this has been fixed in newer versions of angular.
<div *ngIf="things.car; let car">
Nice {{ car }}!
</div>
<!-- Nice Honda! -->
Short answer which help to someone
Template Reference variable often reference to DOM element within a
template.
Also reference to angular or web component and directive.
That means you can easily access the varible anywhere in a template
Declare reference variable using hash symbol(#)
Can able to pass a variable as a parameter on an event
show(lastName: HTMLInputElement){
this.fullName = this.nameInputRef.nativeElement.value + ' ' + lastName.value;
this.ctx.fullName = this.fullName;
}
*However, you can use ViewChild decorator to reference it inside your component.
import {ViewChild, ElementRef} from '#angular/core';
Reference firstNameInput variable inside Component
#ViewChild('firstNameInput') nameInputRef: ElementRef;
After that, you can use this.nameInputRef anywhere inside your Component.
Working with ng-template
In the case of ng-template, it is a little bit different because each template has its own set of input variables.
https://stackblitz.com/edit/angular-2-template-reference-variable
I'm the author of https://www.npmjs.com/package/ng-let
Structural directive for sharing data as local variable into html component template.
Source code:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
interface NgLetContext<T> {
ngLet: T;
$implicit: T;
}
#Directive({
// tslint:disable-next-line: directive-selector
selector: '[ngLet]'
})
export class NgLetDirective<T> {
private context: NgLetContext<T | null> = { ngLet: null, $implicit: null };
private hasView: boolean = false;
// eslint-disable-next-line no-unused-vars
constructor(private viewContainer: ViewContainerRef, private templateRef: TemplateRef<NgLetContext<T>>) { }
#Input()
set ngLet(value: T) {
this.context.$implicit = this.context.ngLet = value;
if (!this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef, this.context);
this.hasView = true;
}
}
/** #internal */
public static ngLetUseIfTypeGuard: void;
/**
* Assert the correct type of the expression bound to the `NgLet` input within the template.
*
* The presence of this static field is a signal to the Ivy template type check compiler that
* when the `NgLet` structural directive renders its template, the type of the expression bound
* to `NgLet` should be narrowed in some way. For `NgLet`, the binding expression itself is used to
* narrow its type, which allows the strictNullChecks feature of TypeScript to work with `NgLet`.
*/
static ngTemplateGuard_ngLet: 'binding';
/**
* Asserts the correct type of the context for the template that `NgLet` will render.
*
* The presence of this method is a signal to the Ivy template type-check compiler that the
* `NgLet` structural directive renders its template with a specific context type.
*/
static ngTemplateContextGuard<T>(dir: NgLetDirective<T>, ctx: any): ctx is NgLetContext<Exclude<T, false | 0 | '' | null | undefined>> {
return true;
}
}
Usage:
import { Component } from '#angular/core';
import { defer, Observable, timer } from 'rxjs';
#Component({
selector: 'app-root',
template: `
<ng-container *ngLet="timer$ | async as time"> <!-- single subscription -->
<div>
1: {{ time }}
</div>
<div>
2: {{ time }}
</div>
</ng-container>
`,
})
export class AppComponent {
timer$: Observable<number> = defer(() => timer(3000, 1000));
}
Try like this
<ng-container
[ngTemplateOutlet]="foo"
[ngTemplateOutletContext]="{ test: 'Test' }"
></ng-container>
<ng-template #foo let-test="test">
<div>{{ test }}</div>
</ng-template>
original answer by #yurzui won't work startring from Angular 9 due to - strange problem migrating angular 8 app to 9.
However, you can still benefit from ngVar directive by having it and using it like
<ng-template [ngVar]="variable">
your code
</ng-template>
although it could result in IDE warning: "variable is not defined"
It is much simpler, no need for anything additional. In my example I declare variable "open" and then use it.
<mat-accordion class="accord-align" #open>
<mat-expansion-panel hideToggle="true" (opened)="open.value=true" (closed)="open.value=false">
<mat-expansion-panel-header>
<span class="accord-title">Review Policy Summary</span>
<span class="spacer"></span>
<a *ngIf="!open.value" class="f-accent">SHOW</a>
<a *ngIf="open.value" class="f-accent">HIDE</a>
</mat-expansion-panel-header>
<mat-divider></mat-divider>
<!-- Quote Details Component -->
<quote-details [quote]="quote"></quote-details>
</mat-expansion-panel>
</mat-accordion>