Marionette layout view -- why is a template necessary - backbone.js

In spite of reading the marionette docs several times over, I am still not able to fully comprehend some aspects of it correctly.
I am creating a layout view 'AppLayout' as below:
var AppLayoutView = Marionette.LayoutView.extend({
regions: {
headerRegion: "#ecp_header",
bodyRegion: "#ecp_layout_region"
},
...
The html snippet for my app is having the two dom nodes for above defined regions:
<div id="ecp_header"></div>
<div class="container" id="ecp_layout_region">
<div class="row" id="ecp_body">
...
in app.js, my calling code is like this..
ECPApp.on('start', function() {
require(['controller_cp', 'header_view'], function(ControllerCP, HeaderView) {
console.log("On start event executing...");
// create a event aggregator vent object and attach to app.
ECPApp.vent = new Backbone.Wreqr.EventAggregator();
var appLayoutView = new AppLayoutView();
appLayoutView.render();
//appLayoutView.showLayout();
//$('div.toolbar > ul > li:first > a').tab('show');
if (Backbone.history) Backbone.history.start();
});
This gives me error Cannot render the template since it is null or undefined.
I thought that the default render() behavior of layout always looks for a template, so I rolled out my own version of render, as below:
render: function() {
var $self = this;
/* if no session exists, show welcome page */
var promise = ECPApp.request('entities:session');
promise.done(function(data) {
if (data.result==0) {
console.log('Valid session exists. Showing home page...!');
$self.showHome();
} else {
console.log('No session exists. Showing welcome page...!');
$self.showWelcome();
}
}).fail(function(status) {
console.log('No session exists. Showing welcome page...!');
$self.showWelcome();
});
return $self;
},
showWelcome: function() {
var self = this;
require(['header_view', 'welcome_view'],
function(HeaderView, WelcomeView) {
var headerView = new HeaderView();
var welcomeView = new WelcomeView();
self.bodyRegion.show(welcomeView);
});
}
This time, I get another error saying, An "el" #ecp_layout_region must exist in DOM. However I am sure that the element is existing in the DOM, as I can see it by checking in the debug console window. Running $('#ecp_layout_region') shows a valid element.
Marionette layout view is pretty confusing. Going forward I need multiple nested views. I am stuck here.

How is your template located? Is your template wrapped by <script type = “text/template”> tag?
It may look like this:
Inside your html, in head section:
<script type = “text/template” id="yourLayout">
<div id="ecp_header"></div>
<div class="container" id="ecp_layout_region">...</div>
</script>
And in Layout definition:
var AppLayoutView = Marionette.LayoutView.extend({
template: '#yourLayout'
...
});

Related

my backbone sample is not working and giving with this.el.html is not a function

I am new bee to backbone and i am trying with a below sample
http://jsfiddle.net/naveencgr/L3orucjm/
While loading i am getting this.el.html is not a function, let me know what is the cause of it.
HTML:
<div id="container"></div>
<script type="text/template" id="template">
<input type="text" name="Name" id="Name"></input>
<input type="button" name="Add" id="Add" value="Add"></input>
<div id="list"></div>
</script>
JavaScript:
NameModel = Backbone.Model.extend({
});
var nameModel = new NameModel();
nameModel.set('name','test');
alert(nameModel.get('name'));
NameView = Backbone.View.extend({
tagName:"li",
render: function(){
var template=_.template("<%=name%>", nameModel.toJSON());
return template;
}
});
var nameView = new NameView();
NameViewList = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template($("#template").html(), {});
this.el.html(template);
},
events : {
"click input#Add" :"addName",
},
addName : function() {
var name = $("#Name").val();
if (name.trim().length > 0) {
//nameModel.set("name", name);
$("#list").append(nameView.render());
}
}
});
var nameViewList = new NameViewList({el : $("div#container")});
You have numerous errors in your code. You know about one of them but not the others.
The one you know about is:
this.el.html(template);
A view's el is just a DOM node and that doesn't have an html function. The html function you're trying to use is part of jQuery so you'd want to call it on this.$el (which is just a cached version of $(this.el)):
this.$el.html(template);
Other problems:
Your fiddle is missing vars all over the place; don't say:
NameModel = ...
say:
var NameModel
to avoid accidental globals.
Your NameView is strange. It has tagName: 'li' so presumably it should be creating list elements but the render doesn't do anything with the view's el, it just returns a string of HTML that ends up inside a <div>. That <div> should be a <ul>:
<ul id="list"></ul>
A render function generally populates the view's el and, to allow chaining, returns this:
render: function() {
var template = _.template('<%= name %>');
this.$el.html(template(nameModel.toJSON()));
return this;
}
You are using Underscore's _.template incorrectly. You used to be able to say:
var h = _.template(template_source, data);
to compile the template and fill it in in one step but as of Underscore 1.7, the second argument to _.template is an options object. Now you need to compile and fill in the template in separate steps:
var t = _.template(template_source);
var h = t(data);
You'll see this change in the render above.
The way you're using your NameView is strange. Apparently you are trying to use one NameView to handle multiple names, this would work with your strange NameView#render implementation but it will fall apart once NameView has anything to do or once NameView is updated (as above) to be more conventional. You should create one NameView for each name you're displaying and each NameView should have its own NameModel as its model property. This would make NameView#render look like:
render: function() {
var template = _.template('<%= name %>');
this.$el.html(template(this.model.toJSON()));
return this;
}
and NameViewList#addName would look like:
addName: function() {
var name = this.$("#Name").val();
if(name.trim().length > 0) {
var nameView = new NameView({
model: new NameModel({ name: name })
});
this.$('#list').append(nameView.render().el);
}
}
You'll note that we're using NameView#render's new return value, this x.append(v.render().el) pattern is quite common and idiomatic in Backbone so it is a good practice. You should also notice that the search for #list is now limited to the view's el by using the view's this.$ function; this.$('#list') is equivalent to this.$el.find('#list') and doing things this way helps you keep your views self-contained.
In real life you'd probably put your new NameModel({ name: name }) instances in a collection somewhere and events on that collection would trigger the creation of new NameViews.
Applying all that to your fiddle gives you this functional and more idiomatic version:
http://jsfiddle.net/ambiguous/8x0ma9qo/

Add custom property to every item of a collection

I'm trying to add a custom property to every item of a collection, but it doesn't show up in the template.
I have a lot of quotations which have a client_id. Now I want to fetch the client by the client_id and add it to the collection entry. In general, it works when inspecting the populated object with console.log, but it doesn't show up in the template.
That's how I tried it:
sprocket.QuotationsView = Backbone.View.extend({
id: 'content-inner',
initialize: function(options) {
// instantiate Collection
this.collection = new Quotations();
// compile Handlebars template
this.tpl = Handlebars.compile(this.template);
},
render: function() {
var self = this;
var obj = this.el;
// get quotations and set data to handlebars template
$.when(this.collection.fetch()).then(function(quotations) {
$.each(quotations, function(i, quotation) {
var loadContact = new Contact({id: quotation.contact_id}).fetch();
$.when(loadContact).then(function(contact) {
quotations[i]['contact'] = contact;
});
});
$(obj).html(self.tpl(quotations));
// if response is empty (no quotations in database), set empty template
}, function() {
$(obj).html(self.tpl);
});
return this;
}
});
My template looks like this:
<div>
{{#if .}}
{{#each .}}
{{number}} <!-- this works -->
{{contact}} <!-- this doesn't work -->
{{contact.name}} <!-- this doesn't work too -->
{{/each}}
{{/if}}
</div>
That is because the callback that actually changes the data inside the Quotation.attribute.contact (ie. your quotations[i]['contact'] = contact; line) is being executed after fetching the Contact which happens to be after the template is being rendered.
$.each(quotations, function(i, quotation) {
var loadContact = new Contact({id: quotation.contact_id}).fetch();
// This is the callback that is being executed after the backend responds
$.when(loadContact).then(function(contact) {
quotations[i]['contact'] = contact;
});
});
// This is the template rendering which is before the server has time to respond
$(obj).html(self.tpl(quotations));
Render instead the template after all Contacts are being fetched and added to Quotations.
A quick way to solve this:
Make the loop which load all the Contacts inside a function that contains a callback.
Call the callback after all Contacts have loaded.
The callback should render the template.
This is a personal opinion and in no way an answer to this question: I don't like the data logic with the backend and the view rendering and logic in the same class. I use Backbone.Marionette to split the View and the Controller in two different entities loosely coupled by events. Only the Controller is aware of the View and only the View is aware of the DOM.

Backbone view's "delete" button is deleting all elements from the collection as opposed to the targeted element only

EDIT1: I have tried many different strategies for deleting the element including this.model.destroy(), this.collection.remove(this.model), etc. I have also bound the events using the events: syntax and using a direct binding strategy in both initialize and render.
If I change the code to use this.collection.add instead of remove it adds an additional X notes where X is the number of notes currently in the collection.
I am writing a basic list app (to be fleshed out eventually) and I want each element in the list to have a button that will delete it from the list.
I am using
require.js
backbone
lo-dash
jquery
My code snippets below show my view code for the Notes View, Note View and some supporting into including the templates being used to render each.
At the moment, the code is not functioning as desired because clicking ANY Note's "hide" button causes all elements in the collection to be removed one at a time. I know that is what is happening because I can insert alerts at the end of the "deletenote" method which allows me to view the deletion piece-wise.
I also know that the parent render method is not the cause of the problem because I can turn off the event callback to re-render the parent NotesView and all Note views are still deleted.
<script type="text/template" id="noteslist-template">
<ul id="noteslist" style="list-style: none;">
</ul>
</script>
<script type="text/template" id="note-template">
<li>
<button type="button" class="notesbutton" id="hidenote">hide</button>
<div id="note"><%= text %></div>
</li>
</script>
NotesView.js
define
([ 'jquery',
'underscore',
'backbone',
'notes/sample/sampletext', //included to populate a collection for rendering *REMOVE ONCE DONE TESTING*
'notes/collections/Notes', //included to create a new instance of the Notes collection during the initialize method of this view
'notes/models/Note', //included to reference the model when creating NoteView instances for rendering
'notes/views/NoteView' ], //included to call render functions on each Note model in the collection
function($,_,Backbone,SampleText,Notes,Note,NoteView)
{
var NotesView = Backbone.View.extend
({
initialize: function()
{
this.template = _.template($('#noteslist-template').html());
_.bindAll(this,'render','rendernote');
this.collection.bind('add',this.render);
this.collection.bind('remove',this.render);
},
render: function()
{
console.log('collection render');
this.$el.html(this.template({})); //change to this.notelist = THISLINE
this.collection.each(this.rendernote);
//add call to render notelist to DOM
return this;
},
rendernote: function(note) //add notelist variable
{
console.log('collection rendernote');
var noteview = new NoteView(
{ model:note,
collection:this.collection} );
//add notelist += LINEBELOW
noteview.setElement(this.$('#noteslist'));
noteview.render();
//change noteview.render to NOT write to DOM
}
});
return NotesView;
}
);
NoteView.js
define
( [ 'jquery',
'underscore',
'backbone',
'notes/models/Note', ], //include the Note model to reference as the model for the collection
function($,_,Backbone,Note)
{
var NoteView = Backbone.View.extend
({
tagName: "li",
className: "note",
events:
{
'click #hidenote':'deletenote',
},
initialize: function()
{
_.bindAll(this,'render','remove','deletenote');
//this.model.bind('change',this.render);
this.template = _.template($('#note-template').html());
this.model.bind('remove', this.remove);
},
render: function()
{
this.notetorender = this.template(this.model.toJSON());
this.$el.append(this.notetorender);
return this;
},
deletenote: function()
{
this.options.collection.remove(this.model);
}
});
return NoteView;
}
);
noteview.setElement(this.$('#noteslist')); causes event delegation.
So eventhough you wrote 'click #hideNotes : deleteNode' inside NoteView, after the setElement call it works like that code is present inside NotesListView.
So when you click hide button of single li and expect only that li to be removed from ul however the click event is received by all li's hide button.That's why all items in collection are deleted .
JsFiddle here i do the same thing without using setElement
var NoteView = Backbone.View.extend({
events: {'click #hidenote': 'deletenote'},
initialize: function(){
this.model.on('destroy', this.$el.remove)
},
deletenote: function(){
this.model.destroy()
}
})

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.js read-only collection view rendering

Quick summary of my problem: first rendering of the view contains the elements in the collection, and the second rendering doesn't. Read on for details...
Also of note: I do realize that the following code represents the happy path and that there is a lot of potential error handling code missing, etc, etc...that is intentional for the purpose of brevity.
I'm pretty new with backbone.js and am having some trouble figuring out the right way to implement a solution for the following scenario:
I have a page shelled out with two main regions to contain/display rendered content that will be the main layout for the application. In general, it looks something like this:
-------------------------------------------
| | |
| | |
| Content | Actions |
| Panel | Panel |
| | |
| | |
| | |
| | |
| | |
| | |
-------------------------------------------
I have an HTTP API that I'm getting data from, that actually provides the resource information for various modules in the form of JSON results from an OPTIONS call to the base URL for each module. For example, an OPTIONS request to http://my/api/donor-corps returns:
[
{
"Id":0,
"Name":"GetDonorCorps",
"Label":"Get Donor Corps",
"HelpText":"Returns a list of Donor Corps",
"HttpMethod":"GET",
"Url":"https://my/api/donor-corps",
"MimeType":"application/json",
"Resources":null
}
]
The application has a backbone collection that I'm calling DonorCorpsCollection that will be a read-only collection of DonorCorpModel objects that could potentially be rendered by multiple different backbone views and displayed in different ways on different pages of the application (eventually...later...that is not the case at the moment). The url property of the DonorCorpsCollection will need to be the Url property of the object with the "GetDonorCorps" Name property of the results of the initial OPTIONS call to get the API module resources (shown above).
The application has a menubar across the top that has links which, when clicked, will render the various pages of the app. For now, there are only two pages in the app, the "Home" page, and the "Pre-sort" page. In the "Home" view, both panels just have placeholder information in them, indicating that the user should click on a link on the menu bar to choose an action to take. When the user clicks on the "Pre-sort" page link, I just want to display a backbone view that renders the DonorCorpsCollection as an unordered list in the Actions Panel.
I'm currently using the JavaScript module pattern for organizing and encapsulating my application code, and my module currently looks something like this:
var myModule = (function () {
// Models
var DonorCorpModel = Backbone.Model.extend({ });
// Collections
var DonorCorpsCollection = Backbone.Collection.extend({ model : DonorCorpModel });
// Views
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.template = _.template($('#pre-sort-actions-template').html());
this.collection.bind('reset', this.render); // the collection is read-only and should never change, is this even necessary??
},
render : function () {
$(this.el).html(this.template({})); // not even exactly sure why, but this doesn't feel quite right
this.collection.each(function (donorCorp) {
var donorCorpBinView = new DonorCorpBinView({
model : donorCorp,
list : this.collection
});
this.$('.donor-corp-bins').append(donorCorpBinView.render().el);
});
return this;
}
});
var DonorCorpListItemView = Backbone.View.extend({
tagName : 'li',
className : 'donor-corp-bin',
initialize : function () {
_.bindAll(this, 'render');
this.template = _.template($('#pre-sort-actions-donor-corp-bin-view-template').html());
this.collection.bind('reset', this.render);
},
render : function () {
var content = this.template(this.model.toJSON());
$(this.el).html(content);
return this;
}
});
// Routing
var App = Backbone.Router.extend({
routes : {
'' : 'home',
'pre-sort', 'preSort'
},
initialize : function () { },
home : function () {
// ...
},
preSort : function () {
// what should this look like??
// I currently have something like...
if (donorCorps.length < 1) {
donorCorps.url = _.find(donorCorpResources, function (dc) { return dc.Name === "GetDonorCorps"; }).Url;
donorCorps.fetch();
}
$('#action-panel').empty().append(donorCorpsList.render().el);
}
})
// Private members
var donorCorpResources;
var donorCorps = new DonorCorpsCollection();
var donorCorpsList = new DonorCorpsListView({ collection : donorCorps });
// Public operations
return {
Init: function () { return init(); }
};
// Private operations
function init () {
getAppResources();
}
function getAppResources () {
$.ajax({
url: apiUrl + '/donor-corps',
type: 'OPTIONS',
contentType: 'application/json; charset=utf-8',
success: function (results) {
donorCorpResources = results;
}
});
}
}(myModule || {}));
Aaannnd finally, this is all using the following HTML:
...
<div class="row-fluid">
<div id="viewer" class="span8">
</div>
<div id="action-panel" class="span4">
</div>
</div>
...
<script id="pre-sort-actions-template" type="text/template">
<h2>Donor Corps</h2>
<ul class="donor-corp-bins"></ul>
</script>
<script id="pre-sort-actions-donor-corp-bin-view-template" type="text/template">
<div class="name"><%= Name %></div>
</script>
<script>
$(function () {
myModule.Init();
});
</script>
...
So far, I've been able to get this to work the first time I click on the "Pre-sort" menu link. When I click it, it renders the list of Donor Corps as expected. But, if I then click on the "Home" link, and then on the "Pre-sort" link again, this time I see the header, but the <ul class="donor-corp-bins"></ul> is empty with no list items in it. ... and I have no idea why or what I need to be doing differently. I feel like I understand backbone.js Views and Routers in general, but in practice, I'm apparently missing something.
In general, this seems like a fairly straight-forward scenario, and I don't feel like I'm trying to do anything exceptional here, but I can't get it working correctly, and can't seem to figure out why. An assist, or at least a nudge in the right direction, would be hugely appreciated.
So, I've figured out a solution here. Not sure if its the best or the right solution, but its one that works at least for now.
There seems to have been a couple of issues. Based on Rob Conery's feedback, I started looking at different options for rendering the collection with my view template, and this is what I came up with:
As I mentioned in the comment, we're using Handlebars for view templating, so this code uses that.
I ended up changing my view template to look like this:
<script id="pre-sort-actions-template" type="text/x-handlebars-template">
<h2>Donor Corps</h2>
<ul class="donor-corp-bins">
{{#each list-items}}
<li class="donor-corp-bin">{{this.Name}}</li>
{{/each}}
</ul>
</script>
Then, I removed DonorCorpListItemView altogether, and changed DonorCorpsListView to now look like this:
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.collection.bind('reset', this.render);
},
render : function () {
var data = { 'list-items' : this.collection.toJSON() };
var template = Handlebars.compile($('#pre-sort-actions-template').html());
var content = template(data);
$(this.el).html(content);
return this;
}
});
So, that seems to be working and doing what I need it to do now.
I clearly still have a lot to learn about how JS and Backbone.js work though, because the following DOES NOT WORK (throws an error)...and I have no idea why:
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.template = Handlebars.compile($('#pre-sort-actions-template').html());
this.collection.bind('reset', this.render);
},
render : function () {
var data = { 'list-items' : this.collection.toJSON() };
var content = this.template(data);
$(this.el).html(content);
return this;
}
});
Hopefully this will be helpful to someone.
Your rendering code looks odd to me for the DonorCorpListView - it looks like you're templating before you have any data. You need to compile that template with the data to get the HTML output.
So, the rendering logic should be somthing like:
var template = $([get the template).html();
template.compile(this.collection.toJSON());
this.el.append(template.render());
I used some pseudo code here as I'm not sure what your templating mechanism is (I like Handlebars myself).

Resources