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
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
Usually, the reference variable can only be accessed inside the template. However, you can use ViewChild decorator to reference it inside your component.
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.
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 .
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.
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? 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.
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.
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With