Focus on newly added input element
Asked Answered
K

5

80

I have a new Angular 2 app with a list of input boxes. When the user hits the return key, I add a new input box immediately after the one they're currently editing. Or rather, I (asynchronously) add a new entry to the array in the model, which causes Angular 2 to automatically generate a new input box in the near future.

How can I make it so that input focus changes to the newly added element automatically?

EDIT 1:
Alternatively, I get a reference to the model object that's causing the DOM to be generated. From the component code, is there a way to search for a DOM element representing a particular model object?

EDIT 2:
Here's my code to just make this work. Hopefully this is offensive enough to some Angular 2 devs to encourage a reply :-)

app.WordComponent = ng.core
    .Component({
        selector: 'word-editor',
        template:'<input type="text" [value]="word.word" (input)="word.update($event.target.value)" (keydown)="keydown($event)"/>',
        styles:[
            ''
        ],
        properties:[
            'list:list',
            'word:word'
        ]
    })
    .Class({
        constructor:[
            function() {
            }
        ],
        keydown:function(e) {
            if(e.which == 13) {
                var ul = e.target.parentNode.parentNode.parentNode;
                var childCount = ul.childNodes.length;

                this.list.addWord("").then(function(word) {
                    var interval = setInterval(function() {
                        if(childCount < ul.childNodes.length) {
                            ul.lastChild.querySelector('input').focus();
                            clearInterval(interval);
                        }
                    }, 1);
                });
            }
        }
    });
Kilohertz answered 30/12, 2015 at 2:56 Comment(3)
Your setInterval should most likely just be a setTimeout.Glochidiate
Whoever visiting this answer, have a look at this. github.com/spirosikmd/angular2-focus Quite easy to use. This does it all easily if you're using *ngForBrainwashing
So easy you need an external lib ??Sommers
D
104

If I'm allowed to do so, I will take part of @Sasxa answer and modify it to make it more like what you're looking for.

A few changes

  • I will use a ngFor so angular2 adds the new input instead of doing it myself. The main purpose is just to make angular2 to iterate over it.
  • Instead of ViewChild I'm going to use ViewChildren that returns a QueryList which has a changes property. This property is an Observable and it returns the elements after they've changed.

Since in ES5, we have no decorators we have to use the queries property to use ViewChildren

Component

Component({
    selector: 'cmp',
    template : `
        <div>
            // We use a variable so we can query the items with it
            <input #input (keydown)="add($event)" *ngFor="#input of inputs">
        </div>
    `,
    queries : {
        vc : new ng.core.ViewChildren('input')
    }
})

Focus on the last element.

ngAfterViewInit: function() {

    this.vc.changes.subscribe(elements => {
        elements.last.nativeElement.focus();
    });

}

Like I said before, ViewChildren returns a QueryList which contains changes property. When we subscribe to it everytime it changes it will return the list of elements. The list elements contains a last property (among others) that in this case returns the last element, we use nativeElement on it and finally focus()

Add input elements This is for pure convenince, the inputs array has no real purpose more than redraw the ngFor.

add: function(key) {
    if(key.which == 13) {
        // See plnkr for "this.inputs" usage
        this.inputs.push(this.inputs.length+1);
    }
}

We push a dummy item on the array so it redraws.

Example using ES5 : http://plnkr.co/edit/DvtkfiTjmACVhn5tHGex

Example using ES6/TS : http://plnkr.co/edit/93TgbzfPCTxwvgQru2d0?p=preview

Update 29/03/2016

Time has passed, things have been clarified and there are always best practices to learn/teach. I've simplified this answer by changing a few things

  • Instead of using @ViewChildren and subscribing to it I made a Directive that will be instatiated everytime a new input is created
  • I'm using Renderer to make it WebWorker safe. The original answer accesses focus() directly on the nativeElement which is discouraged.
  • Now I listen to keydown.enter which simplifies the key down event, I don't have to check which value.

To the point. The component looks like (simplified, full code on the plnkrs below)

@Component({
  template: `<input (keydown.enter)="add()" *ngFor="#input of inputs">`,
})

add() {
    this.inputs.push(this.inputs.length+1);
}

And the directive

@Directive({
  selector : 'input'
})
class MyInput {
  constructor(public renderer: Renderer, public elementRef: ElementRef) {}

  ngOnInit() {
    this.renderer.invokeElementMethod(
      this.elementRef.nativeElement, 'focus', []);
  }
}

As you can see I'm calling invokeElementMethod to trigger focus on the element instead of accessing it directly.

This version is much cleaner and safer than the original one.

plnkrs updated to beta 12

Example using ES5 : http://plnkr.co/edit/EpoJvff8KtwXRnXZJ4Rr

Example using ES6/TS : http://plnkr.co/edit/em18uMUxD84Z3CK53RRe

Update 2018

invokeElementMethod is deprecated. Use Renderer2 instead of Renderer.

Give your element an id, and you can use selectRootElement:

this.renderer2.selectRootElement('#myInput').focus();
Dyscrasia answered 3/1, 2016 at 4:4 Comment(4)
Very elegant. I'm surprised that we can use the same local template variable name/identifier -- #input -- on multiple elements.Foist
I would rename the ngFor local template variable something else, so that it doesn't look like it is related to the other #input: *ngFor="#in of inputs".Foist
Instead of adding an event handler to every input, I suggest putting it on the div and letting the event bubble up: <div (keydown)="add($event)"> <input #input *ngFor="#in of inputs">.Foist
For the 2018 update, instead of using an ID, I found this to work: this.renderer2.selectRootElement(this.elementRef.nativeElement).focus();Eugenioeugenius
P
42

Take a look at ViewChild, here's a example. This might be what you're looking for:

import {Component, ViewChild} from 'angular2/core'

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <div>
      <input #name>
    </div>
  `,
  directives: []
})
export class App {

  @ViewChild('name') vc: ElementRef;

  ngAfterViewInit() {
    this.vc.nativeElement.focus();
  }
}
Peridot answered 3/1, 2016 at 0:17 Comment(2)
still good in Angular 6 and omitting "ElementRef" ViewChild type declaration not a problem or for the vscode linterEuler
The only code that worked for me, thanks.Sperling
K
27

With inline angular code, focus after conditional paint:

  <span *ngIf="editId==item.id">
    <input #taskEditText type="text" [(ngModel)]="editTask" (keydown.enter)="save()" (keydown.esc)="saveCancel()"/>
    <button (click)="save()">Save</button>
    <button (click)="saveCancel()">Cancel</button>
    {{taskEditText.focus()}}
  </span>
Khedive answered 27/7, 2017 at 9:54 Comment(0)
M
9

You can implement a simple input text directive, so that whenever a new input is created, it will auto focus itself. The focus() method is called inside of the ngAfterViewInit() component lifecycle hook after the view is fully initialized.

@Directive({
    selector: 'input[type=text]'
})
export class FocusInput implements AfterViewInit {
    private firstTime: bool = true;
    constructor(public elem: ElementRef) {
    }

    ngAfterViewInit() {
      if (this.firstTime) {
        this.elem.nativeElement.focus();
        this.firstTime = false;
      }
    }
}

Use the FocusInput directive in your component:

@Component({
    selector: 'app',
    directives: [FocusInput],
    template: `<input type="text" (keyup.enter)="last ? addNewWord():0" 
                *ngFor="#word of words; #last = last" [value]="word.word" 
                #txt (input)="word.word = txt.value" />{{words |json}}`
})
export class AppComponent {
    words: Word[] = [];
    constructor() {
        this.addNewWord();
    }
    addNewWord() {
        this.words.push(new Word());
    }
}

Note the following:

  1. The (keyup.enter) event is used to detect when the <enter> key is pressed
  2. ngFor is used to repeat the input element for each word from the array of words
  3. last is a Boolean bound to a local variable which is true when the input is the last one
  4. The keyup event is bound to the expression last ? addNewWord() : 0. This ensures that a new input field is only added when the <enter> key is pressed from the last Input

Demo Plnkr

Mongeau answered 3/1, 2016 at 10:58 Comment(2)
Very nice, after trying out all the above answers this one worked for me! Anguler 5 Thanks!!Rimmer
This is the most solid solution in my opinionScissure
A
3

In order to prioritize which element will get focus when initializing several directives in same cycle, use:

Directive:

@Directive({
  selector: '[focusOnInit]'
})
export class FocusOnInitDirective implements OnInit, AfterViewInit {
  @Input('focusOnInit') priority: number = 0;

  static instances: FocusOnInitDirective[] = [];

  constructor(public renderer: Renderer, public elementRef: ElementRef) {
  }

  ngOnInit(): void {
    FocusOnInitDirective.instances.push(this)
  }

  ngAfterViewInit(): void {
    setTimeout(() => {
      FocusOnInitDirective.instances.splice(FocusOnInitDirective.instances.indexOf(this), 1);
    });

    if (FocusOnInitDirective.instances.every((i) => this.priority >= i.priority)) {
      this.renderer.invokeElementMethod(
        this.elementRef.nativeElement, 'focus', []);
    }
  }
}

Usage:

<input type="text" focusOnInit="9">

https://plnkr.co/edit/T9VDPIWrVSZ6MpXCdlXF

Apparent answered 9/1, 2017 at 22:22 Comment(1)
Wouldn't it be better to use tabindex instead?Propitiatory

© 2022 - 2024 — McMap. All rights reserved.