Managing order of nested view defined in template - javascript

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);
}
});

Related

Rendering more than just model attributes in Parse JS API (backbone.js) view templates

So I'm trying to use a join table to display a list of data in my Parse app. The javascript API is similar enough to backbone.js that I'm assuming anyone who knows that could help me. I can't show my actual source code but I think I simple twitter-like "user follows user" scenario can answer my question. So assume I have a join table called "follows" that simply contains its own objectId, the id of each user in the relationship, and some meta-data about the relationship (needing metadata is why I'm using a join table, instead of Parse.Relation). I want to have a view that finds all of the users the current user follows and renders an instance of another view for each case. From what I have so far, that would looks something like this.
In the intialize of the top level view (let's call it AllFollowsView), I would have something like this.
var currentUser = Parse.User.current();
var followsQuery = new Parse.Query(Follows);
followsQuery.equalTo("userId", currentUser.id);
followsQuery.find({
success: function(followsResult){
for (var i = 0; i < followsResult.length; i++){
var view = new OneFollowView({model:followsResult[i]});
this.$("#followed-list").append(view.render().el);
}//for loop
},
error: function(error){
console.log("error finding plans query");
}
});
OneFollowsView is just a view that renders an showing data about the relationship and listens for changes on that particular relationship (mainly change or delete in my case). I understand that by passing in the corresponding model with
var view = new OneFollowView({model:followsResult[i]});
I can print out attributes of that model in the OneFollowsView template like this
<li>You are following a user with the id of <%= _.escape(followedUserId) %></li>
My problem is that this only gives me access to the information stored in the "follows" object. How would I pass in the corresponding user models (or any other models that I can query for the id of) into the template so I can access them in the html in the same way. I would like to be able to run queries in one of the views and then access those models in the html. I know I can add attributes to the object before declaring a new instance of the lower level class with that object as the model, but that doesn't help me because I don't want to save it with new attributes attached.
EDIT: My render function for the top level function is empty at the moment. It's initilize function contains this line to render the template. I guess this should probably be in the render function and then I would call render from initialize.
this.$el.html(_.template($("#all-follows-template").html()));
Here's the render for the lower (individual li) view
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
this.delegateEvents();
}
From my understanding this just renders the template to el while parsing the model to JSON and then returns to allow chained calls.
The problem here lies in you render method. When you call this.template in your render method. That method, this.template is a template function returned by calling the _.template function. When you call your this.template method, the properties of the object you pass in will be available as instance variables in your template.
In your case you're passing in the JSON of the object. So, the properties of the model become names of variables available in your template. If you want to expose additional variables to the template you have a couple options: 1) Add to the jsonified model's attributes. 2) Send in the model as a top level variable and any additional variables you may want.
// option 1
render: function() {
var templateArgs = _.extend(this.model.toJSON(), { additionalVar: 'new var' });
var content = this.template(templateArgs);
$(this.el).html(content);
this.delegateEvents();
return this;
}
// option 2
render: function() {
var templateArgs = {
followResult: this.model.toJSON(),
additionalVar: 'new var'
};
var content = this.template(templateArgs);
$(this.el).html(content);
return this;
this.delegateEvents();
}
Either option is reasonable. I would probably go with option 2. Which allows you in the template to say something like:
<li> <%= followResult.someProperty %> <%= additionalVar %> </li>
Hope that helps. :)

Creating a layout that accepts a variable number of views (and hence regions)

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'});

Using Marionette to group items in a collection view

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);
};

data-win-bind issues: converter only runs once and unable to bind id of element

I have the following html that is bound to an object containing id and status. I want to translate status values into a specific color (hence the converter function convertStatus). I can see the converter work on the first binding, but if I change status in the binding list I do not see any UI update nor do I see convertStatus being subsequently called. My other issue is trying to bind the id property of the first span does not seem to work as expected (perhaps it is not possible to set this value via binding...)
HTML:
<span data-win-bind="id: id">person</span>
<span data-win-bind="textContent: status converter.convertStatus"></span>
Javascript (I have tried using to modify the status value):
// persons === WinJS.Binding.List
// updateStatus is a function that is called as a result of status changing in the system
function updateStatus(data) {
persons.forEach(function(value, index, array) {
if(value.id === data.id) {
value.status = data.status;
persons.notifyMutated(index);
}
}, this);
}
I have seen notifyMutated(index) work for values that are not using a converter.
Updating with github project
Public repo for sample (not-working) - this is a really basic app that has a listview with a set of default data and a function that is executed when the item is clicked. The function attempts to randomize one of the bound fields of the item and call notifyMutated(...) on the list to trigger a visual updated. Even with defining the WinJS.Binding.List({ binding: true }); I do not see updates unless I force it via notifyReload(), which produces a reload-flicker on the listview element.
To answer your two questions:
1) Why can't I set id through binding?
This is deliberately prevented. The WinJS binding system uses the ID to track the element that it's binding to (to avoid leaking DOM elements through dangling bindings). As such, it has to be able to control the id for bound templates.
2) Why isn't the converter firing more than once?
The Binding.List will tell the listview about changes in the contents of the list (items added, removed, or moved around) but it's the responsibility of the individual items to notify the listview about changes in their contents.
You need to have a data object that's bindable. There are a couple of options:
Call WinJS.Binding.as on the elements as you add them to the collection
Turn on binding mode on the Binding.List
The latter is probably easier. Basically, when you create your Binding.List, do this:
var list = new WinJS.Binding.List({binding: true});
That way the List will call binding.as on everything in the list, and things should start updating.
I've found that if I doing the following, I will see updates to the UI post-binding:
var list = new WinJS.Binding.List({binding: true});
var item = WinJS.Binding.as({
firstName: "Billy",
lastName: "Bob"
});
list.push(item);
Later in the application, you can change some values like so:
item.firstName = "Bobby";
item.lastName = "Joe";
...and you will see the changes in the UI
Here's a link on MSDN for more information:
MSDN - WinJS.Binding.as
Regarding setting the value of id.
I found that I was able to set the value of the name attribute, for a <button>.
I had been trying to set id, but that wouldn't work.
HTH
optimizeBindingReferences property
Determines whether or not binding should automatically set the ID of an element. This property should be set to true in apps that use Windows Library for JavaScript (WinJS) binding.
WinJS.Binding.optimizeBindingReferences = true;
source: http://msdn.microsoft.com/en-us/library/windows/apps/jj215606.aspx

backbone.js - collections and views

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.

Categories