Assign a view template based on a variable in your data? - backbone.js

I'm using backbone and marionette and i'd like to render the views based on a variable in the data. this.model.template I was thinking could pull from my data (returning myTemplate and myOtherTemplate) and then I could do some manipulation in the render function but it's not working. Any suggestions?. Can the view be made aware of the model?
var graph = [{
nodeName: "1st level item",
template: "myTemplate",
nodes: [{
nodeName: "2nd level item",
template: "myOtherTemplate"
}]
}];
TreeView = Backbone.Marionette.CompositeView.extend({
tagName: "ul",
initialize: function(){
this.collection = this.model.nodes;
},
appendHtml: function(collectionView, itemView){
collectionView.$("li:first").append(itemView.el);
},
render: function(){
var that = this;
console.log('Loading template name: ' + name + ' template: ' + this.template + ' data: ' + this.model.template);
TemplateManager.get(this.template, function(template){
var html = $(template).tmpl();
that.$el.html(html);
});
return this;
}
});

How are you initializing the view?
The view generally expects that the model is a Backbone model. When accessing model attribute, you should use mymodel.get('attributeName'). The individual attributes are not available directly on the model. They're available in mymodel.attributes (for example, mymodel.attributes.template) but attributes should not be used directly except for debugging because how attributes are stored and accessed may change in the future or be changed by various plugins if you use any.
Also note that in most cases you shouldn't need to override the render method. Instead, take a look at beforeRender and onRender.

Related

Backbone Model Ids and on click events

Alright. I'm going to give in here and ask for some help. I think I'm running into multiple issues and not sure of the best approach. I'm using handlebars to create an ul of li that are my json objects to backbone models. Now that the template works I want to on click get the model to be able to add to another collection.
My first problem. I thought backbone defined a model id by default? If i set default to "" or null every model's id is "" or null. The json objects have an id I could assign to the backbone id but if I do that in the defaults id: jsonID, the jsonID is undefined. If I do object.toJSON() in the console there is no backbone created id. So I don't have a value to use in the handlebars template to assign my div id to the backbone id for that model. To then use that to get element id so that on click I could get the backbone id. Or at least I've read a lot of examples that do it that way.
My second issue, I think stems from requirejs. All the examples I see for click even use this.collection or this.model. On my View file those always return undefined, assuming this is because of requirejs. I tried this example in particular http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/. I'm wondering if I should just scrap using requirejs, it seems to cause more problems then help.
Here is my code so far, I deleted out my click function code because none of it was working.
Collection File:
define(['jquery', 'backbone', 'lodash', 'Models/GroceryItem'],
function($, Backbone, _, GroceryItem) {
var GroceryItems = Backbone.Collection.extend({
model: GroceryItem,
url: "data.json",
parse: function(response) {
return response.all_coupons;
}
});
var storeItems = new GroceryItems();
storeItems.fetch({
success:function(){
console.log(storeItems.toJSON());
}
});
return storeItems;
});
View File:
define(['jquery', 'backbone', 'lodash', 'handlebars', 'Collections/GroceryItems'],
function($, Backbone, _, Handlebars, storeItems) {
var GroceryItemsView = Backbone.View.extend({
template: Handlebars.compile(
'<ul class="d-row">' +
'{{#each storeItems}}' +
'<li class="lineItem" id="{{coupon_id}}">' +
'<div class="wrapper">' +
'<div class="header">{{coupon_title}}</div>' +
'<div class="column_wrapper">' +
'<div class="two-col">' +
'<div class="product_image"><img src="{{coupon_thumb}}" alt="{{coupon_description}}" height="110" width="110"></div>' +
'<div class="description">{{coupon_description}}</div>' +
'</div>' +
'</div>' +
'<div class="expiration">Valid From: {{valid_from}} to {{valid_to}}</div>' +
'</div>' +
'</li>' +
'{{/each}}' +
'</ul>'
),
events: {
"click li": "getModel"
},
getModel:function(e){
},
render: function() {
var that = this;
storeItems.fetch({
success: function(storeItems) {
var storeTemplate = that.template({storeItems: storeItems.toJSON()});
that.$el.html(storeTemplate);
return that;
}
})
return this;
}
});
return GroceryItemsView;
});
Thanks a bunch for any help. It's much appreciated. If I'm going at this completely wrong, I'm open to any suggestions. I'm just learning backbone and javascript in general so I'm grinding away as I go with a lot of googling.
Thanks!
EDITED CODE:
define(['jquery', 'backbone', 'lodash', 'Collections/GroceryItems', 'Views/GroceryItemView'],
function($, Backbone, _, storeItems, GroceryItemView) {
var GroceryItemsView = Backbone.View.extend({
tagName: 'ul',
className: 'd-row',
el: '#container',
initialize: function () {
//basically says only render when collection syncs
this.listenTo(storeItems, 'sync', this.render);
},
render: function () {
//got keep track of views for when you need close them (not important for now but you'll thank me later)
this.groceryItemsView = [];
storeItems.each(function (GroceryItem) {
//we are making a new view for each model and passing it in as an option
var itemView = new GroceryItemView({
model: GroceryItem
});
//The view creates an element but it is not attached to DOM. We will attach it to the ItemsView's $el (which IS attached to the DOM)
this.$el.append(itemView.$el);
this.groceryItemsView.push(itemView);
}, this);
}
});
var list = new GroceryItemsView();
return list;
});
define(['jquery', 'backbone', 'lodash', 'handlebars', 'Views/GroceryItemsView', 'Models/GroceryItem'],
function($, Backbone, _, Handlebars, GroceryItemsView, GroceryItem) {
var GroceryItemView = Backbone.View.extend({
template: Handlebars.compile(
'<div class="wrapper">' +
'<div class="header">{{coupon_title}}</div>' +
'<div class="column_wrapper">' +
'<div class="two-col">' +
'<div class="product_image"><img src="{{coupon_thumb}}" alt="{{coupon_description}}" height="110" width="110"></div>' +
'<div class="description">{{coupon_description}}</div>' +
'</div>' +
'</div>' +
'<div class="expiration">Valid From: {{valid_from}} to {{valid_to}}</div>' +
'</div>'
),
tagName: 'li',
className: 'lineItem',
events: {
'click': 'getModel'
},
initialize: function () {
this.render();
},
getModel: function () {
return this.model;
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
return GroceryItemView;
});
In backbone models there are actually two kinds of id's, the first is id which is meant to represent the id of your model on the server and isn't atomically assigned. The second is cid (client id) which backbone will atomically generate and assign for you.
In case your server model's id property isn't named id you can map it by setting the idAttributemodel property. For example if the id in your json model is called jsonID
var GroceryItems = Backbone.Collection.extend({
model: GroceryItem,
idAttributemodel: jsonID,
url: "data.json",
parse: function(response) {
return response.all_coupons;
}
});
Some more info on id, cid, idAttribute
I see that in your GroceryItems collection file you are both declaring your collection and instantiating it, it might make more sense in this case to just declare it and return and in your App View (or wherever you declare your collection view) instantiate it there and pass it to the view.
In order to retrieve the model id on the click event of the li, you have to options either you have a seperate view per each li which is bound to a specific model, or in your case where you are rendering all your models using the same view you can retreive it from the DOM.
For example
getModel: function (e) {
//you might want to consider using data attributes instead
var modelId = $(e.currentTarget).attr('id');
var model = this.storeItems.get(modelId);
},
In general regarding using require.js I think that while there is a bit of a learning curve in the long run it is worth it. One thing you might want to consider doing is keeping one file per view/model/collection.
The easiest way to get to a model of something you clicked is surprisingly simple.
I STRONGLY recommend NOT relying on IDs. It's very bad practice. The whole point of using Models is to stop worrying about IDs :P
Creating a Backbone View is not as expensive as some people say. It's actually quite efficient as long as you clean up properly. Break up every logical unit of DOM into it's own View. ESPECIALLY collections of Views
Require is AWESOME. Don't give up on it. Once you figure it out you'll never want to go back. Just think of it as saving bunch of code from another file to a variable defined up top
Don't use success option. Only listen to the sync event. Makes code cleaner, prevents loooooots of weird issues later on.
I haven't tested this code but the logic works (have done it many times)
//Preferrably keep this in a separate file or use require-handlebars
var itemTpl = Handlebars.compile(
'<div class="wrapper">' +
'<div class="header">{{coupon_title}}</div>' +
'<div class="column_wrapper">' +
'<div class="two-col">' +
'<div class="product_image"><img src="{{coupon_thumb}}" alt="{{coupon_description}}" height="110" width="110"></div>' +
'<div class="description">{{coupon_description}}</div>' +
'</div>' +
'</div>' +
'<div class="expiration">Valid From: {{valid_from}} to {{valid_to}}</div>' +
'</div>');
//Your Collection
var GroceryItems = Backbone.Collection.extend({
model: GroceryItem,
url: "data.json",
parse: function (response) {
return response.all_coupons;
}
});
//This Represents all your views
var ItemsView = Backbone.View.extend({
tagName: 'ul',
el: '.where-this-is-supposed-to-go',
initialize: function () {
this.collection = new GroceryItems();
//basically says only render when collection syncs
this.listenTo(this.collection, 'sync', this.render);
},
render: function () {
//got keep track of views for when you need close them (not important for now but you'll thank me later)
this.itemViews = [];
this.collection.each(function (m) {
//we are making a new view for each model and passing it in as an option
var itemView = new ItemView({
model: m
});
//The view creates an element but it is not attached to DOM. We will attach it to the ItemsView's $el (which IS attached to the DOM)
this.$el.append(itemView.$el);
this.itemViews.push(itemView);
}, this);
}
});
var ItemView = Backbone.View.extend({
template: itemTpl,
tagName: 'li',
className: 'lineItem',
events: {
'click': 'getModel'
},
initialize: function () {
this.render();
},
getModel: function () {
//it's already there. No IDs
return this.model;
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});

Backbone Marionette : Add a model (and render its view) to a nested Composite View

If you don't want to see the complete code, here is what I am trying to do.
I have multiple pages and each page has multiple tags. There is a composite View called PageManyView for rendering pages which called its childView PageView. Page View is a nested composite view which renders tags, passing this.model.get('tags') as collection.
Now I can easily add a new page by using pages.add(newPage). Here pages is the collection. I am facing problem in adding a new Tag. How can I do that. Please help.
CODE
var PageModel = Backbone.Model.extend({});
var PageCollection = Backbone.Collection.extend({
model: PageModel
});
My JSON at /data endpoint is coming like this
[
{
_id: '1', 'name': '1', info: 'Page 1',
tags: [{name:'main', color:'red'}, {name:'page', color:'blue'}]
},
{
_id: '1', 'name': '2', info: 'Page 2',
tags: [{name:'section', color:'blue'} {name:'about', color:'yellow'}]
}
]
I have created Nested Views in Marionette like this:
TagView = Marionette.ItemView.extend({
template: '#tagOneTemplate'
});
PageView = Marionette.CompositeView.extend({
template: '#pagesTemplate',
childViewContainer: 'div.tags',
childView: EntityViews.TagView,
initialize: function(){
var tags = this.model.get('tags');
this.collection = new Backbone.Collection(tags);
}
});
PageManyView = Marionette.CompositeView.extend({
template: '#pageManyTemplate',
childView: EntityViews.PageView,
childViewContainer: 'div#all-pages'
});
Now here is where i am facing problem. Inside Controller of my application, lets say if I have to add a new page
showPages: function(){
//Getting pages by using jQuery deferred
var view = PageMainView({collection:pages});
view.on("add:page", function(){
var newPage = Page({_id: 3});
pages.add(newPage);
});
}
Now this add function renders the new page automatically.
BUT I AM FACING PROBLEM IN ADDING a NEW TAG. HOW CAN I ADD A NEW TAG?
Finally it worked. Here is what I have done.
Step 1: Get Current model (page) from pages collection.
var currentpage = pages.get(pageid);
Step 2: Use Marionette BabySitter to get the view of the page where I want to insert a new tag.
var v = view.children.findByModel(currentpage);
Step 3: Add tag to v.collection. Since v is the View of the page where I want to insert new tag, v.collection returns the initialised tags collection
v.collection.add(tag);
This works for me. Let me know if I am wrong somewhere or a better way exists. Hope it helps.
this can be done quite easily by shifting around how your collection is being passed in. Instead of setting the collection on initialize in your compositeView, you should pass it in directly during instantiation. This way when you make a change to the collection from within your model, the compositeView will hear the "add" event on collection and add node automagically for you
For example it might look something like this.
PageView = Marionette.CompositeView.extend({
template: '#pagesTemplate',
childViewContainer: 'div.tags',
childView: EntityViews.TagView,
});
new PageView({
model: myModel,
collection: myModel.get("tags")
});
myModel.get("tags").add([{new: "object"}])

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.

How should a Backbone model or collection handle views?

I have a collection of models, each of which is attached a view. The collection also globally has a view. Is it best for the model views to take care of deleting the corresponding model, or should the collection (or collection view) do that?
Collections have a .add and .remove method, when you use one of them, they fire an add or remove event on the collection. This is how you can bind without using a view at all.
var ships = new Backbone.Collection();
ships.bind('add', function(ship) {
alert('Ahoy ' + ship.get('name') + '!');
});
ships.add([
{name: 'Flying Dutchman'},
{name: 'Black Pearl'}
]);
So attach the collection to the view, inside of the constructor. This simply makes the collection available via this.collection
var ShipView = Backbone.View.extend({
collection: ships
});
This is how you can bind using the view.
// Create collection instance.
var ships = new Backbone.Collection();
// Create view class.
var ShipView = Backbone.View.extend({
collection: ships,
initialize: function() {
// This is binding add, to the view function.
this.collection.bind('add', this.add);
},
add: function(ship) {
alert('Added ' + ship.get('name') + '!');
}
/*Optional for DOM.
events: {
'click .addMyShip': 'addShip'
},
addShip: function(eventObject) {
this.collection.add([models]);
}*/
});
// Create view instance.
var shipView = new ShipView();
// Add two ships.
ships.add([
{name: 'Flying Dutchman'},
{name: 'Black Pearl'}
]);
When the view initializes, it binds the collection's add event to run this.add which is a function of the view. Optionally, you can use the delegateEvents API to handle mapping DOM elements event selector to function that runs the function in the view. That function can call this.collection.add, of which will create the domino effect.
The way the view interacts with the collection, or model, is by binding to events, you can define those events and handle them inside the view. There are several special options that, if passed, will be available to the view: model, collection, el, id, className, and tagName.

Resources