202

I am working a front-end application with Angular 5, and I need to have a search box hidden, but on click of a button, the search box should be displayed and focused.

I have tried a few ways found on StackOverflow with directive or so, but can't succeed.

Here is the sample code:

@Component({
   selector: 'my-app',
   template: `
    <div>
    <h2>Hello</h2>
    </div>
    <button (click) ="showSearch()">Show Search</button>
    <p></p>
    <form>
      <div >
        <input *ngIf="show" #search type="text"  />            
      </div>
    </form>
    `,
  })
  export class App implements AfterViewInit {
  @ViewChild('search') searchElement: ElementRef;

  show: false;
  name:string;
  constructor() {    
  }

  showSearch(){
    this.show = !this.show;    
    this.searchElement.nativeElement.focus();
    alert("focus");
  }

  ngAfterViewInit() {
    this.firstNameElement.nativeElement.focus();
  }

The search box is not set to focus.

How can I do that?

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
Bob
  • 2,043
  • 2
  • 9
  • 5

15 Answers15

230

Edit 2022:
Read a more modern way with @Cichy's answer below


Modify the show search method like this

showSearch(){
  this.show = !this.show;  
  setTimeout(()=>{ // this will make the execution after the above boolean has changed
    this.searchElement.nativeElement.focus();
  },0);  
}
Shadoweb
  • 5,812
  • 1
  • 42
  • 55
Arvind Muthuraman
  • 2,957
  • 1
  • 12
  • 11
  • 25
    Why do we need to use setTimeout? Isn't the change of boolean synchronous? – Andrei Rosu May 02 '19 at 17:40
  • 6
    doesn't that mess with zonejs? – Laurence May 06 '19 at 21:08
  • 1
    @AndreiRosu, without it I was getting an error because the changes were not rendered – Islam Mar 25 '20 at 12:19
  • 29
    This works, but without a clear explanation it's magic. Magic breaks often. – John White Apr 25 '20 at 03:40
  • 2
    I was trying to focus my element inside a panel when the panel was opened, so the 0 timeout didn't work for me, but this did: `setTimeout(() => { this.searchElement.nativeElement.focus() }, 100)` – tclark333 May 03 '20 at 15:50
  • If you want a real explanation of why this works... here it is: https://dev.to/scooperdev/when-using-settimeout-is-not-your-best-option-3p4b – hevans900 Sep 03 '20 at 09:10
  • 8
    tl;dr: using setTimeout makes your code async, by adding a function execution to the event loop, and triggering change detection a second time when it executes. It does have a performance hit. – hevans900 Sep 03 '20 at 09:11
  • @AndreiRosu changing a boolean is in fact synchronous but like React & Vue, Angular batches DOM rendering changes based on 'compute' intervals. – Alex Dunlop Feb 11 '21 at 04:36
  • 2
    @John White Like React & Vue, Angular batches DOM rendering changes based on compute intervals. This improves performance (without the batch the page would reload on each line). Now a reason to the Magic, what is happening is due to the batch, the ngIf statement has not rendered in the DOM, making the element ref DOM missing. Now the solution, Adding a timeout creates a new batch when the timer ends (even if it is 0). Now since the ngIf statement has updated the DOM element is no longer missing. – Alex Dunlop Feb 11 '21 at 04:38
  • @AlexDunlop Thanks for the explanation, so what's happening is breaking Angular's abstraction, it works but it's kinda a hack – John White Feb 13 '21 at 10:36
  • Wouldn't it work without setTimeout in afterViewInit? – hyena Feb 19 '21 at 11:30
  • 4
    setTimeout refreshes all the layout even it is in `onPush`. Better to trigger `this.cdr.detectChanges()` instead of `setTImeout`. – Igor Kurkov Apr 23 '21 at 19:28
70

You should use HTML autofocus for this:

<input *ngIf="show" #search type="text" autofocus /> 

Note: if your component is persisted and reused, it will only autofocus the first time the fragment is attached. This can be overcome by having a global DOM listener that checks for autofocus attribute inside a DOM fragment when it is attached and then reapplying it or focus via JavaScript.

Here is an example global listener, it only needs to be placed in your spa application once and autofocus will function regardless of how many times the same fragment is reused:

(new MutationObserver(function (mutations, observer) {
    for (let i = 0; i < mutations.length; i++) {
        const m = mutations[i];
        if (m.type == 'childList') {
            for (let k = 0; k < m.addedNodes.length; k++) {
                const autofocuses = m.addedNodes[k].querySelectorAll("[autofocus]"); //Note: this ignores the fragment's root element
                console.log(autofocuses);
                if (autofocuses.length) {
                    const a = autofocuses[autofocuses.length - 1]; // focus last autofocus element
                    a.focus();
                    a.select();
                }
            }
        }
    }
})).observe(document.body, { attributes: false, childList: true, subtree: true });
N-ate
  • 6,051
  • 2
  • 40
  • 48
48

This directive will instantly focus and select any text in the element as soon as it's displayed. This might require a setTimeout for some cases, it has not been tested much.

import { Directive, ElementRef, OnInit } from '@angular/core';
    
@Directive({
  selector: '[appPrefixFocusAndSelect]',
})
export class FocusOnShowDirective implements OnInit {    
  constructor(private el: ElementRef) {
    if (!el.nativeElement['focus']) {
      throw new Error('Element does not accept focus.');
    }
  }
    
  ngOnInit(): void {
    const input: HTMLInputElement = this.el.nativeElement as HTMLInputElement;
    input.focus();
    input.select();
  }
}

And in the HTML:

<mat-form-field>
  <input matInput type="text" appPrefixFocusAndSelect [value]="'etc'">
</mat-form-field>
Magiczne
  • 1,586
  • 2
  • 15
  • 23
ggranum
  • 999
  • 8
  • 10
38

html of component:

<input [cdkTrapFocusAutoCapture]="show" [cdkTrapFocus]="show">

controler of component:

showSearch() {
  this.show = !this.show;    
}

..and do not forget about import A11yModule from @angular/cdk/a11y

import { A11yModule } from '@angular/cdk/a11y'
Cichy
  • 4,602
  • 3
  • 23
  • 28
20

I'm going to weigh in on this (Angular 7 Solution)

input [appFocus]="focus"....
import {AfterViewInit, Directive, ElementRef, Input,} from '@angular/core';

@Directive({
  selector: 'input[appFocus]',
})
export class FocusDirective implements AfterViewInit {

  @Input('appFocus')
  private focused: boolean = false;

  constructor(public element: ElementRef<HTMLElement>) {
  }

  ngAfterViewInit(): void {
    // ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
    if (this.focused) {
      setTimeout(() => this.element.nativeElement.focus(), 0);
    }
  }
}
Liam
  • 27,717
  • 28
  • 128
  • 190
Ricardo Saracino
  • 1,345
  • 2
  • 16
  • 37
17

This is working i Angular 8 without setTimeout:

import {AfterContentChecked, Directive, ElementRef} from '@angular/core';

@Directive({
  selector: 'input[inputAutoFocus]'
})
export class InputFocusDirective implements AfterContentChecked {
  constructor(private element: ElementRef<HTMLInputElement>) {}

  ngAfterContentChecked(): void {
    this.element.nativeElement.focus();
  }
}

Explanation: Ok so this works because of: Change detection. It's the same reason that setTimout works, but when running a setTimeout in Angular it will bypass Zone.js and run all checks again, and it works because when the setTimeout is complete all changes are completed. With the correct lifecycle hook (AfterContentChecked) the same result can be be reached, but with the advantage that the extra cycle won't be run. The function will fire when all changes are checked and passed, and runs after the hooks AfterContentInit and DoCheck. If i'm wrong here please correct me.

More one lifecycles and change detection on https://angular.io/guide/lifecycle-hooks

UPDATE: I found an even better way to do this if one is using Angular Material CDK, the a11y-package. First import A11yModule in the the module declaring the component you have the input-field in. Then use cdkTrapFocus and cdkTrapFocusAutoCapture directives and use like this in html and set tabIndex on the input:

<div class="dropdown" cdkTrapFocus cdkTrapFocusAutoCapture>
    <input type="text tabIndex="0">
</div>

We had some issues with our dropdowns regarding positioning and responsiveness and started using the OverlayModule from the cdk instead, and this method using A11yModule works flawlessly.

SNDVLL
  • 215
  • 3
  • 10
  • Hi...I don't try `cdkTrapFocusAutoCapture` attribute, but I changed your first example into my code. For reasons unknown (for me) it did not work with AfterContentChecked lifecycle hook, but only with OnInit. In particolar with AfterContentChecked I can't change focus and move (with mouse or keyoboard) onto another input without that directive in the same form. – timhecker Jul 06 '20 at 13:47
  • @timhecker yeah we faced a similiar issue when migration our components to the cdk overlay (it worked when using just css instead of an overlay for positioning). It focused indefinitely on the input when a component using the directive was visible. Only solution I found was to use the cdk directives mentioned in the update. – SNDVLL Aug 17 '20 at 06:16
15

In Angular, within HTML itself, you can set focus to input on click of a button.

<button (click)="myInput.focus()">Click Me</button>

<input #myInput></input>
shaheer shukur
  • 1,077
  • 2
  • 12
  • 19
  • 1
    This answer shows a simple way to programmatically select another element in HTML, which is what I was looking for (all the "initial focus" answers don't solve how to react to an event by changing focus) - unfortunately, I needed to react to a `mat-select` `selectionChanged` event that happens before other UI stuff, so pulling the focus at that point didn't work. Instead I had to write a method `setFocus(el:HTMLElement):void { setTimeout(()=>el.focus(),0); }` and call it from the event handler: ``. Not as nice, but simple and works well. thx! – Guss Aug 19 '20 at 11:58
  • Didn't work for me. I get ERROR TypeError: Cannot read property 'focus' of undefined – Reza Taba Jan 07 '21 at 00:52
  • @RezaTaba, did you call the function 'myInput.focus()' or you just wrote 'myInput.focus' ? Also, make sure, your input element is inside a rendered div (*ngIf false can cause error, for example) – shaheer shukur Jan 07 '21 at 11:10
  • Same answer of Sandipan Mitra – Pablo Sanchez Manzano Jun 28 '21 at 16:06
12

To make the execution after the boolean has changed and avoid the usage of timeout you can do:

import { ChangeDetectorRef } from '@angular/core';

constructor(private cd: ChangeDetectorRef) {}

showSearch(){
  this.show = !this.show;  
  this.cd.detectChanges();
  this.searchElement.nativeElement.focus();
}
Marcin Restel
  • 280
  • 3
  • 13
7

I'm having same scenario, this worked for me but i'm not having the "hide/show" feature you have. So perhaps you could first check if you get the focus when you have the field always visible, and then try to solve why does not work when you change visibility (probably that's why you need to apply a sleep or a promise)

To set focus, this is the only change you need to do:

your Html mat input should be:

<input #yourControlName matInput>

in your TS class, reference like this in the variables section (

export class blabla...
    @ViewChild("yourControlName") yourControl : ElementRef;

Your button it's fine, calling:

  showSearch(){
       ///blabla... then finally:
       this.yourControl.nativeElement.focus();
}

and that's it. You can check this solution on this post that I found, so thanks to --> https://codeburst.io/focusing-on-form-elements-the-angular-way-e9a78725c04f

Dharman
  • 30,962
  • 25
  • 85
  • 135
Yogurtu
  • 2,656
  • 3
  • 23
  • 23
  • This worked well for my use case: I needed to show an input box if the user selected a specific option in a menu, and I wanted it to achieve focus only in that scenario (not automatically, after the component was initialized, which is what `autofocus` did). – Todd Dec 23 '21 at 04:42
6

There is also a DOM attribute called cdkFocusInitial which works for me on inputs. You can read more about it here: https://material.angular.io/cdk/a11y/overview

Timon
  • 359
  • 3
  • 10
4

Only using Angular Template

<input type="text" #searchText>

<span (click)="searchText.focus()">clear</span>
2

When using an overlay/dialog, you need to use cdkFocusInitial within cdkTrapFocus and cdkTrapFocusAutoCapture.

CDK Regions:

If you're using cdkFocusInitial together with the CdkTrapFocus directive, nothing will happen unless you've enabled the cdkTrapFocusAutoCapture option as well. This is due to CdkTrapFocus not capturing focus on initialization by default.

In the overlay/dialog component:

<div cdkTrapFocus cdkTrapFocusAutoCapture>
  <input cdkFocusInitial>
</div>
Get Off My Lawn
  • 34,175
  • 38
  • 176
  • 338
1

@john-white The reason the magic works with a zero setTimeout is because

this.searchElement.nativeElement.focus();

is sent to the end of the browser callStack and therefore executed last/later, its not a very nice way of getting it to work and it probably means there is other logic in the code that could be improved on.

mad4power
  • 15
  • 7
1

For me, it only worked after using the blur function.

To keep it clean I made a directive for it:

import { Directive, ElementRef } from '@angular/core';
import { IonInput } from '@ionic/angular';

@Directive({
  selector: '[appAutofocus]'
})
export class AutofocusDirective {
  constructor(private el: ElementRef) {
  }
    
  async ngOnInit() {
    const input = this.el.nativeElement as IonInput;

    await input.setBlur();
    await input.setFocus();
  }
}
-1

Easier way is also to do this.

let elementReference = document.querySelector('<your css, #id selector>');
    if (elementReference instanceof HTMLElement) {
        elementReference.focus();
    }
patz
  • 1,306
  • 4
  • 25
  • 42