Share some logic between Backbone.Marionette views - backbone.js

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({...});

Related

Using regions in a CompositeView for subview

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

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.

How to handle click events of similar but different views?

I'm trying to implement a simple toolbar using Backbone.js. I have the following simple Backbone code:
var Toolbox = Backbone.View.extend({
el: $("#toolbox ul"),
initialize : function() {
_.bindAll(this, "addOne");
toolCollection.each(this.addOne);
},
addOne : function(tool) {
var view = new ToolView({ model: tool });
$(this.el).append(view.render().el);
}
});
// Tool model and collections
var Tool = Backbone.Model.extend();
var toolCollection = new Backbone.Collection([
new Tool({
tool: "toolName1"
}),
new Tool({
tool: "toolName2"
})
]);
// The view of the individual tools
var ToolView = Backbone.View.extend({
tagName: "li",
template : _.template($("#tool-template").html()),
events: {
"click #toolbox ul li a": "toolClick"
},
initialize : function() {
_.bindAll(this, "render", "toolClick");
this.model.view = this;
},
render : function() {
var mj = this.model.toJSON();
$(this.el).html(this.template(mj));
return this;
},
toolClick : function() {
console.log("Tool clicked");
}
});
var tb = new Toolbox;
So, with this, I have a question. I obviously need to handle each click on a tool differently.
When I instantiate my view, can I bind a specific click event to handle the click of that specific tool, and if so, where would I write the click event at? I'm not even sure if I'm missing something here, but could anyone suggest a pattern of how I can have a group of related, but different views and how to handle the click of view separately? Any help would be appreciated. Thanks in advance.
I hope I understood you right.
You have a toolbox with different tools. And of course you have to handle clicks on different tools differently.
So, why don't you have all the click events in the ToolBox view with IDs attached to the tools.
events: {
"click #toolbox #zoom": "zoomClick",
"click #toolbox #pen": "penClick",
"click #toolbox #line": "lineClick"
}
You can have the tools created in the ToolBox render() function. Hope that helps.
Another way to handle this. You are already capturing the click on the individual tools and passing it to the toolclick function. The function is aware of the model that was clicked you could just pass it to a switch statement to create your separate behavior.
toolClick: function() {
var toolname = this.model.get("tool");
switch(toolname) {
case "toolName1":
//Do something;
break;
case "toolName2":
//Do something else;
break;
}
}
This way you don't have to do a bunch of prep in render or templates.
If your tool is the same but does something different than the other tools, then you need to create that separate tool by extending your "vanilla" tool. With extend you can either add new properties and functions or override them entirely.
var ExtendedToolView = ToolView.extend({
toolClick: function() {
console.log("Extended Tool clicked");
}
});

Resources