Scope of variable in DOJO when created from within function - javascript

In a DOJO widget there is code in the postCreate and destroy method to create/start and stop a timer like you can see below. Depending on the value in a drop down box the timer is started or stopped. This works fine so far.
postCreate: function() {
var deferred = this.own(<...some action...>)[0];
deferred.then(
lang.hitch(this, function(result) {
this.t = new dojox.timing.Timer(result.autoRefreshInterval * 1000);
this.t.onTick = lang.hitch(this, function() {
console.info("get new data");
});
this.t.onStart = function() {
console.info("starting timer");
};
this.t.onStop = function() {
console.info("timer stopped");
};
})
);
this.selectAutoRefresh.on("change", lang.hitch(this, function(value) {
if (value == "Automatic") {
this.t.start();
} else {
this.t.stop();
}
}));
},
When leaving the page the timer is still active so I want to stop it when I leave the page using DOJOs destroy() method.
destroy: function() {
this.t.stop();
},
This however throws a this.t.stop is not a function exception. It seems like this.t is not created in the context of the widget although I use lang.hitch(this...
What am I missing here?

I solved that by just renaming the variable t to refreshTimer. Maybe t is some kind of reserved variable in Dojo?

Related

How to stop calling a delayed function if some other event happens in that period?

After a certain event happens, I have this function (rightMenu) that triggers with 1.5s delay. However, my challenge is to figure out how to check if the leftMenu is called during this period then stop the setTimeout function to call the rightMenu.
var leftMenu = function() {
// some code here
}
var rightMenu = function() {
// some code here
}
$('#leftMenu').on('click', function() {
leftMenu();
})
$('#rightMenu').on('click', function() {
rightMenu();
})
if (...) {
leftMenu();
} else {
setTimeout(function() {
rightMenu();
}, 1500);
}
I'm not 100% sure if this is what you're asking, but you can clear intervals. For example:
function asyncFun() {
// some code
}
setTimeout(asyncFun, 5000); // this will run after 5 seconds
var asyncHandle = setTimeout(asyncFun, 10000);
clearTimeout(asyncHandle); // this cancels the function call
I feel like this is what you're asking...
If not, the other interpretation that I have is you want to temporarily remove the event handler from the #leftmenu and #rightmenu when you're in the other menu. To do this, you can clear event handlers in jQuery with $("#rightmenu").off("click"). This function is basically the opposite of .on. See here. Good luck!
Yet another possible fix to your code:
/*
initialize your variable here. Technically doesn't change anything
because of hoisting, but I'm guessing based on your question you
haven't learned that yet.
*/
var callingRight;
var leftMenu = function() {
// some code here
}
var rightMenu = function() {
// some code here
}
$('#leftMenu').on('click', function() {
clearTimeout(callingRight); // clear the timeout on your global variable here.
leftMenu();
})
$('#rightMenu').on('click', function() {
rightMenu();
})
if (...) {
leftMenu();
} else {
callingRight = setTimeout(function() { // assign this setTimeout to your global variable
rightMenu();
}, 1500);
}

Multiple click handlers for a single element

I've written a few events to handle opening and closing of a snap js drawer. This code below works, but I feel it could be written more efficiently. Any suggestions?
function openMobileMenu() {
event.preventDefault();
snapper.open('left');
$('#btn-menu').off('click', openMobileMenu);
$('#btn-menu').on('click', closeMobileMenu);
}
function closeMobileMenu() {
event.preventDefault();
snapper.close('left');
$('#btn-menu').on('click', openMobileMenu);
$('#btn-menu').off('click', closeMobileMenu);
}
$('#btn-menu').on('click', openMobileMenu);
Make your code modular and your concepts explicit.
You can start by creating a MobileMenu object which encapsulates the logic.
Note: The following code was not tested.
var MobileMenu = {
_snapper: null,
_$button: null,
_direction: 'left',
init: function (button, snapper, direction) {
this._$button = $(button);
this._snapper = snapper;
if (direction) this._direction = direction;
this._toggleSnapperVisibilityWhenButtonClicked();
},
_toggleSnapperVisibilityWhenbuttonClicked: function () {
this._$button.click($.proxy(this.toggle, this));
},
toggle: function () {
var snapperClosed = this._snapper.state().state == 'closed',
operation = snapperClosed? 'open' : 'closed';
this._snapper[operation](this._direction);
}
};
Then in your page you can just do the following to initialize your feature:
var mobileMenu = Object.create(MobileMenu).init('#btn-menu', snapper);
Modularizing your code will make it more maintainable and understandable in the long run, but also allow you to unit test it. You also gain a lot more flexibily because of the exposed API of your component which allows other code to interact with it.
E.g. you can now toggle the menu visibility with mobileMenu.toggle().
Use a variable to keep track of the state:
var menu_open = false;
$("#btn-menu").on('click', function(event) {
event.preventDefault();
if (menu_open) {
snapper.close('left');
} else {
snapper.open('left');
}
menu_open = !menu_open; // toggle variable
});
snap has a .state() method, which returns an object stuffed with properties, one of which is .state.
I think you want :
$('#btn-menu').on('click', function() {
if(snapper.state().state == "closed") {
snapper.open('left');
} else {
snapper.close('left');
}
});
Or, in one line :
$('#btn-menu').on('click', function() {
snapper[['close','open'][+(snapper.state().state == 'closed')]]('left');
});
Also, check How do I make a toggle button? in the documentation.

clearInterval not working as I expect it too

I made a demo which is here. All you have to do is start typing in the text field, make sure you have the console open. So as you type, you'll instantly see the OMG Saved, and the counter in the console will go nuts.
Now click the button, watching the console you should see something like 11 or some other value, but you'll also see the counter reset and continues going. I do not want this. I want the counter to stop, I have clicked a button and while the page hasn't refreshed, the counter should stop if I understand these docs on setInterval().
the app I am developing which uses code very similar to this, does not refresh as most single page apps don't. So it is imperative that I have control over this setInterval.
So my question is:
How do I reset the counter such that, until I type again in the input box OR if the input box element cannot be found the flash message does not show up, the interval is set back to 0.
update
The following is the JavaScript code, which is run on the link provided above.
var ObjectClass = {
initialize: function() {
$('#flash-message').hide();
},
syncSave: function() {
$('#content').keypress(function(){
SomeOtherClass.autoSave = setInterval( function(){
$('#flash-message').show();
$('#flash-message').delay(1000).fadeOut('slow');
}, 500);
});
},
listenForClick: function() {
$('#click-me').click(function() {
console.log(SomeOtherClass.autoSave);
clearInterval(SomeOtherClass.autoSave);
});
}
};
var SomeOtherClass = {
autoSave: null
};
ObjectClass.initialize();
ObjectClass.syncSave();
ObjectClass.listenForClick();
You have to put this
clearInterval(SomeOtherClass.autoSave);
before this line:
SomeOtherClass.autoSave = setInterval( function(){
So that you kill the previous interval and you ahve ONLY ONE interval at the same time
Your code will be:
var ObjectClass = {
initialize: function () {
$('#flash-message').hide();
},
syncSave: function () {
$('#content').keypress(function () {
clearInterval(SomeOtherClass.autoSave);
SomeOtherClass.autoSave = setInterval(function () {
$('#flash-message').show();
$('#flash-message').delay(1000).fadeOut('slow');
}, 500);
});
},
listenForClick: function () {
$('#click-me').click(function () {
console.log(SomeOtherClass.autoSave);
clearInterval(SomeOtherClass.autoSave);
});
}
};
var SomeOtherClass = {
autoSave: null
};
ObjectClass.initialize();
ObjectClass.syncSave();
ObjectClass.listenForClick();
What you need to do is use a timeout instead of an interval, like this:
var ObjectClass = {
initialize: function() {
$('#flash-message').hide();
},
syncSave: function() {
$('#content').keypress(function(){
SomeOtherClass.autoSave = setTimeout( function(){
$('#flash-message').show();
$('#flash-message').delay(1000).fadeOut('slow');
}, 500);
});
},
listenForClick: function() {
$('#click-me').click(function() {
console.log(SomeOtherClass.autoSave);
if(typeof SomeOtherClass.autoSave === 'number'){
clearTimeout(SomeOtherClass.autoSave);
SomeOtherClass.autoSave = 0;
}
});
}
};
var SomeOtherClass = {
autoSave: 0
};
ObjectClass.initialize();
ObjectClass.syncSave();
ObjectClass.listenForClick();

Backbone Marionette Layout Region not closing when returning to module

I have a Backbone Marionette application whose layout's regions are not working properly. My app is structured using Require modules and some of these modules' regions are failing to close themselves when the module has been returned to a second time. The first time through the regions are closing as expected but upon return the layout object no longer contains the region objects it did during the first visit: I am using the browser debugger to ascertain this difference.
Here is my Module code:-
define(["marionette", "shell/shellapp", "interaction/videoreveal/model", "interaction/videoreveal/controller", "utils/loadingview", "utils/imagepreloader"], function(Marionette, shellApp, Model, Controller, LoadingView, imagePreloader){
var currentModuleModel = shellApp.model.get("currentInteractionModel"); // get module name from menu model
var Module = shellApp.module(currentModuleModel.get("moduleName")); // set application module name from menu model
Module.init = function() { // init function called by shell
//trace("Module.init()");
Module.model = new Model(shellApp.interactionModuleData); // pass in loaded data
Module.controller = new Controller({model: Module.model, mainRegion:shellApp.mainRegion}); // pass in loaded data and region for layout
Module.initMain();
};
Module.initMain = function() {
trace("Module.initMain()");
shellApp.mainRegion.show(new LoadingView());
// do some preloading
var imageURLs = this.model.get('imagesToLoad');
imagePreloader.preload(imageURLs, this.show, this);
};
Module.show = function() {
Module.controller.show();
};
Module.addInitializer(function(){
//trace("Module.addInitializer()");
});
Module.addFinalizer(function(){
//trace("Module.addFinalizer()");
});
return Module;
});
Here is the Controller class which is handling the Layout and Views:-
define(["marionette", "shell/vent", "shell/shellapp", "interaction/videoreveal/layout", "interaction/videoreveal/views/mainview", "ui/feedbackview", "ui/videoview"], function(Marionette, vent, shellApp, Layout, MainView, FeedbackView, VideoView){
return Marionette.Controller.extend({
initialize: function(options){
trace("controller.initialize()");
// store a region that will be used to show the stuff rendered by this component
this.mainRegion = options.mainRegion;
this.model = options.model;
this.model.on("model:updated", this.onModelUpdate, this);
this.layout = new Layout();
this.layout.render();
this.mainView = new MainView({model:this.model, controller:this});
this.feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"});
this.videoView = new VideoView({videoContainerID:"vrVideoPlayer"});
vent.on("feedbackview:buttonclicked", this.onFeedbackClick, this);
vent.on("videoview:buttonclicked", this.onVideoClick, this);
},
// call the "show" method to get this thing on screen
show: function(){
// get the layout and show it
this.mainRegion.show(this.layout);
this.model.initInteraction();
},
initFeedback: function (index) {
this.model.set("currentItem", this.model.itemCollection.models[index]);
this.model.set("itemIndex", index);
this.model.initFeedback();
},
initVideo: function (index) {
this.model.set("currentItem", this.model.itemCollection.models[index]);
this.model.set("itemIndex", index);
this.model.initVideo();
},
finalizer: function() {
this.layout.close();
},
// events
onFeedbackClick: function(e) {
this.layout.overlayRegion.close();
},
onVideoClick: function(e) {
this.layout.overlayRegion.close();
},
onFinishClick: function() {
this.model.endInteraction();
},
onFeedbackClosed: function() {
this.layout.overlayRegion.off("close", this.onFeedbackClosed, this);
if (this.model.get("currentItem").get("correct") === true) {
this.model.initThumb();
}
},
onModelUpdate: function() {
trace("controller onModelUpdate()");
switch (this.model.get("mode")) {
case "initInteraction":
this.layout.mainRegion.show(this.mainView);
break;
case "initFeedback":
this.layout.overlayRegion.on("close", this.onFeedbackClosed, this);
this.feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"})
this.feedbackView.setContent(this.model.get("currentItem").get("feedback"));
this.layout.overlayRegion.show(this.feedbackView );
break;
case "initVideo":
this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
break;
case "interactionComplete":
vent.trigger('interactionmodule:completed', this);
vent.trigger('interactionmodule:ended', this);
break;
}
}
});
});
Here is the FeedbackView class:-
define(['marionette', 'tweenmax', 'text!templates/ui/feedbackWithScrim.html', 'shell/vent'], function (Marionette, TweenMax, text, vent) {
return Marionette.ItemView.extend({
template: text,
initialize: function (options) {
this.model = options.model;
this.content = options.content; // content to add to box
this.feedbackBoxID = options.feedbackBoxID; // id to add to feedback box
this.hideScrim = options.hideScrim; // option to fully hide scrim
},
ui: {
feedbackBox: '.feedbackBox',
scrimBackground: '.scrimBackground'
},
events : {
'click button': 'onButtonClick' // any button events within scope will be caught and then relayed out using the vent
},
setContent: function(content) {
this.content = content;
},
// events
onRender: function () {
this.ui.feedbackBox.attr("id", this.feedbackBoxID);
this.ui.feedbackBox.html(this.content);
if (this.hideScrim) this.ui.scrimBackground.css("display", "none");
this.$el.css('visibility', 'hidden');
var tween;
tween = new TweenMax.to(this.$el,0.5,{autoAlpha:1});
},
onButtonClick: function(e) {
trace("onButtonClick(): "+ e.target.id);
vent.trigger("feedbackview:buttonclicked", e.target.id) // listen to this to catch any button events you want
},
onShow : function(evt) {
this.delegateEvents(); // when rerendering an existing view the events get lost in this instance. This fixes it.
}
});
});
Any idea why the region is not being retained in the layout when the module is restarted or what I can do to correct this?
Much thanks,
Sam
Okay.... I got there in the end after much debugging. I wouldn't have got there at all if it wasn't for the generous help of the others on this thread so THANKYOU!
Chris Camaratta's solutions definitely pushed me in the right direction. I was getting a Zombie layout view in my Controller class. I decided to switch a lot of my on listeners to listenTo listeners to make their decoupling and unbinding simpler and hopefully more effective. The key change though was to fire the Controller class's close method. I should have had this happening all along but honestly it's my first time getting into this mess and it had always worked before without needing to do this. in any case, lesson hopefully learned. Marionette does a great job of closing, unbinding and handling all of that stuff for you BUT it doesn't do everything, the rest is your responsibility. Here is the key modification to the Module class:-
Module.addFinalizer(function(){
trace("Module.addFinalizer()");
Module.controller.close();
});
And here is my updated Controller class:-
define(["marionette", "shell/vent", "shell/shellapp", "interaction/videoreveal/layout", "interaction/videoreveal/views/mainview", "ui/feedbackview", "ui/videoview"], function(Marionette, vent, shellApp, Layout, MainView, FeedbackView, VideoView){
return Marionette.Controller.extend({
initialize: function(options){
trace("controller.initialize()");
// store a region that will be used to show the stuff rendered by this component
this.mainRegion = options.mainRegion;
this.model = options.model;
this.listenTo(this.model, "model:updated", this.onModelUpdate);
this.listenTo(vent, "feedbackview:buttonclicked", this.onFeedbackClick);
this.listenTo(vent, "videoview:buttonclicked", this.onVideoClick);
},
// call the "show" method to get this thing on screen
show: function(){
// get the layout and show it
// defensive measure - ensure we have a layout before axing it
if (this.layout) {
this.layout.close();
}
this.layout = new Layout();
this.mainRegion.show(this.layout);
this.model.initInteraction();
},
initFeedback: function (index) {
this.model.set("currentItem", this.model.itemCollection.models[index]);
this.model.set("itemIndex", index);
this.model.initFeedback();
},
initVideo: function (index) {
this.model.set("currentItem", this.model.itemCollection.models[index]);
this.model.set("itemIndex", index);
this.model.initVideo();
},
onClose: function() {
trace("controller onClose()");
if (this.layout) {
this.layout.close();
}
},
// events
onFeedbackClick: function(e) {
this.layout.overlayRegion.close();
},
onVideoClick: function(e) {
this.layout.overlayRegion.close();
},
onFinishClick: function() {
this.model.endInteraction();
},
onFeedbackClosed: function() {
if (this.model.get("currentItem").get("correct") === true) {
this.model.initThumb();
}
},
onModelUpdate: function() {
trace("controller onModelUpdate()");
switch (this.model.get("mode")) {
case "initInteraction":
this.layout.mainRegion.show(new MainView({model:this.model, controller:this}));
break;
case "initFeedback":
var feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox", controller:this});
feedbackView.setContent(this.model.get("currentItem").get("feedback"));
this.layout.overlayRegion.show(feedbackView);
this.listenTo(this.layout.overlayRegion, "close", this.onFeedbackClosed);
break;
case "initVideo":
this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
break;
case "interactionComplete":
vent.trigger('interactionmodule:completed', this);
vent.trigger('interactionmodule:ended', this);
break;
}
}
});
});
If I understand your question correctly your views do not work well after they are closed and re-opened.
It looks like you are using your layout/views after they are closed, and keeping them for future use with these references:
this.feedbackView = new FeedbackView();
Marionette does not like this, once you close a view, it should not be used again. Check out these issues:
https://github.com/marionettejs/backbone.marionette/pull/654
https://github.com/marionettejs/backbone.marionette/issues/622
I would advise you not to store these views and just recreate them when you show them
layout.overlayRegion.show(new FeedbackView());
#ekeren's answer is essentially right; I'm just expanding on it. Here's some specific recommendations that I believe will resolve your issue.
Since you're utilizing regions you probably don't need to create your views ahead of time:
initialize: function(options) {
this.mainRegion = options.mainRegion;
this.model = options.model;
this.model.on("model:updated", this.onModelUpdate, this);
vent.on("feedbackview:buttonclicked", this.onFeedbackClick, this);
vent.on("videoview:buttonclicked", this.onVideoClick, this);
},
Instead, just create them dynamically as needed:
onModelUpdate: function() {
switch (this.model.get("mode")) {
case "initInteraction":
this.layout.mainRegion.show(new MainView({model:this.model, controller:this}));
break;
case "initFeedback":
var feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"})
feedbackView.setContent(this.model.get("currentItem").get("feedback"));
this.layout.overlayRegion.show(feedbackView);
this.layout.overlayRegion.on("close", this.onFeedbackClosed, this);
break;
case "initVideo":
this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
break;
case "interactionComplete":
vent.trigger('interactionmodule:completed', this);
vent.trigger('interactionmodule:ended', this);
break;
}
}
The layout is a bit of a special case since it can be closed in several places, but the principle applies:
show: function(){
// defensive measure - ensure we have a layout before axing it
if (this.layout) {
this.layout.close();
}
this.layout = new Layout();
this.mainRegion.show(this.layout);
this.model.initInteraction();
},
Conditionally cleanup the layout:
finalizer: function() {
if (this.layout) {
this.layout.close();
}
},

clearTimeout on dynamically updating timer

I've got a timer that I'm updating dynamically.
------------------update --------------------------
when I first posted the question, I didn't think it mattered that the timer was being called from within a backbone view, but I believe as a result of that, I can't use a global variable (or at least a global variable isn't working). I'll be calling multiple timers, so setting only one global variable and deleting it won't work. I need to be able to clear a single timer without clearing the others.
What I start the timer,
function countDown(end_time, divid){
var tdiv = document.getElementById(divid),
to;
this.rewriteCounter = function(){
if (end_time >= MyApp.start_time)
{
tdiv.innerHTML = Math.round(end_time - MyApp.start_time);
}
else {
alert('times up');
}
};
this.rewriteCounter();
to = setInterval(this.rewriteCounter,1000);
}
in my app, I initiate the timer in a backbone view with
MyApp.Views.Timer = Backbone.View.extend({
el: 'div#timer',
initialize: function(){
timer = this.model;
this.render();
},
events: {
"clicked div#add_time": "update_timer"
}
render: function(){
$(this.el).append(HandlebarsTemplates['timer'](timer);
this.start_timer();
},
start_timer: function(){
delete main_timer; // this doesn't work :(
clearTimtout(main_timer); //this doesn't work either :(
var main_timer = setTimeout(new countDown(timed.length, 'main_timer'),timed.length*1000);
},
update_timer: function(){
timed.length=timed.length+30
this.start_timer();
}
});
so what I'm trying to do is to update the timer, kill the old timer, and then restart it with the new values. I have different timers, so just calling the timed.length within the countdown function won't work.
var main_timer = setTimeout(new countDown(timed.length, 'main_timer'),timed_length*1000);
This statement creates a local variable main_timer. Instead you have to create a global variable and use that to clear the time out as shown below
clearTimtout(main_timer);
main_timer = setTimeout(new countDown(timed.length, 'main_timer'),timed_length*1000);
EDIT:
use a function as setTimeout handler as shown below
clearTimeout(main_timer);
main_timer = setTimeout(function(){
new countDown(timed.length, 'main_timer');
},timed_length*1000);
note: hope timed.length and timed_length are correct.
EDIT:
modify countdown like given below.
function countDown(end_time, divid){
var tdiv = document.getElementById(divid),
to;
this.rewriteCounter = function(){
if (end_time >= MyApp.start_time)
{
tdiv.innerHTML = Math.round(end_time - MyApp.start_time);
}
else {
alert('times up');
}
};
this.clearRewriteCounter = function(){
clearInterval(to);
}
this.rewriteCounter();
to = setInterval(this.rewriteCounter,1000);
return this;
}
and in MyApp.Views.Timer
MyApp.Views.Timer = Backbone.View.extend({
el: 'div#timer',
initialize: function(){
timer = this.model;
this.render();
},
events: {
"clicked div#add_time": "update_timer"
}
render: function(){
$(this.el).append(HandlebarsTemplates['timer'](timer);
this.start_timer();
},
start_timer: function(){
clearTimeout(this.main_timer);
this.main_timer = setTimeout(function(){
if(this.countDownInstance){
this.countDownInstance.clearRewriteCounter();
}
this.countDownInstance = new countDown(timed.length, 'main_timer');
},timed_length*1000);
},
update_timer: function(){
timed.length=timed.length+30
this.start_timer();
}
});

Categories

Resources