Using regions in a CompositeView for subview - backbone.js

I'm using a CompositeView to create a grid of images that they had some events on it. This is how it looks like:
Backbone.Marionette.CompositeView.extend({
events: {
'click li.feed-thumb': 'clickElement',
},
template: _.template(template),
itemView: ItemFeedView,
itemViewContainer: "#feed ul.feed",
clickElement: function(event) {
var profile = new ProfileFeedView();
}
});
My template for this CompositeView contains a <li> element that will render the profile when I click on a image. I use the same <li> for all the events of click into a image. I would like to handle this as a region, because I understand that doing it as region Marionette will handle the opening and closing of the views.
I think CompositeView do not support a regions: {profileRegion: '#feed-profile'}, what's my options?
Thanks in advance!

You should use a Layout View in which you can specify as many regions as you want, so you can create a list region in which you can put your composite view and a profile region in which you can put a item view that will render the profile.
Marionette's docs -- Layout View

If for some reason you want to have regions in your CompositeView you can also do something like this:
var YourView = Backbone.Marionette.CompositeView.extend({
regions: {
"someRegion": ".someRegionClass"
},
"initialize": function(options) {
this._initializeRegions(options);
},
"onDestroy": function() {
this.regionManager.destroy();
}
})
_.each(["_initializeRegions", "_initRegionManager",
"_buildRegions", "addRegion", "addRegions",
"removeRegion", "getRegion", "getRegions",
"_reInitializeRegions", "getRegionManager"], function(prop) {
PaginatorView.prototype[prop] = Marionette.LayoutView.prototype[prop];
});
To be honest, it works but i haven't tested it for full functionality.
view.someRegion.show(otherView) works.
(also works for other views i guess and you will have to add your other options needed to extend the view of course)

In addition to what Manfred said I implemented it this way on a Marionette Composite View:
View.ListView = Marionette.CompositeView.extend({
template: listTpl,
emptyView: noItemsTpl,
childView: View.ListItem,
childViewContainer: '#items-list',
regions: {
"someRegion": "#someRegion"
},
initialize: function(options) {
//give this composite view a LayoutView behaviour with added region manager
this.regionManager = new Marionette.RegionManager();
_.each(["_initializeRegions", "_initRegionManager",
"_buildRegions", "addRegion", "addRegions",
"removeRegion", "getRegion", "getRegions",
"_reInitializeRegions", "getRegionManager"], function(prop) {
Marionette.CompositeView.prototype[prop] = Marionette.LayoutView.prototype[prop];
});
var that = this;
_.each(this.regions, function(value, key) {
var region = that.addRegion(key, value);
that[key] = region;
});
},
onDestroy: function() {
this.regionManager.destroy();
}
});
This way you will be able to interact with your CompositeView instance the exact same way you do with a LayoutView instance:
var listView = new View.ListView({ ... });
var anotherView = new View.AnotherView({ ... });
listView.someRegion.show(anotherView);

Related

Share some logic between Backbone.Marionette views

Previously, I made a backbone view for handling a toggle button:
Star = Backbone.View.extend({
events: {
'click': 'toggle'
},
toggle: function() {
this.$('i').toggleClass('icon-star').toggleClass('icon-star-empty');
},
status: function() {
return this.$el.hasClass('active');
}
});
And i was using this subview in my views like this:
initialize: function() {
var star = new Star({ el: this.$('.new .btn.star') });
// ...
}
This way i can reuse this subview in many other independent views in backbone. (FYI: I'm no backbone expert. This code also can be wrong. Please correct me if it's wrong.)
Now i'm trying to learn Backbone.Marionette and i couldn't find a good way to accomplish same functionality. How can i use this view in my ItemViews and/or CompositeViews?
You can keep the exact same idea, just extend (e.g.) a Marionette ItemView:
Star = Marionette.ItemView.extend({...});
and then
var star = new Star({ el: this.$('.new .btn.star') });
Another option is to extend your Star view:
MyView = Star.extend({...});

low coupling: add a model to a collection of a different view

i'm building a Backbone/Marionette application to list different sets of cards. the layout has an ItemView on the left side including an input field to add a new set and a CompositeView on the right side to list the card sets.
Cards.module("Set.SideBar", function(SideBar, App) {
SideBar.SideBarView = Backbone.Marionette.ItemView.extend({
template: "#set-sideBar",
className: "well sidebar-nav",
ui: {
saveBtn: "a.saveSet",
setName: "input[type=text]"
},
events: {
"click .saveSet": "saveSet"
},
saveSet: function(ev) {
ev.preventDefault();
var newSetName = this.ui.setName.val().trim();
var newSet = new Cards.Entities.Set({ name: newSetName });
newSet.save();
// How to add the model to the collection?
}
});
});
i'm looking for the best way to add the newSet to the collection of the CompositeView below. is there any clean low coupling solution to deal with that? i'm quite new to backbone.js and can't imagine that this is something totally unordinary, but somehow i'm not able to find an answer to my question in the regarding docs - or just dont understand them.
Cards.module('Set.List', function(List, App) {
List.SetItemView = Backbone.Marionette.ItemView.extend({
tagName: "tr",
template: "#set-list-item"
});
List.SetView = Backbone.Marionette.CompositeView.extend({
tagName: "table",
className: "table table-bordered table-striped table-hover",
template: "#set-list",
itemView: List.SetItemView,
itemViewContainer: "tbody",
modelEvents: {
"change": "modelChanged"
},
initialize: function() {
this.collection.fetch();
}
});
});
thanks in advance for your help!
how i'm doing it now:
thanks for both answers, they were guiding me in the right direction. the collection.create hint was also very useful and solved another problem i was facing!
inside a Marionette.Controller i do something like this and simply share the collection reference:
var setLayout = new Cards.Set.Layout();
Cards.mainRegion.show(setLayout);
var sets = new Cards.Entities.SetCollection();
var listView = new Cards.Set.List.SetView({ collection: sets });
setLayout.listRegion.show(listView);
var sideBarView = new Cards.Set.SideBar.SideBarView({ collection: sets });
setLayout.sideBarRegion.show(sideBarView);
and the new model is simply added by collection.create instead of .save() and .add().
Backbone.Collection.add can be used to add a model to an existing backbone collection.
http://backbonejs.org/#Collection-add
Also, look in to Collection.Create - http://backbonejs.org/#Collection-create
If your model is being persisted, then immediately added to the collection, you can skip your model.save() then collection.add() and just use collection.create(model)
Edit: And as already mentioned, make the collection instance visible from the sidebar view
To keep views decoupled, you can raise events from one view that other view(s) can listen to and handle however they please.

Creating backbone views with models from other views

Background:
I am making changes to an application that uses backbone.js with Handlebars as the templating engine. After a change event fires I need to create html that is appended to the current DOM structure which is basically just a spit-out of information that is contained in the model. This change needed to fit in the already established application structure.
Issue:
I have created a new view that uses a Handlebars template and the model to create the html. I then instantiate that view and call the render function and append the output using JQuery. What I am noticing is that when the html is rendered the model that is passed in because attributes on the $el instead of filling in the template (like I think it should).
View I'm altering:
$.hart.TestView = Backbone.View.extend({
tagName: "li",
template: Handlebars.compile($('#templateOne').html()),
initialize: function () {
this.model.on('change', function () {
this.createMoreInfoHtml();
}, this);
},
selectSomething: function () {
this.$el.removeClass('policies');
this.createMoreInfoHtml(); //function created for new view stuff
},
createMoreInfoHtml: function () {
var id = this.$el.attr('data-id', this.model.get("ID"));
$('.info').each(function () {
if ($(this).parent().attr('data-id') == id
$(this).remove();
});
var view = new $.hart.NewView(this.model, Handlebars.compile($("#NewTemplate").html()));
$('h1', this.$el).after(view.render().el);
},
render: function () {
... //render logic
}
});
View I Created:
$.hart.NewView = Backbone.View.extend({
initialize: function (model, template) {
this.model = model;
this.template = template;
},
render: function () {
this.$el.html(this.template({ info: this.model }));
this.$el.addClass('.info');
return this;
}
});
Json the is the model:
{
"PetName":"Asdfasdf",
"DateOfBirth":"3/11/2011 12:00:00 AM",
"IsSpayNeutered":false,
"Sex":"F",
"SpeciesID":2,
"ID":"ac8a42d2-7fa7-e211-8ef8-000c2964b571"
}
The template
<script id="NewTemplate" type="text/html">
<span>Pet Name: </span>
<span>{{this.PetName}}</span>
</script>
So now to the question: What am I doing wrong? Why are the properties of the model being created as attributes on the $el instead of filling in the template? Can someone please direct me as to how to get the results I am looking for?
Let's skip the problem Jack noticed.
The way you're creating your view is just wrong. It may work as you get the expected arguments in the initialize function, but it has unexpected behaviors you don't see. See the View's constructor:
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
Now let's have a look at this _configure method:
_configure: function(options) {
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
_.extend(this, _.pick(options, viewOptions));
And of course...
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
Ok here we are... Basically when passing the model as the options argument, you're passing an object with an attributes key (the attributes of your model). But this attributes key is also used in the View to bind attributes to its element! Therefore the behavior your noticed.
Now, other wrong thing. You're compiling your template each time you create a new function, but not using it as a singleton either. Put your template in the view:
$.hart.NewView = Backbone.View.extend({
template: Handlebars.compile($("#NewTemplate").html(),
And change your view's creation to make the whole thing work:
new $.hart.NewView({model: this.model});
Oh, and get rid of this useless initialize method. You're just doing things Backbone already does.

Backbone.Marionette: CompositeView disappear after collection.reset() is fired

I'm quite new in the world of Backbone and I decided to use Marionette for my first serious project with it.
With some difficulties I managed to set up my app's basic options and routing and I was pretty happy with it, but now I'm facing a blocking problem with a CompositeView that represent a Table.
This View is rendered inside a region of a specific layout, called "grid". This layout has 3 region: the top_controls, table_view and bottom_controls. Since I needed to bind some action on some of the elements of the layout I decided to use it as a View, and to include the "master" collection inside it, so I can just rendered a filtered version of the collection inside the CompositeView, without touching the main one.
From my router I call it in this way:
App.grid = new Grid({collection: Clt});
App.page.show(App.grid);
The structure of the layout is this (I'm using requireJS):
var Grid = Backbone.Marionette.Layout.extend({
className: "container-fluid",
template: gridLayout,
regions: {
top_controls: "#top_controls",
table_view: "#table_view",
bottom_controls: "#bottom_controls",
},
initialize: function(){
this.renderTable(this.collection, true);
},
renderTable: function(collection, fetch){
if(fetch){
collection.fetch({success:function(){
var vista = new CompView({collection: collection});
App.grid.table_view.show(vista);
}});
} else {
var vista = new CompView({collection: collection});
App.grid.table_view.show(vista);
}
},
events: {
"keyup input":"filter_grid"
},
filter_grid: function(e){
var $el = e.currentTarget;
var to_filter = $($el).val();
if(to_filter==""){
this.renderTable(this.collection, false);
} else {
var filtered = this.collection.filter(function(item){
return item.get("link_scheda").toLowerCase() == to_filter;
});
if(filtered.length>0){
var filtro = new AssocCollection();
filtro.reset(filtered);
this.renderTable(filtro, false);
}
}
}
});
return Grid;
The Layout template looks like this:
<div class="row-fluid" id="top_controls"><input type="text" id="filter" class="input"/></div>
<div class="row-fluid" id="table_view"></div>
<div class="row-fluid" id="bottom_controls"><button class='add btn btn-primary'>Add</button></div>
My CompositeView is structured like that:
var AssocView = Backbone.Marionette.CompositeView.extend({
tagName: 'table',
className: 'table table-bordered table-striped',
id: 'tableAssoc',
template: assocTemplate,
itemView: assocRow,
appendHtml: function(collectionView, itemView, index){
collectionView.$("tbody").append(itemView.el);
},
events: {
"click .sort_link":"sort_for_link",
},
sort_for_link: function(){
this.collection.comparator = function(model){
return model.get("link_value");
}
this.collection.sort();
},
onRender: function(){
console.log("render table!");
}
});
return AssocView;
The first display of the table is done right, and the filtering too. The problem occur when
I click the table header with the class "sort_link": the entire Table is wiped away from the HTML while the collection stay the same (I suppode the entire layout is re-rendered). If for example I render the CompositeView in another place, like the app main region, it all works as intended. So I guess to problem it's located inside my Layout declaration.
Any help will be much appreciated!
In your Grid, you need to override the initialEvents method and don't do anything in it.
Grid = Backbone.Marionette.Layout.extend({
initialEvents: function(){},
// ... everything you already have
});
Layout extends from ItemView, and ItemView provides the initialEvents implementation. This method checks to see if it was given a collection, and if it does, it wires up the collection "reset" event to the "render" method of the view. In your case, you are passing the collection through and don't want this behavior. So, overriding the initialEvents method will correct it.
Update: I thought I had removed that initialEvents a long time ago. If you're keeping up to date w/ Marionette versions, grab v0.9.10 (or whatever the latest is) and this problem is gone now.

Backbone view event atacched to all views

I'm doing my first application in backbone and i get a strange thing happening trying to attach an event.
I got this code so far:
//View for #girl, EDIT action
GirlEditView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
console.log(attr);
this.render();
},
render: function() {
var template = _.template( $("#girl_edit").html(), this.variables );
$(this.el).html( template );
$("#edit_girl").modal('show');
}
});
//View for #girl
GirlView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
this.render();
},
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
},
events: {
"click p.modify": "modify"
},
modify: function() {
//calls to modify view
new GirlEditView({el : $("#edit_girl")}, this.variables);
}
});
//One girl from the list
Girl = Backbone.Model.extend({
initialize: function() {
this.view = new GirlView({el : $("#content")}, this.attributes );
}
});
//all the girls
Girls = Backbone.Collection.extend({
model: Girl,
});
//do magic!
$(document).ready(function() {
//Underscore template modification
_.templateSettings = {
escape : /\{\[([\s\S]+?)\]\}/g,
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
}
//get initial data and fill the index
var list = [];
$.getJSON('girls.json', function(data) {
list = [];
$.each(data, function(key, val) {
list.push( new Girl(val) );
});
var myGirls = new Girls(list);
console.log( myGirls.models);
});
});
As you can see.
I'm using a collection to store all the girls and the data comes from a REST api in ruby.
Each girls create a new model instance and inside i attached a view instance.
I don't know if it's a good practice but i can't think a better way to do it.
Each view makes a content with a unique id. girl-1 girl-2 and go on.
Now, the template have a edit button.
My original idea is to attack the onclick event and trigger the edit view to get rendered.
That is working as expected.
The proble so far is:
When the events triggers, all the collection (girls) fire the edit view, not the one that "owns" the rendered view.
My question is what i'm doing wrong?
Thanks a lot
All the edit-views come up because all the GirlViews are using the same el:
this.view = new GirlView({el : $("#content")}, this.attributes );
and then you render be appending more HTML:
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
}
Backbone events are bound using delegate on the view's el. So, if multiple views share the same el, you'll have multiple delegates attached to the same DOM element and your events will be a mess of infighting.
You have things a little backwards: models do not own views, views watch models and collections and respond to their events. You'll see this right in the documentation:
constructor / initialize new View([options])
[...] There are several special options that, if passed, will be attached directly to the view: model, collection, [...]
Generally, you create a collection, c, and then create the view by handing it that collection:
var v = new View({ collection: c })
or you create a model, m, and then create a view wrapped around that model:
var v = new View({ model: m })
Then the view binds to events on the collection or model so that it can update its display as the underlying data changes. The view also acts as a controller in Backbone and forwards user actions to the model or collection.
Your initialization should look more like this:
$.getJSON('girls.json', function(data) {
$.each(data, function(key, val) {
list.push(new Girl(val));
});
var myGirls = new Girls(list);
var v = new GirlsView({ collection: myGirls });
});
and then GirlsView would spin through the collection and create separate GirlViews for each model:
var _this = this;
this.collection.each(function(girl) {
var v = new GirlView({ model: girl });
_this.$el.append(v.render().el);
});
Then, GirlView would render like this:
// This could go in initialize() if you're not certain that the
// DOM will be ready when the view is created.
template: _.template($('#girl_template').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON());
return this;
}
The result is that each per-model view will have its own distinct el to localize the events. This also makes adding and removing a GirlView quite easy as everything is nicely wrapped up in its own el.

Resources