As you might already know, javascript is turn based, that means that browsers will execute JS code in turns (cycles). Currently the way to prepare a callback in the next javascript cycle is by setting a callback with the code we want to run on that next cycle in a timeout, we can do that by calling setTimeout
with an interval of 0
miliseconds, that will force the given callback to be called in the next javascript turn, after the browser finishes (gets free from) the current one.
Trying to keep it simple, one browser cycle executes these actions in the given order:
- Scripting (where JS turn happen)
- Rendering (HTML and DOM renderization)
- Painting (Painting the rendered DOM in the window)
- Other (internal browser's stuff)
Take a look at this example:
console.log(1);
console.log(2);
setTimeout(function () {
console.log(3);
console.log(4);
}, 0);
console.log(5);
console.log(6);
/** prints in the console
* 1 - in the current JS turn
* 2 - in the current JS turn
* 5 - in the current JS turn
* 6 - in the current JS turn
* 3 - in the next JS turn
* 4 - in the next JS turn
**/
3 and 4 are printed after 5 and 6, even knowing that there is no interval
(0
) in the setTimeout
, because setTimeout
basically prepares the given callback to be called only after the current javascript turn finishes. If in the next turn, the difference between the current time and the time the callback was binded with the setTimeout
instruction is lower than the time interval, passed in the setTimeout
, the callback will not be called and it will wait for the next turn, the process repeats until the time interval is lower than that difference, only then the callback is called!
Since AngularJS is a framework wrapping all our code, angular updates generally occur after our code execution, in the end of each javascript turn, that means that angular changes to the HTML will only occur after the current javascript turn finishes.
AngularJS also has a timeout service built in, it's called $timeout
, the difference between the native setTimeout
and angular's $timeout
service is that the last is a service function, that happens to call the native setTimeout
with an angular's internal callback, this callback in its turn, is responsible to execute the callback we passed in $timeout
and then ensure that any changes we made in the $scope
will be reflected elsewhere! However, since in our case we don't actually want to update the $scope
, we don't need to use this service, a simple setTimeout
happens to be more efficient!
Knowing all this information, we can use a setTimeout
to solve our problem. like this:
$scope.createInputKeypress = function(e){
if(e.keyCode === 13){
e.preventDefault();
var idx = Number(e.target.id.replace("q_", ""));
if(idx === this.questions.length - 1){
this.addQuestion();
}
// Wait for the next javascript turn
setTimeout(function () {
var nextId = "#q_" + (++idx);
$(nextId).focus();
}, 0);
}
};
To make it more semantic, we can wrap the setTimeout
logic
in a function with a more contextualized name, like runAfterRender
:
function runAfterRender (callback) {
setTimeout(function () {
if (angular.isFunction(callback)) {
callback();
}
}, 0);
}
Now we can use this function to prepare code execution in the next javascript turn:
app.controller('CreatePollController', function($scope) {
// functions
function runAfterRender (callback) {
setTimeout(function () {
if (angular.isFunction(callback)) {
callback();
}
}, 0);
}
// $scope
$scope.questions = [];
$scope.init = function(numOfInputs){
for(var i = 0; i < numOfInputs; i++){
$scope.questions.push({
"questionText":""
});
}
};
$scope.addQuestion = function(){
$scope.questions.push({
"questionText":""
});
};
$scope.createInputKeypress = function(e){
if(e.keyCode === 13){
e.preventDefault();
var idx = Number(e.target.id.replace("q_", ""));
if(idx === this.questions.length - 1){
this.addQuestion();
}
runAfterRender(function () {
var nextId = "#q_" + (++idx);
$(nextId).focus();
});
}
};
});