17

I have a button that, when clicked, is replaced with an input field and a confirmation button, then when input is finished it's replaced with the original button again. When that happens, I want it to focus the original button after it appears (some users have requested better support for tab-navigation), but I can't seem to get it to do that consistently. The best I've been able to do is this:

// component.html
<button #durationButton *ngIf="!enteringDuration" (click)="enterDuration()">Enter Duration</button>
<ng-container *ngIf="enteringDuration">
    <input type="number" [(ngModel)]="duration" (keyup.enter)="setDuration()">
    <button (click)="setDuration()">&#10003;</button>
</ng-container>
// component.ts
@ViewChild("durationButton") durationButton: ElementRef
duration: number
enteringDuration = false
shouldFocusDurationButton = false

ngAfterContentChecked () {
    if (this.shouldFocusDurationButton && this.durationButton) {
        this.shouldFocusDurationButton = false
        this.durationButton.nativeElement.focus()
    }
}

enterDuration () {
    this.enteringDuration = true
}
setDuration () {
    this.enteringDuration = false
    this.shouldFocusDurationButton = true
}

If I click or press enter on the confirmation button, focus moves to the original button as soon as it appears, but if I press enter in the input field the button appears but for some reason it doesn't gain focus until I move the mouse. How do I make it work immediately for both?

John Montgomery
  • 6,739
  • 9
  • 52
  • 68

3 Answers3

13

You can use ViewChildren and the QueryList.changes event to be notified when the button is added to or removed from the view. If the QueryList contains the button element, you can set the focus on it. See this stackblitz for a demo. Suggestion: you may want to do something similar to set the focus on the input field when it becomes visible.

import { Component, ViewChildren, ElementRef, AfterViewInit, QueryList } from '@angular/core';
...

export class AppComponent implements AfterViewInit {

  @ViewChildren("durationButton") durationButton: QueryList<ElementRef>;

  enteringDuration = false

  ngAfterViewInit() {
    this.setFocus(); // If the button is already present...
    this.durationButton.changes.subscribe(() => {
      this.setFocus();
    });
  }

  setFocus() {
    if (this.durationButton.length > 0) {
      this.durationButton.first.nativeElement.focus();
    }
  }

  enterDuration() {
    this.enteringDuration = true
  }

  setDuration() {
    this.enteringDuration = false
  }
}
ConnorsFan
  • 70,558
  • 13
  • 122
  • 146
  • Perfect, with a few minor changes (I still had to use `shouldFocusDurationButton` to keep it from getting incorrectly focused when it appeared in a different context) this did exactly what I needed. I was planning to use it for the input as well but had a problem where it was triggering the `keyup` event prematurely, but once I fixed that your solution worked there as well. – John Montgomery Jul 30 '18 at 18:01
  • 1
    The stackblitz demo fails with on Windows 10 with Firefox 68.0.1, Edge 42.17134.1.0 and IE 11.829.17134.0 : the input does not get the focus. Am I missing something ? – Florian Beaufumé Aug 12 '19 at 11:59
  • @FlorianBeaufumé - I updated the code and stackblitz to account for cases where the button is already present when `ngAfterViewInit` is executed. – ConnorsFan Aug 12 '19 at 13:42
  • Sorry, I still get the same result, no focus on the input after I click the button. – Florian Beaufumé Aug 13 '19 at 14:57
  • Oh, but the question was not about setting the focus on the input; it was about setting the focus on the original button after pressing `Enter` in the input field. As mentioned in the answer, setting the focus on the input field was "left as an exercise"... :-) – ConnorsFan Aug 13 '19 at 15:22
1

Yes, *ngIf and ViewChild don't play well together. I did a course on ViewChild and did an entire section just on handing the *ngIf.

One option is to use the hidden attribute instead of *ngIf.

Another option is to bind to a setter (similar to how you bound to a function):

enter image description here

DeborahK
  • 57,520
  • 12
  • 104
  • 129
1

I´ve been battling focusing issue my self for some time. A great solution in your case would be to create a custom directive:

export class AutoFocus implements OnInit {
  constructor(private elementRef: ElementRef) {}

  ngOnInit(): void {
    this.elementRef.nativeElement.focus();
  }
}

And place it on #durationButton and on Input. Since they both use *ngIf, as soon as an element is created it will be focused. Working example here.

PS. In my case keyup.enter was producing some unwanted behaviour, I would rather stick with keydown.enter.

Max Tuzenko
  • 1,051
  • 6
  • 18