Reputation: 363
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.
Upvotes: 2
Views: 7568
Reputation: 27738
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,
createAndFocusIn(songs)
. 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,
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
/>
Upvotes: 6
Reputation: 16341
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.
Upvotes: 1