Можно ли обновить модель после нажатия клавиши, но до ее нажатия?

Я разрабатывал todo-приложение с jQuery, но хотел бы перейти на Angular.

Существует поле ввода для добавления нового элемента, но как только что-либо вводится в этот ввод, нажатие клавиши и последующие за ним эффективно перемещаются к новому элементу среди группы существующих элементов. Это означает, что текстовое поле ввода нового элемента всегда остается пустым. Лучше показать, чем объяснять:

http://jsfiddle.net/29Z3U/4/ (многие детали удалены, но аспект приложение, о котором я говорю, демонстрируется).

<h1>Song List</h1>
<form id="songs">
    <ul id="sortable_songs"></ul>
</form>

<ul id="new_song">
    <script id="song_form_template" type="text/x-handlebars-template">
        <li class="song" id="{{song_id}}">
            <input type="text" placeholder="enter song" autofocus />                        
        </li>
    </script>
</ul>

Немного jQuery:

var template = Handlebars.compile($('#song_form_template').html()),

    counter = (function(){var i=0; return function(){return ++i};})(),

    cloneNewSong = function(){
        var count = counter(),
            templateVals = {song_id: 'song_' + count};
        $('ul#new_song').append(template(templateVals));
    },

    addSong = function(event){
        //exclude certain keys here
        cloneNewSong();
        container = $(event.target).closest('li.song');
        container.appendTo('ul#sortable_songs');
        $(event.target)
            .removeAttr('placeholder')
            .focus(); //what I just typed disappears without this! why?
    };

$('ul#new_song').on('keypress', 'input', addSong);
cloneNewSong();

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

Код приложения становится длинным, и я даже еще не пытался отобразить существующий список песен, полученный из JSON. Конечно, в Angular это легко сделать с помощью ngRepeat. Однако моя попытка создать версию для Angular не работает: http://plnkr.co/edit/xsGRiHFzfsVE7qRxgY8d?p=preview

<!DOCTYPE html>
<html ng-app="songListApp">
  <head>
    <script src="//code.angularjs.org/1.2.7/angular.js"></script>
    <link href="style.css" rel="stylesheet" />
    <script src="script.js"></script>
  </head>

  <body ng-controller="songListController">
    <h1>Song List</h1>

    <ul id="songs">
        <li ng-repeat="song in songs">
            <input type="text" ng-model="song.song_name" />
        </li>
    </ul>

    <form>
        <input 
          ng-keypress="newSong(new_song)" 
          ng-model="new_song.title" 
          placeholder="enter song" autofocus 
        />
    </form>

  </body>
</html>

JS:

var myapp = angular.module('songListApp', []);

myapp.controller('songListController', function($scope){
    songs = [
        {"song_name":"song 1","more_info":""},
        {"song_name":"song 2","more_info":""},
        {"song_name":"song 3","more_info":""}
    ];
    $scope.songs = songs;

    $scope.newSong = function(new_song){
        var song = {"song_name": new_song.title, "more_info":""};
        songs.push(song);
        new_song.title = '';
    };
});

Еще до того, как заняться проблемой управления фокусом, я замечаю, что обновление модели Angular всегда отстает на одно нажатие клавиши. Я предполагаю, что это связано с тем, что событие нажатия клавиши происходит перед символом вставляется в DOM.

Я понимаю, что переключение с нажатия клавиши на нажатие клавиши изменит ситуацию, но первоначальный дизайн был основан на отзывчивости нажатия клавиши.

Я попробовал пользовательскую директиву для привязки к событию ввода, но все работает не так, как я надеялся: http://plnkr.co/edit/yjdbG6HcS3ApMo1T290r?p=preview

Я заметил, что первый пример кода на angularjs.org (базовая привязка без контроллера), похоже, не страдает от проблемы, с которой я столкнулся: модель примера обновляется до выпуска ключа.


person KnewB    schedule 09.01.2014    source источник


Ответы (2)


решено :) кстати. классный компонент! вот планкр: http://plnkr.co/edit/wYsFRUcqTZFv5uIE0MWe?p=preview

HTML:

  <body ng-controller="songListController">
    <h1>Song List</h1>

    <ul id="songs">
        <li ng-repeat="song in songs">
            <input type="text" ng-model="song.song_name" focus-me="song === newSong"/>
        </li>
    </ul>

    <form>
        <input 
          ng-keypress="createNewSong()" 
          ng-model="newSongTitle" 
          placeholder="enter song" autofocus 
        />
    </form>

  </body>

Я изменил функцию ng-keypress, чтобы в контроллере полностью создавалась новая песня. Также не новая директива focus-me - если текущая песня является новой созданной песней, поле ввода получает фокус.

контроллер:

myapp.controller('songListController', function($scope, $timeout){
    $scope.songs = [
        {"song_name":"song 1","more_info":""},
        {"song_name":"song 2","more_info":""},
        {"song_name":"song 3","more_info":""}
    ];

    $scope.newSongTitle = '';
    $scope.newSong = null;

    $scope.createNewSong = function(){
        $timeout(function(){
          $scope.newSong = {"song_name": $scope.newSongTitle, "more_info":""};
          $scope.songs.push($scope.newSong);
          $scope.newSongTitle = '';
        });
    };
});

Как видите, создание новой песни завершается вызовом $timeout. Этот вызов отложил выполнение до следующего цикла дайджеста, поэтому никакие ожидающие события не могут нас прервать.

наконец директива:

myapp.directive('focusMe', function(){
  return function($scope, element, attr){
    if($scope.$eval(attr.focusMe)){
        element[0].focus();
    }
  };
});

наверняка общий, так что каждое выражение может вызвать фокус.

person michael    schedule 12.01.2014
comment
Я пробовал это в IE11 и Chrome 32; работает только для ввода первой песни - не могли бы вы еще раз посмотреть? - person KnewB; 13.01.2014
comment
@KnewB, часть которого не сработала. хром проверен. IE11 - у меня нет IE - person michael; 13.01.2014
comment
@KnewB извините. это была поздняя оптимизация перед публикацией этого. имя функции должно быть createNewSong, а не newSong. - person michael; 13.01.2014
comment
@KnewB решил - см. мой последний комментарий и новый plunkr - person michael; 13.01.2014
comment
Не могли бы вы уточнить, зачем нужен $timeout? Вы упомянули, что никакие ожидающие события не могут нас прервать, но можно ли прервать текущий обратный вызов? (Я пробовал без тайм-аута и, очевидно, это не работает, но я не понимаю, почему) - person KnewB; 17.01.2014
comment
@KnewB Нам нужно дождаться, пока angular завершит цикл дайджеста. после этого цикла мы можем быть уверены, что входное значение доступно в портере newSongTitle. С помощью $timout мы ждем ровно один цикл, и значение безопасно доступно в свойстве newSongTitle. - person michael; 17.01.2014

Хотя есть принятый ответ, я представляю другой подход, который намного проще с менее испорченным контроллером.

Во-первых, контроллер. это просто.

myapp.controller('songListController', function($scope, $timeout){
    $scope.songs = [
        {"song_name":"song 1","more_info":""},
        {"song_name":"song 2","more_info":""},
        {"song_name":"song 3","more_info":""}
    ];
}); 

Во-вторых, часть тега

<input ng-keypress="createAndFocusIn(songs)" 
  create-and-focus-in="#songs input"
  placeholder="enter song" 
  autofocus 
/>

Объяснить,

  1. при нажатии клавиши создается песня и фокусируется на списке песен, createAndFocusIn(songs).
  2. Директива create-and-focus-in обеспечивает scope.createAndFocusIn(), так что контроллер не должен иметь эту функцию, если только эта директива не используется. Он принимает селектор, где создать новый элемент, в данном случае #songs input

Последняя, ​​но самая важная, директивная часть:

myapp.directive('createAndFocusIn', function($timeout){
  return function(scope, element, attrs){
    scope.createAndFocusIn = function(collection) {
      collection.push({song_name: String.fromCharCode(event.keyCode)});
      $timeout(function() {
        element[0].value = '';
        var el = document.querySelectorAll(attrs.createAndFocusIn); 
        el[el.length-1].focus();
      });
    };
  };
});

в директиве он не делает ничего, кроме того, что указано в атрибутах.

Вот и все.

Это рабочая демонстрация: http://plnkr.co/edit/mF15utNE9Kosw9FHwnB2?p=preview

---- РЕДАКТИРОВАТЬ ---- @KnewB сказал, что это не работает в FF. Рабочая версия Chrome/FF здесь.

In FF,

  1. window.event не устанавливается при нажатии клавиши. нам нужно передать как параметр
  2. event.keyCode не существует, нам нужно проверить оба keyCode || charCode
  3. значение и фокус переводят курсор в первую позицию, поэтому необходимо сначала установить фокус, а затем добавить значение

http://plnkr.co/edit/u2RtHWyhis9koQfhQEdW?p=preview

myapp.directive('createAndFocusIn', function($timeout){
  return function(scope, element, attrs){
    scope.createAndFocusIn = function(ev, collection) {
      collection.push({});
      $timeout(function() {
        element[0].value = '';
        var el = document.querySelectorAll(attrs.createAndFocusIn);
        el[el.length-1].focus();
        el[el.length-1].value = String.fromCharCode(ev.keyCode||ev.charCode);
      });
    };
  };
});

и $event теперь прошло:

<input ng-keypress="createAndFocusIn($event, songs)" 
    create-and-focus-in="#songs input"
    placeholder="enter song" 
    autofocus 
/>
person allenhwkim    schedule 13.01.2014
comment
он отлично работает в Chrome 31, но не работает в FF 26 (Linux) - person KnewB; 17.01.2014
comment
Рабочая версия FF26/Chrome31, просто нужно передать $event в качестве параметра, plnkr.co/edit/u2RtHWyhis9koQfhQEdW ?p=предварительный просмотр - person allenhwkim; 17.01.2014
comment
Спасибо, что посмотрели снова. Боюсь, версия FF все еще не работает - первый символ нового названия всегда теряется. - person KnewB; 17.01.2014
comment
не знал, что FF использует charCode. developer.mozilla.org/en-US/docs/Web/ API/event.keyCode. обновлен plnkr.co/edit/u2RtHWyhis9koQfhQEdW?p=preview. - person allenhwkim; 17.01.2014