I have a backbone.js app which on select change, changes the subview. The problem is, I'm basically duplicating a lot of code when all I'm changing is the product name.
I have a view that looks like:
var myView = Backbone.View.extend({
events: {
"change .changeType" : "changeHomeType"
},
render: function () {
this.$el.append( render("homes/home") );
var homeBlueView = new CityHomeBlueView({
el: $('.home-view:last')
});
homeBlueView.render();
return this;
},
changeHomeType: function (e) {
var homeType = $(e.currentTarget).val();
var thisHome = $(e.currentTarget).closest(".home");
switch (homeType) {
case "Blue":
var homeBlueView = new CityHomeBlueView({
el: thisHome.find( $('.home-view') )
});
homeBlueView.render();
break;
case "Red":
var homeRedView = new CityHomeRedView({
el: thisHome.find( $('.home-view') )
});
homeRedView.render();
break;
And I continue to render subviews (for atleast 20 more), while all I'm changing is the color. Is there a better way to do this?
I suggest two solutions.
Solution1 : Create common child View. recommended
You can pass custom options to Backbone.View
var homeRedView = new CityHomeView({ //create instance of common Child View
el: thisHome.find( $('.home-view') ),
color : 'red' // or 'blue'
});
You can use custom options by this.options.color in child View
Solution2 : Use Class properties.
var myView = Backbone.View.extend({
changeHomeType: function (e) {
var homeType = $(e.currentTarget).val();
var thisHome = $(e.currentTarget).closest(".home");
var homeView = new this.constructor.ChildClasses[thisHome]({
el: thisHome.find( $('.home-view') )
});
homeRedView.render();
}
}, {
ChildClasses : { //Class properties
red : CityHomeRedView,
blue : CityHomeBlueView
}
})
Related
How can I create a new Backbone view from within that view? For example; my view ModalDialog1 needs to (re)instantiate itself when ModalDialog2 is closed.
define('ModalDialog1.View',
[
'modal_dialog1.tpl'
, 'ModalDialog2.View'
, 'Backbone'
, 'underscore'
],
function(
modal_dialog1_tpl
, ModalDialog2View
, Backbone
, _
)
{
'use strict';
return Backbone.View.extend({
template: modal_dialog1_tpl
, events: {
'click a[data-modal-id="why-need-info"]': 'openModalDialog2'
}
, openModalDialog2: function() {
var self = this;
var closeCallback = function() {
// How to reinstantiate this view/self??
var modalDialog1 = new self();
modalDialog1 .showInModal();
}
var view = new ModalDialog2View({closeCallback: closeCallback})
.showInModal();
// On calling showInModal the current modal view (this) is destroyed
}
, getContext: function()
{
return {
}
}
})
});
You could use the constructor of the view in question.
var modalDialog1 = new self.constructor();
modalDialog1.showInModal();
The second option would be is to have a method which initializes modalDialog1 when modalDialog2 closes.
I have an 'email' style app that displays messages grouped by the date. When the app loads, a shallow collection of messages are fetched and loaded into a backbone collection. Each model in the collection represents a list of messages within a grouping. The MessageGroup represents a group of messages and the MessagesView displays the groups of messages.
This all works well until the collection is fetched again like after a filter is applied, only the group headers are displayed, not the messages inside. I've tried triggering an event that the MessagesView can listen for, then re-render itself but I get an error: listening.obj.off is not a function.
var MessageModel = Backbone.Model.extend({});
var MessageCollection = Backbone.Collection.extend({
model: MessageModel
});
var GroupModel = Backbone.Model.extend({});
var GroupCollection = Backbone.Collection.extend({
model: GroupModel,
url: '/messages/recipient',
parse: function (response) {
// Create a grouped JSON to render nested views with
var messageArray = [];
var groupedlist = _.groupBy(response.messages, function(model) {
return model.publishDate;
});
_.forEach(groupedlist, function(n, key) {
var grouping = {};
grouping.group = key;
grouping.list = n;
messageArray.push(grouping);
});
return messageArray;
},
fetchMessages: function() {
this.fetch({
data: filtermodel.toJSON(),
success: function() {
var messagecollection = new MessageCollection();
// Loop through each grouping and set sub-collections
groupcollection.each(function(group) {
var list = group.get('list');
messagecollection.reset(list);
group.set('list', messagecollection);
});
}
});
}
});
// Model to track applied filters
var FilterModel = Backbone.Model.extend({
defaults: {
folder: 0
}
});
// ------------------------ VIEWS ------------- //
// View for a single Message
var MessageView = Backbone.Marionette.ItemView.extend({
template: require('../../../templates/activities/message-item.ejs'),
events: { 'click li.item': 'getMessageDetail' },
getMessageDetail: function(e){
this.triggerMethod('showDetail', this.model);
//initMessageDetail(this.model);
}
});
// Grouped container view for a list of Messages within a group
var MessageGroup = Backbone.Marionette.CompositeView.extend({
template: require('../../../templates/activities/message-list.ejs'),
className: "list-view-group-container",
childView: MessageView,
childViewContainer: "ul.viewcontainer",
initialize: function() {
this.collection = this.model.get('list');
}
});
// Top level view for all grouped messages
var MessagesView = Backbone.Marionette.CollectionView.extend({
childView: MessageGroup,
initialize: function() {
this.collection.on('change', this.log, this);
},
log: function() {
console.log('triggered log');
}
});
// View for selected message detail
var MessageDetailView = Backbone.Marionette.ItemView.extend({
template: require('../../../templates/activities/message-detail.ejs'),
className: "message-content-wrapper"
});
// View for filter selection bar
var MessageFilterView = Backbone.Marionette.ItemView.extend({
template: require('../../../templates/activities/message-filter-bar.ejs'),
events: {
'click #search-btn': function() {
filtermodel.set('search', $('#search-input').val());
groupcollection.fetchMessages();
}
}
});
var filtermodel = new FilterModel();
var groupcollection = new GroupCollection();
// Fetch messages first run
groupcollection.fetchMessages();
// LayoutView to display in center panel of application
module.exports = ViewMessages = Marionette.LayoutView.extend({
template: require('../../../templates/activities/viewmessages.ejs'),
className: 'content full-height',
regions: {
'messagelistregion': '#messageList',
'messagedetailregion': '.message-detail',
'messagefilterregion': '.filter-bar'
},
childEvents: { 'showDetail': 'onMessageSelected' },
onMessageSelected: function (childView, childViewModel) {
var that = this;
var detailModel = childViewModel.clone();
var messageDetailView = new MessageDetailView({model:detailModel});
that.messagedetailregion.show(messageDetailView);
},
onShow: function(){
var that = this;
var messagesview = new MessagesView({
collection: groupcollection
});
var messageFilterView = new MessageFilterView();
that.messagelistregion.show(messagesview);
$("#messageList").ioslist();
that.messagefilterregion.show(messageFilterView);
this.messagedetailregion.on('show', function() {
console.log('message detail region shown:' + that.messagedetailregion.currentView);
})
}
});
I'm thinking its because the work that is done to build out the groupings of messages inside the success callback doesn't finish before the reset event is triggered and the view is refreshed. How can I get the MessagesView to update after subsequent fetches?
UPDATE:
I moved the post-success logic of grouping the collection into its hierarchical tree/leaf structure to a custom event (fetchSuccess) in the top level collectionview (MessagesView):
var MessagesView = Backbone.Marionette.CollectionView.extend({
childView: MessageGroup,
initialize: function() {
this.collection.on('fetch:success', this.fetchSuccess, this);
},
fetchSuccess: function() {
var messagecollection = new MessageCollection();
groupcollection.each(function(group) {
var list = group.get('list');
messagecollection.reset(list);
group.set('list', messagecollection);
});
}
});
It is being triggered in the success callback of the fetch. I'm pretty sure this is a good way of rendering the collection, but I cant seem to get around the error in Marionette:
**Uncaught TypeError: listening.obj.off is not a function**
Anyone have any ideas why this collectionview will not re-render??
I was able to determine that the creation of the models in the collection occurred after the reset event, but before the structure of the nested models was built out:
success: function() {
var messagecollection = new MessageCollection();
// Loop through each grouping and set sub-collections
groupcollection.each(function(group) {
var list = group.get('list');
messagecollection.reset(list);
group.set('list', messagecollection);
});
};
After any filter event, grouping, sorting etc, the collection structure needs to be modified into this nested hierarchy each time. The view was picking up the reset event before the structure was built out so the child views had no data to render. I fixed this by cloning the original collection after the changes and having the views render the cloned collection:
groupcollection.fetch({
reset: true,
data: filtermodel.toJSON(),
success: function() {
groupcollection.each(function(group) {
var list = group.get('list');
var messagecollection = new MessageCollection(list);
group.set('list', messagecollection);
});
filteredcollection.reset(groupcollection.toJSON());
}
});
I'm kind of new to Backbone and I'm having trouble understanding how to set the attributes of a View. I'm using a view without a model.
This is the View:
var OperationErrorView = Backbone.View.extend({
attributes: {},
render: function(){
var html = "<h3>" + this.attributes.get("error") +"</h3>";
$(this.el).html(html);
}
})
Then later on:
if (errors.length > 0){
errors.forEach(function(error){
// var errorView = new OperationErrorView({attributes: {"error": error} }); Doesn't work
var errorView = new OperationErrorView();
errorView.set({attributes: {"error": error}})
errorView.render()
$("#formAdd_errors").append(errorView.$el.html());
});
}
Which is the correct approach to do this? Right now it doesn't work: When I try the method that is not commented out, it gives me the error TypeError: errorView.set is not a function, if I try it the first way, it just doesn't call the render() function.
UPDATE:
var OperationErrorView = Backbone.View.extend({
attributes: {},
initialize: function(attributes){
this.attributes = attributes;
},
render: function(){
var html = "<h3>" + this.attributes.get("error") +"</h3>";
console.log("html");
$(this.el).html(html);
}
})
if (errors.length > 0){
errors.forEach(function(error){
console.log(error);
var errorView = new OperationErrorView({"error": error});
errorView.render()
$("#formAdd_errors").append(errorView.$el.html());
});
}
I tried including this.render() in the initialize function. Doesn't work. Doesn't even call the render function. Why?
A couple things:
set is not a function of a Backbone View. Check the API
In your commented code, calling new OperationErrorView(...) does not automatically evoke the render function. You have to do this manually.
The attributes property of the View does not have a get method. Again, Check the API
So, what should you do?
Research different ways to initialize a View with properties. Then figure out how to get those properties on the HTML that your View controls.
Here's a bit to get you started
var OperationErrorView = Backbone.View.extend({
tagName: 'h3',
initialize: function(attributes) {
this.attributes = attributes;
this.render();
},
render: function(){
// attach attributes to this.$el, or this.el, here
// insert the element into the DOM
$('#formAdd_errors').append(this.$el);
}
});
// later in your code
if ( errors.length > 0 ) {
errors.forEach(function(error) {
new OperationErrorView({ error: error });
});
}
Thanks to chazsolo for the answer, all the info is there. So, I'll write the code that worked just in case someone finds it useful someday:
var OperationErrorView = Backbone.View.extend({
initialize: function(attributes){
this.attributes = attributes;
},
render: function(){
var html = "<h3>" + this.attributes.error +"</h3>";
$(this.el).html(html);
}
});
if (errors.length > 0){
errors.forEach(function(error){
var errorView = new OperationErrorView({'error':error});
errorView.render()
$("#formAdd_errors").append(errorView.$el.html());
});
}
I loop data in quoteTemplate.html(using text! plug-in) then I got one variable total=100, and I want to print total value in my view according to checkbox event :
var testView = Backbone.View.extend({
initialize: function() {
},
el: '#container',
events : {
'change [name=chkIncludeTotal]' : 'checkboxHandler'
},
checkboxHandler : function (e) {
if($(e.currentTarget).is(':checked')){
$('#total').html(total);
}else{
$('#total').html(0);
}
},
render: function(){
var cartPanel = _.template(CartPanel);
this.$el.html(cartPanel);
var aa = _.template(AATemplate);
var bb = _.template(BBTemplate);
var cc = _.template(CCTemplate);
var dd = _.template(eeTemplate, {data : test.showEEtemplate() , total:total});
$('#div1').html(bb);
$('#div2').html(cc);
$('#div3').html(dd);
}
});
I really don't know how can I take total from html template file to use in the view.
Any help would be much appreciated, thank you.
My content box module enumerates a collection and creates a container view for each item ( passing the model to the view). It sets the initial content to the content property of its model. Base on a layout property in the model the container view is attached to the DOM. This is kicked off by the “_contentBoxCreate” method.
The content box module responds to clicks to sub items in a sidemenu. The sidemenu is implemented in a different module. The sidemenu sub click event passes an object along as well that contains a sub_id and some text content. I want to take the content from this object and use it to update container view(s).
Currently I’m doing this via the “_sideMenuClick” method. In backbonejs is there a best practice for updating a views content, given that no data was changed on its model?
thanks,
W.L.
APP.module("contentbox", function(contentbox) {
//Model
var Contentbox = Backbone.Model.extend({});
//Collection
var Contentboxes = Backbone.Collection.extend({
model: Contentbox,
url: 'ajax/contentboxResponse/tojson'
});
/*
* View:
*/
var Container = Backbone.View.extend({
initialize: function() {
contentbox.on('update', jQuery.proxy(this.update, this));
contentbox.on('refresh', jQuery.proxy(this.render, this));
var TemplateCache = Backbone.Marionette.TemplateCache;
this.template = TemplateCache.get("#contentbox-container");
},
render: function() {
var content = this.model.get('content').toString();
var html = this.template({content: content});
this.$el.html(html);//backbone element
return this;
},
update: function(fn) {
var content = fn.apply(this);
if (content !== null) {
var html = this.template({content: content});
this.$el.html(html);
}
}
});
//collection
var contentboxes = new Contentboxes();
var _sideMenuToggle = function() {
contentbox.trigger('refresh');
};
var _sideMenuClick = function(sideMenu) { //view contex
var fn = function() {
// this fn will have the context of the view!!
var linksub = this.model.get('linksub').toString();
if (linksub === sideMenu.id.toString()) {
return sideMenu.content.toString();
}
return null;
};
contentbox.trigger('update', fn);
};
var _contentBoxCreate = function() {
var create = function(cboxes) {
cboxes.each(function(model) {
var layout = "#body-" + model.get('layout');
var $el = jQuery(layout);
var container = new Container({model: model});
$el.append(container.render().$el);
});
};
contentboxes.fetch({
success: create
});
};
this.on("start", function() {
_contentBoxCreate();
});
this.addInitializer(function() {
APP.vent.on('sidemenu:toggle', _sideMenuToggle);
APP.reqres.setHandler('sidemenu:submenu', _sideMenuClick);//event and content...
//from another module
});
});
UPDATE:
Changed the view...
/*
* View
*/
var Container = Backbone.View.extend({
initialize: function() {
this.renderableModel = this.model; // Define renderableModel & set its initial value
contentbox.on('update', this.update, this);
contentbox.on('refresh', this.reset, this); // 3rd param gives context of view
var TemplateCache = Backbone.Marionette.TemplateCache;
this.template = TemplateCache.get("#contentbox-container");
},
render: function() {
var content = this.renderableModel.get('content').toString();
var html = this.template({content: content});
this.$el.html(html);//backbone element
return this;
},
update: function(fn) {
/**
* The "update" event is broadcasted to all Container views on the page.
* We need a way to determine if this is the container we want to update.
* Our criteria is in the fn
*/
var content = fn.apply(this); //criteria match return content, else null.
/*
* The render method knows how to render a contentbox model
*/
if (content !== null) {
this.renderableModel = new Contentbox();
this.renderableModel.set({content: content}); //add content to new contentbox model
this.render(); //Rerender the view
}
},
reset: function() {
this.renderableModel = this.model;
this.render(); // restore view to reflect original model
}
});