Pregunta

I was developing a todo-like application with jQuery but I'd like to switch to Angular.

There is an input field for adding a new item, but as soon as anything is typed into this input, the key stroke -and those following it -are effectively moved to a new item among the group of existing items. This means that the new item input text box always remains empty. Better to show than explain:

http://jsfiddle.net/29Z3U/4/ (Many details removed, but the aspect of the app to which I'm referring is demonstrated).

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

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

Note that the new item input text box always remains empty and the focus behaves as it should so you can continue typing without interruption.

The application code is getting lengthy and I've not even yet attempted to display an existing list of songs derived from JSON. Of course in Angular, ngRepeat makes this easy. However, my attempt at an Angular version doesn't work: 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 = '';
    };
});

Before even tackling the problem of focus management, I notice that the updating of Angular's model always lags behind by one keystroke. I assume this is because the keypress event happens before the character is inserted into the DOM.

I realise that switching from keypress to keyup would change things but the original design was based on the responsiveness of keypress.

I tried a custom directive to bind to the input event but things don't behave as I'd hoped: http://plnkr.co/edit/yjdbG6HcS3ApMo1T290r?p=preview

I notice that the first code example at angularjs.org (basic binding with no controller) doesn't seem to suffer from the issue I have -the example model is updated before a key is released.

¿Fue útil?

Solución

solved:) btw. neat component! here is the plunkr: http://plnkr.co/edit/wYsFRUcqTZFv5uIE0MWe?p=preview

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

I have changed the ng-keypress function, so that a new song is completely created in the controller. Also not the new directive focus-me - if the current song is the new created song, the input field gets the focus.

the controller:

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

As you can see the creation of the new song is wrapped by a $timeout call. This call delayed the execution until the next digest cycle happens, so no pending events can interrupt us.

finally the directive:

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

for sure generic, so that every expression can trigger the focus.

Otros consejos

Although there is an accepted answer, I present a different approach, which is a lot simpler with less tainted controller.

First, controller. it's just simple.

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":""}
    ];
}); 

Second, tag part

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

To explain,

  1. when a key is pressed, it create a song and focus in songs list, createAndFocusIn(songs).
  2. create-and-focus-in directive provides scope.createAndFocusIn(), so that controller does not have to have this function unless this directive is used. It accepts the selector, where to create new element, in this case, #songs input

Last but most importantly, directive part:

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

in directive, it isn't doing much else except that is specified by attributes.

That's it.

This is working demo: http://plnkr.co/edit/mF15utNE9Kosw9FHwnB2?p=preview

---- EDIT ---- @KnewB said it does not work in FF. Chrome/FF working version here.

In FF,

  1. window.event is not set when key pressed. we need to pass as parameter
  2. event.keyCode does not exist, we need to check both keyCode || charCode
  3. value and focus makes cursor to the first position, so needed to set focus first then add value

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

and $event now passed:

<input ng-keypress="createAndFocusIn($event, songs)" 
    create-and-focus-in="#songs input"
    placeholder="enter song" 
    autofocus 
/>
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top