I am working in backbone.js. In the following code I am making a call to the Nutritionix API in order to populate my collection with the JSON response. I am having trouble populating my collection and appending the result as a list. I am doing this in order to test if my collection has been properly populated and in order to test that it will append to the page. However when I test the code out in the browser I don't see the field[brand_name] attribute appended to the page. Is my collection properly populated? How can I see the aformentioned attribute appended to the page? What is wrong with my code?
Here is my Javascript:
$(function(){
var SearchList = Backbone.Collection.extend({
url: "https://api.nutritionix.com/v1_1/search/taco?results=0%3A20&cal_min=0&cal_max=50000&fields=item_name%2Cbrand_name%2Citem_id%2Cbrand_id&appId=26952a04&appKey=78e2b31849de080049d26dc6cf4f338c",
initialize: function(){
this.bind("reset", function(model, options){
console.log("Inside event");
console.log(model);
});
}
});
var terms = new SearchList();
terms.fetch({
success: function(response,xhr) {
console.log("Inside success");
console.log(response.toJSON());
},
ERROR: function (errorResponse) {
console.log(errorResponse)
}
});
// The main view of the application
var App = Backbone.View.extend({
// Base the view on an existing element
el: $('.container'),
initialize: function(){
this.listenTo(this.model, 'sync', this.render);
// Cache these selectors
// this.total = $('#total span');
this.list = $('#listing');
},
render: function(){
// Calculate the total order amount by agregating
// the prices of only the checked elements
terms.each(function(term){
this.list.append("<li>"+ term.get('field[brand_name]')+"</li>");
}, this);
}
});
});
Here is my HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Bootstrap 101 Template</title>
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h1>Interactive Food Guide</h1>
<div>
<input type="text" id="searchBox"> <br/><br/>
</div>
<ul id="listing"></ul>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<!-- Backbone and Underscore -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.2.1/backbone-min.js"></script>
<!-- apps functionality -->
<script src="js/app.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
</body>
</html>
I can see some errors in the code:
JSON returned by url is an object, and what you should charge in the collection is the array "hits" that is inside the object. The logic of this process is defined in the method "parse" the collection.
The collection was declared out of view. When indicated in view to listen to your collection, you will have problems because the fetch method call before the view can be instantiated, so the view does not realize when the fetch is executed
Here is your code with comments.
$(function(){
var SearchList = Backbone.Collection.extend({
url: "https://api.nutritionix.com/v1_1/search/taco?results=0%3A20&cal_min=0&cal_max=50000&fields=item_name%2Cbrand_name%2Citem_id%2Cbrand_id&appId=26952a04&appKey=78e2b31849de080049d26dc6cf4f338c",
initialize: function(){
},
//** 1. Function "parse" is a Backbone function to parse the response properly
parse:function(response){
//** return the array inside response, when returning the array
//** we left to Backone populate this collection
return response.hits;
}
});
// The main view of the application
var App = Backbone.View.extend({
// Base the view on an existing element
el: $('.container'),
initialize: function(){
//** 2. the view must listen to an object inside in the view
//** so we create a new instance of SearchList and save it into model var of the view
this.model = new SearchList();
this.model.fetch();
this.listenTo(this.model, 'sync', this.render);
// Cache these selectors
// this.total = $('#total span');
this.list = $('#listing');
},
render: function(){
//** 2. Continue
var terms = this.model;
// Calculate the total order amount by agregating
// the prices of only the checked elements
terms.each(function(term){
this.list.append("<li>"+ term.get('fields')["brand_name"]+"</li>");
}, this);
}
});
//** Create an instance of the view to start the program
var foo = new App();
});
Regards
I think the main issue here is that you forgot to instantiate your app:
new App();
Secondly you need to refer to your data with the correct structure:
term.get('hits')
As term is the model, which contains an array of hits
Lastly, you need your collection in your view, and listen for the sync on your views collection:
this.listenTo(this.collection, 'sync', this.render);
I updated your app.js like so:
$(function(){
var SearchList = Backbone.Collection.extend({
url: "https://api.nutritionix.com/v1_1/search/taco?results=0%3A20&cal_min=0&cal_max=50000&fields=item_name%2Cbrand_name%2Citem_id%2Cbrand_id&appId=26952a04&appKey=78e2b31849de080049d26dc6cf4f338c",
initialize: function(){
this.bind("reset", function(model, options){
console.log("Inside event");
console.log(model);
});
}
});
// The main view of the application
var App = Backbone.View.extend({
// Base the view on an existing element
el: $('.container'),
initialize: function () {
this.collection = new SearchList();
this.collection.fetch({
success: function (response, xhr) {
console.log("Inside success");
console.log(response.toJSON());
},
ERROR: function (errorResponse) {
console.log(errorResponse)
}
});
this.listenTo(this.collection, 'sync', this.render);
// Cache these selectors
// this.total = $('#total span');
this.list = $('#listing');
},
render: function(){
var context = this;
this.collection.each(function (term) {
_.each(term.get('hits'), function (item) {
context.list.append("<li>" + item.fields.brand_name + "</li>");
});
}, this);
}
});
new App();
});
The correct way is not to use var context = this; but an element on your view. I just wanted to point you in the right direction :-)
Related
Having understood how Backbone.js communicates with the server, I'm now having trouble with Backbone.View.render().
My javascript code is this:
var myObject = Backbone.Model.extend( {
fetch: function (options) {
// some modifications
}
});
var Templates = myObject.extend({
urlRoot: 'templates/load.php',
initialize: function() {
this.fetch();
}
});
var appTemplates = new Templates();
load.php loads all template files and transforms them to one JSON object that is then returned to the client. So appTemplates will have all my Mustache.js templates ready for the page.
To test that, I created a 'main' template and a View:
var Output = Backbone.View.extend({
template: function() {
return appTemplates.get('main');
},
render: function() {
var rendered = Mustache.to_html(this.template(), this.model.toJSON() );
console.log( rendered ); // has exactly the data I want.
this.$el.html( rendered ); // <body> ... </body> remains empty
$('body').html( rendered ); // works fine.
return this;
},
});
var skeleton = new Output({ el: 'body', model: SomeModelData });
skeleton.listenTo(appTemplates, 'change', skeleton.render);
So why doesn't this.$el.html() work? I thought, this.$el is just a shortcut for $(this.el) but this.el is undefined, regarding the output of console.log (I did not define it? Thought I did...) while this.model works just fine.
The initial HTML is not very spectacular:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="./javascript/jquery-2.1.4.js" ></script>
<script src="./javascript/underscore.js"></script>
<script src="./javascript/backbone.js" ></script>
<script src="./javascript/mustache.min.js"></script>
<script src="./javascript/js.js"></script>
<title></title>
</head>
<body>
</body>
</html>
There is another issue with the 'main' string in in the View constructor. If I send 'main' as a value to the constructor ( { tpl: 'main', ... } ) tpl remains undefined.
What am I doing wrong here?
Edit: Problem solved
As it seems, the problem was that my js-file was included within the <head>...</head> of the html but it needs to be included within <body>...</body> to work as expected. If someone knows a reason for this I would love to hear about it.
This question already has an answer here:
Unable to display Todo Collection on the page
(1 answer)
Closed 8 years ago.
In the below code, unable to render 'TodoList'. Seems like fetching taking time and so displaying '0' and <div id="demo"></div> before only.
and Iam not sure why '3' and 'Descriptions' got displayed later. All I need is to display 'Descriptions List' in the page. Iam able to get data from server but somehow not able to display as soon as the data arrived. Please tell me what changes need to do in the below code?
<html>
<head>
<link rel="stylesheet"
href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.1.1/css/bootstrap.min.css">
</head>
<body>
<div id="demo"></div>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
<script type="text/javascript">
var TodoItem = Backbone.Model.extend({
urlRoot: 'api',
})
var TodoCollection = Backbone.Collection.extend({
model: TodoItem,
url: 'api/todos'
})
var TodoView = Backbone.View.extend({
template: _.template('<h3> ' +'<input type=checkbox ' +'<% if(status === "complete") print("checked") %>/>' +' <%= description %></h3>'),
render: function(){
this.$el.html(this.template(this.model.toJSON()))
}
})
var TodoListView = Backbone.View.extend({
initialize: function(){
this.listenTo(this.collection,'reset',this.render)
this.collection.fetch({reset:true})
},
render: function(){
console.log(this.collection.length)
this.collection.forEach(this.addOne,this)
},
addOne: function(todoItem){
console.log(todoItem.get('description'))
var todoView = new TodoView({model: todoItem})
this.$el.append(todoView.render())
}
})
var todoItem = new TodoItem()
var todoList = new TodoCollection()
var todoListView = new TodoListView({el: '#demo', collection: todoList})
todoListView.render()
console.log(todoListView.el)
</script>
</body>
</html>
Here is the CONSOLE output Iam getting:
0
<div id="demo"></div>
3
pick up cookies
Milk
Cookies
For starters you might want to take out the {reset: true} from your fetch.
A fetch wil automatically clear the model/collection anyway.
Please also use semicolons at the end of your command, not using them will let the browser interpret where the semicolon should be. This takes time and is error prone (the browser might just place it where you didn't think it would).
if this does not work you might want to do add the fetch into the render doing this:
render: function(){
var that = this;
this.collection.fetch().done(function(data) {
console.log(that.collection.length);
that.collection.forEach(that.addOne,that);
});
},
What also might work, but you need to test this, I personally always use the one above:
render: function(){
this.collection.fetch().done(function(data) {
console.log(this.collection.length);
this.collection.forEach(this.addOne,this);
}, this);
},
and Iam not sure why '3' and 'Descriptions' got displayed later - Because it the result of a async Ajax request.
now, try to change your code (watch comment):
var TodoView = Backbone.View.extend({
template: _.template('<h3> ' +'<input type=checkbox ' +'<% if(status === "complete") print("checked") %>/>' +' <%= description %></h3>'),
clearItem : function(){
this.$el.find("h3").remove();
},
render: function(){
//all DOM manipulation in view
this.$el.append(this.template(this.model.attributes));
return this;
}
})
var TodoListView = Backbone.View.extend({
initialize: function(){
// split "reset" event and "add" event
this.listenTo(this.collection,'reset',this.removeAll);
this.listenTo(this.collection,'add',this.addOne);
this.collection.fetch({reset:true});
},
removeAll : function(){
//method to remove all element from view
//your problem is that this event will fire before ajax request done
console.log("reset!");
var todoView = new TodoView();
todoView.clearItem();
},
addOne: function(todoItem){
//fire when a model in the collection change (automatic after fetch result) for each model.
console.log("add ITEM:",todoItem);
var todoView = new TodoView({model: todoItem})
todoView.render();
}
});
NOTE: Remove todoListView.render() in your code.
Sorry but, my english is too bad. I do not have time to explain better. Try if my code work
EVENTS in backbone: http://backbonejs.org/#Events
FETCH in collection: http://backbonejs.org/#Collection-fetch
RENDER a view: http://backbonejs.org/#View-render
Heres what I've been working on: http://jsfiddle.net/leapin_leprechaun/29aysou5/3/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Results App Training</title>
<script src="js/libs/jquery.js"></script>
<script src="js/libs/underscore.js"></script>
<script src="js/libs/backbone.js"></script>
<script type="text/javascript">
$( document ).ready(function() {
/*
***********
Models
***********
*/
var MatchInfo = Backbone.Model.extend({
defaults: {
season:"1415"
}
});//model class
var matchInfo = new MatchInfo(); //model instance
/*
***********
Collections
***********
*/
var Matches = Backbone.Collection.extend({
model: MatchInfo, //note this references the model class, not the model instance
url : "http://www.hookhockey.com/index.php/temp-gillian/",
sync : function(method, collection, options) {
// By setting the dataType to "jsonp", jQuery creates a function
// and adds it as a callback parameter to the request, e.g.:
// [url]&callback=jQuery19104472605645155031_1373700330157&q=bananarama
// If you want another name for the callback, also specify the
// jsonpCallback option.
// After this function is called (by the JSONP response), the script tag
// is removed and the parse method is called, just as it would be
// when AJAX was used.
//console.log('sync');
options.dataType = "jsonp";
return Backbone.sync(method, collection, options);
},
parse : function(response) {
// console.log(response.matches);
//.matches is what the json at http://www.hookhockey.com/index.php/temp-gillian/ is putting out
return response.matches;
}
}); //collection class
var matches = new Matches(); //collection instance
matches.bind("sync", matches.render, matches);
matches.fetch({
success : function(collection, response, options) {
/* notes: calling these outside of the success listener meant that nothing got returned. This is because they fire before the fetch returns http://stackoverflow.com/questions/9431673/load-data-into-a-backbone-collection-from-json-file
the alternative is to call them within the success function or to call them like so:
.complete(function() {
console.log(matches);
console.log('length: ' + matches.length);
});
..after the fetch call.
*/
console.log('in collection instance fetch success: ' + matches.length);
return response;
},
error : function(collection, response, options) {
console.log(response.statusText);
},
// A timeout is the only way to get an error event for JSONP calls!
timeout : 5000
});
/*
***********
Views
***********
*/
var MatchModelView = Backbone.View.extend({
// template: _.template( $("#matchTemplate").html() ), // removed because template was not being found - uses underscore and the content from index.html script tag with the id of matchElement that contains the template tags
id : 'someID',
className: 'someClassName',
initialize: function () {
_.bindAll(this, "render");
this.collection.bind("reset", this.render);
},
render: function() {
//var matchTemplate = this.template(this.model.toJSON()); //passes in all of the model data (using this.model.toJSON()) into the template (this.template) so that info is available to the template tags
var matchTemplate = '<p>' + this.model.get('title') + '</p>';
this.$el.html(matchTemplate); //pass the templated info into the el element and return it for render
return this;
}
}); //model view class
//var matchModelView = new MatchModelView({model:matchInfo}); //model view instance
//console.log(matchModelView.render().el);
var MatchesModelView = Backbone.View.extend({
id: 'somethingelse',
initialize: function () {
_.bindAll(this, "render");
this.collection.bind("reset", this.render);
},
render: function(){
console.log('collection length in view:' + this.collection.length); //returns 0
this.collection.each(this.oneMatch, this);
return this;
},
oneMatch: function (aMatch){
console.log(aMatch);
var matchView = new MatchModelView ({ model: aMatch });
this.$el.append(MatchView.render().el);
}
}); //collection view class
var matchesModelView = new MatchesModelView({collection: matches });
$("#allMatches").html(matchesModelView.render().el);
/*
***********
Routers
***********
*/
}); //end doc ready
</script>
</head>
<body>
<div class="site">
<div id="allMatches">adasdasd</div>
<div id="copy"></div>
</div>
<script id="matchTemplate" type="text/template">
<%= title %>
</script>
</body>
</html>
I've just put 2 alerts in there to show where I think my issue is. I can see that the call to json is working and returning items. But at the time the view kicks in I suspect the call hasn't gone through fully.
Is it bad practice to call a view from within the .fetch success callback? Am I losing the whole modularity advantage of backbone by doing that? Or am I missing something to get the returned objects into the collection?
I'm new to Backbone so you can ignore all my comments within the code, just trying to keep track!! :) I realise they should all be separated out into different js files too, am just getting to grips with things first.
Thanks for your time!
I see a lot of good things, here. You're embracing the Backbone event-driven model (by rendering on sync, for example) and you're off to a good start.
Your problem is that you're calling render on the collection, and not the view, in your sync callback.
matches.bind("sync", matches.render, matches);
You want to move
matches.bind("sync", matches.render, matches);
matches.fetch({ ... });
until after you've instantiated your view. So, you'd do:
var matchesModelView = new MatchesModelView({collection: matches });
matches.bind("sync", matchesModelView.render, matches);
matches.fetch({ ... });
and notice that I replaced matches.render with matchesModelView.render as the callback of the sync event.
Im new to Backbone (dont hate me) but am pulling my hair out trying to do a very simple thing.
Im loading a json file (correctly as I can see it loading in firebug) and I just want to pull some info from it purely for testing (as its my first backbone code)
However, I cant get this working and end up with one blank li tag (code below)
<ul id="phones"></ul>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.2/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.rc.1/handlebars.min.js"></script>
<script id="foo" type="text/template">
<li><%= name %></li>
</script>
<script>
var Phones = Backbone.Collection.extend({
url:'http://backbone.local/phones/phones.json'
})
var PhonesView = Backbone.View.extend({
el:'#phones',
initialize:function(){
this.collection = new Phones();
this.collection.fetch();
this.render();
},
template:_.template($('#foo').html()),
render:function(){
var foo = this.collection.toJSON();
$(this.el).html(this.template(foo));
return this;
}
})
var phonesView = new PhonesView();
</script>
Any pointers greatly appreciated.
Cheers,
UPDATE 1
I thought it may be due to fetch being async so i called render in success callback of fetch as below. The console.log fires fine but still no json data in rendered html (i also changed to using handlebars)
<script>
var Phones = Backbone.Collection.extend({
url:'http://backbone.local/phones/phones.json'
})
var PhonesView = Backbone.View.extend({
el:'#phones',
initialize:function(){
var self = this;
this.collection = new Phones();
this.collection.fetch({
success:function(){
console.log('json loaded');
self.render();
}
});
},
template: Handlebars.compile('<li>sdsadsadsadsad {{name}} dsfcdfd</li>'),
render:function(){
var foo = this.collection.toJSON();
$(this.el).html(this.template(foo));
return this;
}
})
var phonesView = new PhonesView();
</script>
With Handlebars, a collection template looks like this:
{{#items}} <li> {{name}} </li> {{/items}}
You also need to wrap your collection JSON in an items object so that the Handlebars template can reference it as above:
var foo = { items: this.collection.toJSON() };
Edit
There's actually one more issue ... collection.toJSON() doesn't convert each model to JSON. So you need to write:
this.collection.models.map(function(x) { return x.toJSON(); });
Fiddle Demo
On your view:
'initialize': function() {
this.template = _.template('<p><% model.text $></p>');
this.collection.fetch({error: function() { console.log(arguments); }, 'success': _.bind(this.onFetch,this) });
return this;
},
'onFetch': function(collection) {
this.collection.on("add", this.onAdd, this);
this.collection.each( _.bind(this.onAdd, this) );
return this;
},
'onAdd': function(model){
//do something like:
this.$el.find('.items').append(this.template({model: model}) )
);
To answer your explicit question of why you only get an empty li. You must send the template some name data. For example:
render:function(){
var firstModel = this.collection.at(0);
var firstName = firstModel.get("name");
$(this.el).html(this.template({name: firstName}));
return this;
}
Of course, the above code is only to understand what's missing, and not how a backbone application should be implemented. I really recommend that you go over the annotated TODO example linked from Backbone's website, and understand the basic patterns that are implemented there.
Update based on your comment:
Different ways of solving this. Really recommend reading: http://backbonejs.org/docs/todos.html
To continue on the "hacky path" of solving this so you can see something:
addOne: function(phone) {
this.$el.append(this.template(phone.toJSON()));
return this;
},
render: function() {
this.$el.html(); //clear everything in the view
this.collection.forEach(_.bind(this.addOne,this)); //Will call addOne for every model in the collection.
return this;
}
Hope you can have a quick look at what I'm doing here. Essentially, am I doing it right?
Live demo of it here too: http://littlejim.co.uk/code/backbone/messing-around/
I just wanted to get a solid understanding in Backbone before I go too wild. So this is a simple demonstration of creating a collection from a JSON object, passing it to a view and handling simple events. But am I approaching this right? What can I do that's better?
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Showing a simple view with events</title>
<script type="text/javascript" src="../../media/scripts/jquery-1.5.1.min.js"></script>
<script type="text/javascript" src="../../media/scripts/underscore-min.js"></script>
<script type="text/javascript" src="../../media/scripts/backbone-min.js"></script>
<script type="text/javascript" src="application.js"></script>
</head>
<body>
<header>
<h1>Showing views from a collection and basic events</h1>
<p>The list below is made from JSON, passed to the view as a collection and has basic events</p>
</header>
<article>
</article>
</body>
</html>
Here is the JavaScript I currently have. I just need to know if I'm approaching this correctly?
window.App = {
// namespaces
Controller: {},
Model : {},
Collection : {},
View : {},
// code that starts when the app is first fired
initialize : function () {
var collection = new App.Collection.Inputs([
{title: "Item 1"},
{title: "Item 2"},
{title: "Item 3"}
]);
var view = new App.View.InputSet({collection: collection});
$('article').html(view.render().el);
}
}
/*
Collection: Inputs */
App.Collection.Inputs = Backbone.Collection.extend();
/*
View: _Input */
App.View._Input = Backbone.View.extend({
events: {
"click a": "close"
},
// called as soon as a view instance is made
initialize: function() {
// this makes the render, clear etc available at this
// if not setting this, both render() and clear() method will not have themselves in this
_.bindAll(this, "render", "close");
},
// backbone required method, which renders the UI
render: function() {
// this is using underscore templating, which can be passed context
$(this.el).html(_.template('<p><%=title%> [close]</p>', this.model.toJSON()));
return this;
},
close: function() {
// removes the UI element from the page
$(this.el).fadeOut(300);
return false; // don't want click to actually happen
}
});
/*
View: InputSet, uses _Input */
App.View.InputSet = Backbone.View.extend({
events: {
'click a': 'clear'
},
initialize: function() {
// this makes the render, clear etc available at this
// if not setting this, both render() and clear() method will not have themselves in this
_.bindAll(this, "render");
},
// backbone required method, which renders the UI
render: function() {
var that = this;
views = this.collection.map(function(model) {
var view = new App.View._Input({model: model});
$(that.el).append(view.render().el);
return view;
});
$(that.el).append('[clear]');
return this;
},
clear: function() {
$(this.el).find('p').fadeOut(300);
}
});
// wait for the dom to load
$(document).ready(function() {
// this isn't backbone. this is running our earlier defined initialize in App
App.initialize();
});
This looks fine to me. However, I found that things can get tricky once you start doing non-trivial stuff: complex views, nested collections etc.
One thing that could be done differently is that instead of generating input views using collection.map you could bind the collection's add event to a function that generates an _Input view for that item in the collection instead. So you'd have something like this in your InputSet view:
initialize: function() {
_.bindAll(this, "addInput", "removeInput");
this.collection.bind("add", this.addInput);
this.collection.bind("remove", this.removeInput);
}
addInput: function(model) {
var view = new App.View._Input({model: model});
$(this.el).append(view.render().el);
}
I looks good to me - really the only thing I would suggest is that you bind the collection's 'change' event to _Input.render that way changes to your collection automatically re-render the view:
// called as soon as a view instance is made
initialize: function() {
_.bindAll(this, "render", "close");
this.collection.bind('change', this.render);
},
Other than that I think it looks good!