Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why template local variables are not usable in templates when using *ngIf?

Tags:

angular

Part 1: "#test" is undefined when using *ngIf

When referencing an input that can be hidden/"destroyed" (because the *ngIf is used and some of the elements are destroyed), the local variable created by the hashtag syntax # (#test in the example below) does not work, even when the element exists in the page.

The code was:

@Component({     selector: 'my-app',     template: `<h1>My First Angular 2 App</h1>     <button (click)="focusOther(test)">test</button>     <input #test *ngIf="boolValue" >     ` }) export class AppComponent {     private isVisible = false;    focusOther(testElement){     this.isVisible = true;     alert(testElement);     testElement.focus();   }  } 

The alert displays undefined, because nothing is passed to that function.

Is there a solution to make it work? My goal is to focus an element that will be created.

Solution given by Mark Rajcok: make a directive with an afterViewInit that uses elementRef and calls .focus() on the element.

See this plunker for a working version of part 1: http://plnkr.co/edit/JmBBHMo1hXLe4jrbpVdv?p=preview

Part 2 how to re-focus that element after initial creation

Once this problem of "focus after creation" is fixed, I need a way to re-focus() a component, like in "test.focus()" (where #test is the local variable name for the input, but cannot be used like that as I demonstrated before).

Multiple solutions given by Mark Rajcok

like image 667
Micaël Félix Avatar asked Dec 29 '15 00:12

Micaël Félix


People also ask

Can we use template reference variable in component?

Usually, the reference variable can only be accessed inside the template. However, you can use ViewChild decorator to reference it inside your component.

How do I use a variable in a template?

In the template, you use the hash symbol, # , to declare a template variable. The following template variable, #phone , declares a phone variable with the <input> element as its value. Refer to a template variable anywhere in the component's template.

How do I access the template reference variable in component?

To get started using template reference variables, simply create a new Angular component or visit an existing one. To create a template reference variable, locate the HTML element that you want to reference and then tag it like so: #myVarName .

Why do we need template reference variables?

Template Reference Variable in angular is used to access all the properties of any element inside DOM. It can also be a reference to an Angular component or directive or a web component.

How to use local template variables in angular?

Local template variables in Angular can be used to reference HTML elements easily and use their properties either on sibling or child nodes. It’s pretty easy to create and use those local template variables. Let’s say you have got an input file for dealing the first name of someone.

What is ng-template in angular?

What is ng-Template? The <ng-template> is an Angular element, which contains the template. A template is an HTML snippet. The template does not render itself on DOM.

Is it possible to declare a variable inside of *ngif?

Only the opposite way (i.e. declare a variable inside of *ngIf and use it outside of *ngIf) is not working, and won't work by design. Sorry, something went wrong.

How to use the ngtemplateoutlet directive?

The ngTemplateOutlet, is a structural directive, which renders the template. To use this directive, first, we need to create the template and assign it to a template reference variable ( sayHelloTemplate in the following template).


1 Answers

As for a solution to the focus problem, you could create an attribute directive, focusMe:

import {Component, Directive, ElementRef} from 'angular2/core'; @Directive({   selector: '[focusMe]' }) export class FocusDirective {   constructor(private el: ElementRef) {}   ngAfterViewInit() {     this.el.nativeElement.focus();   } } @Component({     selector: 'my-app',     directives: [FocusDirective],     template: `<h1>My First Angular 2 App</h1>       <button (click)="toggle()">toggle</button>       <input focusMe *ngIf="isVisible">     ` }) export class AppComponent {    constructor() { console.clear(); }   private isVisible = false;   toggle() {     this.isVisible = !this.isVisible;   } } 

Plunker

Update 1: Adding the solution for the re-focus feature:

import {Component, Directive, ElementRef, Input} from 'angular2/core';  @Directive({   selector: '[focusMe]' }) export class FocusMe {     @Input('focusMe') hasFocus: boolean;     constructor(private elementRef: ElementRef) {}     ngAfterViewInit() {       this.elementRef.nativeElement.focus();     }     ngOnChanges(changes) {       //console.log(changes);       if(changes.hasFocus && changes.hasFocus.currentValue === true) {         this.elementRef.nativeElement.focus();       }     } } @Component({     selector: 'my-app',     template: `<h1>My First Angular 2 App</h1>     <button (click)="showInput()">Make it visible</button>     <input *ngIf="inputIsVisible" [focusMe]="inputHasFocus">     <button (click)="focusInput()" *ngIf="inputIsVisible">Focus it</button>     `,     directives:[FocusMe] }) export class AppComponent {   private inputIsVisible = false;   private inputHasFocus = false;   constructor() { console.clear(); }   showInput() {     this.inputIsVisible = true;   }   focusInput() {     this.inputHasFocus = true;     setTimeout(() => this.inputHasFocus = false, 50);   } } 

Plunker

An alternative to using setTimeout() to reset the focus property to false would be to create an event/output property on the FocusDirective, and emit() an event when focus() is called. The AppComponent would then listen for that event and reset the focus property.

Update 2: Here's an alternative/better way to add the re-focus feature, using ViewChild. We don't need to track the focus state this way, nor do we need an input property on the FocusMe directive.

import {Component, Directive, ElementRef, Input, ViewChild} from 'angular2/core';  @Directive({   selector: '[focusMe]' }) export class FocusMe {     constructor(private elementRef: ElementRef) {}     ngAfterViewInit() {       // set focus when element first appears       this.setFocus();     }     setFocus() {       this.elementRef.nativeElement.focus();     } } @Component({     selector: 'my-app',     template: `<h1>My First Angular 2 App</h1>     <button (click)="showInput()">Make it visible</button>     <input *ngIf="inputIsVisible" focusMe>     <button (click)="focusInput()" *ngIf="inputIsVisible">Focus it</button>     `,     directives:[FocusMe] }) export class AppComponent {   @ViewChild(FocusMe) child;   private inputIsVisible = false;   constructor() { console.clear(); }   showInput() {     this.inputIsVisible = true;   }   focusInput() {     this.child.setFocus();   } } 

Plunker

Update 3: Here's yet another alternative that does not require a directive, which still uses ViewChild, but we access the child via a local template variable rather than an attribute directive (thanks @alexpods for the tip):

import {Component, ViewChild, NgZone} from 'angular2/core';  @Component({     selector: 'my-app',     template: `<h1>Focus test</h1>     <button (click)="showInput()">Make it visible</button>     <input #input1 *ngIf="input1IsVisible">     <button (click)="focusInput1()" *ngIf="input1IsVisible">Focus it</button>     `, }) export class AppComponent {   @ViewChild('input1') input1ElementRef;   private input1IsVisible = false;   constructor(private _ngZone: NgZone) { console.clear(); }   showInput() {     this.input1IsVisible = true;     // Give ngIf a chance to render the <input>.     // Then set the focus, but do this outside the Angualar zone to be efficient.     // There is no need to run change detection after setTimeout() runs,     // since we're only focusing an element.     this._ngZone.runOutsideAngular(() => {        setTimeout(() => this.focusInput1(), 0);    });   }   setFocus(elementRef) {     elementRef.nativeElement.focus();   }   ngDoCheck() {     // if you remove the ngZone stuff above, you'll see     // this log 3 times instead of 1 when you click the     // "Make it visible" button.     console.log('doCheck');   }   focusInput1() {     this.setFocus(this.input1ElementRef);   } } 

Plunker

Update 4: I updated the code in Update 3 to use NgZone so that we don't cause Angular's change detection algorithm to run after the setTimeout() finishes. (For more on change detection, see this answer).

Update 5: I updated the code in the above plunker to use Renderer to make it web worker safe. Accessing focus() directly on nativeElement is discouraged.

focusInput1() {   this._renderer.invokeElementMethod(     this.input1ElementRef.nativeElement, 'focus', []); } 

I learned a lot from this question.

like image 113
Mark Rajcok Avatar answered Oct 12 '22 02:10

Mark Rajcok