170

I have a component that has a <p> element. It's (click) event will change it into a <textarea>. So, the user can edit the data. My question is:

  • How can I make the focus on the textarea?
  • How can I get the element, so I can apply the .focus() on it?
  • Can I avoid using document.getElemenntById()?

I have tried to use the "ElementRef" and the "@ViewChild()" however it seems that I'm missing something:

app.component.ts

@ViewChild('tasknoteId') taskNoteRef:ElementRef;

noteEditMode: boolean = false;

get isShowNote (){
  return  !this.noteEditMode && this.todo.note  ? true : false;
}
taskNote: string;
toggleNoteEditMode () {
  this.noteEditMode = !this.noteEditMode; 
  this.renderer.invokeElementMethod(
    this.taskNoteRef.nativeElement,'focus'
  );
}

app.component.html

<span class="the-insert">
  <form [hidden]="!noteEditMode && todo.note">
    <textarea #tasknoteId id="tasknote"
     name="tasknote"
     [(ngModel)]="todo.note"
     placeholder="{{ notePlaceholder }}"
     style="background-color:pink"
     (blur)="updateNote()" (click)="toggleNoteEditMode()"
     [autofocus]="noteEditMode"
     [innerHTML]="todo.note">
   </textarea>
 </form>
</span>
ng-hobby
  • 2,077
  • 2
  • 13
  • 26
ßastian
  • 1,814
  • 3
  • 13
  • 22

3 Answers3

231

Use ViewChild with #TemplateVariable as shown here,

<textarea  #someVar  id="tasknote"
                  name="tasknote"
                  [(ngModel)]="taskNote"
                  placeholder="{{ notePlaceholder }}"
                  style="background-color: pink"
                  (blur)="updateNote() ; noteEditMode = false " (click)="noteEditMode = false"> {{ todo.note }} 

</textarea>
 

In component,

OLDEST Way

import {ElementRef} from '@angular/core';
@ViewChild('someVar') el:ElementRef;

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

OLD Way

import {ElementRef} from '@angular/core';
@ViewChild('someVar') el:ElementRef;
    
constructor(private rd: Renderer) {}
ngAfterViewInit() {
    this.rd.invokeElementMethod(this.el.nativeElement,'focus');
}

Updated on 22/03(March)/2017

NEW Way

Please note from Angular v4.0.0-rc.3 (2017-03-10) few things have been changed. Since Angular team will deprecate invokeElementMethod, above code no longer can be used.

BREAKING CHANGES

since 4.0 rc.1:

rename RendererV2 to Renderer2
rename RendererTypeV2 to RendererType2
rename RendererFactoryV2 to RendererFactory2

import {ElementRef,Renderer2} from '@angular/core';
@ViewChild('someVar') el:ElementRef;

constructor(private rd: Renderer2) {}

ngAfterViewInit() {
      console.log(this.rd); 
      this.el.nativeElement.focus();      //<<<=====same as oldest way
}

console.log(this.rd) will give you following methods and you can see now invokeElementMethod is not there. Attaching img as yet it is not documented.

NOTE: You can use following methods of Rendere2 with/without ViewChild variable to do so many things.

enter image description here

micronyks
  • 54,797
  • 15
  • 112
  • 146
  • it works only for the first time, do you have any idea ? – ßastian Aug 15 '16 at 11:32
  • Yes it will work only once. What do u want? – micronyks Aug 15 '16 at 15:16
  • @CoffeeCode you can move the this.rd.invokeElementMethod(this.el.nativeElement,'focus'); into another function and call it any time you want to make it the focus again – Nico Jan 25 '17 at 21:06
  • In fact, the whole `Renderer` will go away, but there's a `Renderer2` available (with other methods) – Nicky Mar 22 '17 at 09:31
  • 1
    Can I find a child element of a Renderer2, maybe through selector perhaps? – Rosdi Kasim Apr 06 '17 at 06:56
  • What if you have an arbitrary number of elements that are unknown before loading the page? –  Aug 30 '17 at 14:07
  • @brianthomps please give us your scenario in a different question. – micronyks Aug 31 '17 at 03:22
  • 2
    Can this all be true? In vue, you put a `ref="myDomEl"` on the element, then `this.$refs.myDomEl.focus()` or `.click()` or `.getBoundingClientRect()` or whatever. Nothing to import, works x times, works inside or outside vue. You don't want to be doing this all the time, but you don't want it to be impossible either??? – bbsimonbb Feb 12 '19 at 16:11
  • Why do we have import statement with only ElementRef and no `ViewChild` itself? Why ViewChild is merged to imports and not in the class? I never will understand this. – Rantiev Feb 22 '19 at 08:14
  • How do we grab an element by tag name? I cannot place a #someVar ref on the element since it doesn't exist yet. – Crystal Jun 04 '19 at 17:05
  • @Crystal you still can use #someVar as you have to grab that element in `ngAfterViewinit`. By that time, element will become visible and you can grab & manipulate it. – micronyks Jun 06 '19 at 11:01
  • 1
    So I was attempting to place a tabindex=0 on a component that was being dynamically created by a library - the template doesn't exist so I could not add the template ref to it (#someVar)... I have to wait for the dom to finish rendering to then grab it via vanilla javascript. However, as I understand Angular discourages that... this article helped though -> https://blog.angularindepth.com/working-with-dom-in-angular-unexpected-consequences-and-optimization-techniques-682ac09f6866 – Crystal Jun 06 '19 at 22:56
98

Update (using renderer):

Note that the original Renderer service has now been deprecated in favor of Renderer2

as on Renderer2 official doc.

Furthermore, as pointed out by @GünterZöchbauer:

Actually using ElementRef is just fine. Also using ElementRef.nativeElement with Renderer2 is fine. What is discouraged is accessing properties of ElementRef.nativeElement.xxx directly.


You can achieve this by using elementRef as well as by ViewChild. however it's not recommendable to use elementRef due to:

  • security issue
  • tight coupling

as pointed out by official ng2 documentation.

1. Using elementRef (Direct Access):

export class MyComponent {    
constructor (private _elementRef : ElementRef) {
 this._elementRef.nativeElement.querySelector('textarea').focus();
 }
}

2. Using ViewChild (better approach):

<textarea  #tasknote name="tasknote" [(ngModel)]="taskNote" placeholder="{{ notePlaceholder }}" 
style="background-color: pink" (blur)="updateNote() ; noteEditMode = false " (click)="noteEditMode = false"> {{ todo.note }} </textarea> // <-- changes id to local var


export class MyComponent implements AfterViewInit {
  @ViewChild('tasknote') input: ElementRef;

   ngAfterViewInit() {
    this.input.nativeElement.focus();

  }
}

3. Using renderer:

export class MyComponent implements AfterViewInit {
      @ViewChild('tasknote') input: ElementRef;
         constructor(private renderer: Renderer2){           
          }

       ngAfterViewInit() {
       //using selectRootElement instead of depreaced invokeElementMethod
       this.renderer.selectRootElement(this.input["nativeElement"]).focus();
      }

    }
Hien Nguyen
  • 24,551
  • 7
  • 52
  • 62
candidJ
  • 4,289
  • 1
  • 22
  • 32
  • 2
    When I do all of your approach I get Cannot read property nativeElement pf undefined – Azoulay Jason Sep 21 '17 at 09:36
  • 4
    Where did you use `renderer`? Just injected that on constructor – Mohammad Kermani Dec 18 '17 at 12:47
  • Same error here, when i try to do this approach on an i get "Cannot read property 'nativeElement' of undefined.." I'm using Angular 5 and i tried to import the Elevant Zoom script. – Terai Dec 19 '17 at 10:46
  • You must add reference name (string after the '#') : – Filip Savic Jan 15 '18 at 11:45
  • In your better approach, you're not actually doing anything different, still just using the nativeElement. Injecting the Renderer2 is no good if you don't use it:-) – Drenai Jan 25 '18 at 11:27
  • @Ryan updated answer to include `renderer` way as well. @M98 – candidJ Mar 16 '18 at 16:49
  • @Ryan I actually tested the code given in the question above. as it had single element so it selected `textarea` as root element. :P Thanks for the info and the link. – candidJ Mar 18 '18 at 05:26
  • Thank you for your post. I'd like to ask a follow-up: Why is it recommended to put the focus code in `ngAfterViewInit()` as opposed to `ngOnInit()`? When I run it in the latter, it runs fine. When I run it in the former, I have some weird UI glitches. – Knight Aug 08 '18 at 14:25
  • @Knight we want our view to be initialized before setting focus on any particular element, so focus code should be in `ngAfterViewInit()`. If you can reproduce the issue in stackblitz, I can have a look. – candidJ Aug 12 '18 at 08:10
  • 1
    `constructor (private _elementRef : ElementRef)` with capital E for me, import from core. – bbsimonbb Feb 12 '19 at 17:12
  • In the first approach, I use querySelector and I got the first element of that tag. Could you please update with how to get all elements with that particular selector? – PrasadW Mar 18 '21 at 09:20
  • 1
    @PrasadW you can use `@ViewChildren('elemName')` (second approach) to get all the elements. – candidJ Jun 07 '21 at 05:56
11

Angular 2.0.0 Final:

I have found that using a ViewChild setter is most reliable way to set the initial form control focus:

@ViewChild("myInput")
set myInput(_input: ElementRef | undefined) {
    if (_input !== undefined) {
        setTimeout(() => {
            this._renderer.invokeElementMethod(_input.nativeElement, "focus");
        }, 0);
    }
}

The setter is first called with an undefined value followed by a call with an initialized ElementRef.

Working example and full source here: http://plnkr.co/edit/u0sLLi?p=preview

Using TypeScript 2.0.3 Final/RTM, Angular 2.0.0 Final/RTM, and Chrome 53.0.2785.116 m (64-bit).

UPDATE for Angular 4+

Renderer has been deprecated in favor of Renderer2, but Renderer2 does not have the invokeElementMethod. You will need to access the DOM directly to set the focus as in input.nativeElement.focus().

I'm still finding that the ViewChild setter approach works best. When using AfterViewInit I sometimes get read property 'nativeElement' of undefined error.

@ViewChild("myInput")
set myInput(_input: ElementRef | undefined) {
    if (_input !== undefined) {
        setTimeout(() => { //This setTimeout call may not be necessary anymore.
            _input.nativeElement.focus();
        }, 0);
    }
}
KTCO
  • 2,115
  • 23
  • 21