I'm trying to learn Backbone and can't seem to match data from the fetch function into my Underscore template. How can can I get the children array in my JSON and match it to the template?
The Backbone.View looks like this:
var Projects = Backbone.Collection.extend({
url: '/tree/projects'
});
var Portfolio = Backbone.View.extend({
el: '.page',
render: function () {
var that = this;
var projects = new Projects();
projects.fetch({
success: function (projects) {
var template = _.template($('#projects-template').html());
that.$el.html(template({projects: projects.models}));
}
})
}
});
At the url: http://localhost:3000/portfolio/api/tree/projects
The JSON returned looks like this:
{
id:"projects",
url:"http://localhost:8888/portfolio/projects",
uid:"projects",
title:"Projects",
text:"",
files:[
],
children:[
{
id:"projects/example-1",
url:"http://localhost:8888/portfolio/projects/example-1",
uid:"example-1",
title:"Example 1",
images:"",
year:"2017",
tags:"Website",
files:[
],
children:[
]
},
{
id:"projects/example-2",
url:"http://localhost:8888/portfolio/projects/example-2",
uid:"example-2",
title:"Example #"2
text:"Example 2's text",
year:"2016",
tags:"Website",
files:[
{
url:"http://localhost:8888/portfolio/content/1-projects/4-example-2/example_ss.png",
name:"example_ss",
extension:"png",
size:244845,
niceSize:"239.11 kB",
mime:"image/png",
type:"image"
}
],
children:[
]
},
]
}
My Underscore file looks like this:
<script type="text/template" id="projects-template">
<h4>tester</h4>
<div>
<% _.each(projects.children, function (project) { %>
<div>
<div><%= project.get('year') %></div>
<div><%= project.get('title') %></div>
<div><%= project.get('tags') %></div>
</div>
<% }); %>
</div>
</script>
You can define a parse method on the collection:
var Projects = Backbone.Collection.extend({
url: '/tree/projects',
parse: function(response){
/* save other data from response directly to collection if needed.
for eg this.title = response.title; */
return response.children; // now models will be populated from children array
}
});
Do not use parse
While I usually agree with TJ, using parse on the collection is more like a hack than a definite solution. It would work only to get the children projects of a project and nothing more.
The parse function shouldn't have side-effects on the collection and with this approach, changing and saving fields on the parent project wouldn't be easily possible.
It also doesn't deal with the fact that it's a nested structure, it's not just a wrapped array.
This function works best when receiving wrapped data:
{
data: [{ /*...*/ }, { /*...*/ }]
}
Models and collections
What you have here are projects that have nested projects. A project should be a model. You also have files, so you should have a File model.
Take each resource and make a model and collection classes with it. But first, get the shared data out of the way.
var API_ROOT = 'http://localhost:8888/';
File
var FileModel = Backbone.Model.extend({
defaults: {
name: "",
extension: "png",
size: 0,
niceSize: "0 kB",
mime: "image/png",
type: "image"
}
});
var FileCollection = Backbone.Collection.extend({
model: FileModel
});
Project
var ProjectModel = Backbone.Model.extend({
defaults: function() {
return {
title: "",
text: "",
files: [],
children: []
};
},
getProjects: function() {
return this.get('children');
},
setProjects: function(projectArray, options) {
return this.set('children', projectArray, options);
},
getFiles: function() {
return this.get('files');
},
getSubProjectUrl: function() {
return this.get('url');
}
});
var ProjectCollection = Backbone.Collection.extend({
model: ProjectModel,
url: API_ROOT + '/tree/projects'
});
Project view
Then, make a view for a project. This is a simple example, see the additional information for tips on optimizing the rendering.
var ProjectView = Backbone.View.extend({
template: _.template($('#projects-template').html()),
initialize: function(options) {
this.options = _.extend({
depth: 0, // default option
}, options);
// Make a new collection instance with the array when necessary
this.collection = new ProjectCollection(this.model.getProjects(), {
url: this.model.getSubProjectUrl()
});
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$projectList = this.$('.list');
// use the depth option to avoid rendering too much projects
if (this.depth > 0) this.collection.each(this.renderProject, this);
return this;
}
renderProject: function(model) {
this.$projectList.append(new ProjectView({
model: model,
depth: depth - 1
}).render().el);
}
});
With a template like this:
<script type="text/template" id="projects-template">
<h4><%= title %></h4>
<span><%= year %></span><span><%= tags %></span>
<p><%= text %></p>
<div class="list"></div>
</script>
Using the view:
var model = new ProjectModel({ id: "project" });
model.fetch({
success: function() {
var project = new ProjectView({
model: model,
depth: 2
});
}
});
Additional info
Nested models and collections
Efficiently rendering a list
Related
I am struggling with displaying two models/collections in the same page.
<body>
<div id="mainContainer">
<div id="contentContainer"></div>
</div>
<div id="mainContainer2">
<div id="contentContainer2"></div>
</div>
<script id="list_container_tpl" type="text/template">
<div class="grid_5 listContainer">
<div class="box">
<h2 class="box_head grad_colour">Your tasks</h2>
<div class="sorting">Show: <select id="taskSorting"><option value="0">All Current</option><option value="1">Completed</option></select>
<input class="search round_all" id="searchTask" type="text" value="">
</div>
<div class="block">
<ul id="taskList" class="list"></ul>
</div>
</div>
</div>
</script>
<script id="list2_container_tpl" type="text/template">
<div class="grid_5 mylistContainer">
<div class="box">
<h2 class="box_head grad_colour">Your facets</h2>
<div class="sorting">
%{--Show: <select id="taskSorting"><option value="0">All Current</option><option value="1">Completed</option></select>--}%
<input class="search round_all" id="searchFacet" type="text" value="">
</div>
<div class="block">
<ul id="facetList" class="list"></ul>
</div>
</div>
</div>
</script>
<script id="task_item_tpl" type="text/template">
<li class="task">
<h4 class="name searchItem">{{ name }}</h4>
</li>
</script>
<script id="facet_item_tpl" type="text/template">
<li class="facet">
<h5 class="label searchItem">{{ label }}</h5>
</li>
</script>
<script>
var myapp = {
model: {},
view: {},
collection: {},
router: {}
};
var facetsSearch = {
model: {},
view: {},
collection: {},
router: {}
};
</script>
<script src="underscore-min.js"></script>
<script src="handlebars.min.js"></script>
<script src="backbone-min.js"></script>
<script>
/* avoid */
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
</script>
<script>
// model.tasks.js
myapp.model.Tasks = Backbone.Model.extend({
default:{
completed: 0,
name: ""
},
//url:"/js/libs/fixtures/task.json"
});
var tasks1 = new myapp.model.Tasks({
completed: 0,
name: "Clear dishes"
}
);
var tasks2 = new myapp.model.Tasks({
completed: 1,
name: "Get out the trash"
}
);
var tasks3 = new myapp.model.Tasks({
completed: 0,
name: "Do the laundry"
}
);
var tasks4 = new myapp.model.Tasks({
completed: 1,
name: "Vacuuming the carpet"
}
);
// collection.tasks.js
myapp.collection.Tasks = Backbone.Collection.extend({
currentStatus : function(status){
return _(this.filter(function(data) {
return data.get("completed") == status;
}));
},
search : function(letters){
if (letters == "") return this;
var pattern = new RegExp(letters,"gi");
return _(this.filter(function(data) {
return pattern.test(data.get("name"));
}));
}
});
myapp.collection.tasks = new myapp.collection.Tasks([tasks1, tasks2, tasks3, tasks4]);
// route.tasks.js
myapp.router.Tasks = Backbone.Router.extend({
routes: {
"": "list",
},
list: function(){
this.listContainerView = new myapp.view.TasksContainer({
collection: myapp.collection.tasks
});
$("#contentContainer").append(this.listContainerView.render().el);
this.listContainerView.sorts()
}
});
myapp.router.tasks = new myapp.router.Tasks;
<!-- render views -->
myapp.view.TasksContainer = Backbone.View.extend({
events: {
"keyup #searchTask" : "search",
"change #taskSorting" : "sorts"
},
render: function(data) {
$(this.el).html(this.template);
return this;
},
renderList : function(tasks){
$("#taskList").html("");
tasks.each(function(task){
var view = new myapp.view.TasksItem({
model: task,
collection: this.collection
});
$("#taskList").append(view.render().el);
});
return this;
},
initialize : function(){
this.template = _.template($("#list_container_tpl").html());
this.collection.bind("reset", this.render, this);
},
search: function(e){
var letters = $("#searchTask").val();
this.renderList(this.collection.search(letters));
},
sorts: function(e){
var status = $("#taskSorting").find("option:selected").val();
if (status == "") status = 0;
this.renderList(this.collection.currentStatus(status));
}
});
myapp.view.TasksItem = Backbone.View.extend({
events: {},
render: function(data) {
$(this.el).html(this.template(this.model.toJSON()));
console.log(this.model.toJSON(), "became", this.template(this.model.toJSON()));
return this;
},
initialize : function(){
this.template = _.template($("#task_item_tpl").html());
}
});
</script>
<script>
// model.facets.js
facetsSearch.model.Facets = Backbone.Model.extend({
default: {
id: 0,
label: "",
facetValues: []
}
});
var facet1 = new facetsSearch.model.Facets({
id: 1,
label: "Organism",
facetValues: ["Orga1", "Orga2"]
});
var facet2 = new facetsSearch.model.Facets({
id: 2,
label: "Omics",
facetValues: ["Omics1", "Omics2"]
});
var facet3 = new facetsSearch.model.Facets({
id: 3,
label: "Publication Date",
facetValues: ["2016-11-01", "2016-11-02"]
});
// collection.facets.js
facetsSearch.collection.Facets = Backbone.Collection.extend({
search : function(letters){
if (letters == "") return this;
/**
* the g modifier is used to perform a global match (find all matches rather than stopping after the first match).
* Tip: To perform a global, case-insensitive search, use this modifier together with the "i" modifier.
*/
var pattern = new RegExp(letters, "gi");
return _(this.filter(function(data) {
return pattern.test(data.get("label"));
}));
}
});
facetsSearch.collection.facets = new facetsSearch.collection.Facets([facet1, facet2, facet3]);
// route.facets.js
facetsSearch.router.Facets = Backbone.Router.extend({
routes: {
"": "list",
},
list: function(){
this.mylistContainerView = new facetsSearch.view.FacetsContainer({
collection: facetsSearch.collection.facets
});
console.log("Facet collection: ", facetsSearch.collection.facets);
$("#contentContainer2").append(this.mylistContainerView.render().el);
this.mylistContainerView.sorts()
}
});
facetsSearch.router.Facets = new facetsSearch.router.Facets;
facetsSearch.view.FacetsContainer = Backbone.View.extend({
events: {
"keyup #searchFacet" : "search",
"change #facetSorting": "sorts"
},
render: function(data) {
$(this.el).html(this.template);
return this;
},
renderList : function(facets){
$("#facetList").html("");
facets.each(function(facet){
var view2 = new facetsSearch.view.FacetsItem({
model: facet,
collection: this.collection
});
$("#facetList").append(view2.render().el);
});
return this;
},
initialize : function(){
this.template = _.template($("#list2_container_tpl").html());
this.collection.bind("reset", this.render, this);
},
search: function(e){
var letters = $("#searchFacet").val();
this.renderList(this.collection.search(letters));
},
sorts: function(e){
/*var status = $("#taskSorting").find("option:selected").val();
if (status == "") status = 0;
this.renderList(this.collection.currentStatus(status));*/
}
});
facetsSearch.view.FacetsItem = Backbone.View.extend({
events: {},
render: function(data) {
$(this.el).html(this.template(this.model.toJSON()));
console.log(this.model.toJSON(), "became", this.template(this.model.toJSON()));
return this;
},
initialize : function(){
this.template = _.template($("#facet_item_tpl").html());
}
});
</script>
<script>
Backbone.history.start();
</script>
</body>
The problem
To display Tasks above Your facets. I created two bunches of codes to render Tasks and Facets but modified the variable names respectively. Unfortunately, the former cannot be displayed.
As Emile mentioned in his detailed answer, your problem is that you're initializing multiple routers with the same route.
Since you seem to be beginning with Backbone, I'll give you a simpler answer than creating a complicated Layout (parent) view:
Just have a single handler for the specific route, and initialize both your views inside it.
It'll look something like:
myapp.router = Backbone.Router.extend({
routes: {
"": "list",
},
list: function() {
this.listContainerView = new myapp.view.TasksContainer({
collection: myapp.collection.tasks
});
$("#contentContainer").append(this.listContainerView.render().el);
this.listContainerView.sorts(); //this can be done inside the view
this.mylistContainerView = new facetsSearch.view.FacetsContainer({
collection: facetsSearch.collection.facets
});
$("#contentContainer2").append(this.mylistContainerView.render().el);
this.mylistContainerView.sorts(); //this can be done inside the view
}
});
You simply initialize 2 views in the same route.
You made 2 routers, both with an empty route. Each route is registered in Backbone.history, so when the facets router is initialized, its route overrides the tasks router route.
How to have multiple routers?
For the scope of your application, you should just start by making a single router, and handling the page with 2 lists within a parent view. Make a sort of Layout view for that page, which will handle the 2 lists:
var Layout = Backbone.View.extend({
template: _.template($('#layout-template').html()),
// keep the selector strings in a simple object
selectors: {
tasks: '.task-container',
facets: '.facet-container',
},
initialize: function() {
this.view = {
tasks: new TaskList(),
facets: new FacetList()
};
},
render: function() {
this.$el.html(this.template());
var views = this.views,
selectors = this.selectors;
this.$(selectors.tasks).append(views.tasks.render().el);
this.$(selectors.facets).append(views.facets.render().el);
return this;
}
});
Then, only one router:
var Router = Backbone.Router.extend({
routes: {
"": "list",
},
list: function() {
this.listContainerView = new Layout();
$("body").html(this.listContainerView.render().el);
}
});
This won't work with your code as-is, you'll have to incorporate the concepts yourself into your app.
Otherwise, if you really want multiple routers, you must understand that they can't share a route and that at any moment, only one route can be triggered.
When you have multiple routers, each manages the routes of a single module.
var TaskRouter = Backbone.Router.extend({
routes: {
'tasks': 'taskList',
'tasks/:id': 'taskDetails'
}
// ...snip...
});
var FacetsRouter = Backbone.Router.extend({
routes: {
'facets': 'facetList',
'facets/:id': 'facetDetails'
}
// ...snip...
});
Other improvements
Compile the template once
It's more efficient to compile the template once, when the view is being extended, than each time a new view is initialized.
myapp.view.TasksContainer = Backbone.View.extend({
// gets compiled once
template: _.template($("#list_container_tpl").html()),
initialize: function() {
// not here, as it gets compiled for each view
// this.template = _.template($("#list_container_tpl").html())
},
});
Avoid the global jQuery function
Avoid $(this.el) in favor of this.$el.
Avoid $('#divInTemplate') in favor of this.$('.divInTemplate'). It's a shortcut to this.$el.find.
See What is the difference between $el and el for additional informations.
Cache the jQuery objects
Whenever you want to select a child of the view's element, do it once and put the result in a variable.
render: function(data) {
this.$el.html(this.template);
// I like to namespace them inside an object.
this.elements = {
$list: this.$('.task-list'),
$search: this.$('.task-sorting')
};
// then werever you want to use them
this.elements.$list.toggleClass('active');
return this;
},
Use listenTo
Avoid bind/unbind and on/off/once (aliases) in favor of listenTo/stopListening/listenToOnce.
listenTo is an improved version of bind which solves problem with memory leaks.
this.collection.bind("reset", this.render, this);
// becomes
this.listenTo(this.collection, "reset", this.render);
Pass an array of objects to collection
myapp.collection.Tasks = Backbone.Collection.extend({
model: myapp.model.Tasks
// ...snip...
});
myapp.collection.tasks = new myapp.collection.Tasks([{
completed: 0,
name: "Clear dishes"
}, {
completed: 1,
name: "Get out the trash"
}, {
completed: 0,
name: "Do the laundry"
}, {
completed: 1,
name: "Vacuuming the carpet"
}]);
This would be enough, Backbone collection takes care of the rest.
I am building a web page to show recent donations by all users of a Parse based app. I am building this off of the example Todo application found here:https://parse.com/tutorials/todo-app-with-javascript. Here's the code in my main .js file(which mostly mirrors the tutorial todo.js other than the replaced class names and such):
//Donation Model
//--------------
var Donation = Parse.Object.extend("Donation", {
//instance methods
//Default attributes
defaults: {
},
//Ensure that each donation created has content
initialize: function() {
}
});
// This is the transient application state, not persisted on Parse
var AppState = Parse.Object.extend("AppState", {
defaults: {
filter: "all"
}
});
//Donation Collection
//-------------------
var DonationList = Parse.Collection.extend({
model: Donation
});
//Donation Item View
//-----------------
var DonationView = Parse.View.extend({
tagName: "tr",
template: _.template($('#donation-template').html()),
//The DOM events specific to donation
events: {
},
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
//The Application
//---------------
var AdminView = Parse.View.extend({
el: ".content",
initialize: function() {
var self = this;
_.bindAll(this, 'addOne', 'addAll', 'render');
this.$el.html(_.template($('#admin-template').html()));
////create out collection of donations
this.donations = new DonationList;
//setup the Parse query
var query = new Parse.Query(Donation);
query.include("user");
query.include("charity");
query.include("business");
this.donations.query = query;
this.donations.bind('add', this.addOne);
this.donations.bind('reset', this.addAll);
this.donations.bind('all', this.render);
this.donations.fetch({
success: function(donations) {
for(var i = 0; i < donations.length;i++) {
console.warn(donations.models[i]);
}
}
});
state.on("change", this.filter, this);
},
render: function() {
this.delegateEvents();
},
filter: function() {
this.addAll();
},
addOne: function(donation) {
var view = new DonationView({model: donation});
this.$("#donation-list").append(view.render().el);
},
addAll: function(collection, filter) {
this.$('#donation-list').html("");
this.donations.each(this.addOne);
}
});
//The main view for the app
var AppView = Parse.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
//the App already present in the HTML.
el: $("#adminapp"),
initialize: function() {
this.render();
},
render: function() {
new AdminView();
}
});
var state = new AppState;
new AppView;
When the models are fetched from Parse in
this.donations.fetch({
success: function(donations) {
for(var i = 0; i < donations.length;i++) {
console.warn(donations.models[i]);
}
}
});
I have all of the pointer relations on Donation in full, such as the user and all of their properties and the charity and all of its properties etc. However, I'm interested in displaying the username of the user who made a donation in an underscore template, but at that point the user object on donation no longer has a username property. It's as if some properties are being removed between the time the query returns and when the template is provided with the new collection of donations. Here's the underscore template:
<script type="text/template" id="donation-template">
<td><%= donationAmount %></td>
<td><%= charity %></td>
<td><%= user %></td>
<td><%= business %></td>
<td><%= createdAt %></td>
</script>
donationAmount and createdAt are displayed as expected, but charity, user, and business are just displayed as [Object object], and I can't access any of their properties with dot notation. How can I ensure that the properties I need on all of these pointer relations are available for consumption by the underscore view?
setup the UserList as a model collection
return Parse.Collection.extend({
model: User
});
do the 'relation' query and set local model/var
this.collection = new UserList();
this.model = this.options.model;
var relation = this.model.relation("users");
this.collection.query = relation.query();
var user = this.model.id;
var username = this.model.get("name");
this.collection.fetch();
I asked question the other day on this app; after some good advice, I moved on and I now think this is a different issue.
Before, I was not getting any display on the screen/no errors or any console.logs. After working on it some more, I now have my model/view and some of my render function working.
I think the issue is with my template or with my append. Below is the full code as it stands now. There are //comments where I think there maybe some issues.
Any help with this would be greatly appreciated.
EDIT :: Thanks for the advice Niranjan. I made some the changes you mentioned; I took away the counter and sample data. With these new changes, my newsFeed.js is no longer being read and so I am unclear as to how to populate my collection. When I console.log out my collection I get an empty array with my defaults shown, but with the json file not being read in the first place how do I get anything to work?
EDIT#2 :: Thank you again Niranjan. With the changes you suggested and a few of my own, I now have the code below. The issue I have right now, Is my array is being populated far too many times. the JSON file has 8 entries in total and because of my _.each statement in my template it is looping 8 times where I only want it to loop once and then to split the array into separate entries. I tried first splitting it during my response parse but this didn't work, do you have any advice for this?
below the code is links to the live views of code and html/broswer content including a link to the JSON file.
My end goal is to click on one title and have the corresponding content show.
(function(){
var NewsFeedModel = Backbone.Model.extend({
defaults: {
title: '',
content: ''
}
});
var NewsFeedCollection = Backbone.Collection.extend({
model: NewsFeedModel,
url : 'newsFeed.js',
parse: function(response) {
console.log('collection and file loaded');
return response.responseData.feed.entries;
}
});
var NewsFeedView = Backbone.View.extend({
el : '.newsContainer ul',
template: _.template($("#feedTemp").html()),
initialize: function(){
var scopeThis = this;
_.bindAll(this, 'render');
this.collection.fetch({
success: function(collection){
scopeThis.render();
}
});
this.collection.bind( 'add', this.render, this);
console.log('View and Template read');
},
render: function () {
this.$el.html(this.template({
feed: this.collection.toJSON()
}));
console.log(this.collection.toJSON());
}
});
var newsFeedCollection = new NewsFeedCollection();
var newsFeedView = new NewsFeedView({
collection: newsFeedCollection
});
var title = newsFeedCollection.find('title');
var content = newsFeedCollection.find('content > title');
$(document).on("click", "#add", function(title, content) {
console.log("I have been clicked");
if($(title) == $(content)){
console.log('they match');
}
else{
console.log('they dont match');
}
$('.hide').slideToggle("slow");
});
}());
This is my underscore template.
<div class="span12">
<script id="feedTemp" type="text/template">
<% _.each(feed, function(data) { %>
<div id = "titleContent">
<%= data.title %>
<div id="content" class="hide">
<%= data.content %>
</div>
</div>
<% }); %>
</script>
</div>
I am using google drive as a testing ground; links for the full html/code.
https://docs.google.com/file/d/0B0mP2FImEQ6qa3hFTG1YUXpQQm8/edit [code View]
https://googledrive.com/host/0B0mP2FImEQ6qUnFrU3lGcEplb2s/feed.html [browser View]
https://docs.google.com/file/d/0B0mP2FImEQ6qbnBtYnVTWnpheGM/edit [JSON file]
There are lot more things in your code that can be improved.
Here is the JSFIDDLE.
Please go through the comments mentioned in the code.
For trying out things in Underscore's template, check Underscore's Template Editor.
Template:
<button id=add>Add</button>
<div class="newsConatiner">
<ul></ul>
</div>
<script id="feedTemp">
<% _.each(feed, function(data) { %>
<div id = "titleContent">
<h2> <%= data.title %> </h2>
<div id="content">
<%= data.content %>
</div>
</div>
<% }); %>
</script>
Code:
(function () {
var NewsFeedModel = Backbone.Model.extend({
//url: 'newsFeed.js',
defaults: {
title: '',
content: ''
}
});
var NewsFeedCollection = Backbone.Collection.extend({
model: NewsFeedModel,
url: 'newsFeed.js',
parse: function (response) {
console.log('collection and file loaded');
return response.responseData.feed.entries;
}
});
var NewsFeedView = Backbone.View.extend({
el: '.newsConatiner',
//template should not be inside initialize
template: _.template($("#feedTemp").html()),
initialize: function () {
_.bindAll(this, 'render');
this.render();
//ADD event on collection
this.collection.bind('add', this.render, this);
console.log('View and Template read');
},
/*
This initialize will fetch collection data from newsFeed.js.
initialize: function () {
var self = this;
_.bindAll(this, 'render');
this.collection.fetch({
success: function(collection){
self.render();
}
});
//ADD event on collection
this.collection.bind('add', this.render, this);
console.log('View and Template read');
},
*/
render: function () {
//This is how you can render
//Checkout how this.collection is used
this.$el.html(this.template({
feed: this.collection.toJSON()
}));
}
});
//Initialized collection with sample data
var newsCounter = 0;
var newsFeedCollection = new NewsFeedCollection([{
title: 'News '+newsCounter++,
content: 'Content'
}]);
//Created view instance and passed collection
//which is then used in render as this.collection
var newsFeedView = new NewsFeedView({
collection: newsFeedCollection
});
$('#add').click(function(){
newsFeedCollection.add(new NewsFeedModel({
title: 'News ' + newsCounter++,
content: 'Content'
}));
});
}());
I'm fetching some data from my MySQL database with a php file and now I want to display this data in my View by passing it through a Model with the json_encode method. So far I created a Router, a Model, a Collection (is it necessary?) and a View. When i console.log the collection in my View, I can see that the data is actually there but my View shows nothing. When i console.log the Model I get the "undefined" message. So it seems that the Model is not instantiated, but I dont really know how to solve it. I use RequireJS and the HandlebarsJS for HTML templating purpose.
So here is my Router.
define(['backbone',
'views/firstpage',
'views/secondpage',
'views/thirdpage',
'collections/BandCollection']),
function( Backbone,FirstpageView, SecondpageView, ThirdpageView,BandCollection ) {
var Router = Backbone.Router.extend({
routes: {
'': 'index',
'firstpage' : 'firstpage',
'secondpage' : 'secondpage',
'thirdpage' : 'thirdpage'
},
initialize: function () {
this.bandCollection = new BandCollection();
this.bandCollection.fetch({
error: function () {
console.log("error!!");
},
success: function (collection) {
console.log("no error");
}
});
},
thirdpage: function() {
var thirdpageView = new ThirdpageView({ el:'#topContent', collection:this.bandCollection}).render();
},
});
return Router;
}
);
My Model looks like this:
define([
"jquery",
"backbone"
],
function($, Backbone) {
var BandModel = Backbone.Model.extend({
url: "../metal/db/bands.php",
defaults: {
"id": '',
"band": '',
"label": ''
}
});
return BandModel;
});
My Collection:
define([
"backbone",
"models/BandModel"
],
function(Backbone, BandModel) {
var BandCollection = Backbone.Collection.extend({
model: BandModel,
url: "../metal/db/bands.php"
});
return BandCollection;
});
My HTML template:
<div>
<p><%= id %></p>
<p><%= band %></p>
<p><%= label %></p>
</div>
And My View looks like this:
define(['backbone','handlebars', 'text!templates/Thirdpage.html'],
function(Backbone,Handlebars, Template) {
'use strict';
var ThirdpageView = Backbone.View.extend({
template: Handlebars.compile(Template),
initialize: function () {
_.bindAll(this, 'render');
this.render();
},
render: function() {
console.log(this.collection);
this.$el.html(this.template(this.collection.toJSON()));
return this;
}
});
return ThirdpageView;
}
);
As said before, the console.log(this.collection) tells me that the data is available..
{length: 6, models: Array[6], _byId: Object, constructor: function, model: function…}
but console.log(this.model) gives me "undefined" - and the View actually displays the HTML mentioned before and not the data, meaning it actually shows
<div>
<p><%= id %></p>
<p><%= band %></p>
<p><%= label %></p>
</div>
So, can anyone help me out? I'm out of ideas...
Change your render() method in your view like this:
render: function() {
var self = this;
console.log(this.collection);
self.collection.each(function(model){
console.log(this.model); // can view all models here
self.$el.append(self.template({id:model.get('id'),band:model.get('band'),label:model.get('label')}));
});
return this;
}
Change your Template like this:
<div>
<p>{{id}}</p>
<p>{{band}}</p>
<p>{{label}}></p>
</div>
I have a collection which contains one model. In my template I should be able to do:
<% _.each(collection, function(model) { %>
<p>logged in <%= model.username %>!</p>
<% }); %>
But I've found I need to do:
<% _.each(models, function(model) { %>
<p>logged in <%= model.attributes.username %>!</p>
<% }); %>
I'm not sure exactly what the problem is but the model isn't being populated properly. Anyone know why this is happening and how I can set the values within the model so I can loop over a collection of models and access the values simply with model.username ?
Thank you in advance.
Here's my model and collection:
var AccountModel = Backbone.Model.extend({
defaults:
{
username: "bob"
}
});
var AccountCollection = Backbone.Collection.extend(
{
model: AccountModel,
url: "/php/account-details.php",
parse: function(data, xhr)
{
return data
},
initialize: function()
{
}
});
Here's my fetch function:
fetchAccountCollection: function(){
var $this = this;
$this.homepageAccountCollection = new AccountCollection();
$this.homepageAccountCollection.fetch(
{
dataType: "json",
cache: false,
success: function(collection)
{
Backbone.trigger('accountcollection:loaded', collection);
},
error: function()
{
console.log("fetchAccountCollection: error");
}
});
},
When the success function is called the trigger invokes the render function within the controller:
renderAccount: function(collection)
{
var $this = this;
$this.loginPageView = new LoginView(
{
el: '#login-form',
template: 'loggedin-template',
collection: collection
});
$this.loginPageView.render();
},
When $this.loginPageView.render(); is called the following code is executed:
render: function()
{
var collection = this.options.collection;
var tpl = _.template($(this.options.template).html(), collection);
this.$el.html(tpl);
return this;
},
The value of the username is being returned from a PHP script like so:
$array=array('username' => $user['username']);
echo json_encode($array);
Underscore's each method (and many others) are mixed directly into the collection. Use collection.each(function () { ... }) instead of _.each(collection ...).
The problem is that your passing a Backbone.Collection to your template, so when you end up calling the following in your template:
_.each(models,function(model) { ... });
You are iterating through collection.models array which is an array of Backbone.Model's. Since a Backbone.Model stores model values within the attributes property, you have to access via model.attributes.username, you could also use model.get('username'), which would work as well.
As you stated, you would like to access via model.username, and you can do that by calling collection.toJSON() before passing it to the template, you have to put it in an object such as, {collection:collection.toJSON()};. Check out the documentation for toJSON. The method exists for both Backbone.Collection and a Backbone.Model.
The code would be the following:
render: function() {
var collection = this.options.collection;
var tpl = _.template($(this.options.template).html(), {collection:collection.toJSON());
this.$el.html(tpl);
return this;
}
Then you could use your template like:
<% _.each(collection, function(model) { %>
<p>logged in <%= model.username %>!</p>
<% }); %>
Here is a minimal JSBIN DEMO.
As a side note: It looks like your using a version of backbone < 1.1.0. Just be aware this.options goes away in the newer versions. When you upgrade be sure to have a look at the Change Log.