When a user closes the browser, I want to run some code before the window gets closed out, because if not it causes problems for other users. Part of the application I'm working on has a random video chat. The room is created by a user and while they're waiting for someone to join they close their browser tab/window and then the room is not closed out correctly and still open for another user to join.
I've seen many examples using beforeunload, but they just aren't working for me with Ember. Maybe I'm missing a more ember way of doing things, or maybe I need to rely on a heartbeat approach?
What I've been trying to do is use the window beforeunload event, but I haven't been having any luck. If there is a better way to go about solving my above problem, I'm all ears!
Right now I bind the method in the setupController such as:
$(window).bind('beforeunload', function() {
console.log('ending chat');
this.endChat();
});
I've also tried this code in the route, and maybe in the view (tried so many things over the past month that I don't remember everything).
UPDATE: Code update
Here is the setupController referring to the controller object instead of the window.
setupController: function(controller, hash){
$(window).on('beforeunload', () => {
controller.hasStarted ? controller.endChat() : controller.send('leaveChat');
});
$(window).on('click', () => {
controller.hasStarted ? controller.endChat() : controller.send('leaveChat');
console.log('you just clicked');
});
}
The window click event fires perfectly, but nothing happens on the beforeunload event - the window just closes normally without firing any action/method.
Where is endChat defined? The way you currently have it written, this.endChat() is scoped to the window. I'm guessing you want it scoped to the controller or route.
If you're using Ember CLI, you can use fat arrow syntax to remain in the outer scope like this:
// routes/application.js
import Ember from 'ember';
export default Ember.Route.extend({
setupController: function () {
$(window).on('beforeunload', () => {
this.endChat();
});
},
endChat: function () {
// do the thing
}
});
If not, then you can do it the old fashioned way:
App.ApplicationRoute = Ember.Route.extend({
setupController: function () {
var _this = this;
$(window).on('beforeunload', function () {
_this.endChat();
});
},
endChat: function () {
// do the thing
}
});
Does that work?
export default Ember.Route.extend({
setupController: function () {
Ember.$(window).on('beforeunload', function(e){
e.returnValue = "hi";
return e.returnValue;
});
}
});
Related
I've been trying to debug my Backbone multi-page app for most of the day now to get rid of 'zombies', but unfortunately to no avail. Before today, I didn't even realize I have a zombie problem. What am I doing wrong?
This is my RegionManager:
var regionManager = (function() {
var currView = null;
var rm = {};
var closeView = function(view) {
if (view && view.close) {
view.close();
}
};
var openView = function(view) {
view.render();
if (view.onShow) {
view.onShow();
}
};
rm.show = function(view) {
closeView(currView);
currView = view;
openView(currView);
};
return rm;
})();
This is my View cleaning up function:
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
if (this.views) {
_.invoke(this.views, 'close');
}
// Unbind any view's events.
this.off();
// Unbind any model and collection events that the view is bound to.
if (this.model) {
this.model.off(null, null, this);
}
if (this.collection) {
this.collection.off(null, null, this);
}
// Clean up the HTML.
this.$el.empty();
};
I tried appending the View els directly to the body and using this.remove(); in the View clean-up function (instead of using a common el: $('#content') to which I am appending elements, then cleaning up by this.$el.empty()), but that didn't work either.
It might have something to do with my "global Events":
Backbone.Events.on('letterMouseDown', this.letterMouseDown, this);
But I take care of them with the onClose function:
onClose: function() {
Backbone.Events.off('letterMouseDown');
}
One problem I see is that your close function never removes the event delegator from the view's el. A view's events are handled by using the delegator form of jQuery's on to attach a single event handler to the view's el. Your close does:
this.$el.empty();
but that only removes the content and any event handlers attached to that content, it does nothing at all to the handlers attached directly to this.el. Consider this minimal example:
var V = Backbone.View.extend({
events: {
'click': 'clicked'
},
clicked: function() {
console.log('still here');
}
});
var v = new V({ el: '#el' });
v.close();
After that, clicking on #el will throw a 'still here' in the console even though you think that the view has been fully cleaned up. Demo: http://jsfiddle.net/ambiguous/aqdq7pwm/
Adding an undelegateEvents call to your close should take care of this problem.
General advice:
Don't use the old-school on and off functions for events, use listenTo and stopListening instead. listenTo keeps track of the events on the listener so it is easier to remove them all later.
Simplify your close to just this:
Backbone.View.prototype.close = function() {
if(this.onClose)
this.onClose();
if(this.views)
_.invoke(this.views, 'close');
this.remove();
};
Don't bind views to existing els. Let the view create (and own) its own el and let the caller place that el into a container with the usual:
var v = new View();
container.append(v.render().el);
pattern. If you must attach to an existing el then the view should override remove with a slightly modified version of the standard implementation:
remove: function() {
this.$el.empty(); // Instead of removing the element.
this.undelegateEvents(); // Manually detach the event delegator.
this.stopListening();
return this;
}
I'm pretty sure I found the root for my problem.
mu is too short was right, with the close() method I wasn't removing the events bound directly to my el (which I tried to do by this.off() - this.$el.off()/this.undelegateEvents() is the correct way). But for me, it only fixed the problem that events got called multiple times unnecessarily.
The reason I was plagued by 'zombie views' or unintended behavior was that I wasn't freeing up the memory in the View..
this.remove() only gets rid of the el and it's elements/events, but not the View's internal variables. To elaborate - in my View I have an array declared like so this.array: [] and I didn't have it freed in the onClose function.
All I had to do was empty it in the onClose function or initially declare the array as this.array: null so on recurrent View renderings it would at least free the previous array (it still should be freed on the onClose method though, because the array/object is still going to sit in the memory until browsing away from the page).
It was excruciating to debug, because it's a crossword game (at least my code is hard to read there) and sometimes the words didn't match up, but I didn't know where the problem was coming from.
Lessons learned.
I'm having the most difficult time trying to find a way to make sure the parent scope's collection is updated with the saved information from a modal.
The parent has a collection of event speakers, and the modal adds one speaker to the event. I want for the new speaker to be added to the page once the modal OK button is clicked.
The data is saved and the modal closes, but the model isn't updated unless I refresh the page.
Here's my controller method that updates the collection of speakers. $scope.speakers gets bound to a repeating object in the page.
$scope.updateSpeakersList = function () {
factory.callGetService("GetSpeakers?eventId=" + $scope.event.eventId)
.then(function (response) {
var fullResult = angular.fromJson(response);
var serviceResponse = JSON.parse(fullResult.data);
$scope.speakers = serviceResponse.Content;
LogErrors(serviceResponse.Errors);
},
function (data) {
console.log("Unknown error occurred calling GetSpeakers");
console.log(data);
});
}
Here's the promise where the modal should be calling the previous method, and therefore updating the page.
$scope.openModal = function (size) {
var modalInstance = $modal.open({
templateUrl: "AddSpeakerModal.html",
controller: "AddSpeakerModalController",
size: size,
backdrop: "static",
scope: $scope,
resolve: {
userId: function() {
return $scope.currentUserId;
},
currentSpeaker: function () {
return ($scope.currentSpeaker) ? $scope.currentSpeaker : null;
},
currentSessions: function () {
return ($scope.currentSpeakerSessions) ? $scope.currentSpeakerSessions : null;
},
event: function () {
return $scope.event;
},
registration: function() {
return $scope.currentUserRegistration;
}
}
});
modalInstance.result.then(function (savedSpeaker) {
$scope.savedSpeaker = savedSpeaker;
$scope.updateSpeakersList();
}, function () {
console.log("Modal dismissed at: " + new Date());
});
};
Why is the model not updating?
It's hard to know for sure without having a minimal example that reproduces the problem, but your problem might be that you are updating a 'shadow scope'. This is a pecularity in Javascript which causes the object to be copied instead of modified.
Try adding $scope.$apply() after doing the change in updateSpeakerList().
Check this post and then play around with it.
AngularJS - Refresh after POST
What worked for me was pushing the data to my scope on success then setting the location, so after posting new data mine looked like:
$scope.look.push(data);
$location.path('/main');
The issue here isn't that my data wasn't updating necessarily... the real issue is that the moments that I was attempting to update it were the wrong moments for how my app is put together. I was attempting to update the underlying model BEFORE the updates were available, without realizing it. The AJAX calls were still in progress.
Within my modal, when OK is clicked, it runs through a few different GETs and POSTs. I was calling the update outside of these calls, assuming that it would be called sequentially once the AJAX calls were done. (This was wrong.)
Once I moved the $scope.updateSpeakersList() call to be within the final AJAX success call to my factory, everything appears to be working as desired.
This is a tricky problem to explain but I will try my best:
In Short: I have a Phonegap application that is using Backbone. When a touch event (on element A) is triggered on a view (lets say View A) and that event navigates to a new view (View B). An event is fired on an element (element B) on View B if element B is in the same position as element A.
Detailed: As mentioned above, the application makes use of Backbone. The problem only occurs on a mobile device and not on a browser on my machine. I have implemented jQuery Touch events to work with normal jQuery.
Snippet from my Router:
routes : {
"cart" : "cart",
"menu" : "menu"
},
cart: function (args, options) {
var options = options || {};
var view = App.Router.loadView(new App.Views.CartView(options), {access:true});
return view;
},
menu: function (args, options) {
var options = options || {};
var view = App.Router.loadView(new App.Views.MenuView(options));
return view;
},
loadView: function (view, options) {
var options = options || {},
returnview = view;
if (App.view) {
//Close views and subviews
_.each(App.view.subViews, function (subView) {
subView.close();
});
App.view.close();
}
App.view = returnview;
return $('#app-content').append(App.view.render().$el);
}
Snippet from MenuView
events: {
'tap #cart': function () {
App.Router.navigate('cart', {trigger:true});
}
},
Snippet from my CartView
'change #article-stock': function (e) {
alert('this should not happen!')
}
The scenario presents itself when I tap on an element on my menu (#cart), which in turn calls navigate, which creates the new view (CartView). CartView has a checkbox that is in the same position as where the #cart element was on the previsou view. When CartView is rendered the checkbox is toggled and I receive the alert, even though there was not event on that view. Its as if the event on the previous view bubbles through to the next view.
I obviously don't intent for this to happen. Does anyone know why this occurs, and how can this be prevented?
I hope I explained the issue well enough.
I have searched for a solution to my problem, but the only results I find are relating to events firing twice on the same view and not a single event firing on multiple views
Whilst going through my questions today I saw this one, and thought I would post my solution to this problem if anybody ever has the same issue.
I was never able to fully figure out what the actual cause of the problem was, my guess is that it is just the way the Android browser handles events, but by adding e.preventDefault(); to my events fixed it:
Snippet from MenuView
events: {
'tap #cart': function (e) {
e.preventDefault();
App.Router.navigate('cart', {trigger:true});
}
},
Snippet from my CartView
'change #article-stock': function (e) {
e.preventDefault();
alert('this should not happen!')
}
I'm building a little quiz game using backbone.js
Once you get to the end of the quiz you have the option to start again.
How can I achieve this? I've tried recalling the initialize function, as well as the first function that gets fired when you start the game. This is just returning errors. The two calls are success but a subsequent function for rending each question is failing.
I think I need to empty my model/collection. This is my first outing with Backbone, I'm still trying to get my head around how every works.
Any help, greatly appreciated.
You could use the backbone router (don't forget to start it):
So in your game view, you'd have an event which triggered when the start again button was clicked. This event would trigger a function which would redirect the user to a newgame route. You could also setup a function in the router to close your old view. This is important to avoid zombie views
e.g. view
//initialize code
//...
events: {
'click .startAgainButton':'restart'
}
restart: function(e) {
e.preventDefault();
window.location.href = '#/new_game';
}
//rest of view code...
e.g. router
//router code
routes: {
"new_game":"reset"
},
trackView: function (next) {
if (this.current) this.current.close();
this.current = next;
},
//kill zombie views
close: function () {
this.undelegateEvents();
this.$el.off();
this.$el.children().remove();
},
reset: function() {
this.trackView(new View());
}
I am new to Javascript and backbone.js, so hopefully I am missing something simple here. I am experimenting with some sample code I found which is supposed to check for unsaved changes before allowing a user to navigate away to another page. I have created a JSfiddle here:
http://jsfiddle.net/U43T5/4/
The code subscribes to the hashchange event like this:
$(window).on("hashchange", router.hashChange);
And the router.hashChange function checks a "dirty" flag to determine whether or not to allow the navigation, like this:
hashChange: function (evt) {
if (this.cancelNavigate) { // cancel out if just reverting the URL
evt.stopImmediatePropagation();
this.cancelNavigate = false;
return;
}
if (this.view && this.view.dirty) {
var dialog = confirm("You have unsaved changes. To stay on the page, press cancel. To discard changes and leave the page, press OK");
if (dialog == true) return;
else {
evt.stopImmediatePropagation();
this.cancelNavigate = true;
window.location.href = evt.originalEvent.oldURL;
}
}
},
The problem is that the code is not working because this.view is undefined, so the 2nd if block is never entered.
I would like the sample program to always ask for confirmation before navigating away from the page (in my sample program, I have set this.view.dirty to always be true, which is why it should always ask for confirmation). Or if there is a better approach, I am open to alternatives.
The main issue is the this context in the methods , this corresponds to the Window Object and not the Router. So it always remains undefined as you are defining view on the router. Declare a initialize method which binds the context inside the methods to router.
initialize: function() {
_.bindAll(this, 'loadView', 'hashChange');
},
Check Fiddle
I spent a lot of time to make at least something decent.
I ended up writing a wrapper for Backbone function:
var ignore = false;
Backbone.history.checkUrl = function() {
if (ignore) {
ignore = false;
return;
}
app.dirtyModelHandler.confirm(this, function () {
Backbone.History.prototype.checkUrl.call(Backbone.history);
},
function() {
ignore = true;
window.history.forward();
});
};
app.dirtyModelHandler.confirm is a function which shows confirmation (Ok, Cancel) view and takes success and cancel functions as arguments.