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
}
});
Related
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 have created in backgridjs table custom "TagCell" (with implemented THIS).
So my cell looks like:
var TagCell = Backgrid.TagCell = Cell.extend({
className: "tag-cell",
events: {
'click .tag a': 'removetag',
},
initialize: function (options) {
TagCell.__super__.initialize.apply(this, arguments);
this.title = options.title || this.title;
this.target = options.target || this.target;
var model = this.model;
var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model);
},
removetag: function(event) {
var that = this;
that.model.set({location: ""},{success: alert("removed!"));
},
render: function () {
this.$el.empty();
var rawValue = this.model.get(this.column.get("name"));
var formattedValue = this.formatter.fromRaw(rawValue, this.model);
this.$el.append('<input name="location" class="tagggs" value="'+formattedValue+'" />');
this.delegateEvents();
return this;
},
});
If I trying to call removetag function with event click to ".tag" model with empty location is saved. But If I trying to call function with click event to ".tag a" or directly to class ".rmvtag" function is not called. I think because jquery tags input is designed like this:
$('<span>').addClass('tag').append(
$('<span>').text(value).append(' '),
$('<a>', {
href : '#',
class : 'rmvtag',
text : 'x'
}).click(function () {
return $('#' + id).removeTag(escape(value));
})
).insertBefore('#' + id + '_addTag');
So there is click function with removetag() written directly after append element. How can I call save model function from backbone on click to rmvtag link?
Thanks for any help!
I'm getting data back and it does render Title, but can't seem to render rich text.
markup:
<div id="bodyarea">
<div data-bind=foreach:list>
<span data-bind="text:Title" />
<div data-bind="html: RichData"></div>
</div>
</div>
<p id="myarea"></p>
ko:
function LoadLists() {
var listItems = [];
var count = 0;
$.getJSON("https://myserver.com/sites/knockout/_api/lists/getbytitle('List%20One')/items?$filter=Title eq
'zzzz'",
function (data, textstatus, jqXHR) {
$(data.value).each(function (index, item) {
count++;
var koItem = {};
koItem.Title = item.Title;
koItem.RichData = item.Rich;
listItems.push(koItem);
if (data.value.length == count) {
var vm =
{
list: ko.observableArray(listItems)
};
ko.applyBindings(vm, document.getElementById("bodyarea"));
}
})
});
}
$(document).ready(function () { LoadLists(); });
In general, with knockout, you should not:
Call applyBindings more than once
Call applyBindings inside an ajax call
Create a viewModel as a plain object (usually, sometimes it's alright)
Do knockout databinding inside jQuery event handlers.
Code:
// this is a reusable view model for each item. it takes a raw item from the ajax return, and creates observables for each property.
var ListItem = function (item) {
var self = this;
self.Title = ko.observable(item.Title);
self.RichData = ko.observable(item.Rich);
}
// Your viewModel. Constructor-esque syntax is pretty standard.
var ViewModel = function () {
var self = this;
// this is your list array.
self.list = ko.observableArray();
// This is a your reusable function to load lists, when it returns, it maps each item
// in data.value to a ListItem viewModel and puts them all in the lists observableArray
self.loadList = function() {
$.getJSON('yourUrl', function(data) {
var items = data.value.map(function(item) { return new ListItem(item); });
self.list(items);
}
};
};
// When the document is ready, create a view model and apply bindings once. Then call loaLists to initialize
$(document).ready(function () {
var vm = new ViewModel();
ko.applyBindings(vm);
vm.loadList();
});
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
}
})
I am working on a web page using BackboneJS. The HTML page contains 2 divs which are acting as columns and each item will be either on the first column, either on the second. I am not sure how to set the el element on the collection view. Currently I am setting it dinamically in the collection render function, but for some reason there are more items (divs) created in the right column. Here is the code. Am I doing something wrong? Is there a better approach?
HTML snippet:
<div class="columns" id="col1"></div>
<div class="columns" id="col2"></div>
The Backbone view should distribute the items on both col1 and col2 divs, so I cannot set el in the collection view as usual, I have to set it somehow dynamically.
Here is the MenusView collection corresponding view:
var application = application || {};
application.MenusView = Backbone.View.extend({
//el: '#col1',
initialize: function(initialmenus) {
console.log("Initializaing MenusView");
this.collection = new application.Menus(initialmenus);
this.render();
},
render: function() {
var count = true;
this.collection.each(function(item) {
//setting el dynamically at rendering, so we can distribute divs to col1 and col2
this.setElement($('#col' + (count ? '1' : '2')));
count = !count;
this.renderMenu(item);
}, this);
},
renderMenu: function(item) {
var menuView = new application.MenuView({
model: item
});
this.$el.append(menuView.render().el);
}
});
How about this? Don't modify the parent dynamically at rendering, instead, alternatively pick which child-DIV to render to.
<div class="columnWrapper">
<div class="columns"></div>
<div class="columns"></div>
</div>
var application = application || {};
application.MenusView = Backbone.View.extend({
// Your collection view's "el" is now the wrapper div
el: "div.columnWrapper",
initialize: function(initialmenus) {
console.log("Initializaing MenusView");
this.collection = new application.Menus(initialmenus);
this.render();
},
render: function() {
var count = true;
this.collection.each(function(item) {
this.renderMenu(item, count);
count = !count;
}, this);
},
renderMenu: function(item, count) {
var menuView = new application.MenuView({
model: item
});
var childIndex = count ? 1 : 2;
this.$(".columns:nth-child(" + childIndex + ")").append(menuView.render().el);
}
});