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.
Related
UPDATE
I finally learned Backbone back in late 2017. I'd delete this post but StackOverflow says it's not wise to delete answered questions. Please ignore this question.
I've read countless posts here on StackExchange as well as countless tutorials across the Internet but I seem to be just off from understanding basic Backbone use and implementation.
I'm attempting to build a custom Twitter timeline using pre-filtered JSON that is generated from a PHP file on my work's server.
I feel close but I just can't seem to get things to work. At times I'm able to view 20 tweets in my console but am only able to get 1 tweet to render via my template.
Here is my current Backbone setup:
(function($){
if(!this.hasOwnProperty("app")){ this.app = {}; }
app.global = this;
app.api = {};
app.api.Tweet = Backbone.Model.extend({
defaults: {}
});
app.api.Tweets = Backbone.Collection.extend({
model: usarugby.api.Tweet,
url: "https://custom.path.to/api/tweets/index.php",
parse: function(data){
return data;
}
});
app.api.TweetsView = Backbone.View.extend({
el: $('#tweet-wrap'),
initialize: function(){
_.bindAll(this, 'render');
this.collection = new app.api.Tweets();
this.collection.bind('reset', function(tweets) {
tweets.each(function(){
this.render();
});
});
return this;
},
render: function() {
this.collection.fetch({
success: function(tweets){
var template = _.template($('#tweet-cloud').html());
$(tweets).each(function(i){
$(this).html(template({
'pic': tweets.models[i].attributes.user.profile_image_url,
'text': tweets.models[i].attributes.text,
'meta': tweets.models[i].attributes.created_at
}));
});
$(this.el).append(tweets);
}
});
}
});
new app.api.TweetsView();
}(jQuery));
And here is my current HTML and template:
<div id="header-wrap"></div>
<div id="tweet-wrap"></div>
<script type="text/template" id="tweet-cloud">
<div class="tweet">
<div class="tweet-thumb"><img src="<%= pic %>" /></div>
<div class="tweet-text"><%= text %></div>
<div class="tweet-metadata"><%= meta %></div>
</div>
</script>
<script> if(!window.app) window.app = {}; </script>
I also have a CodePen available for testing. Any advice would be greatly appreciated.
Like the comments suggest, additional reading and code rewrite may be needed. The simplest example for a view rendering multiple views is here adrianmejia's backbone tutorial example.
The snippet below includes an additional view and a couple of added functions along with updating the render and initialize functions. Search for 'cfa' to review changes.
(function($){
if(!this.hasOwnProperty("app")){ this.app = {}; }
app.global = this;
app.api = {};
app.api.Tweet = Backbone.Model.extend({
idAttribute: 'id_str'
});
app.api.Tweets = Backbone.Collection.extend({
model: app.api.Tweet,
url: "https://cdn.usarugby.org/api/tweets/index.php",
parse: function(data){
return data;
}
});
app.api.TweetView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#tweet-cloud').html()),
initialize: function(){
},
render: function(){
var j = {};
j.pic = this.model.get('user').profile_image_url;
j.text = this.model.get('text');
j.meta = this.model.get('meta');
this.$el.html(this.template(j));
return this;
},
});
app.api.TweetsView = Backbone.View.extend({
el: $('#tweet-wrap'),
initialize: function(){
this.collection = new app.api.Tweets();
this.collection.on('reset', this.onReset, this);
this.collection.on('add', this.renderATweet, this);
this.collection.fetch();
},
onReset: function(){
this.$el.html('');
this.collection.each(this.renderATweet, this);
},
renderATweet: function (tweet) {
var tweetView = new app.api.TweetView({ model: tweet });
this.$el.append(tweetView.render().el);
},
});
}(jQuery));
$(document).ready(function(){
new app.api.TweetsView();
});
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.13.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
<script src="https://static.usarugby.org/lib.min.js"></script>
<div id="header-wrap"></div>
<div id="tweet-wrap"></div>
<script type="text/template" id="tweet-cloud">
<div class="tweet">
<div class="tweet-thumb"><img src="<%= pic %>" /></div>
<div class="tweet-text">
<%= text %>
</div>
<div class="tweet-metadata">
<%= meta %>
</div>
</div>
</script>
<div id="footer-wrap"></div>
<script>
if(!window.app) window.app = {};
</script>
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.
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?
What i want to do:
Render a select dropdown with option tags inside, and when user selects an option in the dropdown, get the newly selected model and do stuff with it.
Problem:
I'm having a hard time to get the change event to be triggered in an ItemView that's been called through a CompositeView.
For some reason the CompositeView:change (log: holy moses) is being triggered, however it doesn't help me much, since it won't give me the selected model.
I've tried a ton of stuff but nothing really worked.
any help would be greatly appreciated!
code:
Configurator.module('Views.Ringsizes', function(Views, Configurator, Backbone, Marionette, $, _) {
Views.DropdownItem = Marionette.ItemView.extend({
tagName: 'option',
template: "#dropdown-item",
modelEvents: {
'change': 'modelChanged'
},
onRender: function(){
console.log('tnt');
this.$el = this.$el.children();
this.setElement(this.$el);
},
modelChanged: function(model) {
console.log("holy mary");
}
});
Views.DropdownView = Marionette.CompositeView.extend({
template: "#dropdown-collection",
className: 'configurator-ringsizes-chooser',
itemView: Views.DropdownItem,
itemViewContainer: '.product_detail_ring_sizes',
events: {
"change": "modelChanged"
},
initialEvents: function(){},
initialize: function(){
console.log(this.model);
this.collection = new Backbone.Collection(this.model.getRingsizes());
},
modelChanged: function(model) {
console.log("holy moses");
}
});
Views.List = Marionette.CollectionView.extend({
className: 'configurator-ringsizes',
itemView: Views.DropdownView
});
});
template code: (if needed)
<script type="text/template" id="dropdown-item">
<option value="<#- code #>" <# if(current) { #> selected="selected" <#}#> ><#- name #> </option>
</script>
<script type="text/template" id="dropdown-collection">
<div class="accordionContent accordionContent_ringsizes">
<div class="configurator-ringsizes-chooser-ringsizes-region">
<select class="product_detail_ring_sizes"></select>
</div>
</div>
</script>
A "change" event won't trigger on a option when you select it, instead it will fire on the select when you change the choosen option (that's why it triggers on the composite view).
So you should use this in your itemView:
events: {
'click' : 'modelChanged'
}
Okay, i finally got this to work.
I'm a bit dissapointed that i have to rely on a data- attribute for this,
but this is the only way i found. took me long enough already :)
Here's how i did it now:
Template code:
<script type="text/template" id="dropdown-item">
<option data-cid="<#- cid #>" value="<#- code #>" <# if(current) { #> selected="selected" <#}#> ><#- name #></option>
</script>
<script type="text/template" id="dropdown-collection">
<div class="configurator-ringsizes-chooser-ringsizes-region">
<select class="product_detail_ring_sizes"></select>
</div>
</script>
Code:
Configurator.module('Views.Ringsizes', function(Views, Configurator, Backbone, Marionette, $, _) {
Views.DropdownItem = Marionette.ItemView.extend({
tagName: 'option',
template: "#dropdown-item",
serializeData: function() {
var data = {
cid: this.model.cid,
code: this.model.get('code'),
name: this.model.get('name'),
current: this.model.get('current')
};
return data;
},
onRender: function(){
this.$el = this.$el.children();
this.setElement(this.$el);
}
});
Views.DropdownView = Marionette.CompositeView.extend({
template: "#dropdown-collection",
className: 'configurator-ringsizes-chooser',
itemView: Views.DropdownItem,
itemViewContainer: '.product_detail_ring_sizes',
events: {
"change select": "modelChanged"
},
initialEvents: function(){},
initialize: function(){
this.collection = new Backbone.Collection(this.model.getRingsizes());
},
modelChanged: function(e) {
var cid = $(e.currentTarget+"option:selected").data('cid');
var currentModel = this.collection.find(function(elem) {
return elem.get('current');
});
var model = this.collection.find(function(elem) {
return elem.cid === cid;
});
currentModel.set({
current: false
});
model.set({
current: true
});
// AND here i'm doing my stuff, getting the overall model through this.model, the collection of options through this.collection and the currently selected model through currentModel.
}
});
Views.List = Marionette.CollectionView.extend({
className: 'configurator-ringsizes',
itemView: Views.DropdownView,
model: this.model
});
});
I'm trying to learn nested views with Backbone.js and have run into a problem. No errors are thrown however, it does not display any output or data. Any help would be much appreciated. V/R Chris
link to jsFiddle: http://jsfiddle.net/cpeele00/PcmMW/8/
var User = Backbone.Model.extend({});
var Users = Backbone.Collection.extend({
model: User
});
var UserItemView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#user-list-template').html()),
render: function() {
this.$el.html(this.model.toJSON());
return this;
}
});
var UserListView = Backbone.View.extend({
render: function() {
this.$el.empty();
var self = this;
this.collection.each(function(model) {
self.renderItem(model);
});
},
renderItem: function(item) {
var itemView = new UserItemView({
model: item
});
this.$el.append(itemView.render().el);
}
});
var user1 = new User();
user1.set({
firstname: 'momo',
lastname: 'peele'
});
var user2 = new User();
user2.set({
firstname: 'bobo',
lastname: 'peele'
});
var users = new Users([user1, user2]);
var listView = new UserListView({
collection: users
});
listView.render();
Here's the html and template markup
<div id="user-list">
<fieldset>
<legend>Users</legend>
<ul></uL>
</fieldset>
</div>
<script id="user-list-template" type="text/template">
<%= firstname %>
<%= lastname %>
</script>
There seem to be two problems:
First, typo in UserItemView: you're not using the template, just appending JSON. Instead of
this.$el.html(this.model.toJSON());`
it should be
this.$el.html(this.template(this.model.toJSON()));
Second, the UserListView isn't attached to the DOM anywhere, so when it gets "rendered", it doesn't appear. I added
el: $("#user-list ul")
to the view, so that rendering appends the sub-view items to an element that's actually in the DOM.
Forked Fiddle
PS, Firebug is your friend.