Backbone Marionette Nested Composite View - backbone.js

So I am stuck. I got the great Backbone.Marionette to handle my nested childs/parents relationships and rendering(doing it with the bare backbone was a nightmare), but now i'm facing problems with my nested composite view,
I'm always getting a The specified itemViewContainer was not found: .tab-content from the parent composite view - CategoryCollectionView, although the itemViewContainer is available on the template, here is what I'm trying to do, I have a restaurant menu i need to present, so I have several categories and in each category I have several menu items, so my final html would be like this:
<div id="order-summary">Order Summary Goes here</div>
<div id="categories-content">
<ul class="nav nav-tabs" id="categories-tabs">
<li>Appetizers</li>
</ul>
<div class="tab-content" >
<div class="tab-pane" id="category-1">
<div class="category-title">...</div>
<div class="category-content">..the category items goes here.</div>
</div>
</div>
Here is what I have so far:
First the templates
template-skeleton
<div id="order-summary"></div>
<div id="categories-content"></div>
template-menu-core
<ul class="nav nav-tabs" id="categories-tabs"></ul>
<div class="tab-content" ></div>
template-category
<div class="category-title">
<h2><%=name%></h2>
<%=desc%>
</div>
<div class="category-content">
The menu items goes here
<ul class="menu-items"></ul>
</div>
template-menu-item
Item <%= name%>
<strong>Price is <%= price%></strong>
<input type="text" value="<%= quantity %>" />
Add
Now the script
var ItemModel = Backbone.Model.extend({
defaults: {
name: '',
price: 0,
quantity: 0
}
});
var ItemView = Backbone.Marionette.ItemView.extend({
template: '#template-menuitem',
modelEvents: {
"change": "update_quantity"
},
ui: {
"quantity" : "input"
},
events: {
"click .add": "addtoBasket"
},
addtoBasket: function (e) {
this.model.set({"quantity": this.ui.quantity.val() });
},
update_quantity: function () {
//#todo should we do a re-render here instead or is it too costy
this.ui.quantity.val(this.model.get("quantity"));
}
});
var ItemCollection = Backbone.Collection.extend({
model: ItemModel
});
var CategoryModel = Backbone.Model.extend({
defaults: {
name: ''
}
});
var CategoryView = Backbone.Marionette.CompositeView.extend({
template: '#template-category',
itemViewContainer: ".menu-items",
itemView: ItemView,
className: "tab-pane",
id: function(){
return "category-" + this.model.get("id");
},
initialize: function () {
this.collection = new ItemCollection();
var that = this;
_(this.model.get("menu_items")).each(function (menu_item) {
that.collection.add(new ItemModel({
id: menu_item.id,
name: menu_item.name,
price: menu_item.price,
desc: menu_item.desc
}));
});
}
});
var CategoryCollection = Backbone.Collection.extend({
url: '/api/categories',
model: CategoryModel
});
var CategoryCollectionView = Backbone.Marionette.CompositeView.extend({
el_tabs: '#categories-tabs',
template: '#template-menu-core',
itemViewContainer: ".tab-content", // This is where I'm getting the error
itemView: CategoryView,
onItemAdded: function (itemView) {
alert("halalouya");
//this.$el.append("<li>" + tab.get("name") + "</li>");
//$(this.el_tabs).append("<li><a href='#category-" + itemView.model.get("id") + "'>"
//+ itemView.model.get("name") + "</a></li>")
}
});
I know It's a bit hard to follow but you guys are my last resort. There is no problems with the templates and the cateogry fetching and the other stuff(it was already working before converting the CategoryCollectionView from a Marionette collection to a composite view.)
Edit 1
Added App initalizer on request:
AllegroWidget = new Backbone.Marionette.Application();
AllegroWidget.addInitializer(function (options) {
// load templates and append them as scripts
inject_template([
{ id: "template-menuitem", path: "/js/templates/ordering-widget-menuitem.html" },
{ id: "template-category", path: "/js/templates/ordering-widget-category.html" },
{ id: "template-menu-core", path: "/js/templates/ordering-widget-menu-core.html" },
{ id: "template-skeleton", path: "/js/templates/ordering-widget-skeleton.html" }
]);
// create app layout using the skeleton
var AppLayout = Backbone.Marionette.Layout.extend({
template: "#template-skeleton",
regions: {
order_summary: "#order-summary",
categories: "#categories-content"
}
});
AllegroWidget.layout = new AppLayout();
var layoutRender = AllegroWidget.layout.render();
jQuery("#allegro-ordering-widget").html(AllegroWidget.layout.el);
// Initialize the collection and views
var _category_collection = new CategoryCollection();
var _cateogories_view = new CategoryCollectionView({ api_key: window.XApiKey, collection: _category_collection });
_category_collection.fetch({
beforeSend: function (xhr) {
xhr.setRequestHeader("X-ApiKey", window.XApiKey);
},
async: false
});
//AllegroWidget.addRegions({
/// mainRegion: "#allegro-ordering-widget"
//});
AllegroWidget.layout.categories.show(_cateogories_view);
});
AllegroWidget.start({api_key: window.XApiKey});

You are adding to the collection via fetch before you call show on the region.
Marionette.CompositeView is wired by default to append ItemViews when models are added to it's collection. This is a problem as the itemViewContainer .tab-content has not been added to the dom since show has not been called on the region.
Easy to fix, rework you code as below and it should work without overloading appendHtml.
// Initialize the collection and views
var _category_collection = new CategoryCollection();
// grab a promise from fetch, async is okay
var p = _category_collection.fetch({headers: {'X-ApiKey': window.XApiKey});
// setup a callback when fetch is done
p.done(function(data) {
var _cateogories_view = new CategoryCollectionView({ api_key: window.XApiKey, collection: _category_collection });
AllegroWidget.layout.categories.show(_cateogories_view);
});

okay this is pretty weird but adding this in the CategoryCollectionView class:
appendHtml: function (collectionView, itemView, index) {
//#todo very weird stuff, assigning '.tab-content' to itemViewContainer should have been enough
collectionView.$(".tab-content").append(itemView.el);
}
solved the problem, however i have no idea why it works, asssigning '.tab-content' to the itemViewContainer should have been enough, any idea?

Related

how to use marionette CollectionView to fetch data deom the server?

I am trying to create a simple Marionette(2.3.2) Application, that retrieves data from a RESTFUL web service,
What did I do wrong? how to fix this code? and what is the best structure for this sample App?
here the code on jsfiddle:
http://jsfiddle.net/kdureidy/zaze13fw/
here is my code:
var Library = new Backbone.Marionette.Application();
var LayoutView = Backbone.Marionette.LayoutView.extend({
el: 'body',
template: '#content-template',
regions: {
mainRegion: "#main-region",
collectionRegion: "#collection-region"
}
});
var Book = Backbone.Model.extend({
url: "http://localhost:9090/library-0.1/books"
});
var MyChildView = Backbone.Marionette.ItemView.extend({
model: Book,
tagName: 'li',
template: '#list-template',
});
var BooksCollection = Backbone.Collection.extend({
url: 'http://localhost:9090/library-0.1/books',
model: Book
});
var collection1 = new BooksCollection({
model: Book
});
var MyCollectionView = Backbone.Marionette.CollectionView.extend({
url: "http://localhost:9090/library-0.1/books",
tagName: 'ul',
childView: MyChildView,
collectionEvents: {
'sync': 'render'
},
});
var c = new MyCollectionView({
collection: collection1
});
Library.layout_view = new LayoutView();
Library.layout_view.render();
Library.layout_view.collectionRegion.show(c);
Library.start();
my templates are:
<script id="list-template" type="text/template">
<h2><%=title %></h2>
<span><%=author %></span>
<button>Click Here</button>
</script>
<script id="content-template" type="text/template">
<h1>This is Lesson 1</h1>
<div id="main-region" class="content">
</div>
<hr>
<div id="collection-region"></div>
</script>
CollectionView does not fetch data, it renders it, Backbone.Collection is the one responsible for fetching the data.
You don't need url in MyCollectionView
Marionette has collectionEvents, which you can use the same way as modelEvents, so your initialize can be replaced with
collectionEvents: {
'reset': 'render'
'change': 'render'
}
and call, which I would do separately
`collection1.fetch()` explicitly
For good Marionette app structure look at: http://www.backbonerails.com/

how to display list through backboneview?

This is my first backbone code :)
How can I display my list here:
<title>list</title>
<ul id="container">
<li>
<%- name %>
</li>
</ul>
js:
var app = {}; // create namespace for our app
app.Mymodel = Backbone.Model.extend({
defaults:
{
name: ''
}
});
app.List = Backbone.Collection.extend({
model: app.Mymodel,
localStorage:new Store('vandaag')
});
// renders individual todo items list (li)
app.MyView = Backbone.View.extend({
el: '#container',
initialize: function () {
app.list = new app.List();
app.list.add({ name: 'piet' });
app.list.add({ name: 'ed' });
this.render();
},
render: function(){
this.$el.append(app.list);
//var view = new app.MyView({ model: new app.Mymodel({name:'ed',city:'ny'}));
//$('#todo-list').append(view.render().el);
}
});
app.myView = new app.MyView();
jsfiddle:http://jsfiddle.net/dingen2010/YBPG6/2/
First have your template created. In below fiddle it is template with ID list-template.
Then you can compile the template, add data to it and render the view.
Check this updated fiddle.
To know how Underscore Templates work try this.

Proper way to create a collection list view in Backbone

I'm currently learning Backbone.js and I'm having a hard time learning how to properly use Views (since I have experienced when it comes to MVC), so here is what I'm trying to do:
templates:
<script type="text/template" id="todolist-template">
<ul></ul>
</script>
<script type="text/template" id="todo-template">
<li>
<%= item.name %>
<%= item.description %>
<%= item.priority %>
</li>
</script>
html:
<div id="container"></div>
Views:
var TodoView = Backbone.View.extend({
tagName: 'li',
className: 'todo',
initialize: function() {
this.template = _.template($('#todo-template').html());
this.render();
},
render: function() {
this.$el.html(this.template({item: this.model}));
return this;
}
});
var TodoListView = Backbone.View.extend({
el: '#container',
tagName: 'ul',
className: 'todolist',
initialize: function() {
this.template = _.template($('#todolist-template').html());
this.render();
},
render: function() {
that = this;
this.$el.empty();
this.$el.append(this.template());
this.collection.each(function(model) {
that.$el.append(new TodoView({model: model.toJSON()}));
});
return this;
}
});
Models and Collections:
var Todo = Backbone.Model.extend({
defaults : {
name : '',
priority: '',
description: ''
}
});
var TodoList = Backbone.Collection.extend({
model: Todo
});
var todoList = new app.TodoList([
new Todo({
name: 'unclog the sink',
priority: '10',
description: 'FIX THE SINK!!!'
}),
new Todo({
name: 'get bread',
priority: '0',
description: 'We are out of bread, go get some'
}),
new Todo({
name: 'get milk',
priority: '2',
description: 'We are out of milk, go get some'
})
]);
"misc":
$(function() {
new HeaderView();
new TodoListView({collection: todoList});
router = new AppRouter();
Backbone.history.start();
});
What I'm trying to do is to create a ul which will then get populated with lis that contain the collection's data. I've been trying to fix/debug this code for a while now (at least 3 hours) but I'm constantly hitting errors or wrong results, so please someone explain to me the proper way of implementing this.
edit (resulting HTML):
<div id="container">
<ul></ul>
</div>
At least one problem lies here:
that.$el.append(new TodoView({model: model.toJSON()}));
Should be
that.$el.append(new TodoView({model: model.toJSON()}).render().el);
Since you can't append a view to $el, but rather you should be appending the rendered html
You don't need <li> in your template as your view already wraps the template in those tags. If it still doesn't work, check the DOM and post it here. Same goes for <ul>...
Also, I don't see where you add your ListView to the DOM. render only operates on a local element which isn't part of the DOM yet. Once rendered, you have to add it to the DOM.

Nested views in Backbonejs, a post & comments relation

Array Of Objects
The data is received from server
var Updates = [
{"post_id":"1","post_desc":"This is my first post",
"comments":[{"id":1,"comment":"some comments","like":7},
{"id":9,"comment":"some comments","like":3}
]
},
{"post_id":"2","post_desc":"This is my second post",
"comments":[{"id":5,"comment":"some comments","like":5}]
}]
Model:
var Update = Backbone.Model.extend({
defaults:{
photo: "default.png"
}
});
Collection:
var latestUpdates = Backbone.Collection.extend({
model: Update
});
Single View:
var UpdateView = Backbone.View.extend({
tagName: "div",
className: "post-container",
template: $("#postTemplate").html(),
render: function () {
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
}
});
Master view:
var UpdatesView = Backbone.View.extend({
el: $("#postContainer"),
initialize: function () {
this.collection = new latestUpdates(Updates);
this.render();
},
render: function () {
var that = this;
_.each(this.collection.models, function (item) {
that.renderUpdates(item);
}, this);
},
renderUpdates: function (item) {
var updateView = new UpdateView({
model: item
});
this.$el.append(updateView.render().el);
}
});
//create app instance
var wallUpdates = new UpdatesView();
How can I render comments section under each post?
Trying to achieve layout similar to facebook post-comment system
I'd use a CommentListView, owned by your UpdateView. tagName: "ul", className: "post-comments"
Then have a CommentView owned by the CommentListView. CommentView's render should not append anything to the DOM, but return its $el.
CommentListView would tell each of the CommentView's to render, appending each of their $el's to the CommentListView's $el.
For the containers, I'd use:
<div class="post-container" data-post-id="<%= YourPostId %>">
<div class="post-body">
<!--Your post can go in here-->
</div>
<ul class="post-comments">
<!--Append your comments in here-->
</ul>
</div>

Backbone events and Twitter bootstrap popover

Insert backbone rendered view in twitter bootstrap popover like below. Problem is that when insert throught content option backbone events for that view don`t fire. I inserted view in div for test with $(selector).html attendanceShow.render().el events work without problem. Thank you in advance
attendance = new Attendance()
attendance.url = "#{attendanceUrl}/#{attendanceId}"
attendance.fetch
success: ->
attendanceShow = new ExamAttendanceShow({model: attendance })
currentTarget.popover
html : true
content: ->
attendanceShow.render().el
Best regards,
Georgi.
From what I understand, based on your code and your description you are only creating an instance of the popover but never showing it. I have a live demo working but not with CoffeeScript (I personally hate CoffeeScript), you can see the code below and at this jsfiddle.
data1.json
{"content": "lorem ipsum dolor sit amet"}
index.html
<div class="container">
<div class="row">
<button class="btn" data-target="popover">Popover</button>
</div>
<div class="row"> </div>
<div class="row">
<button class="btn" data-action="change-content">Change Content</button>
</div>
</div>
main.js
var Main = Backbone.View.extend({
model: null,
item: null,
popover: false,
events: {
'click .btn[data-target]': 'button_click',
'click .btn[data-action="change-content"]': 'change_content'
},
initialize: function() {
_.bindAll(this);
this.model = new PopoverModel();
this.model.view = new PopoverContentView({model: this.model});
this.item = this.$('.btn[data-target]');
this.item.popover({
html: true,
content: this.model.view.render().el
});
},
button_click: function(event) {
if (!this.popover) {
this.model.url = 'js/data1.json';
this.model.fetch({
success: this.model_fetched
});
} else {
this.popover = false;
}
},
model_fetched: function() {
if (!this.popover) {
this.item.popover('show');
} else {
this.item.popover('hide');
}
this.popover = !this.popover;
},
change_content: function(event) {
this.model.set('content', 'Some random content... ' + parseInt(Math.random() * 10));
}
});
var PopoverModel = Backbone.Model.extend({
defaults: {
content: ''
}
});
var PopoverContentView = Backbone.View.extend({
initialize: function() {
_.bindAll(this);
this.listenTo(this.model, 'change', this.render);
},
render: function() {
this.$el.html(_.template('<%= content %>', this.model.toJSON()));
return this;
}
});
var main = new Main({
el: '.container'
});
I have a similar issue, to expand on Georgi's answer, please try this:
Place a button or a link in your popup (instead of the dynamic text you place) and handle an event, say click event on it.

Resources