Backbone routes and view states - backbone.js

Apologies for the possibly poorly formulated title. New to Backbone.
I'm having trouble wrapping my head around how to deal with routes in association with views. Basically I have a view (let's call it ListView) that, depending on its viewMode, renders ItemViews using different templates. It looks something like this:
var ListView = Backbone.View.extend({
// Cache a bunch of templates here
viewMode: 'list', // Default is list
render: function() {
switch(this.viewMode) {
case 'list':
// Render ItemView based on list template
break;
case 'gallery':
// Render ItemView based on gallery template
break;
}
// Render all items in list
this.collection.each(function(model, index) {
new ItemView(); // Maybe pass viewMode as a parameter
});
}
});
My goal is that whenever ListView uses the viewMode "list" or "gallery", this should be reflected in the address bar, and likewise manually entering or clicking a link that leads to e.g. mysite.com/page.html#items/list or #items/gallery should render the same results.
Is there a way of automating this process, or in some other way solve it?

Think your router would be something like:
var yourRouter = Backbone.Router.extend({
routes: {
"items/list": "showList",
"items/gallery": "showGallery"
},
showList: function() {
listView.viewMode = "list"
listView.render();
}
showGallery: function() {
listView.viewMode = "gallery"
listView.render();
}
});
Then in your view events, you can call the navigate method of your router. This will update the address bar.
yourRouter.navigate("items/list")

Related

Backbonejs - Back button doesn't work if page transition on same page

Short description of my program and finally the problem:
I have got two pages. The first page list products in rows with a short description. If you click on one you will land on a detail page.
The detail page lists the product details and underneath a couple of related products. If you click on one of the releated products the same page is rendered again with the new information fetched from a REST interface.
If I want to use the browser-back-button or the own back-button to get to the previous product-detail-page a blank page appears. This only happens on my iPad. Using Chrome on a desktop browser works fine. I debugged the application and I figured out, that the backbonejs route is never called. I have no idea why.
Here is my code of the details page:
define([
"jquery",
"lib/backbone",
"lib/text!/de/productDetails.html"
],
function(
$,
Backbone,
ContentTemplate
){
var PageView = Backbone.View.extend({
// product details template
template: _.template(ContentTemplate),
// back-button clicked
events:{
'click a#ac-back-button':'backInHistory',
},
// init
initialize: function(options){
this.options=options;
// bind functions
_.bindAll(this,
'render',
'renderRelatedSeriePlainproduct',
'backInHistory'
);
// listen for collection
this.listenTo(this.options.relatedCollectionPlainproduct, 'reset',this.renderRelatedSeriePlainproduct);
},
// back button
backInHistory: function(e){
e.preventDefault();
window.history.back();
},
// render template
render: function(){
// render template
this.$el.html(this.template(this.model.models[0].attributes));
return this;
},
// render related products
renderRelatedSeriePlainproduct: function (){
var models = this.options.relatedCollectionPlainproduct.models;
if(models.length==0){
$('.ac-plainproduct').hide();
} else{
var elem = $('#ac-related-listing-plainproduct');
var ct="";
ct+='<ul id="ac-list-related-plainproduct">';
$.each(models, function(key, value){
ct+='<li>';
ct+='<a href="index.html?article_id='+value.get('article_id')+'&type='+value.get('type')+'&serie='+value.get('series')+'#product-detail">Link';
ct+='</a>';
ct+='</li>';
});
ct+='</ul>';
elem.append(ct);
}
}
});
// Returns the View class
return PageView;
});
I follow one of the links from renderRelatedSeriePlainproduct.If I click on the back button on the new page the backInHistory function is called, but the window.history.back(); does not call the backbone router.
Maybe the problem is the #hash in the URL, that is not changed during page transition. But this would not explain, why it works perfectly with my Chrome on my desktop machine. For me it seemed to be a problem of asynchronous calls but even there I could not find a problem.
Maybe it helps to list my router code as well. First of all I was thinking it is an zombie issue in backbone, but I remove all events and views while making the transition.
// function called by the route
// details page
productdetail: function() {
$.mobile.loading("show");
_self = this;
// lazy loading
require([
'collection/ProductDetailCollection',
'collection/RelatedCollection',
'view/ProductDetailView'
],
function(ProductDetailCollection, RelatedCollection, ProductDetailView){
// get URL parameters
var articleID = _self.URLParameter('article_id');
var type = _self.URLParameter('type');
var serie = _self.URLParameter('serie');
// product - details
var productDetail = new ProductDetailCollection.ProductDetail({id: articleID});
// related products
_self.relatedCollectionPlainproduct = new RelatedCollection({serie:serie, type:"Electronics", article_id:articleID});
// assign binded context
productDetail.fetch({
// data fetched
success: function (data) {
// page transition
_self.changePage(new ProductDetailView({
model:data,
relatedCollectionPlainproduct:_self.relatedCollectionPlainproduct
}));
// fetch data
_self.relatedCollectionPlainproduct.fetch({reset:true});
}
});
});
},
// page transition
changePage:function (page) {
// remove previous page from DOM
this.page && this.page.remove() && this.page.unbind();
// assign
this.page = page;
// assign page tag to DOM
$(page.el).attr('data-role', 'page');
// render template
page.render();
// append template to dom
$('body').append($(page.el));
// set transition
var transition = "fade";
// we want to slide the first page different
if (this.firstPage) {
transition = "fade";
this.firstPage = false;
}
// make transition by jquery mobile
$.mobile.changePage($(page.el), {changeHash:true, transition: transition});
// page was rendered - trigger event
page.trigger('render');
$.mobile.loading("hide");
},
I tried to use allowSamePageTransition but with no success. Maybe someone could give me a hint. Thanks!
Looks like jQuery Mobile and Backbone's routers are conflicting. Take a look here:
http://coenraets.org/blog/2012/03/using-backbone-js-with-jquery-mobile/
Thats not the reason. I disabled the routing of jquery mobile.
// Prevents all anchor click handling
$.mobile.linkBindingEnabled = false;
// Disabling this will prevent jQuery Mobile from handling hash changes
$.mobile.hashListeningEnabled = false;

events not firing after re-render in backbone.js

I am facing a problem while trying to click submit after re-render.
This is my view:
ShareHolderInfoView = Backbone.View.extend( {
template : 'shareholderinfo',
initialize: function() {
this.model = new ShareHolderInfoModel();
},
render : function() {
$.get("shareholderinfo.html", function(template) {
var html = $(template);
that.$el.html(html);
});
//context.loadViews.call(this);
return this;
},
events:{
"change input":"inputChanged",
"change select":"selectionChanged",
"click input[type=submit]":"showModel"
},
inputChanged:function(event){
var field = $(event.currentTarget);
var data ={};
data[field.attr('id')] = field.val();
this.model.set(data);
},
showModel:function(){
console.log(this.model.attributes);
alert(JSON.stringify(this.model.toJSON()));
}
});
This is my Router
var shareholderInfo, accountOwnerInfo;
App.Router = Backbone.Router.extend({
routes:{
'share':'share',
'joint':'joint'
},
share:function(){
$("#subSection").empty();
if(!shareholderInfo){
shareholderInfo = new ShareHolderInfoView();
$("#subSection").append(shareholderInfo.render().el);
} else{
$("#subSection").append(shareholderInfo.$el);
}
},
joint:function(random){
$("#subSection").empty();
if(!accountOwnerInfo){
accountOwnerInfo = new AccountOwnerInfoView();
$("#subSection").append(accountOwnerInfo.render().el);
} else{
$("#subSection").append(accountOwnerInfo.$el);
}
}
});
This is my HTML a div with id='subSection'.
if I check in console, I can able to see the events bound to that view.
Object {change input: "inputChanged", change select: "selectionChanged", click input[type=submit]: "showModel"}
But its not calling that showModel function afer i click submit. Please help.
Your fundamental problem is that you're improperly reusing views.
From the fine manual:
.empty()
Description: Remove all child nodes of the set of matched elements from the DOM.
[...]
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So when you say:
$("#subSection").empty();
you're not just clearing out the contents of #subSection, you're also removing all event handlers attached to anything inside #subSection. In particular, you'll remove any event handlers bound to accountOwnerInfo.el or shareholderInfo.el (depending on which one is already inside #subSection).
Reusing views is usually more trouble than it is worth, your views should be lightweight enough that you can destroy and recreate them as needed. The proper way to destroy a view is to call remove on it. You could rewrite your router to look more like this:
App.Router = Backbone.Router.extend({
routes: {
'share':'share',
'joint':'joint'
},
share: function() {
this._setView(ShareHolderInfoView);
},
joint: function(random){
this._setView(AccountOwnerInfoView);
},
_setView: function(view) {
if(this.currentView)
this.currentView.remove();
this.currentView = new view();
$('#subSection').append(this.currentView.render().el);
}
});
If your views need any extra cleanup then you can override remove on them to clean up the extras and then chain to Backbone.View.prototype.remove.call(this) to call the default remove.
If for some reason you need to keep your views around, you could call delegateEvents on them:
delegateEvents delegateEvents([events])
Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
and you'd say things like:
$("#subSection").append(shareholderInfo.$el);
shareholderInfo.delegateEvents();
instead of just:
$("#subSection").append(shareholderInfo.$el);
I'd strongly recommend that you treat your views and cheap ephemeral objects: destroy them to remove them from the page, create new ones when they need to go on the page.

BB Marionette: best way to update a collection without re-rendering

I have a very simple page that shows a collection in a table. Above it theres a search field where the user enters the first name of users.
When the user types I want to filter the list down.
Edit: I have updated the code to show how the current compositeView works. My aim is to integrate a searchView that can _.filter the collection and hopefully just update the collection table.
define([
'marionette',
'text!app/views/templates/user/list.html',
'app/collections/users',
'app/views/user/row'
],
function (Marionette, Template, Users, User) {
"use strict"
return Backbone.Marionette.CompositeView.extend({
template: Template,
itemView: User,
itemViewContainer: "tbody",
initialize: function() {
this.collection = new Users()
this.collection.fetch()
}
})
})
Divide your template in a few small templates, this increases performance at the client side, you don't have problems with overriden form elements and you have more reuseable code.
But be aware of too much separation, cause more templates means more views and more code/logic.
You don't seem to be making use of CollectionView as well as you could be. If I were you I would separate the concerns between the search box and the search results. Have them as separate views so that when one needs to rerender, it doesn't effect the other.
This code probably won't work straight away as I haven't tested it. But hopefully it gives you some clue as to what ItemView, CollectionView, and Layout are and how they can help you remove some of that boiler plate code
//one of these will be rendered out for each search result.
var SearchResult = Backbone.Marionette.ItemView.extend({
template: "#someTemplateRepresentingEachSearchResult"
)};
//This collectionview will render out a SearchResult for every model in it's collection
var SearchResultsView = Backbone.Marionette.CollectionView.extend{
itemView: SearchResult
});
//This layout will set everything up
var SearchWindow = Backbone.Marionette.Layout.extend({
template: "#someTemplateWithASearchBoxAndEmptyResultsRegionContainer",
regions:{
resultsRegion: "#resultsRegion"
},
initialize: function(){
this.foundUsers = new Users();
this.allUsers = new Users();
this.allUsers.fetch({
//snip...
});
events: {
'keyup #search-users-entry': 'onSearchUsers'
},
onSearchUsers: function(e){
var searchTerm = ($(e.currentTarget).val()).toLowerCase()
var results = this.allUsers.filter(function(user){
var firstName = user.attributes.firstname.toLowerCase();
return firstName.match(new RegExp(searchTerm))
});
this.foundUsers.set(results); //the collectionview will update with the collection
},
onRender: function(){
this.resultsRegion.show(new SearchResultsView({
collection: this.foundUsers
});
}
});
I think the most important thing for you to take note of is how CollectionView leverages the Backbone.Collection that you provide it. CollectionView will render out an itemView (of the class/type you give it) for each model that is in it's collection. If the Collection changes then the CollectionView will also change. You will notice that in the method onSearchUsers all you need to do is update that collection (using set). The CollectionView will be listening to that collection and update itself accordingly

rerender Backbone views without losing references to dom

I have the following problem with backbone and I'd like to know what strategy is the more appropriated
I have a select control, implemented as a Backbone view, that initially loads with a single option saying "loading options". So I load an array with only one element and I render the view.
The options will be loaded from a collection, so I fire a fetch collection.
Then I initialize a component that is in charge of displaying in line errors for every field. So I save a reference of the dom element of the combo.
When the fetch operation is finally ready, I rerender the control with all the options loaded from the collection.
To render the view I user something like this:
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
pretty standard backbone stuff
the problem is that after rendering the view for the second time the reference of the dom is no longer valid,
perhaps this case is a bit strange, but I can think of lots of cases in which I have to re-render a view without losing their doms references (a combo that depends on another combo, for example)
So I wonder what is the best approach to re-render a view without losing all the references to the dom elements inside the view...
The purpose of Backbone.View is to encapsulate the access to a certain DOM subtree to a single, well-defined class. It's a poor Backbone practice to pass around references to DOM elements, those should be considered internal implementation details of the view.
Instead you should have your views communicate directly, or indirectly via a mediator.
Direct communication might look something like:
var ViewA = Backbone.View.extend({
getSelectedValue: function() {
return this.$(".combo").val()
}
});
var ViewB = Backbone.View.extend({
initialize: function(options) {
this.viewA = options.viewA;
},
doSomething: function() {
var val = this.viewA.getSelectedValue();
}
});
var a = new ViewA();
var b = new ViewB({viewA:a});
And indirect, using the root Backbone object as a mediator:
var ViewA = Backbone.View.extend({
events: {
"change .combo" : "selectedValueChanged"
},
selectedValueChanged: function() {
//publish
Backbone.trigger('ViewA:changed', this.$('.combo').val());
}
});
var ViewB = Backbone.View.extend({
initialize: function(options) {
//subscribe
this.listenTo(Backbone, 'ViewA:changed', this.doSomething);
},
doSomething: function(val) {
//handle
}
});
var a = new ViewA();
var b = new ViewB();
The above is very generic, of course, but the point I'm trying to illustrate here is that you shouldn't have to worry whether the DOM elements are swapped, because no other view should be aware of the element's existence. If you define interfaces between views (either via method calls or mediated message passing), your application will be more maintainable and less brittle.

Backbone.Marionette: CompositeView disappear after collection.reset() is fired

I'm quite new in the world of Backbone and I decided to use Marionette for my first serious project with it.
With some difficulties I managed to set up my app's basic options and routing and I was pretty happy with it, but now I'm facing a blocking problem with a CompositeView that represent a Table.
This View is rendered inside a region of a specific layout, called "grid". This layout has 3 region: the top_controls, table_view and bottom_controls. Since I needed to bind some action on some of the elements of the layout I decided to use it as a View, and to include the "master" collection inside it, so I can just rendered a filtered version of the collection inside the CompositeView, without touching the main one.
From my router I call it in this way:
App.grid = new Grid({collection: Clt});
App.page.show(App.grid);
The structure of the layout is this (I'm using requireJS):
var Grid = Backbone.Marionette.Layout.extend({
className: "container-fluid",
template: gridLayout,
regions: {
top_controls: "#top_controls",
table_view: "#table_view",
bottom_controls: "#bottom_controls",
},
initialize: function(){
this.renderTable(this.collection, true);
},
renderTable: function(collection, fetch){
if(fetch){
collection.fetch({success:function(){
var vista = new CompView({collection: collection});
App.grid.table_view.show(vista);
}});
} else {
var vista = new CompView({collection: collection});
App.grid.table_view.show(vista);
}
},
events: {
"keyup input":"filter_grid"
},
filter_grid: function(e){
var $el = e.currentTarget;
var to_filter = $($el).val();
if(to_filter==""){
this.renderTable(this.collection, false);
} else {
var filtered = this.collection.filter(function(item){
return item.get("link_scheda").toLowerCase() == to_filter;
});
if(filtered.length>0){
var filtro = new AssocCollection();
filtro.reset(filtered);
this.renderTable(filtro, false);
}
}
}
});
return Grid;
The Layout template looks like this:
<div class="row-fluid" id="top_controls"><input type="text" id="filter" class="input"/></div>
<div class="row-fluid" id="table_view"></div>
<div class="row-fluid" id="bottom_controls"><button class='add btn btn-primary'>Add</button></div>
My CompositeView is structured like that:
var AssocView = Backbone.Marionette.CompositeView.extend({
tagName: 'table',
className: 'table table-bordered table-striped',
id: 'tableAssoc',
template: assocTemplate,
itemView: assocRow,
appendHtml: function(collectionView, itemView, index){
collectionView.$("tbody").append(itemView.el);
},
events: {
"click .sort_link":"sort_for_link",
},
sort_for_link: function(){
this.collection.comparator = function(model){
return model.get("link_value");
}
this.collection.sort();
},
onRender: function(){
console.log("render table!");
}
});
return AssocView;
The first display of the table is done right, and the filtering too. The problem occur when
I click the table header with the class "sort_link": the entire Table is wiped away from the HTML while the collection stay the same (I suppode the entire layout is re-rendered). If for example I render the CompositeView in another place, like the app main region, it all works as intended. So I guess to problem it's located inside my Layout declaration.
Any help will be much appreciated!
In your Grid, you need to override the initialEvents method and don't do anything in it.
Grid = Backbone.Marionette.Layout.extend({
initialEvents: function(){},
// ... everything you already have
});
Layout extends from ItemView, and ItemView provides the initialEvents implementation. This method checks to see if it was given a collection, and if it does, it wires up the collection "reset" event to the "render" method of the view. In your case, you are passing the collection through and don't want this behavior. So, overriding the initialEvents method will correct it.
Update: I thought I had removed that initialEvents a long time ago. If you're keeping up to date w/ Marionette versions, grab v0.9.10 (or whatever the latest is) and this problem is gone now.

Resources