Preventing full page reload on Backbone pushState - backbone.js

I'm trying to prevent full page reloads using Backbone's pushState. When I call navigate() from my view's event, I see the messages marked // 1 below, but not // 2. In addition, when I try to open the same tab, the page reloads again.
Must I stop the event myself? I tried using jQuery's preventDefault(), which does prevent the page reload, but I haven't seen this documented anywhere.
Below is my current code:
App.Router = Backbone.Router.extend({
routes:{
"analytics":"analytics"
, "realtime":"realtime"
}
, analytics:function(page) {
console.log("analytics route hit: %o", page); // 2
}
, realtime:function(page) {
console.log("realtime route hit: %o", page); // 2
}
});
App.TabSetView = Backbone.View.extend({
initialize:function() {
this.collection.bind("reset", this.render, this);
this.collection.bind("add", this.render, this);
this.collection.bind("change", this.render, this);
this.collection.bind("remove", this.render, this);
}
, events:{
'click li.realtime a': "onRealtime"
, 'click li.analytics a': "onAnalytics"
}
, render:function() {
// omitted for brevity
}
, onAnalytics:function() {
console.log("onAnalytics"); // 1
if (this.collection.activateAnalytics()) {
App.app.navigate("analytics", true);
this.render();
console.log("navigated");
} else {
console.log("do nothing"); // 1
}
}
, onRealtime:function() {
console.log("onRealtime");
if (this.collection.activateRealtime()) {
App.app.navigate("realtime", true);
this.render();
console.log("navigated");
} else {
console.log("do nothing"); // 1
}
}
});
var tabs = ...; // omitted for brevity
var tabSetView = new App.TabSetView({collection: tabs});
var App.app = new App.Router;
Backbone.history.start({pushState:true});

to stop the page reload when a user clicks a link, you have to call e.preventDefault() like you were suggesting.
MyView = Backbone.View.extend({
events: {
"click .some a": "clicked"
},
clicked: function(e){
e.preventDefault();
// do your stuff here
}
});
you're also right that this isn't documented in the backbone docs. events are handled by jQuery, though. so you can assume that any valid jQuery things you would do - such as have an e parameter to an event callback - will work with backbone's events.
as for this:
in addition, when I try to open the same tab, the page reloads again.
are you saying when a user opens a new browser tab to your site's url? if so, then there's nothing you can do about this. when the browser opens the tab it makes the request to the server to load the page.
if you're referring to a "tab" as part of your site's user interface, though, then the use of e.preventDefault() on your link / "tab" clicks should take care of that.

The answer is actually in here https://stackoverflow.com/a/9331734/985383, if you enable pushState you want links to work and not prevent them as suggested above, or well, is not just preventing them. here it is:
initializeRouter: function () {
Backbone.history.start({ pushState: true });
$(document).on('click', 'a:not([data-bypass])', function (evt) {
var href = $(this).attr('href');
var protocol = this.protocol + '//';
if (href.slice(protocol.length) !== protocol) {
evt.preventDefault();
app.router.navigate(href, true);
}
});
}

$('a').click(function(e){
e.preventDefault();
Backbone.history.navigate(e.target.pathname, {trigger: true});
});

Just a follow up to Derick answer.
It worked for me, but to keep it clean, I overwrote the Backbone.View class:
(coffeescript)
class NewBackboneView extends Backbone.View
events:
'click a' : 'pushstateClick'
pushstateClick: (event) ->
event.preventDefault()
Backbone.View = NewBackboneView
So every link from my backbone views have the prevent default.

It depends on how you've generated the HTML mark-up. It looks like you're using anchor tags (<a>), so if those anchor tag href have values or even an empty string, then you need to cancel the default browser behavior otherwise you'll get a page reload. You can cancel the default behaviour using jQuery're event.preventDefault() like you mentioned. Alternatively, if you're not concerned about progressive enhancement or SEO, then you can set your anchor tag href to # or javascript:void(0);, which will also prevent to the page from reloading. e.g.
Click me
or
Click me

Related

Undelegate view events when browser back button pressed

Could anyone explain please how to remove events in order to prevent triggering duplication when clicking browser back button. Or is there any way to undelegate events when initalizing view again. Really stuck how to deal with it.
Pressing back button and then back again causes firing events for multiple times. When saving model form data for instance. Thank you.
var App = {};
// extending models, collections etc.
App.SamplesCollectionView = Backbone.View.extend({
el: '#samples',
template: _.template($('#sample-edit-template').html()),
events: {
'click a.sample-item': 'onEdit'
},
render: function(){
this.$el.append(this.template());
var $sample_list = this.$el.find('ul#sample-list');
this.collection.each(function(sample) {
var rendered = new App.CategoryView({model: sample}).render().el;
$sample_list.append(rendered);
});
},
onEdit: function(e) {
this.undelegateEvents();
// go to edit view
Backbone.history.navigate(e.target.getAttribute('href'), {trigger: true});
return false;
}
});
App.SampleEditView = Backbone.View.extend({
el: '#samples',
template: _.template($('#sample-edit-template').html()),
events: {
'click button.save': 'onSave',
'click button.cancel': 'onCancel',
},
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this;
},
onSave: function() {
this.undelegateEvents();
var data = Helpers.getFormData(this.$el.find('form'));
this.model.save(data);
// go back to index view
Backbone.history.navigate('/samples', {trigger: true});
return false;
}
});
App.SamplesRouter = Backbone.Router.extend({
routes: {
'samples': 'index',
'samples/edit/:id': 'edit'
},
index: function() {
App.samples = new App.SamplesCollection;
App.samplessView = new App.SamplesCollectionView({collection: App.samples});
},
edit: function(id) {
App.sampleEdit = new App.SampleEdit({id: id});
App.sampleEditView = new App.SampleEditView({model: App.sampleEdit})
}
});
App.samplesRouter = new App.SamplesRouter;
Backbone.history.start({pushState: true, hashChange: false});
The problem is that you have many views pointing to same element #samples. You can't remove one view because if you call view.remove() your other view's element is gone.
And as long as that that element exists in DOM, the view you thought to be gone will exist in memory since the shared element has event handlers referring the view instance.
If you want to delegate display functionality and edit functionality under same element, do it in same view using something like show/hide techniques without creating a new view instance.
Otherwise they should have it's own elements, you shouldn't have two view instances pointing to same element. While switching to a different view, make sure you call it's remove() method which removes the element from DOM and invokes undelegateEvents so that it get's garbage collected properly.

BackboneJS - Router issues - how to get clean URL's

ok I have
{{#each mainmenu}}
<li>
<h2>{{this.name}}</h2>
</li>
{{/each}}
and in my router
routes: {
'': 'index',
'pages/firstpage' : 'firstpage',
'pages/secondpage' : 'secondpage',
'pages/thirdpage' : 'thirdpage'
},
initialize: function () {
Backbone.history.start({pushState: false});
}
and my json file is:
{
"name":"First page",
"id":"1",
"url":"firstpage"
},
{
"name":"Second page",
"id":"2",
"url":"secondpage"
},
{
"name":"Third page",
"id":"3",
"url":"thirdpage"
}
the way it is right now my URL is "#pages/secondpage" - how can i get the URL to display "pages/secondpage" - i tried "pushState:true" which didnt work... then in my mainmenu.js view I added an event:
events: {
'click a.second': 'secondpage'
},
secondpage: function() {
var secondpageRouter = new Backbone.Router();
var route = 'pages/secondpage';
secondpageRouter.navigate(route, {trigger: true});
}
but that didnt work either... when I remove the "#pages/" from the anchor above, I almost get the URL I want "pages/secondpage" - but it says "URL could not be found" after clicking the link. So what's going on here???
Please help anyone?
Understand that you need to be able to serve these urls from the server if necessary, read the docs here: http://backbonejs.org/#History
Then you need to do at least three things.
First, after you have defined your routers, call Backbone.history and make sure pushState is true:
Backbone.history.start({ pushState: true })
Second, get rid of all the # in your links, like this:
<h2>{{this.name}}</h2>
Third, and this is important, you need to intercept all clicks on local links and prevent their default behavior, which would be to route you to a new page. This is easy with jQuery:
$(document).on("click", "a[href^='/']", function(event){
event.preventDefault();
RouterNameGoesHere.navigate($(event.target).attr("href"), {trigger: true});
});
If you aren't using jQuery, listen to link click events in your views deal with the url in your event handlers. I modified your code to add the event argument and event.preventDefault():
events: {
'click a.second': 'secondpage'
},
secondpage: function(event) {
event.preventDefault();
var secondpageRouter = new Backbone.Router();
var route = 'pages/secondpage';
secondpageRouter.navigate(route, {trigger: true});
}
Here is an interesting blog post about this issue

backbone.js <button> click not firing

We have researched and tried all we could find but cannot see why button click even is not firing. If we change the view render html color it shows the change, so view render is working okay but when the login button is clicked -> nothing. No error shows in js console. Tried with
button#login_button and #login_button and login_button - all nothing. what are we missing? thks for any help
SessionView = Backbone.View.extend({
el: ('#session'),
initialize:function () {
this.render();
},
render:function () {
if (!session) {
$(this.el).html(
"<button id=\"login_button\" class=\"login_button black\">"+
"Login"+
"</button>");
return this;
} else {
$(this.el).html(
"<button id=\"logout_button\" class=\"login_button black\">"+
"Logout</button>");
return this;
}
},
events: {
"click login_button" : "login",
"click logout_button": "logout"
},
login: function(){
alert("login");
console.log("login dialog");
//var loginView = new LoginView();
//loginView.render().showModal();
},
logout: function(){
alert("You Have Logged Out");
}
});
You should keep '#' in selectors:
events: {
"click #login_button" : "login",
"click #logout_button": "logout"
}
UPDATE
Do you wait for DOM ready to use new on this Backbone view class ?
The fact that your buttons are altered by the html calls means that you do.
As soon as you have checked that, you have to use this.$el instead of $(this.el).
If it still does not work, search for event blocking and be sure ids are unique.
This very simple JSFiddle works with the same conditions you are telling us.

Backbone Layout Manager subview's events doesn't work after reload

I'm using the Backbone Layout Manager Boilerplate. Unfortunately, a quite frustrating bug occurred. I like render a list of items as subviews inserted by insertView function. At the first load everthing works fine. But after a reload the the click events doesn't work anymore :(. I already tried to call delegateEvents() on the TableItem View manually but nothing changed. I hope anyone can give me a clue.
App.Views.Item = Backbone.View.extend({
template: "templates/item",
tagName: "li",
events: {
"click .applyButton" : "apply",
"click .viewDetailsButton" : "showDetail"
},
serialize: function() {
return { table : this.model.toJSON() };
},
apply: function(ev) {
ev.preventDefault();
alert("apply button clicked");
},
showDetail: function(ev) {
ev.preventDefault();
var id = this.model.get("_id");
app.router.navigate("#events/"+ id, {trigger : true})
}
});
/*
* List View
*/
App.Views.List = Backbone.View.extend({
template: "templates/list",
tagNam: "ul",
className: "tableList",
beforeRender: function() {
var events = this.model.get("userEvents").get("hosting");
events.each(function(model) {
this.insertView(new App.Views.Item({ model : model }));
}, this);
},
serialize: function() {
return {};
}
});
I think you might want to add a cleanup function on your Item view to undelegate the events when layoutmanager removes the view. I don't know if this will fix your problem, but it seems like good practise.
When you say after a reload, do you mean reloading the page with the browser reload button? if so, how do you get it to work in the first place?
It would help if you could provide a jsfiddle of your setup, or point us to a repo so we can test it on our machines. Make sure you include the router so that we can have a look at how the view and the layout that contains it are initialised.

Display a view using an existing rendered HTML with Backbone Marionette

I am having an application layout like the one attached. The upper panel is already to the page (i.e. in the server's HTML response). While the user interacts with the elements in that panel the content of the dynamic panel below changes accordingly.
I've studied Backbone Marionette various View types and Region Manager. But I still can't figure out a way to implement this. I need to capture events from the already rendered elements and change the dynamic content accordingly. As I understand, every time a region is created to show a specific Marionette view, the region's content is replaced by that view's el. And with that I cannot have a Layout view for the container of the whole thing.
So can this be done in anyway using Marionette?
You can certainly support what I would call a "pre rendered" or partial view. In fact, here's a Marionette View that I use quite a bit, as I'm working under with an app that includes server side partial views:
My.PartialView = Backbone.Marionette.Layout.extend({
render: function () {
//noop
if (this.onRender) {
this.onRender();
}
return this;
},
onShow: function () {
// make sure events are ready
this.delegateEvents();
}
});
It's simple to use:
My.NavBar = My.PartialView.extend({
events: {
"change #search-input": "searchRequested",
"click #faq-link": "faqRequested",
"click #home-link": "homeRequested",
},
searchRequested: function (e) {
// search
},
faqRequested: function (e) {
// show the faq
},
homeRequested:function () {
// go home
}
});
var navbar = new main.views.NavBar({ el: ".my-nav" });
someRegion.show();
// or, just wire up the events manually
navbar.delegateEvents();
I think the better way is using constructor.
Make your rendered layout class.
App.RenderedLayout = Marionette.Layout.extend({
render: function () {
if (this.onRender) {
this.onRender();
}
return this;
},
constructor: function(){
this._ensureElement();
this.bindUIElements();
Marionette.Layout.prototype.constructor.apply(this, slice(arguments));
}
});
Then you can use full of Marionette capabilities.
App.module('Some.Page', function (Mod, App, Backbone, Marionette, $, _) {
Mod.SomeLayout = App.RenderedLayout.extend({
el: '#renderedDiv',
events: {
'click .something': 'onSomethingClick'
},
regions: {
'innerRegion': '#innerRegion'
},
ui: {
something: '.something div'
},
initialize: function () {
},
onSomethingClick: function(e){
return false;
}
});
Mod.addInitializer(function(){
App.addRegions({renderedRegion: '#renderedDiv'});
Mod.someLayout = new Mod.SomeLayout();
App.renderedRegion.attachView(Mod.someLayout);
});
});

Resources