I'm experimenting with javascript and MVC models. I want to (simplified example) move an object across the screen a random number of pixels between 1 and 10 and then have it stop when it gets to, say, 400 pixels.
The view is set up to observe the model, which has a notifyObservers() function.
When the start button on the view is clicked it sends a startButtonClicked message to the controller.
controller.startButtonClicked = function () {
var animate = function () {
controller.getModel().shift(); // get the model and run the shift() function
setTimeout(animate, 20);
};
animate();
}
This runs the model's shift() function:
model.shift = function () {
if(model.x < 400) {
model.x += Math.floor(Math.random()*11); // Add up to 10 pixels
}
model.notifyObservers(); // Tells view to update,
};
This works fine, and the object stops at around 400 pixels as it should. However, the setTimeout loop in controller.startButtonClicked() is still whirring away.
[Edit: As I understand it, the traditional MVC model doesn't allow the model to communicate with the controller directly, so the model can't just tell the controller to stop the timer.]
So, finally to the question: How do I make the loop in the controller stop?
The possible solutions I've thought of:
Get the model to tell the view, which then tells the controller. But that seems very long-winded.
Get the controller to ask the model if it's done. But that seems to go against the MVC structure.
Get the shift() function to return false to the controller when it's done.
Anyone who's been doing MVC for a while know what the right way of doing it would be?
Thanks!
Something like this:
var t; // feel free to make this a non global variable
controller.startButtonClicked = function () {
var animate = function () {
controller.getModel().shift(); // get the model and run the shift() function
t = setTimeout(animate, 20);
};
animate();
}
model.shift = function () {
if(model.x < 400) {
model.x += Math.floor(Math.random()*11); // Add up to 10 pixels
}
else {
clearTimeout(t);
}
model.notifyObservers(); // Tells view to update,
};
You need to use clearTimeout(arg) where arg is the return value from a setTimeout call.
Also, be careful with low (< 50) values for setTimeout(), what you have coded calls animate 50 times per second.
controller.startButtonClicked = function () {
var animate = function () {
var m = controller.getModel();
m.shift(controller); // get the model and run the shift()
m.timerInterval = setTimeout(animate, 20);
};
animate();
}
model.shift = function () {
if(model.x < 400) {
model.x += Math.floor(Math.random()*11); // Add up to 10 pixels
}
else if (model.timerInterval)
{
clearTimeout(model.timerInterval);
}
model.notifyObservers(); // Tells view to update,
};
Related
I have a form with a dynamic number of inputs, controlled by AngularJS.
<body ng-app="mainApp" ng-controller="CreatePollController" ng-init="init(3)">
<form id="createPollForm">
<input class="create-input" ng-repeat="n in questions" id="q_{{$index}}" name="q_{{$index}}" type="text" ng-keypress="createInputKeypress($event);"/>
Add Question
</form>
</body>
This is being controlled by the following angular code:
app.controller('CreatePollController', function($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();
}
// Wait for angular update ????
var nextId = "#q_" + (++idx);
$(nextId).focus();
}
};
});
Currently, when the user hits the Enter key while focused on a text input, the createInputKeypress function is called and the browser focuses the next input in the form. However, if you are currently focused on the last element in the form, it adds a new question to the questions array, which will cause another input to be generated in the DOM.
However, when this new element is created, the focus() call isn't working. I suspect this is because angular doesn't add the new element right away, so trying to use jQuery to locate and focus the new element isn't working.
Is there a way to wait for the DOM to be updated, and THEN focus the new element?
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();
});
}
};
});
I have a function triggerWave() which makes the points on the canvas animate in the wave form. I am using d3.ease('quad-in') for easing and I would like to use d3.timer() to make the triggerWave() function call over 200ms timeframe. I am out of luck in finding the tutorials or examples on d3.timer.
triggerWave() {
//function logic
let count = 0;
let xScale = d3.scale.linear().range([1,2]); // want the value to change from 1 to 2.
let zScale = d3.scale.linear().domain([0, 200]); // 200 ms.
let value = xScale(d3.ease('quad-in')(zScale(count)));
if(count < 200){
count++;
d3.timer(() => triggerWave());
} else {
// do something
}
this.wave.next({currentFrame: value});
}
When I call d3.timer() as above, the triggerWave() function gets called infinite times and never stops. I want to manipulate or control the time. In my case, I want the timer() to be triggered for 200ms.
How can I understand how to use the d3.timer() function?
(EDIT: I totally and completely missed the huge, big "V3" which is right there, in the title of the question. Sorry. I'll keep this answer here as reference for v4 users)
Since you are calling triggerWave inside the triggerWave function itself, you don't need d3.timer, but d3.timeout instead. According to the API, d3.timeout:
Like timer, except the timer automatically stops on its first callback. A suitable replacement for setTimeout that is guaranteed to not run in the background. The callback is passed the elapsed time.
Also, pay attention to the fact that you are reseting count every time the function runs, which will not work. Set its initial value outside the function.
Here is a demo with those changes. I'm calling the function every 200 ms, until count gets to 50:
var p = d3.select("p")
var count = 0;
triggerWave();
function triggerWave() {
p.html("Count is " + count)
if (count < 50) {
count++;
d3.timeout(triggerWave, 200)
} else {
return
}
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<p></p>
You can also keep track of the total elapsed time, using the argument passed to triggerWave by d3.timeout:
var p = d3.select("p")
var count = 0;
var elapsed = 0;
var format = d3.format(".2")
triggerWave();
function triggerWave(t) {
elapsed = t ? elapsed + t : elapsed;
p.html("Count is " + count + ", and the elapsed time is " + format(elapsed/1000) + " seconds")
if (count < 50) {
count++;
d3.timeout(triggerWave, 200)
} else {
return
}
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<p></p>
Since you are using D3 v3, and as there is no d3.timeout in v3, you can do the same approach using vanilla JavaScript: setTimeout.
Here is a demo:
var p = d3.select("p")
var count = 0;
triggerWave();
function triggerWave() {
p.html("Count is " + count)
if (count < 50) {
count++;
setTimeout(triggerWave, 200)
} else {
return
}
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<p></p>
In version 3, there is no d3.timer.stop() function. You have to return true after a certain period of time to stop the timer.
In Gerardo's answer, he explained fabulously how to use the d3 timeout which would be a valid solution to your problem i.e., to call the function over and over for a certain period of time. But looking at your comments on Gerardo's answer, I think you are looking for something else.
Here's what I came up with and I think this is what you are looking for:
You can create an another function called as activateTriggerWave() which will be invoked on the button click and inside this function, you can call your triggerWave() method using the d3 timer.
function activateTriggerWave() {
d3.timer(elapsed => {
this.triggerWave();
if(elapsed >= 200){
return true; // this will stop the d3 timer.
}
});
}
triggerWave() {
// here you can do whatever logic you want to implement.
}
I hope this helps.
I use d3.js v3, and the timer can be stopped by any user action. In the d3.js docs it is shown to use it as:
d3.timer(function(elapsed) {
console.log(elapsed);
return elapsed >= 1000;
});
I have few examples in which the animation is forever and there is no reason to set a limit on it. Checking the standalone d3.timer which comes with a stop(), I found that it behaves quite slow comparing it with the default timer included in the v3 toolset, probably for some version incompatibility.
The solution is use is to:
var timer_1_stop=false;
set it as global var accesible from the page scope. Then the timer:
const run=function(){
//...
d3.timer(function() {
voronoi = d3.geom.voronoi(points).map(function(cell) { return bounds.clip(cell); });
path.attr("d", function(point, i) { return line(resample(voronoi[i])); });
return timer_1_stop;
});
}
const stopVoro=function(){
timer_1_stop=true;
}
It allows to do:
class Menu extends React.Component {
render(){
return(<ul>
<li><span onClick={()=>{stopVoro()}}>StopVoro</span></li>
</ul>)
}
}
I have a game in which two people play against each other. After the clock runs down I call the function below, which is supposed to increase the current question by 1. However, it increases it by 1 TWICE.
increaseQuestion: function() {
GameCollection.update({current:true}, { $inc: { currentQuestion: 1}});
},
Here is specifically the code where it is called:
Template.gamePage.clock = function () {
var game = GameCollection.findOne({current: true});
var currentQuestion = game.currentQuestion;
var question = game.gameQuestions[currentQuestion];
var clockQuestion = Clocks.findOne({gameId: game._id, questionId: question._id});
var clock = clockQuestion.clock;
if(clock === 0) {
Meteor.call('increaseQuestion');
} else {
Meteor.call('windDown', clockQuestion, clock);
}
// format into M:SS
var min = Math.floor(clock / 60);
var sec = clock % 60;
return min + ':' + (sec < 10 ? ('0' + sec) : sec);
};
Here is the method within the code above (which could be causing problems)
Meteor.methods({
windDown: function(clockQuestion, clock) {
var interval = Meteor.setInterval(function () {
clock -= 1;
Clocks.update(clockQuestion._id, {$set: {clock: clock}});
// end of game
if (clock === 0) {
// stop the clock
Meteor.clearInterval(interval);
// declare zero or more winners
}
}, 1000);
}
});
Why is the function being called twice? I tried moving the method from both client and server folder to a server only folder and it is still called twice.
I'm surprised it's only being called twice, to be honest.
The clock helper has a dependency on the ClockQuestion document which has the same gameId as the current game. However, when that helper is run, the windDown method is called, which is going to update that ClockQuestion document, which is going to cause the helper to be reactively rerun, which is going to call the windDown method again, etc...
To be honest, this sort of control logic really should not be included in a helper function for exactly this reason - it's going to be impossible to control. windDown needs to be called by whatever it is that's making the gamePage template render in the first place, or else a Deps.autorun block, the reactivity of which you can have much more control over, as opposed to a reactive UI element which is (by design) being managed by Meteor rather than your own app explicitly.
My suspicion is this: Template.gamePage.clock uses GameCollection, so when that collection is updated, then the function will be re-run reactively.
Since increaseQuestion doesn't depend on any arguments from the client, why not just move it to the // end of game if block in windDown?
I'm sure this must be a common problem, but after much searching I can't find an answer.
I have a twitter-bootstrap loading bar that I would like to update after each stage of a calculation is completed.
Here is the function for updating the loading bar:
var lb = $('#loading-bar');
var lbc = 0;
function increment_loading_bar(pc) {
setTimeout(function(){
lbc = lbc + pc;
lb.width(lbc+"%");;
}, 1);
}
And the calls to update the bar are within a .each() loop
var inc = 100/array.length();
$.each(array,function(index,element){
increment_loading_bar(inc/2);
//
//Gnarly processing ....
//
increment_loading_bar(inc/2);
}
However, this only updates after all the processing has finished. How can the redraw of the bar be forced as the code is executed?
Many thanks!
As I said in my question, the /redraw/ needs to be forced as the code is executed
To my knowledge, you can only indirectly force the Redraw by pausing your Process once in a while.
For example like this:
var inc = 100/array.length();
var processQueue = array;
var currentIndex;
setTimeout(runProcess, 5);
function runProcess() {
var element = processQueue[currentIndex];
// Process the element here
// ....
increment_loading_bar(inc);
currentIndex++;
if (currentIndex < processQueue.length) {
setTimeout(runProcess, 5);
} else {
// processing has finished
}
}
This way you give the browser some time (5ms in this example) between each step to redraw the loading bar.
I want to have multiple numbers on my web page "spin up" as the page loads, giving an impression like a fruit machine.
This involves a simple function with a delayed loop. The way to do this seems to be to use setTimeout recursively. This works fine for just one number on the page.
However for multiple numbers spinning at the same time, each needs its own spinner object. I used prototypes like this:
var Spinner = function(id){
this.element = $('#' + id);
this.target_value = this.element.text()
this.initial_value = this.target_value - 30;
};
Spinner.prototype.spinUp = function() {
loop(this.element);
function loop(element) {
element.html(this.initial_value += 1);
if (this.initial_value == this.target_value) {
return;
};
clr = setTimeout(loop(element), 30); // 30 millisecond delay
};
};
var Spinner1 = new Spinner('number')
Spinner1.spinUp();
However putting a recursive function inside the prototype method causes a big crash. Can you see a way around this?
Many thanks!
Derek.
A couple of issues:
loop() is not how you pass a function, it's how you invoke a function.
You are not calling the function as a method of the object
Try this:
Spinner.prototype.spinUp = function() {
var loop = function() {
this.element.html(this.initial_value += 1);
if (this.initial_value == this.target_value) {
return;
};
setTimeout(loop, 30); // 30 millisecond delay
}.bind(this); //Just flat out bind the function to this instance so we don't need to worry about it
loop();
};
Demo http://jsfiddle.net/KAZpJ/
When you say:
clr = setTimeout(loop(element), 30);
you are "calling" the function (then and there), and passing the value it returns as the first parameter to setTimeout(..).
You would want an anonymous function doing that job:
setTimeout(function(){loop(element);}, 30);