Here is my backbone model constructor
define([], function(){
return Backbone.Model.extend({
urlRoot:'/dummy-api/Instances',
defaults:{
name:null,
read:false,
write:false
},
initialize: function () {
this.fetch();
console.log("after init "+this.get("id")+" name="+this.get("name"));
}
})
});
and at /dummy-api/Instances/1 is have put this
{"id":1,"name":"bangladesh"}
And I have attached this model to a view with this
define(['models/instance.js'], function(Model){
View = Backbone.View.extend({
initialize: function() {
this.model = new Model({
id:1
});
});
return new View();
});
The URL is getting called, and it's content is as above, I can see that in firebug, but "name" isnt getting set.
I know I can provide a parse function, which as I am using sequelize-restful-extended I may need to do, but I'd first like backbone to read and set from a fixed file. The doco is straight forward enough, what I have should work, so am I doing something else bad ?
You're logging the values before the model.fetch has completed.
Set a callback instead to log the values after fetch has successfully completed, and it should work as expected.
define([], function(){
return Backbone.Model.extend({
urlRoot:'/dummy-api/Instances',
defaults:{
name:null,
read:false,
write:false
},
initialize: function () {
this.fetch({
success: function() {
console.log("after init "+this.get("id")+" name="+this.get("name"));
}.bind(this)
});
}
})
});
This is necessary because this.fetch() executes an XMLHttpRequest asynchronously, and continues on to the next line of code while that request is executed by your browser in a separate "thread" (for all intents and purposes).
Related
I'm learning Backbone and want to "mock" the results of a .fetch() call within a model. I do not want to use a testing library or actually hit an external service.
Basically I have a setting in my model, where if this.options.mock === true, then just use an internal JSON object as the "result" of the fetch. Else, actually hit the API with a real AJAX request.
However, this doesn't seem to work. My view successfully renders with the model data when I hit the actual API ("real" fetch), but not whenever I try and pass in fake data.
Is there a way to fake a Fetch response in Backbone, without bringing in a testing library like Sinon?
here is the complete model (at least the relevant portions of it). Basically, the model fetches data, and formats it for a template. and then the view which owns the model renders it out.
'use strict';
(function (app, $, Backbone) {
app.Models.contentModel = Backbone.Model.extend({
/**
* Initializes model. Fetches data from API.
* #param {Object} options Configuration settings.
*/
initialize: function (options) {
var that = this;
that.set({
'template': options.template,
'mock': options.mock || false
});
$.when(this.retrieveData()).then(function (data) {
that.formatDataForTemplate(data);
}, function () {
console.error('failed!');
});
},
retrieveData: function () {
var that = this, deferred = $.Deferred();
if (typeof fbs_settings !== 'undefined' && fbs_settings.preview === 'true') {
deferred.resolve(fbs_settings.data);
}
else if (that.get('mock')) {
console.info('in mock block');
var mock = {
'title': 'Test Title',
'description': 'test description',
'position': 1,
'byline': 'Author'
};
deferred.resolve(mock);
}
else {
// hit API like normal.
console.info('in ajax block');
that.fetch({
success: function (collection, response) {
deferred.resolve(response.promotedContent.contentPositions[0]);
},
error: function(collection, response) {
console.error('error: fetch failed for contentModel.');
deferred.resolve();
}
});
}
return deferred.promise();
},
/**
* Formats data on a per-template basis.
* #return {[type]} [description]
*/
formatDataForTemplate: function (data) {
if (this.get('template') === 'welcomead_default') {
this.set({
'title': data.title,
'description': data.description,
'byline': data.author
});
}
// trigger the data formatted event for the view to render.
this.trigger('dataFormatted');
}
});
})(window.app, window.jQuery, window.Backbone);
Relevant bit from the view (ContentView):
this.model = new app.Models.contentModel({template: this.templateName});
this.listenTo(this.model, 'dataFormatted', this.render);
Is the data being set so fast that the listener hasn't been set up yet?
You can override the fetch function like this.
var MockedModel = Backbone.Model.extend({
initialize: function(attr, options) {
if (options.mock) {
this.fetch = this.fakeFetch;
}
},
url: 'http://someUrlThatWIllNeverBeCalled.com',
fakeFetch: function(options) {
var self = this
this.set({
'title': 'Test Title',
'description': 'test description',
'position': 1,
'byline': 'Author'
});
if (typeof options.success === 'function') {
options.success(self, {}, {})
}
}
});
var mockedModel = new MockedModel(null, {
mock: true
})
mockedModel.fetch({
success: function(model, xhr) {
alert(model.get('title'));
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Problem here isn't with the actual implementation of retrieveData but with the way it's being called. When you resolve the deferred before returning you're basically making it instant. This leads to formatDataForTemplate being called while your model is still initializing.
So when you do
this.model = new app.Models.contentModel({template: this.templateName});
this.listenTo(this.model, 'dataFormatted', this.render);
The dataFormatted event ends up being triggered before the listener has registered.
One solution is to use a timeout which should work with just
setTimeout(function() {
deferred.resolve(mock);
});
as that will delay the resolve untill the next round of the event loop when the listener is in place.
Another solution, not involving the setTimeout would be to not call retrieveData during model initialization but rather let the view do it after it has attached its listeners.
this.model = new app.Models.contentModel({template: this.templateName});
this.listenTo(this.model, 'dataFormatted', this.render);
this.model.retrieveData();
I would prefer the latter but if this is just about mocking data to work offline it doesn't really matter in my opinion.
Unrelated to that it's worth noting that the actual signature for initialize on a model is new Model([attributes], [options]) so your initialize should probably look like this
initialize: function (attributes, options) {
var that = this;
that.set({
'template': options.template,
'mock': options.mock || false
});
Just for the sake of readability. That again means that since you are passing only one object you should not need to call set at all.
Here is my problem
I have a very simple backbone collection getting some data for me. Everything works fine like this:
DealershipContacts.Collection = Backbone.Collection.extend({
url:jarvisUrl ("dealership_contacts"),
parse:function(response) {
console.log('parse called');
return response.data;
},
initialize : function(){
_.bindAll(this, 'reset', 'parse');
}
});
When fetch is called parse logs to the console as expected.
But after that point I would like to listen for the reset event so I can use the collection to populate the source data of a bootstrap typeahead input. So I did this:
DealershipContacts.Collection = Backbone.Collection.extend({
url:jarvisUrl ("dealership_contacts"),
parse:function(response) {
console.log('parse called');
console.log(response);
return response.data;
},
reset:function(){
console.log("change fired");
$('.dealership_typeahead').typeahead({source:this.pluck('user_name')});
return true;
},
initialize : function(){
_.bindAll(this, 'reset', 'parse');
}
});
And now the parse event is never fired and the collection does not populate I can't figure out why.
Any insights much appreciated, thanks.
You are not hooking up to the reset event with that code. You are overriding the default Backbone.Collection reset method (you don't want to do that).
DealershipContacts.Collection = Backbone.Collection.extend({
url:jarvisUrl ("dealership_contacts"),
initialize: function(models, options){
this.on('reset', this.doStuff, this);
},
parse:function(response) {
// you DO want to override the default parse method
return response.data;
},
// don't call this method `reset` :)
doStuff:function(){
// do something now that collection is fetched
}
});
I think you were confusing _.bindAll with listening for Backbone events. bindAll does something different, and you don't need it for this.
I am using Backbone.js and trying to populate my model using fetch(). The problem I am having is that the returned data is not populating my model. I have found a similar question here. The difference is that inside of my success function I am not seeing any data changes nor is a 'change' event being fired.
The code:
Model
window.Company = Backbone.Model.extend({
urlRoot: "/api/company",
defaults:{
"id":null,
"name":"",
"address":"",
"city":"",
"state":"",
"phone":""
},
events: {
'change': 'doChange'
},
doChange: function(event) {
alert('company changed');
}
})
The Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"home",
"company/:id":"companyDetails"
},
initialize:function () {
var user = new User();
this.headerView = new HeaderView({
model: user
});
$('.header').html(this.headerView.el);
console.log("router initialized.");
},
companyDetails: function (id) {
var company = new Company({
id: id
});
company.fetch({
success: function(){
console.log('company.id is ' + company.id);
console.log('company.name is ' + company.name);
console.log('company.address is ' + company.address);
$("#content").html(new CompanyView({
model: company
}).el);
}
});
}
});
JSON
{"address":"555 Main St","name":"Confused Technologies","id":"8dc206cc-1524-4623-a6cd-97c185a76392","state":"CO","city":"Denver","zip":"80206","phone":"5551212"}
The name and address are always undefined. I have to be overlooking something simple???
Edit
Including the view that erroneously left out passing the model to the template.
View
window.CompanyView = Backbone.View.extend({
initialize:function () {
this.render();
console.log('CompanyView initialized');
},
render:function (eventName) {
$(this.el).html(this.template());
return this;
}
})
The attributes are not stored directly on the model. They are stored in an attributes hash, so you would access them through company.attributes, though company.get(attribute) is the way it's usually done. Along the same lines, you would pass company.toJSON() to your template function, as that returns a cloned hash of the model's attributes.
As for your change event not firing, I assume you mean the change: doChange in the model's events hash. Backbone Models do not actually do anything with an events hash. That's for delegating DOM events on Backbone Views. I bet if you put company.on("change", function (model) { console.log(model.toJSON()); }) before your fetch call and removed the success callback, you'd see your model in the console.
Also, I don't think your $("#content").html... line is going to work like you expect. I'd rewrite your router callback like this:
companyDetails: function (id) {
var company = new CompanyView({
el: "#content",
model: new Company({ id: id })
});
// This line would be better in your view's initialize, replacing company with this.
company.listenTo(company.model, "change", company.render);
company.model.fetch();
}
CompanyView#render would typically pass this.model.toJSON() to a template function that returns html, and pass that to this.$el.html(). So something like this.$el.html(this.template(this.model.toJSON()));
OK. The problem with not updating my model was as far as I can tell an async issue. I updated the success callback to include the data parameter like so:
success: function (data) {
$('#content').html(new CompanyView({
model: data
}).el);
}
Note that I am not passing the company object as the model rather the raw returned data. This solved my model problem.
I mentioned in a comment that this started with my underscore template variables `<%= name %>' etc... being empty. I changed my view to this:
window.CompanyView = Backbone.View.extend({
initialize:function () {
this.render();
console.log('CompanyView initialized');
},
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
})
Those to things got both my model updated and variables propagating to the template.
I am using this.collection.each() to iterate through the collection fetched from the backend.
Problem: I notice that when I bind the reset event of the collection to the render method of the view in the initialize method and place a console.log() within this.collection.each, I see the console output as expected.
However, If I dont do the binding above, and simply use this.render() within initialize, the console.log() does not output anything. This seems really strange to me, can anyone provide an explaination?
I also placed a console.log(this.collection); just before the loop, and this always outputs the collection correctly! I was guessing that the collection has not been populated on initialization of the View, but that will cause console.log(this.collection); to not show anything.
This Works
SimilarPhotoListView = Backbone.View.extend({
el: '#modal_similar_items',
initialize: function() {
this.collection.on('reset', this.render, this);
},
render: function() {
console.log(this.collection);
this.collection.each(function(photo, index) {
console.log('hello');
}, this);
return this;
}
});
This does not output from within this.collection.each()
SimilarPhotoListView = Backbone.View.extend({
el: '#modal_similar_items',
initialize: function() {
this.render();
},
render: function() {
console.log(this.collection);
this.collection.each(function(photo, index) {
console.log('hello');
}, this);
return this;
}
});
Both classes are instantiated via:
renderSimilarPosts: function() {
this.similarPhotoList = new SimilarPhotoCollection();
this.similarPhotoListView = new SimilarPhotoListView({ collection: this.similarPhotoList });
this.similarPhotoList.fetch({
data: {post_id: this.model.id},
processData: true
});
}
When you initialize your view, this.similarPhotoList is an empty collection. Therefore, when you create your similarPhotoListView, you're passing it an empty collection. similarPhotoListView.initialize calls render thus with an empty collection, all before the collection is populated by fetch.
The reason the first method works is because reset is triggered in collection.fetch. From the backbone source:
fetch:
...
options.success = function(resp, status, xhr) {
collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
if (success) success(collection, resp);
};
...
initialize runs on instantiation, therefore you run render before you even pass in your collection. Additionally, render should not be called directly from initialize
I wanted to see how i could save a model to server using model.save() method when urlRoot is specified on the extended model, but ajax request never fires when i ask for model.fetch() or do model.save(). note: Is hope this is possible without using Collection i suppose?.
HTML
<div id="placeholder"></div>
<script type="text/template" id="view_template">
Hello <%= name %>, here is your script <%= script %>
</script>
Model
window["model"] = Backbone.Model.extend({
initialize: function () {
console.log("CREATED");
},
defaults:{
name:"Please enter your name",
script:"Hello World"
},
urlRoot: "index.aspx",
validate: function (attrs) {
},
sync: function (method, model, success, error) {
console.log("SYNCING", arguments);
}
});
View
window["view"] = Backbone.View.extend({
template:_.template($("#view_template").html()),
initialize: function () {
console.log("INITIALISED VIEW");
this.model.bind("change","render",this);
},
render: function (model) {
console.log("RENDERING");
$(this.el).append(this.template(model));
return this;
}
});
Application
$("document").ready(function () {
var myModel = new model({
name: "Stack Overflow",
script: "alert('Hi SO')"
});
var myView = new view({
model: myModel,
el: $("#placeholder")
});
console.log("SAVING");
myModel.save();
console.log("FETCHING");
myModel.fetch();
});
as you can see in application i call save & fetch but as per documentation this should fire ajax request with POST -> SAVE & GET -> FETCH. But all it does is log's arguments into console in the sync function.
I think the only reason you are not seeing any Ajax requests is that you have overridden the Model.sync method. Normally you would only do this if you wanted to replace the default Ajax syncing implemented in Backbone.sync. See the following line in Model.fetch in backbone.js:
return (this.sync || Backbone.sync).call(this, 'read', this, options);
I did a quick test with your code and I am seeing the Ajax requests if I rename your Model.sync method.