I'm building an application using backbone and marionette.js. I'm planning on using a collection view to present some items and then allow them to be filtered, sorted and grouped.
I was wondering if there are any good design ideas for actually appending the html in a grouped fashion. I have a few ideas but I was wondering if someone might have input on which would be better design.
My first idea is to change the appendHtml method on the collection view, and if grouping is enabled, I can have the appendHtml function either find or create the child group's bin and place the child view in it.
appendHtml: function(collectionView, itemView, index){
var $container = this.getItemViewContainer(collectionView);
// get group from model
var groupName = itemView.model.get("group");
// try to find group in child container
var groupContainer = $container.find("." + groupName);
if(groupContainer.length === 0){
// create group container
var groupContainer = $('<div class="' + groupName + '">')
$container.append(groupContainer);
}
// Append the childview to the group
groupContainer.append(itemView);
}
My second idea is to break apart the collection into groups first and then maybe render multiple views... This one seems like it might be more work, but might also be a bit better as far as the code structure is concerned.
Any suggestions or thought eliciting comments would be great!
Thanks
Maybe not exactly what you're looking for, but here's a somewhat related question:
Backbone.Marionette, collection items in a grid (no table)
My solution to that issue -- one fetched collection that could be rendered as a list or a grid ("items grouped in rows") was to use _.groupBy() in a "wrapper" CompositeView and pass modified data down the chain to another CompositeView (row) and then down to an ItemView.
Views.Grid = Backbone.Marionette.CompositeView.extend({
template: "#grid-template",
itemView: Views.GridRow,
itemViewContainer: "section",
initialize: function() {
var grid = this.collection.groupBy(function(list, iterator) {
return Math.floor(iterator / 4); // 4 == number of columns
});
this.collection = new Backbone.Collection(_.toArray(grid));
}
});
Here's a demo:
http://jsfiddle.net/bryanbuchs/c72Vg/
I've done both of the things your suggesting, and they both work well. It largely comes down to which one you prefer and maybe which one fits your scenario better.
If you have data that is already in a grouped hierarchy, using one of the many hierarchical model / collection plugins or your own hierarchy code, then the idea of rendering a list of groups, with each group rendering a list of items is probably easier.
If you have data that is flat, but contain a field that you will group by, then the appendHtml changes will probably be easier.
hth
This is in addition to Derick's and bryanbuchs' answers. My method uses a main collection view and another collection view as its childView.
Collection views have a 'addChild' method, which is called whenever a model is added to the view's collection. The 'addChild' method is responsible for rendering the child's view and adding it to the HTML for the collection view at a given index. You can see the source code on github here. I'll paste it here for simplification:
addChild: function(child, ChildView, index) {
var childViewOptions = this.getOption('childViewOptions');
if (_.isFunction(childViewOptions)) {
childViewOptions = childViewOptions.call(this, child, index);
}
var view = this.buildChildView(child, ChildView, childViewOptions);
// increment indices of views after this one
this._updateIndices(view, true, index);
this._addChildView(view, index);
return view;
}
As you can see the 'addChild' method calls the 'buildChildView' method. This method actually builds the view.
// Build a `childView` for a model in the collection.
buildChildView: function(child, ChildViewClass, childViewOptions) {
var options = _.extend({model: child}, childViewOptions);
return new ChildViewClass(options);
}
So for your use case you can override the 'addChild' method and make a call to the original method if your grouping criteria is matched. And then in the overridden 'buildChildView' method you can pass the group (which is a subset of your collection) to its childView, which is another Marionette.CollectionView.
Example:
MyCollectionView.prototype.addChild = function(child, ChildView, index) {
if(mycriteria){
return ProductGroup.__super__.addChild.apply(this, arguments);
}
};
MyCollectionView.prototype.buildChildView = function(child, ChildViewClass,
childViewOptions) {
options = _.extend({
collection: "Pass your group collection which is a subset of your main collection"},
childViewOptions
);
return new ChildViewClass(options);
};
Related
I am looking for some description of best practices for views and models/collections in Backbone. I know how to add models to collections, render views using templates and use views in parent views, however I'm looking for more context and perhaps some example links.
I've updated this question to be more specific.
Let's say you have a more grid layout with all kinds of variation, that gets pulled from the same collection. What would you do here to create this page? A simple child view repeated in a parent view won't work because the variation of the grid items is too great. Do you:
create tons of tiny views and collections and render all of these different views using the relevant collections into that one page?
create a complex template file that has a loop in it, that as you go through the loop, the loop outputs different markup?
Do people put multiple views inside a parent view, all from the same model?
Similarly, do people mix different models into the same parent view? For example movies and tv shows - these different models, can get they added to the same collection that renders that list?
Thanks!
You've asked good question. To answer it lets take a look to this from other angle:
On my exp i used to check first is there any logic on parent view, like sorting, validation, search and so on. Second - Collection with models or just model with array as property : is the model is independent and may exist without collection , for example you have navigation item, and there are no sense to make separate model for each item and navigation as collection as you will never use item itself. Another case you have user list. You may use user model in a lot of places and its better to make a separate model for user and collection to combine it.
Your case with UL may be resolved with single model and items properties with array of li, simple grid may have same approach as i don't see some special logic on wrap from your description.
But i should point out - i had close task to build mansory grid with collection parsed from server, items were models as it had different data structure, different templates and different events listener.
Taking decision i considered folowing:
item as independent tile, may be used as in grid and also independent.
item is model + template + view. different Models types helped to support different data structure, different Views types helped to support different events listeners and user interaction, different templates - diff looks.
collection as a tool to fetch initial data + load extra items + arrange mansonry view + create models according to fetched result.
UPDATE
Lets consider this pseudo code as masnonry implementation:
Models may looks like these:
var MansonryModel = Backbone.Model.extend({
/* Common methods and properties */
}),
MansonryVideoModel = MansonryModel.extend({
defaults: {
type: 'video',
videoUrl: '#',
thumbnail: 'no-cover.jpg'
}
}),
MansonryImageModel = MansonryModel.extend({
defaults: {
type: 'image',
pictureUrl: '#',
alt: 'no-pictire'
}
});
var MansonryCollection = Backbone.Collection.extend({
model: MansonryModel
});
Views could be like this:
var MansonryTileView = Marionette.ItemView.extend({
/* place to keep some common methods and properties */
}),
MansonryVideoTile = MansonryTileView.extend({
template: '#video-tile',
events: {
'click .play': 'playVideo'
},
playVideo: function(){}
}),
MansonryImageTile = MansonryTileView.extend({
template: '#image-tile',
events: {
'click .enlarge': 'enlargePicture'
},
enlargePicture: function(){}
});
var MansonryListView = Marionette.CollectionView.extend({
childView : MansonryItem
})
Hope this help
My goal
I need to create a custom layout (a flow layout) that can receive a variable numbers of views and based on them, it creates regions as necessary and within those regions it shows the views that are passed in. The views can be arranged vertically or horizontally.
Requirement
The layout has a template where initially regions are not defined. It only contains a wrapper (data-role="region-wrapper") where added regions will be rendered.
My approach.
1 - Extend a Marionette.Layout (obviously)
2 - Ovveride the construtor like the following
constructor: function(options) {
// call super here...
this.viewList= options.viewList || [];
this._defineRegions(); // see 3
}
3 - Define the regions dynamically
_defineRegions: function() {
_.each(this.viewList, function(view, index) {
var name = 'flowRegion_' + index;
var definition = { selector: "[data-region='flow-region-" + index + "']" };
this.addRegion(name, definition);
}, this);
},
4 - Render regions and views in onRender method within the same layout
onRender: function() {
_.each(this.viewList, function(view, index) {
// if the view has not been instantiated, instantiate it
// a region is a simple div element
var $regionEl = // creating a region element here based on the index
// append the region here
this.$el.find("[data-role='flow-wrapper']").append($regionEl);
var region = this.getRegion(index); // grab the correct region from this.regionManager
region.show(view);
}, this);
}
This solution seems working but I would like to know if I'm following a valid one or not. Any other approach to follow?
Maybe I'm not fully followed, but I can't see any reason a collectionView can't be used in this case.
The child views are all in same pattern, having data-region='flow-region-", and even have index. This is an obvious pattern of collection and its view.
With collectionView you can do things similar, adding/removing child views, fully reset, close etc.
If this is the case I would definitely recommend to use CollectionView or CompositeView, instead of overriding Region here.
Update
About why removing a model will cause removing view.
Because Marionette CollectionView has such listener in constructor:
this.listenTo(this.collection, "remove", this.removeItemView, this);
Add region in runtime.
It's totally legit though I have not done that before. Layout has prototype method addRegion(name, definition), so any instance of layout should be able to add/remove region/regions in runtime. The usage would be like this:
foo_layout.addRegion({region1: '#region-1'});
Question:
How can I get list of nested views (as they are defined in template) and reorder them? Or move view from one parentView to another?
For example, I'd like to switch places column with date and column with image, or hide any of them on user action
{{#data-grid}}
{{#grid-column}}
{{format-date date}}
{{/grid-column}}
{{#grid-column}}
{{#link-to 'somewhere'}}<img scr="i.png" title="hello"/>{{/link-to}}
{{/grid-column}}
{{/data-grid}}
Reason:
I'm implementing datagrid with reordering and hiding collumns in runtime. Declaring view classes for all cases and then using them in controller seems ugly to me.
Already tried to use ContainerView but could not find the way to fill childViews with template contents
UPDATE
Source code of data grid in current state: http://pastebin.com/E61e6WCt
If you want to implement this yourself, you should have a look at the CollectionView. Each of your columns should be one item in the content array of your view. Reordering the array should also reorder the DOM elements correctly.
Here is a rough sketch: Basically you are overriding createChildView within your subclass. You could pass strings into the content array indicating their type. Within createChildView you then can access the current item via the attrs object and its content property.
App.ColumnsCollectionView = Ember.CollectionView.extend({
content : ["date", "image"],
createChildView: function(viewClass, attrs) {
var itemFromContent = attrs.content; // is either 'date' or 'image'
if (itemFromContent == 'date') {
viewClass = App.YourDateColumnView;
} else {
viewClass = App.YourImageColumnView;
}
return this._super(viewClass, attrs);
}
});
Hi all I am creating my first Backbone.js app. It is basically a collection that renders the data in a table. What I want to do is to be able to filter and sort data.
What is the best way to do it? Should I use the Router or store some params that render will take into consideration.
I think the Router will get really complex soon as I am going to have 3-4 filters and 1 order option.
What do you think?
In my backbone-based project, I've subclassed Backbone.Collection to allow Controller to add arbitrary GET parameters to it.
Here is a snippet for you:
RailsCollection = Backbone.Collection.extend({
initialize: function() {
_.bindAll(this, 'url');
},
url: function() {
var base = this.baseUrl || this.model.prototype.baseUrl;
if(!this.params) {
return base;
} else {
return base + '?' + $.param(this.params);
}
}
});
I would add methods on my collections for the filtering and sorting, and using a view to just render an arbitrary collection, that might be filtered or ordered.
For ordering there is a built in hook: http://documentcloud.github.com/backbone/#Collection-comparator
For filtering, check out the underscore helper methods on collections, and extend with your own.
You could for example have a collection.doFiltering([filter1, filter2, filter3]);
that returns a filtered array.
I understand how to get a collection together, or an individual model. And I can usually get a model's data to display. But I'm not clear at all how to take a collection and get the list of models within that collection to display.
Am I supposed to iterate over the collection and render each model individually?
Is that supposed to be part of the collection's render function?
Or does the collection just have it's own view and somehow I populate the entire collection data into a view?
Just speaking generally, what is the normal method to display a list of models?
From my experience, it's the best to keep in your collection view references to each model's view.
This snippet from the project I'm currently working on should help you get the idea better:
var MyElementsViewClass = Backbone.View.extend({
tagName: 'table',
events: {
// only whole collection events (like table sorting)
// each child view has it's own events
},
initialize: function() {
this._MyElementViews = {}; // view chache for further reuse
_(this).bindAll('add');
this.collection.bind('add', this.add);
},
render: function() {
// some collection rendering related stuff
// like appending <table> or <ul> elements
return this;
},
add: function(m) {
var MyElementView = new MyElementViewClass({
model: m
});
// cache the view
this._MyElementViews[m.get('id')] = MyElementView;
// single model rendering
// like appending <tr> or <li> elements
MyElementView.render();
}
});
Taking this approach allows you to update views more efficiently (re-rendering one row in the table instead of the whole table).
I think there are two ways to do it.
Use a view per item, and manipulate the DOM yourself. This is what the Todos example does. It's a nice way to do things because the event handling for a single model item is clearer. You also can do one template per item. On the downside, you don't have a single template for the collection view as a whole.
Use a view for the whole collection. The main advantage here is that you can do more manipulation in your templates. The downside is that you don't have a template per item, so if you've got a heterogeneous collection, you need to switch in the collection view template code -- bletcherous.
For the second strategy, I've done code that works something like this:
var Goose = Backbone.Model.extend({ });
var Gaggle = Backbone.Collection.extend({
model: Goose;
};
var GaggleView = Backbone.View.extend({
el: $('#gaggle'),
template: _.template($('#gaggle-template').html()),
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
}
};
var g = new Gaggle({id: 69);
g.fetch({success: function(g, response) {
gv = new GaggleView({model: g});
gv.render();
}});
The template code would look something like this:
<script type="text/template" id="gaggle-template">
<ul id="gaggle-list">
<% _.each(gaggle, function(goose) { %>
<li><%- goose.name %></li>
<% }); %>
</ul>
</script>
Note that I use the _ functions (useful!) in the template. I also use the "obj" element, which is captured in the template function. It's probably cheating a bit; passing in {gaggle: [...]} might be nicer, and less dependent on the implementation.
I think when it comes down to it the answer is "There are two ways to do it, and neither one is that great."
The idea of backbone is that view rendering is event driven.
Views attach to Model data change events so that when any data in the model changes the view updates itself for you.
What you're meant to do with collections is manipulate a collection of models at the same time.
I would recommend reading the annotated example.
I've also found this a confusing part of the Backbone framework.
The example Todos code is an example here. It uses 4 classes:
Todo (extends Backbone.Model). This represents a single item to be todone.
TodoList (extends Backbone.Collection). Its "model" property is the Todo class.
TodoView (extends Backbone.View). Its tagName is "li". It uses a template to render a single Todo.
AppView (extends Backbone.View). Its element is the "#todoapp" . Instead of having a "model" property, it uses a global TodoList named "Todos" (it's not clear why...). It binds to the important change events on Todos, and when there's a change, it either adds a single TodoView, or loops through all the Todo instances, adding one TodoView at a time. It doesn't have a single template for rendering; it lets each TodoView render itself, and it has a separate template for rendering the stats area.
It's not really the world's best example for first review. In particular, it doesn't use the Router class to route URLs, nor does it map the model classes to REST resources.
But it seems like the "best practice" might be to keep a view for each member of the collection, and manipulate the DOM elements created by those views directly.