Please consider following scenario:
<ParentView>
<FilterSubview></FilterSubview>
<ListSubview></ListSubview>
</ParentView>
To give you and example: I have a view which in turn shows view with filter (user can select to display books, magazines or both of them) and the list with items.
Both filter and list have corresponding models. Filter - what can we filter. List - list of all items.
Use case: user sees the full list and then can filter results by selecting only desired category.
Questions:
How those two views should interact? Should they know about each other or should parent view handle it?
Who should store filtered list to display? It could be list subview model directly or parent view can filter complete list and then pass it to render.
There is no one correct answer to your questions, but I'll try to explain a common, idiomatic way here.
Two sibling views should not know of each other. Instead they should interact via events through some kind of a mediator. Since in your case both FilterView and ListSubView share a common parent view which is responsible for rendering both of them, you could let the parent view mediate the events:
var ParentView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.filterView, "filter", this.filterChanged);
},
filterChanged: function(filterValue) {
this.listSubView.filter(filterValue);
}
});
var FilterView = Backbone.View.extend({
events: {
"change .filter" : "filterValueChanged"
},
filterValueChanged: function() {
var filterValue = //get filter value...
this.trigger("filter", filterValue);
}
});
Alternatively (preferrably, even) you can cut out a middle man and use the Mediator pattern. For that you need a third component whose job it is to pass messages between parties who should not know of each other. If you're using Backbone 0.9.9, there's just such a mediator built in: the Backbone root object works as a global event bus for this purpose.
So:
//ListSubView
this.listenTo(Backbone, "listfilterchanged", this.filterChanged);
//FilterView
Backbone.trigger("listfilterchanged", filterValue);
Then there's the question of who should be responsible of the list data. I tend to prefer to have the most specialized component be in charge, but so that only one component is in charge. In your case that would mean that the ListSubView should manage the filtered list, but only if the ParentView doesn't need to operate on it. That's just a generalization though, so take it with a grain of salt and do what feels right for your case.
Related
So I have a form that appears on a few pages and contains a number of groups of inputs. Say group a, group b and group c. On some pages they might have a and b and on others the form contains b and c. Each group may require its own client side custom validation maybe executed from the form controller.
What is the best way to achieve this using backbone and marionette?
Conceptually, and i'm fairly new to both, I'd assume I'd need a FormController that is instantiated from a page specific Controller which also instantiates the group controllers that I need for that page. Any advice would be great.
TL;DR
To make this work you would create two objects per input group. One for its custom functions, and one for its events. In the form controller you'd _.extend the function objects of each input group with a base form view to get the custom functions into your new form view. Next you'd pass in your custom events objects into the constructor for the new form view, where a utility function of your basic view would add the new custom events to the basic form view events. Finally, you'd have to ensure that you have a template for that form that contains the correct input groups.
I should also mention that you can accomplish this with Marionette.LayoutViews and dynamic regions, in a possibly cleaner way (you could specify an atomic template for each input group), albeit with a lot more overhead.
The expanded explanation
I've been toying with this idea a bit. I've been thinking a lot lately about using Underscore _.extend() to plug-in stock functionality to different views. Your problem would be ideal for this solution. Here's a sample of how you'd implement it.
Including functionality
Say that your input group A had a validate function that did it's thing,
validateA: function () {
// custom validation routine
},
and a submit function to handle form submissions,
submitA: function () {
// custom submit routine
}
The first thing you do is package these functions inside an object:
groupA = {
validateA: function() {...},
submitA: function() {...}
}
You'd have as many function objects as you have input groups.
You'd also build a generic form view that would house common form functionality, and which would also serve as the basic view you'd use to render your form
var GenericForm = Backbone.Marionette.ItemView.extend({...});
In here you'll put all your baseline events and functions common to all forms.
Then, as you mentioned you'd set up a FormController that would plug in the custom functionality, like this,
var formController = function () {
callFormZ: function() {
var genericForm = new GenericForm({ groupEvents: [eventsA, eventsB] });
var formZ = _.extend(genericForm, groupA, groupB);
SomeRegion.show(formZ);
}
}
By using _.extend on your basic view and your two input groups you end up with a new view, formZ that is a composite of the two input groups. If you'd look inside it would have all the functionality of GenericForm plus
{
validateA: function () { ... },
submitA: function () { ... },
validateB: function () { ... },
submitB: function () { ... },
}
Events
At this point, though there is no way to bind any events to your custom functions.
You may have noticed in the callFormZ controller that I passed in an array in a property called groupEvents. These are the custom events for your input groups. They'd normally have the form
eventsA: {
'blur .groupA': 'validateA',
'click #groupA button': 'submitA'
}
Ideally, we would just use extend to merge your events the way we did your functions. But since we want to end up with just one events property in your view, we run into a problem. _.extend will overwrite all the events properties in your view object with the events property of the last object passed into _.extend. So to get around this problem we have to pass an array of custom events, one for each form group.
First, you'd create a config array of events objects with your custom events for the particular form you're working with,
customEvents = [
eventsA: {
'blur .groupA': 'validateA',
'click #groupA button': 'submitA'
},
eventsB: {
'blur .groupB': 'validateB',
'click #groupB button': 'submitBA'
}];
You'd pass this array into your constructor, as we did in the callFormZ controller function. The array will now be loaded in your options parameter. In your initialize you can call something like this,
initialize(options) {
combineEvents(options.groupEvents);
this.delegateEvents();
}
where, combinedEvents is
combineEvents: function() {
var extendEvents = [this.eventsA, this.eventB]; // Make an array of the extended groups
_.each(extendEvents, function (extendEvent) {
for (prop in extendEvent)
this.event[prop] = extendEvent[prop];
}
}
Also, note that after combining events I called delegateEvents so that we'd rewire the new events.
Templates
To make all this work you'll have to provide a template that has the portions of each input group. I'm not aware of how we construct the template programmatically from individual input group templates. Instead, you'd have to have a template with the input groups relevant to the form.
Putting it all together
So, to make this work you would create two objects per input group. One for its custom functions, and one for its events. In the form controller you'd _.extend the function objects of each input group with a base form view to get the custom functions into your new form view. Next you'd pass in your custom events objects into the constructor for the new form view, where a utility function of your basic view would add the new custom events to the basic form view events. Finally, you'd have to ensure that you have a template for that form that contains the correct input groups.
I am building an app using backbone.js and find myself with a view with a lot of conditional logic in the templating of the Model's View. The type property of the model is used to determine which html to render. I would like to avoid this logic if possible, because it is hard to read. There are a couple of ways that I think I could deal with this (Note that the actual templates I've got here are very much simplified):
1. Conditional Logic in Collection View rather than in Model View - Multiple Subviews
I could put the conditional logic that acts on each model's type into the Collection View:
var CollectionView = Backbone.View.extend({
....
render: function() {
this.collection.each(function(thing) {
if(thing.get("type") === "wotsit") {
this.$el.append(new WotsitView({ model: thing });
} else if(thing.get("type") === "oojamaflip") {
this.$el.append(new OojamaflipView({ model: thing });
}
}, this);
},
....
}
Pros
This way I could have each subview with a template method that had no logic in it, but rather just builds html.
var WotsitView = new Backbone.View.extend({
....
template: _.template('<h2>{{ title }}</h2>');
});
var OojamaflipView = new Backbone.View.extend({
....
template: _.template('<h3>{{ title }}</h3>');
});
Cons
The thing is, the things in the collection are all very similar. The events for each thing are likely to be the same or very similar and I can see there being a lot of code duplication. I really only want the actual template for these subviews to be different with everything else the same.
2. Conditional Logic in Model View - Multiple Template Methods
var ModelView = Backbone.View.extend({
....
render: function() {
if(this.model.get("type") ==== "wotsit") {
this.$el.html(this.wotsitTemplate(this.model.attributes));
} else if(this.model.get("type") === "oojamaflip") {
this.$el.html(this.oojamaflipTemplate(this.model.attributes));
}
},
wotsitTemplate: _.template('<h2>{{ title }}</h2>'),
oojamaflipTemplate: _.template('<h3>{{ title }}</h3>')
});
Pros
There is only one view for the model.
All of the events etc are handles in one view rather that being duplicated.
Cons
I actually quite like this way, but I would be very interested to hear some other peoples options on it.
Option #1 = polymorphism
Option #2 = switch statement
Switch statements are useful if you never (or rarely) have to add new types, but you might want to add new methods. Imagine you want to add a validate method to ModelView, which checks view input for that kind of model for correctness and reports the results to the user. With option #2, you'd just add one new method that switches on the model type (just like the render method) to handle validation.
Now let's assume we already have a render method and a validate method, and we want to handle a new type of model, a thingamajig. With option #2, you'd have to add logic to both render and validate. Now imagine we don't have just 2 methods, but 10 — option #2 gets real complicated real quick when you have to handle new types. But if we followed option #1, then no matter how many methods there were, we'd only have to create a new view in one place, and update CollectionView to map the new type to the new view.
Most of the time polymorphism (option #1) is the clean way to go. It lets you separate all logic specific to a given type and put it one place, making it easy to handle new types.
Keeping DRY
If you're worried with option #1 that you'll end up with a lot of duplicate code between all the views, don't forget that it's easy to make Views that inherit from other Backbone Views:
var ItemView = Backbone.View.extend({
initialize: function() {
/* some common logic for all views */
}
});
var WotsitView = ItemView.extend({
template: _.template('<h2>{{ title }}</h2>')
});
var OojamaflipView = ItemView.extend({
template: _.template('<h3>{{ title }}</h3>')
});
I'm new to Backbone and I don't fully understand it yet, and I've come across a situation I can't find any documentation on. What I if I have a view that contains multiple views? For example, I have a view called StackView. The purpose of this view is to neatly lay out a set of cards. It manages the animation of adding, removing, and adjusting cards in the stack. Each card is a CardView. How would I handle this? I've seen people talk about views within views by simple creating a variable in the view and assigning the View instance to that variable. Should I just be adding an array of CardViews in a variable of a StackView?
That's what I do, and it works well. Here's a snippet of a View I use in an application. I've re-written it back into regular javascript from my coffeescript, so I apologize for any typos:
render: function() {
var _this = this;
this.$el.html(this.template());
this.listItemViews = [];
// for each model in the collection, create a new sub-view and add
// it to the parent view
this.collection.each(function(model){
var view = new App.Views.Projects.ListItem({model:model}); // create the new sub-view
_this.listItemViews.push(view); // add it to the array
_this.$('#project-table tbody').append(view.render().$el); // append its rendered element to the parent view's DOM
});
return this;
}
This lets my Table view maintain a reference to all the listItemView views.
Of course, if you do this, you should make sure to properly remove these child views and unbind any events when you remove the parent view.
Suppose you are making a music library app.
You have one view with a list on genres and another that shows the contents of the selected genre. When the user clicks a genre on the list, the contents in the other view should be updated accordingly.
What is the best way to do this so that there are minimal dependencies?
I have not found any other place to listen to the mouse clicks than the view that draws the individual genre. I can send an event from there, but what is the optimal way of getting that event to update the other view which draws the genre contents? Who should listen to that event, the genre content view or its collection?
EDIT: I instantiate both views from the router of the app and I did manage to get this to work by making the views aware of each other, but that's not optimal of course.
You could make a simple model to hold the application state, you don't need anything fancy, just a bag of data that implements the usual Backbone event methods:
var AppState = Backbone.Model.extend({});
var app_state = new AppState();
Then the genre list view would listen for click events (as you already have) and set the current genre on the app-state model when someone changes it:
var Genres = Backbone.View.extend({
//...
choose: function(ev) {
// This would be the click handler for the genre,
// `.html()` is just for demonstration purposes, you'd
// probably use a data attribute in real life.
app_state.set({genre: $(ev.target).html() });
},
});
The view for the individual genre would listen for "change:genre" events on the app-state model and react as the genre changes:
var Genre = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'change_genre');
app_state.on('change:genre', this.change_genre);
},
//...
change_genre: function() {
// Refill the genre display...
}
});
Demo: http://jsfiddle.net/ambiguous/mwBKm/1/
You can make models for any data you want and models are a convenient way of working with data events in Backbone. As an added bonus, this approach makes it fairly easy to persist your application's state: just add the usual Backbone persistence support to AppState and away you go.
If you only need a simple event bus to push non-data events around, you can use Backbone's Events methods to build a simple event aggregator:
app.events = _.extend({}, Backbone.Events);
Then, assuming you have a global app namespace, you can say things like this:
app.events.on('some-event', some_function);
and
app.events.trigger('some-event', arg1, arg2, ...);
The best way I have seen is in the method proposed by Derick Bailey. In short you can create an event aggregator, which provides a centralized object for raising events to which different views can subscribe. The beauty of this solution is that it is very simple to implement as it makes use of Backbone's existing event system.
I am very new at using Backbone. Please forgive me in advance as I am struggling to think in new ways building web apps.
I am confused about how to go about using it for items that are never really covered in any of the tutorials. All the tutorials give the basic "here is a model", "here is a collection of models", "here is a view that uses the model", etc. for entities that we all understand, such as a to-do item.
I do have those cases, and I am doing OK with those, but I am having trouble figuring out how to use Backbone for the following situation.
I have a to-do app (of course.) My UI needs to have several menus that allow the user to filter the to-dos by things like priority, due date, and other properties. So, my filter menus might look like this...
All To-Dos (100)
Inbox (15)
Important (10)
Someday (15)
Today (0)
Tomorrow (6)
This Week (7)
These are all somewhat static menus, except that when a filter is clicked, it should highlight, and possibly cause another filter to be turned off. Also, this would trigger an update in my results by performing a search and re-rendering my to-do list.
So, should I represent these items with views only? Are models needed to represent the state of the menus? Should I create a FilterMenuItem model and FilterMenu model, and also the corresponding views?
Again, I understand the samples when it comes to a model for a to-do item and a to-do collection, but I am stumped on how to tackle these seemingly simple items using Backbone.
I appreciate any suggestions or guidance.
The important thing to remember here is that collections in backbone.js inherit a bunch of cool features from underscore.js. Included in these is filter (or select), which allows you to get only those members of a collection which match your perameters. For example:
render: function(){
myCollection.filter(function(item){return item.folder === "inbox"});
}
If the menus are actually static, then you can use a case select statement to determine which page you are on & therefore which filter to use. Otherwise, you can have an array of objects representing the views, which describe how to filter, i.e.:
[
{view: "all", filter: function(item){return item;}}.
{view: "inbox", filter: function(item){return item.foler === "inbox";}},
{view: "important", filter: function(item){return item.type === "important;}}
]
As far as producing the view for your menu items goes, you have to decide if the the menu is static or not. If it is, then you can simply hard-code the items to different controller routes. If it is not, then you should probably use a collection of menuItem models:
var menuItem = Backbone.Model.extend({});
var menuList = Backbone.Collection.extend({
model: menuItem
});
var menu = new menuList([{name: "All To-Dos", url: "#!/all"}, {name: "index", url: "#!/index"}]);
which you can add or remove items to dynamically, as well as having the options built from the server specifications (i.e. the user may be able to save custom folders, which will get added here) use the refresh command to avoid unneccesary http calls. Then, you can pass this collection to your view, and render the items out however you want.
Hope this helps!