So inside a model I have a structure like this. Below is a sample of the structure.
var myModel {
_id: 798698,
username: "John",
message: {
message1: "Some cool messsage",
message2: "I'm mad Ohio State lost"
}
}
I am using backbone marionette and handlebars of course, I have a collection view which at first was just a collection of the models.
var Marionette = require('backbone.marionette');
var ChatView = Marionette.ItemView.extend({
template: require('../../templates/message.hbs')
});
module.exports = CollectionView = Marionette.CollectionView.extend({
initialize: function() {
this.listenTo(this.collection, 'change', this.render);
},
itemView: ChatView
});
Then in the /message.hbs the template is basic like this.
<div class="message"><b>{{username}}:</b>{{message}}</div>
Well if you look at the structure above message is obviously an [object object].
So to the question which I am sure is easy and obvious, how do I loop this view for each object inside the message: attribute, so that the template itself is looped. I think I worded that right.
See ideally It would be nice to create a collection within the model, but I didn't know how and the objects inside message: are built in a vanilla way.
So that said I am hoping there is a way to loop those objects with backbone/backbone.marionette and handlebars.
Thanks
In your template, use #each:
<div class="message">
<b>{{username}}:</b>
{{#each message}}
<div>{{this}}</div>
{{/each}}
</div>
Related
So I'm really new to backbone.js and I'm trying to understand the basic concept when it comes to getting parts(views/models/routes) to interact with other parts.
Here's an example. I have a 'screen' model object being rendered by a 'singleScreen' View to the page. I also have a 'sidebar' model and view being rendered. When i click on a link in the sidebar i want it to render a different screen model object and alter some of the HTML in a separate html div (the heading) according to the 'name' attribute i gave my screen model.
So first question, should all of the code for re-rendering a different view and changing the heading html be done in the routes.js file? It seems like that file would get pretty big pretty fast. If so how do i get a reference to the model object i want to render in the routes.js file so i can access things like myModel.name (which is instantiated in the app.js file)?
Do I handle the browser history and rendering of my view as separate things and add code for 'link is clicked, render this' functionality in my app.js file (the file where I instantiate all my objects)? If that's the case how does my app know what to render if a user tries to go directly to a view by typing in the URL, rather than clicking?
OR, and this is the most likely scenario as far as i can tell,
Do i use the initialize functions of my models/views to trigger/listenTo an event for when a link is clicked(or backbone.history() is changed?) and call render?
I've messed around with all 3 approaches but couldn't understand how to pass references of objects to other parts of the app, without just making those objects global variables (which feels so wrong).
For the last scenario, I messed around with events but everywhere I've read says you have to include a reference to the object that it's listening too, which seems to defeat the whole purpose of setting up an events object and listening for an event rather than just querying the state of a variable of that object...
eg.
this.listenTo(sidebarModel , "change:selected", this.render());
How do i pass a reference to sidebarModel object to the singleScreen view for it to know what it's meant to be listening to.
I'm not really looking for a coded answer, more so just an understanding of best practices and how things are meant to be done.I feel like I'm close but I know i'm missing/not understanding something which is why I'm not able to figure this out myself, so a little enlightening on the whole topic would be greatly appreciated. Thanks.
First, you need to understand the role of each Backbone classes. Reading on MVC first might help.
Model
The model isn't necessary when rendering a view. Its role is to handle the data, which can be local, or from an API. All the functions that affect the data should be in the model, and a model shouldn't have anything related to rendering.
There are always exception, where you could use a model to handle data related only to rendering, but you'll know when you find such case.
A simple example is a book:
// The Book model class
var Book = Backbone.Model.extend({
idAttribute: 'code',
urlRoot: '/api/book'
});
// a Book instance
var solaris = new Book({ code: "1083-lem-solaris" });
Fetching from an API would call:
// GET http://example.com/api/book/1083-lem-solaris
solaris.fetch(); // this is async
When fetching, the API returns the JSON encoded data.
{
"code": "1083-lem-solaris",
"title": "Test title",
}
The attributes are merged with the existing attributes, adding the ones that are not there yet, and overwriting the values of the ones already there.
Collection
A collection's role is to manage an array of models, which, again, can be local or fetched from an API. It should contain only functions related to managing the collection.
var Library = Backbone.Collection.extend({
model: Book,
url: '/api/book'
});
var myLibrary = new Library();
// just adds an existing book to our array
myLibrary.add(solaris);
You can fetch a collection to get an array of existing books from an API:
myLibrary.fetch();
The API should return:
[
{ "code": "blah-code-123", "title": "Test title" },
{ "code": "other-code-321", "title": "Other book title" }
]
Using the collection to create a new book and sync with the API:
var myNewBook = myLibrary.create({ title: "my new book" });
This will send a POST request with the attributes and the API should return:
{ "code": "new-code-123", "title": "my new book" },
View
The view handles its root DOM element. It should handle events from its DOM. It's best used to wrap small component and build bigger components from smaller components.
Put links directly in the templates, in the href of an anchor tag. There's no need to use events for that.
link`
Here's how I render (simplified) a list.
// book list item
var BookView = Backbone.View.extend({
tagName: 'li',
template: _.template('<%= title %>'),
initialize: function() {
// when the model is removed from the collection, remove the view.
this.listenTo(this.model, 'remove', this.remove);
},
render: function() {
this.$el.empty().append(this.template(this.model.toJSON()));
return this;
}
});
// book list
var LibraryView = Backbone.View.extend({
template: '<button type="button" class="lib-button">button</button><ul class="list"></ul>',
events: {
'click .lib-button': 'onLibButtonClick'
},
initialize: function(options) {
this.listenTo(this.collection, 'add', this.renderBook);
},
render: function() {
// this is a little more optimised than 'html()'
this.$el.empty().append(this.template);
// caching the list ul jQuery object
this.$list = this.$('.list');
this.collection.each(this.renderBook, this);
return this; // Chaining view calls, good convention http://backbonejs.org/#View-render
},
addItem: function(model) {
// Passing the model to the Book view
var view = new BookView({
model: model
});
this.$list.append(view.render().el);
},
onLibButtonClick: function() {
// whatever
}
});
Router
The router handle routes and should be as simple as possible to avoid it getting too big too fast. It can fetch collections and models, or the view can handle that, it's a matter of pattern at that point.
var LibraryRouter = Backbone.Router.extend({
routes: {
'*index': 'index',
'book/:code': 'bookRoute',
},
index: function() {
var library = new LibraryView({
el: 'body',
// hard-coded collection as a simple example
collection: new Library([
{ "code": "blah-code-123", "title": "Test title" },
{ "code": "other-code-321", "title": "Other book title" }
])
});
library.render();
},
bookRoute: function(code) {
var model = new Book({ code: code });
// here, I assume an API is available and I fetch the data
model.fetch({
success: function() {
var view = new BookPageView({
el: 'body',
model: model
});
view.render();
}
});
}
});
Events
Everything in Backbone has the Events mixin, even the global Backbone object. So every class can use listenTo to bind callbacks to events.
It would be very long to go in depth for everything in Backbone, so ask questions and I'll try to extend my answer.
I'm trying to make a simple list in Backbone but am running into an odd problem. This is my first time using underscore templating, so I'm not sure if that's the reason. It seems like it's a simple problem, but I can't figure it out.
I have a backbone model with the attributes name and band. When I only have it render name, like below, it succeeds in rendering a list of the names.
var template = _.template("<h2><%= name %></h2><br><h3></h3>")
This renders fine. But when I have any iteration with the band variable in, it throws me the error Uncaught ReferenceError: band is not defined, and I can't figure out why. It's getting the reference for name from the attributes object I'm passing it. Why does it see name but not band?
Full code below.
var Album = Backbone.Model.extend({
defaults: {name: "The Album", band: "The Band"}
});
var albumOne = new Album({name: "SGT PEPPER", band: "The Beatles"});
//Eight more album instances here
var template = _.template("<h2><%= name %></h2><br><h3><%=band%></h3>")
var AlbumCollection = Backbone.Collection.extend({
model: Album,
initialize: function(){
this.render()
},
render: function(){
this.each(function(item){
new tempView(item);
});
}
});
var tempView = Backbone.View.extend({
tagName: 'li',
template: template(),
initialize: function(){
this.render();
},
el: $("#shuff"),
render: function(){
this.$el.append(template(this.attributes))
}
});
window.myCollection = new AlbumCollection([albumOne, albumTwo, albumThree, albumFour,albumFive,albumSix,albumSeven,albumEight,albumNine]);
window.myCollection.render();
The breaking point is not your render method, but the template attribute.
You're assigning an evaluated template with template: template() but there is no band on the global context (window.band) and the evaluation breaks. It works with only a namevariable because window.name is in fact a valid property.
Try template: template and your example will run http://jsfiddle.net/nikoshr/6j58kf2w/
Note : I must add that mixing a model and a view will probably lead to unforeseen problems. I would advise to try and keep the models as ignorant of the views as you can.
I've recently started building my first Backbone.js project and am having a difficult time grasping how to handle "individual views" for single items.
If for example, I had a list of todos that were being displayed by a Backbone collection. If I wanted to have the application provide a link and view to an individual todo item, how would I go about that? Would I create a new type of collection and somehow filter the collection down by the id of an individual item? Where would this logic live? Within a router?
Edit
I've since updated my router as such:
var Backbone = require('backbone');
var IndexListing = require('./indexlisting');
var BookView = require('./bookview');
var LibraryCollection = require('./library');
module.exports = Backbone.Router.extend({
routes: {
'': 'allBooks',
'book/:id': 'singleBook'
},
allBooks: function(){
new IndexListing({
el: '#main',
collection: LibraryCollection
}).render();
},
singleBook: function(bookID){
console.log(bookID);
new BookView({
el: '#main',
collection: LibraryCollection.get(bookID)
}).render();
}
});
The bookID is coming straight from my MongoDB ID and returns within the console.log but when I attempt to run this in the browser I get the following error: Uncaught TypeError: undefined is not a function which is referring to the line collection: LibraryCollection.get(bookID)
My LibraryCollection (./library) contains the following:
'use strict';
var Backbone = require('backbone');
var Book = require('./book');
module.exports = Backbone.Collection.extend({
model: Book,
url: '/api/library'
});
Both of my endpoints for GET requests return the following:
/api/library/
[{"_id":"54a38070bdecad02c72a6ff4","name":"Book Name One"},{"_id":"54a38070bdecad02c72a6ff5","name":"Book Name Two"},{"_id":"54a38070bdecad02c72a6ff6","name":"Book Name Three"},{"_id":"54a38070bdecad02c72a6ff7","name":"Book Name Four"}]
/api/library/54a38070bdecad02c72a6ff4
{"_id":"54a38070bdecad02c72a6ff4","name":"Book Name One"}
Edit no 2
I've updated my router for singleBook as follows:
singleBook: function(id){
var lib = new LibraryCollection;
lib.fetch({
success: function(){
book = lib.get(id);
new BookView({
el: '#main',
model: book
}).render();
}
});
}
I really recommend the codeschool backbone courses here for an intro to backbone.
You would have a collection of models (todos). Each todo would have its own view and would show the model data, i.e the todo description. You would probably want to have a wrapper view also known as a collection view that would have each view as a subview, but it might be better to leave that out while you're getting started.
With regards to the router logic, try and think of the functions in a router as controller actions (assuming you are familiar with MVC). There are a number of backbone extensions that create MVC controller-like functionality.
For your question you would create the link in the view when you render it with a template engine. A few engines that come to mind are Underscore, Handlebars and Mustache. A template might look like this:
{{ description }}
In this template we pass in the model and between the curly braces you can see we are trying to pull out the id and description of that model. Out model attributes might look like this:
{
id: 1,
description: "Go shopping"
}
If you want to just get one model you need to make sure that the model has urlRoot property set. Then you need to fetch the model like this:
var singleTodo = new Todo({id: 1});
singleTodo.fetch();
As I said before I would really recommend watching a number of good tutorials on backbone before you get too deep into your app. Good luck!
I guess you mean you're trying to navigate to a page that shows a single todo item? if yes the logic would live in the router
var TodosRouter = Backbone.Router.extend({
routes: {
"todo": "allTodos"
"todo/:todoId": "singleTodo"
},
allTodos: function() {
// assume you have a todo listing view named TodoListing, extended from Backbone.View
// which the view inside has the link to click and navigate to #todo/<todoId>
var view = new TodoListing({
el: $('body'),
collection: YourTodosCollection
}).render();
},
singleTodo: function(todoId) {
// assume you have a todo single view named TodoView, extended from Backbone.View
var view = new TodoView({
el: $('body'),
model: YourTodosCollection.get(todoId)
}).render();
}
});
update based on edit 2
Yes this would works but it will need to fetch the whole collection from the DB and then only get the single model to display, which might cost a lot of the network resource/ browser memory performance.
In my point of view is good to just pass the ID of the book to the BookView, and then we fetch single book from there (provided we have a REST GET call for the single book with ID as param).
var BookView = Backbone.View.extend({
render: function () {...},
initialize: function(option) {
this.model.fetch(); // fetch only single book, with the bookId passed below
//alternative - we do not pass the model option at below, we could parse the bookId from the router's since it will be part of param in URL eg: http://xxxx/book/:bookId, then we can do the below
this.model = new BookModel({id: parsedFromRouter});
this.model.fetch();
}
});
new BookView({
el: '#main',
model: new BookModel({id: bookId})
}).render();
the fetch call should be async, so we might want to show some loading indicator and only render the view when the fetch done
this.model.fetch({
success: function () { //call render }
})
I'm trying to use two models in one view, and template using both of them. I'm working with Marionette. Here is me initialization of the view:
main_app_layout.header.show(new APP.Views.HeaderView({
model: oneModel,
model2 : twoModel}
));
Here is my view:
APP.Views.HeaderView = Backbone.Marionette.ItemView.extend({
template : '#view_template',
className: 'container',
initialize: function() {
//This correctly logs the second model
console.log(this.options.model2);
}
});
And here is the template:
<script id="view_template" type="text/template">
<p>{{twoModel_label}} {{oneModel_data}}</p>
<p>{{twoModel_label2}} {{oneModel_data2}}</p>
</script>
It renders everything correctly using the oneModel data, but doesn't render the second, even though it logs it correctly. I'm using Mustache as my templating language.
Can anyone help?
You can override the serializeData method on your view and have it return both models' data:
APP.Views.HeaderView = Backbone.Marionette.ItemView.extend({
// ...
serializeData: function(){
return {
model1: this.model.toJSON(),
model2: this.options.model2.toJSON()
};
}
});
Then your template would look like this:
<script id="view_template" type="text/template">
<p>{{model1.label}} {{model1.data}}</p>
<p>{{model2.label}} {{model2.data}}</p>
</script>
try creating a complex model that contains the two models, this new model will have the other two as properties and you can acccess the properties of each one like this answer explains..
Access Nested Backbone Model Attributes from Mustache Template
I have the following ItemView template which is filled with customer data (firstName, lastName) and I want to add a CollectionView into the div .addresses.
Template
<script type="text/html" id="template-customer-details">
<h4><%= firstName %> <%= lastName %></h4>
<button class="edit">Edit</button>
<h5>Addresses</h5>
<div class="addresses">...</div>
</script>
Layout
Layout.Details = Backbone.Marionette.ItemView.extend({
template: '#template-customer-details',
regions: {
addresses: ".addresses"
},
serializeData: function () {
return this.model.attributes;
},
initialize: function () {
this.addressList = new App.Models.AddressList();
// Error!
this.regions.addresses.show(this.addressList);
this.bindTo(this, "render", this.$el.refresh, this.$el);
this.model.bind("change", this.render.bind(this));
}
});
I am getting the error "Uncaught TypeError: Object .addresses has no method 'show'."
Do I have to wait until the view is loaded?
I think you've got things a bit confused. An ItemView doesn't do anything with a regions property (you may be thinking of the Application class), so when you try to call this.regions.addresses.show that's the same as calling ".addresses".show.
I think you probably want to use a CompositeView in this case as it combines an ItemView (which you can use for your customer data) and a CollectionView which you can use for your AddressList. You'll also need to define a separate ItemView for an address (as a CollectionView just creates an ItemView for each item in a collection).
Something a little like this (which I've not tested, so may not be entirely correct):
AddressView = Backbone.Marionette.ItemView.extend({
template: '#addressTemplate'
});
Layout.Details = Backbone.Marionette.CompositeView.extend({
template: '#template-customer-details',
itemView: AddressView,
itemViewContainer: '.addresses'
});
// Then create your view something like this:
new Layout.Details({
model: new App.Models.CustomerDetails(),
collection: new App.Models.AddressList()
});
I also don't think you need to specifically bind the change & render events like in your example as marionette will usually take care of that (the same with your implementation of serializeData, which looks like it's vaguely the same as the default implementation)