Focus on input inside ngFor

Viewed 1715

When user clicks on button, addElement method will push object to array of elements and new input field will be created. Is it possible to focus on the newly created element? Inputs don't have distinctive IDs.

Template:

<div *ngFor="let foo of elements">
  <input class="input" value="{{ foo.value }}" />
</div>
<button (click)="addElement()"></button>

Method:

public addElement() {
  this.elements.push({ value: 'new' });
}

I'm using Angular 9 in this application.

3 Answers

You need to query all inputs via ViewChildren decorator, then every time addElement called the ngAfterViewChecked will be called too. So you may use this hook to focus the last already created input.

<div *ngFor="let foo of elements">
  <input 
    #input 
    class="input" 
    [value]="foo.value"
  />
</div>
<button (click)="addElement()"></button>
@ViewChildren('input') inputs: QueryList<ElementRef>;

addElement() {
  this.elements.push({ value: 'new' });
}

ngAfterViewChecked() {
  this.inputs.last.nativeElement.focus();
}

Otherwise you can do check by yourself and don't rely on lifecycle hook because this logic in ngAfterViewChecked may affect smth in your app:

@ViewChildren('input') inputs: QueryList<ElementRef>;

constructor(private changeDetectorRef: ChangeDetectorRef) {
}

addElement() {
  this.elements.push({ value: 'new' });
  /* run change detection manually to create input element and have access to the last input */
  this.changeDetectorRef.detectChanges();
  this.inputs.last.nativeElement.focus();
}

https://angular.io/api/core/ViewChildren#description

https://angular.io/guide/lifecycle-hooks#lifecycle-event-sequence

Angular Elements inside ngFor can be accessed using ViewChildren.

You can update your addElement function in the following way to achieve the results.

@ViewChildren("row") rows;
public addElement() {
    this.elements.push({ value: "new" });
    setTimeout(() => {
      this.rows.last.nativeElement.focus();
    }, 0);
  }

And update your ngFor Template as below:

<div *ngFor="let foo of elements">
  <input #row class="input" value="{{ foo.value }}" />
</div>

Here is the example stackblitz: https://stackblitz.com/edit/angular-ivy-xtkvaz

You can achieve this goal by tracking any change in child components of your div. Each time div is created or refreshed, set focus on last input child.

To do that, you need :

  • one directive to add some behavior logic to div container
  • one directive to identity each input tag

In this way, your template could be :

<div track-focus *ngFor="let foo of elements">
  <input auto-focus ... />
</div>
<button (click)="addElement()">Add</button>
@Directive({
  selector: 'input[auto-focus]',
})
export class InputAutoFocusDirective {}
@Directive({
  selector: '[track-focus]',
})
export class TrackFocusDirective implements AfterContentInit {
  @ContentChildren(InputAutofocusDirective, {
    read: ElementRef,
  })
  children: QueryList<InputAutoFocusDirective>;

  ngAfterContentInit() {
    const input = (this.children.last as ElementRef)
      .nativeElement as HTMLElement;
    if (input) {
      input.focus();
    }
  }
}

Here, you have a reusable solution.

Related