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);
}
});
Related
In knockout is it possible to collapse all other opened row and expand only clicked row.
I am referring this Fiddle example for it.
view -
<ul data-bind="foreach: items">
<button data-bind="text:name"></button>
<div data-bind="visible:expanded">
<input data-bind="value:name"></input>
</div>
</ul>
viewModel -
function Sample(item) {
var self = this;
self.name = ko.observable(item.name);
self.id = ko.observable(item.id);
self.expanded = ko.observable(false);
self.toggle = function (item) {
self.expanded(!self.expanded());
};
self.linkLabel = ko.computed(function () {
return self.expanded() ? "collapse" : "expand";
}, self);
}
var viewModel = function () {
var self = this;
var json = [{
"name": "bruce",
"id": 1
}, {
"name": "greg",
"id": 2
}]
var data = ko.utils.arrayMap(json, function (item) {
return new Sample(item); // making things independent here
});
self.items = ko.observableArray(data);
};
ko.applyBindings(new viewModel());
Here its not collapsing already opened row. I tried to fetch complete items in toggle function but it did not work.
I am new to knock out. please suggest.
Update -
I tried this code to make first one extended by default -
var index=0;
var data = ko.utils.arrayMap(json, function(item) {
if(index++===0){
return new Sample(item,true);
}else{
return new Sample(item,false);
}
});
But above given code is not working as expected.
This is very common "problem" when you're working with knockout. You want to keep your Sample instances independent, while their behavior might still influence the behavior of any siblings... I usually pick one of three options:
Move the functionality that influences siblings to the parent viewmodel. For example:
var viewModel = function() {
/* ... */
self.toggle = function(sample) {
self.items().forEach(function(candidateSample) {
candidateSample.expanded(sample === candidateSample);
});
}
};
With data-bind:
<a data-bind="click: $parent.toggle"></a>
Personally, I'd go with this option. Here's it implemented in your fiddle: http://jsfiddle.net/cxzLsz56/
Pass siblings to each item:
self.items = ko.observableArray();
var data = ko.utils.arrayMap(json, function (item) {
return new Sample(item, self.items);
});
self.items(data);
And in Sample:
function Sample(item, siblings) {
self.toggle = function() {
siblings().forEach(/* collapse */);
self.expanded(true); // Expand
};
};
Create some sort of postbox/eventhub/mediator mechanism and make a Sample trigger an event. Each Sample listens to this event and collapses when another Sample triggers it.
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 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
}
})
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
}
});