Фокус на вновь добавленном элементе ввода

У меня есть новое приложение Angular 2 со списком полей input. Когда пользователь нажимает клавишу возврата, я добавляю новое поле input сразу после того, которое они редактируют в данный момент. Вернее, я (асинхронно) добавляю новую запись в массив в модели, что заставляет Angular 2 автоматически генерировать новое поле input в ближайшем будущем.

Как сделать так, чтобы фокус input автоматически изменялся на добавленный элемент?

РЕДАКТИРОВАТЬ 1:
В качестве альтернативы я получаю ссылку на объект модели, который вызывает генерацию DOM. Из кода компонента есть ли способ поиска элемента DOM, представляющего конкретный объект модели?

РЕДАКТИРОВАТЬ 2:
Вот мой код, чтобы просто сделать эту работу. Надеюсь, это достаточно оскорбительно для некоторых разработчиков Angular 2, чтобы поощрить ответ :-)

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);
                });
            }
        }
    });

Ответ 1

Если мне будет позволено это сделать, я приму участие в ответе @Sasxa и внесу в него изменения, чтобы сделать его более похожим на то, что вы ищете.

Несколько изменений

  • Я буду использовать ngFor поэтому angular2 добавляет новый ввод вместо того, чтобы делать это самому. Основная цель - сделать так, чтобы angular2 итерировал по нему.
  • Вместо ViewChild я собираюсь использовать ViewChildren который возвращает QueryList, у которого есть свойство changes. Это свойство является Наблюдаемым и возвращает элементы после их изменения.

Поскольку в ES5 у нас нет декораторов, мы должны использовать свойство queries чтобы использовать ViewChildren

Составная часть

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')
    }
})

Сосредоточиться на последнем элементе.

ngAfterViewInit: function() {

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

}

Как я уже говорил ранее, ViewChildren возвращает QueryList, который содержит свойство changes. Когда мы подписываемся на него каждый раз, когда он меняется, он возвращает список элементов. elements списка содержат last свойство (среди прочего), которое в этом случае возвращает последний элемент, мы используем его nativeElement и, наконец, focus()

Добавление элементов ввода Это просто для удобства, массив входов не имеет реальной цели, кроме перерисовки ngFor.

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

Мы помещаем фиктивный элемент в массив, чтобы он перерисовывался.

Пример использования ES5: http://plnkr.co/edit/DvtkfiTjmACVhn5tHGex

Пример использования ES6/TS: http://plnkr.co/edit/93TgbzfPCTxwvgQru2d0?p=preview

Обновление 29/03/2016

Время прошло, все прояснилось, и всегда есть лучшие практики для изучения/преподавания. Я упростил этот ответ, изменив несколько вещей

  • Вместо использования @ViewChildren и подписки на него я создал директиву, которая будет утверждаться каждый раз при создании нового ввода
  • Я использую Renderer чтобы сделать его безопасным. Оригинальный ответ обращается к focus() непосредственно к nativeElement который не рекомендуется.
  • Теперь я слушаю keydown.enter который упрощает событие нажатия клавиш, мне не нужно проверять, which значение.

К точке. Компонент выглядит так (упрощенно, полный код приведен ниже)

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

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

И директива

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

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

Как вы можете видеть, я invokeElementMethod чтобы вызвать focus на элементе вместо прямого доступа к нему.

Эта версия намного чище и безопаснее, чем оригинальная.

plnkrs обновлен до бета 12

Пример использования ES5: http://plnkr.co/edit/EpoJvff8KtwXRnXZJ4Rr

Пример использования ES6/TS: http://plnkr.co/edit/em18uMUxD84Z3CK53RRe

Обновление 2018

invokeElementMethod устарел. Используйте Renderer2 вместо Renderer.

Дайте вашему элементу идентификатор, и вы можете использовать selectRootElement:

this.renderer2.selectRootElement('#myInput').focus();

Ответ 2

Взгляните на ViewChild, вот пример. Это может быть то, что вы ищете:

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();
  }
}

Ответ 3

С встроенным angular кодом, фокус после условной краски:

  <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>

Ответ 4

Вы можете реализовать простую текстовую директиву ввода, чтобы при создании нового ввода он автоматически фокусировался. Метод focus() вызывается внутри ngAfterViewInit() жизненного цикла компонента ngAfterViewInit() после полной инициализации представления.

@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;
      }
    }
}

Используйте директиву FocusInput в вашем компоненте:

@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());
    }
}

Обратите внимание на следующее:

  1. (keyup.enter) используется для обнаружения нажатия клавиши <enter>
  2. ngFor используется для повторения элемента ввода для каждого слова из массива words
  3. last является логическим значением, связанным с локальной переменной, которая имеет значение true, когда вход является последним
  4. Событие keyup связано с last? addNewWord(): 0 выражением last? addNewWord(): 0 last? addNewWord(): 0. Это гарантирует, что новое поле ввода будет добавлено только при нажатии клавиши <enter> с последнего ввода

Демо Плнкр

Ответ 5

Чтобы определить приоритет, какой элемент получит фокус при инициализации нескольких директив в одном цикле, используйте:

Директива

@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', []);
    }
  }
}

Применение:

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

https://plnkr.co/edit/T9VDPIWrVSZ6MpXCdlXF