Backbone add ? after page url when an error occurred - javascript

I am experiencing a strange error when I run my code. I am using an application with Backbone.js and RequireJS. The problem arises when there is an error in the code (any), seems that a backbone or jquery redirect to the url existing adding a question mark ? at the end of the page name:
URL before the error: http://localhost/home#users
URL after the error : http://localhost/home?#users
This causes the page to refresh. Anyone know why this behavior?
This is my code:
var myView = Backbone.View.extend({
events: {
'click button#lookUp':'lookUp'
},
lookUp: function(){
//Force error to show the error calling a function does not exist
this.triggerSomeError();
}
});
Router:
var navigate = function(url) {
appRouter.navigate(url, {
trigger: true
});
}
var initialize = function(){
appRouter = new AppRouter;
Backbone.history.start();
}
Html:
<button class="alinear-btn btn btn-primary" id="lookUp">Imprimir</button>
Chrome developer console:

This is a pretty common error that we've run into. You need to change your code to the following:
var myView = Backbone.View.extend({
events: {
'click button#lookUp':'lookUp'
},
lookUp: function(event) {
event.preventDefault();
//Force error to show the error calling a function does not exist
this.triggerSomeError();
}
});
By default an event object is passed to a function called from the events collection, and you need to use this to prevent the default behavior. You can name the parameter whatever. I just typically call it event to be explicit.
What you are actually suppressing here is the default behavior of that button in the browser, which is to do a form submit. When you do a form submit in the browser it tries to take everything that's in the form and append it on to the url as query parameters. In this case the button is not in any form and so it's basically appending nothing and just putting the question mark for the query parameters.
The reason you got down votes and the reason you're getting an error is because you didn't create a method in your view called triggerSomeError() so of course you're going to get an error on that. However that's completely independent and has nothing to do with what's going on with your url path.

Related

Prevent XUL notificationBox from closing when button is hit

I have a problem concerning the notificationBox. I create a notification using
appendNotification( label , value , image , priority , buttons, eventCallback )
and supply a button in the buttons argument.
Now, I want to prevent the notificationBox from closing when I hit the button. The XUL Documentation states that this can be done by throwing an error in the eventCallback function:
This callback can be used to prevent the notification box from closing on button click. In the callback function just throw an error. (For example: throw new Error('prevent nb close');)
This does not work for me, however, it works when I add the throw-statement to the callback function of the button itself.
Is this a bug in XUL or an inconsistency with the documentation?
Is there any harm done by adding it to the button's callback function?
In my opinion, this is an error in the documentation not a bug in the code. However, throwing an error in your button callback to prevent closure is not the best way to accomplish that goal.
Looking at the source code, there were clearly multiple discrepancies between the code and the documentation regarding how buttons work on a notification.
There is a specifically coded method of preventing the notification closing from within the button callback (return true from the callback).
Throwing an error in order to accomplish a normal functionality is usually a bad programming practice. Doing so also results in an error showing in the console every time your button is pressed. Having errors intentionally showing in the console under normal operation is bad. It also can result in your add-on not being approved in review.
As it was documented (not as operational), if you wanted to close when one button was pressed and not close when another was pressed, you would have to store in a global variable which button callback was last called and then choose based on that information if you wanted to prevent closure when your notificationBox callback was executed. That would be an inappropriately complex way to design operation of these notification buttons.
Given all that, I would say that intentionally throwing an error in order to prevent closure is not the "correct" way to do it. While, trowing an error to prevent closure doesn't cause any harm to the operation of the notification box, it does show the error in the console, which is bad.
The correct way to prevent the notification from closing from within the notification button callback is to return a True value from the callback.
While it is possible that the previously inaccurately documented way of doing this the way they intended to have it operate, it is not the way it actually works. Given
It is easier to update the documentation than it is to make changes to the code.
The code works in a way that is better than the documented method.
There were other inaccuracies in the documentation that would have prevented people from using functionality which was supposedly working (popups/menu buttons).
I have, therefore, updated the documentation to reflect what is actually in the source code and copied, with some modification, the code from this answer to an example there.
Here is some code I used to test this:
function testNotificationBoxWithButtons() {
//Create some common variables if they do not exist.
// This should work from any Firefox context.
// Depending on the context in which the function is being run,
// this could be simplified.
if (typeof window === "undefined") {
//If there is no window defined, get the most recent.
var window=Components.classes["#mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator)
.getMostRecentWindow("navigator:browser");
}
if (typeof gBrowser === "undefined") {
//If there is no gBrowser defined, get it
var gBrowser = window.gBrowser;
}
function testNotificationButton1Callback(theNotification, buttonInfo, eventTarget) {
window.alert("Button 1 pressed");
//Prevent notification from closing:
//throw new Error('prevent nb close');
return true;
};
function testNotificationButton2Callback(theNotification, buttonInfo, eventTarget) {
window.alert("Button 2 pressed");
//Do not prevent notification from closing:
};
function testNotificationCallback(reason) {
window.alert("Reason is: " + reason);
//Supposedly prevent notification from closing:
//throw new Error('prevent nb close');
// Does not work.
};
let notifyBox = gBrowser.getNotificationBox();
let buttons = [];
let button1 = {
isDefault: false,
accessKey: "1",
label: "Button 1",
callback: testNotificationButton1Callback,
type: "", // If a popup, then must be: "menu-button" or "menu".
popup: null
};
buttons.push(button1);
let button2 = {
isDefault: true,
accessKey: "2",
label: "Button 2",
callback: testNotificationButton2Callback,
type: "", // If a popup, then must be: "menu-button" or "menu".
popup: null
};
buttons.push(button2);
//appendNotification( label , value , image (URL) , priority , buttons, eventCallback )
notifyBox.appendNotification("My Notification text", "Test notification unique ID",
"chrome://browser/content/aboutRobots-icon.png",
notifyBox.PRIORITY_INFO_HIGH, buttons,
testNotificationCallback);
}

Completely remove Backbone view from memory

I have a very simple backbone dialog which simply shows a bootstap modal with a message. Im using it throughout the app and have built it so that you pass the title and message to be displayed, and the callback to be executed on click of the button. I have an errorListener and in there the view is created, attached to DOM and rendered:
var messageDialog;
var callback = function() {
....
messageDialog.remove();
messageDialog.unbind();
};
....
var errorListener = function() {
if (!messageDialog) {
messageDialog = new MessageDialog({
title: 'Error',
message: 'We have encountered an error. Please try again.',
buttonText: 'Try Again'
});
$('body').append(messageDialog.$el);
messageDialog.render();
}
messageDialog.setCallback(tryAgain);
messageDialog.show();
}
The problem is after the first time the messageDialog is created, attached to the DOM and shown, it wont be shown again. This is because if i do a console.log() on messageDialog, I stil see it's a varaible containing a Backbone view. I thought after calling remove() and unbind() in the callback, the messageDialog variable would be garbage collected. Do I need to do:
messageDialog = null;
after the unbind()? Is this the correct way of doing things?
I think that .remove() is enough, it removes it from the DOM and also removes binded events to avoid phantom views.
http://backbonejs.org/#View-remove
Is there any reason that myview.remove(); doesn't feet your needs ?
Hope it helps.

Understanding click events in Backbone and Express

I am trying to learn some javascript and I've gone through several tutorials, now I'm trying to understand a real-life system. Here is a demo site that has been pretty well put together:
http://nodecellar.coenraets.org/
https://github.com/ccoenraets/nodecellar
I think I understand the basics of how events can be assigned to elements on the page but then when I look through his source code I can't figure out how even the first click works. When you click "Start Browsing" it should be caught by javascript somehow which fires off an asynchronous request and triggers the view to change with the data received. But in his / public/js/views/ nowhere is there event catching plugged in (except in the itemdetail view but that's a different view entirely).
I also tried using chrome developer tools to catch the click and find out what script caught it.
Under sources I tried setting an event breakpoint for DOM mutation and then clicked.... but no breakpoint (how is that possible? There's definitely a DOM mutation happening)
I then went under elements and checked under the "click" event listener and didn't see anything revealing there either.
Obviously I don't know what I'm doing here. Can anyone help point me in the right direction?
This app is using backbones routing capabilities to switch contexts.
It is basically using hash tags and listening for location change events to trigger updates to the page.
The routing configuration is in main.js:
See: Backbone.Router for more information.
Code Reference: http://nodecellar.coenraets.org/#wines
var AppRouter = Backbone.Router.extend({
routes: {
"" : "home",
"wines" : "list",
"wines/page/:page" : "list",
"wines/add" : "addWine",
"wines/:id" : "wineDetails",
"about" : "about"
},
initialize: function () {
this.headerView = new HeaderView();
$('.header').html(this.headerView.el);
},
home: function (id) {
if (!this.homeView) {
this.homeView = new HomeView();
}
$('#content').html(this.homeView.el);
this.headerView.selectMenuItem('home-menu');
},
list: function(page) {
var p = page ? parseInt(page, 10) : 1;
var wineList = new WineCollection();
wineList.fetch({success: function(){
$("#content").html(new WineListView({model: wineList, page: p}).el);
}});
this.headerView.selectMenuItem('home-menu');
},
// etc...
});
utils.loadTemplate(['HomeView', 'HeaderView', 'WineView', 'WineListItemView', 'AboutView'], function() {
app = new AppRouter();
Backbone.history.start();
});

Jasmine Testing Fancybox in Backbone - Spied Methods incorrectly passing through

I have a modal login view that I'm testing with Jasmine. I have a nasty problem testing that the login button redirects. I've put the redirect in a method so that I can mock it and test the call but when I trigger the click event on the sign in button the spy seems to be ignored.
Here's the view I'm testing...
define(['ministry', 'models/m-auth', 'helpers/cookie-manager', 'text!templates/modals/login.html', 'jquery.custom'],
function (Ministry, Authentication, CookieManager, TemplateSource) {
var loginView = Ministry.SimpleView.extend({
name: 'Login',
el: "#popupLoginWrapper",
template: Handlebars.compile(TemplateSource),
events: {
'submit #loginForm': 'signIn'
},
redirectToOrigin: function () {
// TODO: Change location to member home when implemented
location.href = '/#/jqm';
},
signIn: function (e) {
var that = this;
e.preventDefault();
// As we have no input yet log in manually using the auth model...
var authData = new Authentication.Request({
requestSource: $('#requestSource').text(),
apiKey: $('#apiKey').text(),
username: '*****',
password: '*****'
});
// This saves the login details, generating a session if necessary, then creates a cookie that lasts for 1 hour
authData.save(authData.attributes, {
success: function () {
if (authData.generatedHash !== undefined && authData.generatedHash !== null && authData.generatedHash !== '')
CookieManager.CreateSession(authData.generatedHash);
that.redirectToOrigin();
}
});
},
});
return loginView;
});
(It currently logs in with a hard coded account - wiring up the actual controls is the next job). And here's the test in question (I've combined all the before and afters into the test for simplicity here)...
var buildAndRenderView = function (viewObject) {
viewObject.render();
$('#jasmineSpecTestArea').append(viewObject.el);
return viewObject;
};
it("signs in when the 'Sign In' button is clicked", function () {
spyOn(objUt, 'redirectToOrigin').andCallFake(Helper.DoNothing);
spyOn(Backbone.Model.prototype, 'save').andCallFake(function (attributes, params) {
params.success();
});
$('body').append('<div id="jasmineSpecTestArea"></div>');
$('#jasmineSpecTestArea').append('<section id="popupLoginWrapper"></section>');
objUt = new View();
buildAndRenderView(objUt);
objUt.$('#loginForm button').click();
expect(objUt.redirectToOrigin).toHaveBeenCalled();
$('#jasmineSpecTestArea').remove();
});
Some brief justification: I do the vast majority of my views as independent components that I then render in a custom view variant called a Region (modeled on Marionette) - I generally find this to be a nice tidy practice that helps me keep track of what sits where in the app separately from what is held in a given 'space'. This strategy doesn't seem to work with fancybox modals, such as this, so the render takes place in a hidden existing DOM element and is then moved into the region, hence the need for the code to append 'popupLoginWrapper' into the test area.
The same test above will also fail if I call the sign in method directly with empty event arguments. Jasmine gives me the following error 'Error: Expected a spy, but got Function.' (Which makes sense, as it's calling the actual implementation of redirectToOrigin rather than the spy.
I have similar tests which pass, and the issue seems to be around the triggering of params.success(), but this is necessary to the test.
ADDITIONAL: After disabling the test, I found this issue affecting most of the tests for this modal. By stepping through, I can see that the spy is applied and the spy code is executed in some cases, but then the real success call seems to then be triggered afterwards.
I seem to have fixed this issue - I'm not strictly sure how; I just approached it completely fresh a few days later - The test now looks like this...
var buildAndRenderView = function (viewObject) {
viewObject.render();
$('#jasmineSpecTestArea').append(viewObject.el);
viewObject.postRender();
return viewObject;
};
it("signs in when the 'Sign In' button is clicked", function () {
$('body').append('<div id="jasmineSpecTestArea"></div>');
$('#jasmineSpecTestArea').append('<section id="popupLoginWrapper"></section>');
objUt = new View();
spyOn(objUt, 'redirectToOrigin').andCallFake(Helper.DoNothing);
spyOn(objUt, 'displayError').andCallFake(Helper.DoNothing);
spyOn(Backbone.Model.prototype, 'save').andCallFake(function (attributes, params) {
params.success();
});
objUt = buildAndRenderView(objUt);
objUt.$('#loginForm button').click();
expect(objUt.redirectToOrigin).toHaveBeenCalled();
expect(objUt.displayError).not.toHaveBeenCalled();
$('#jasmineSpecTestArea').remove();
});
The displayError call was added as I'd wrapped the error alert mechanism since the initial issue, so isn't particularly relevant. I split some variable sets into a postRender after fixing the test, so that is also not relevant.
The problem seemed to be calling the buildAndRenderView method without the return value being set to the object. By changing this...
buildAndRenderView(objUt);
to...
objUt = buildAndRenderView(objUt);
the issue resolved itself.

Event triggered multiple times after using back button in Backbone.js

I'm building a Backbone app and I came across this weird issue. In the state A (route: ""), I've got a view like that:
var view = Backbone.View.extend({
events : {
"click a.continue" : "next"
},
next : function(e) {
//Some stuff
Backbone.history.navigate("/page2");
}
});
and once I click on the anchor with "continue" class, I am redirected to a state B (route: "/page2"). If I click on the back button of my browser, and then I click on the anchor, debugging I've noticed that the next function is triggered twice. Actually if I keep going back and forth the number of times the event is triggered keeps increasing.
Any clue?
You've got a zombie view hanging around.
The gist of it is that when you are instantiating and displaying the second view ("state B"), you are not disposing of the first view. If you have any events bound to the view's HTML or the view's model, you need to clean those up when you close the form.
I wrote a detailed blog post about this, here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Be sure to read the comments as "Johnny O" provides an alternative implementation which I think is quite brilliant.
I Have the same problem, the solution is...
App.Router = Backbone.Router.extend({
routes: {
"fn1": "fn1",
"fn2": "fn2"
},
stopZombies: function(objView){
if(typeof objView === "object"){
objView.undelegateEvents();
$(objView.el).empty();
}
},
fn1: function(){
this.stopZombies(this.lastView);
var view1 = new App.v1();
this.lastView = view1;
},
fn2: function(){
this.stopZombies(this.lastView);
var view2 = new App.v2();
this.lastView = view2;
}
});
Store the last execute view in this.lastView, then stopZoombies() remove the events from this view.

Categories