I have been tasked with writing the tests for the following Backbone view. However I did not write the code.
In the following code example I want to stub/spy the update method so that I can check the function is called when the liked attribute of the view's model is changed, but I seem to not be able to target the method.
Is it possible to stub the update function at all?
When I run my tests it logs Hello but the test return the following error:
AssertionError: expected update to have been called at least once, but it was never called
my.Special.Class.LikeButton = function (options) {
/* ***** Other private variables ******* */
var LikeButton = Marionette.ItemView.extend({
model: null,
sSolidPoly: null,
sEmptyPoly: null,
events: {
'click': 'toggleLike'
},
initialize: function (options) {
//Listen to changes in like property in case it's changed from another location in the UI
this.listenTo(this.model, 'change:liked', this.update);
},
onRender: function () {
this.setElement(this.el.innerHTML);
},
update: function () {
console.log('Hello');
.....
}
});
return new LikeButton(options);
}
I have tried to stub the update function in the beforeEach function for the tests:
this._view = new my.Special.Class.LikeButton({
template: '#like-button-template',
model: this.model
});
this.updateStub = sinon.stub(this._view, 'update');
In my test suite:
it('change to model liked attribute calls update', function () {
var __view = this._view.render();
this.model.set({liked: true});
expect(this.updateStub).has.been.called;
});
I think this is a good example of a test suite exposing something that is too nested. Imo rather than trying to fix this you should have the nested LikeButton in a separate module and import it into that file and test it in it's own file.
Otherwise try:
var model = ...;
var likeButton = my.Special.Class.LikeButton({ model: model });
spyOn(likeButton, 'update');
model.set('liked', true);
expect(likeButton.update).toHaveBeenCalledWith(model, true);
Related
I'm trying to get my head around using CommonJS modules within a Backbone application, so I have a skeleton Backbone View defined in /views/categories/edit.js:
app.Views.quoteCategoriesEdit = app.Ui.ModalView.extend({
className: '',
template: JST["templates/quotes/categories/quote-categories-edit.html"],
events: {
'click [data-key="save"]': 'save',
'click [data-key="cancel"]': 'cancel'
},
initialize: function (options) {
var that = this;
_.bindAll(this, 'save', 'cancel');
app.Collections.quotesCategories.on('change add', function () {
that.remove();
});
},
render: function () {
var that = this;
// boilerplate render code
return this;
}
});
If someone could show me how I can convert this into a CommonJS module to be used with Browserify, then I would be very grateful and it'd really help me understand how I go about modularising the rest of the application! Thanks
//once you get things into folders/files, this path may change
//but for now I'm assuming all your views will live in the same directory
var ModalView = require('./modal-view');
var QuoteCategoriesEdit = ModalView.extend({
className: '',
template: JST["templates/quotes/categories/quote-categories-edit.html"],
events: {
'click [data-key="save"]': 'save',
'click [data-key="cancel"]': 'cancel'
},
initialize: function (options) {
var that = this;
_.bindAll(this, 'save', 'cancel');
app.Collections.quotesCategories.on('change add', function () {
that.remove();
});
},
render: function () {
var that = this;
// boilerplate render code
return this;
}
});
//Simplest convention is just 1-class-per-module
//Just export the constructor function
module.exports = QuoteCategoriesEdit;
Follow-up question from the comments:
Very much appreciate this! How would you approach: app.Collections.quotesCategories as I house everything under the app namespace? Do I just require the Collection itself?
So the idea of an "app" namespace is the opposite of being modular/commonjs/browserify/requirejs. You don't need an app object anymore. Any module that needs to create a new instance of this collection would just do var QuotesCategories = require('app/collections/quotes-categories'); and that is all. No more globals or namespace objects. Mostly your views will get the models/collections they need in their constructor function options. Most of your models will get created by calling fetch on a collection, and most of your collections will be instantiated by your router.
Oh, and yes in this specific example it's probably best if non-view code creates the collection and passes it to the view via the constructor options.collection parameter. However, if you decided yes you really wanted your view to instantiate the collection, it wouldn't come from the app global namespace object, it would just come from a require call as you describe in your comment.
I've a model listen on the vent for a event update:TotalCost, which is triggered from (unrelated) Collection C when any model M belonging to collection C changes.
This event is coded in the initialize method as below. On receiving the event I get the following error:
TypeError: this.set is not a function
this.set({ "totalsale": value});
CostModel = Backbone.Model.extend({
defaults: {
totalSale: 0,
totalTax: 0
},
initialize: function(attrs, options) {
if(options) {
if(options.vent) {
this.vent = options.vent;
}
}
this.vent.on("update:TotalCost", function(value) {
this.set({ "totalSale": value}); **//ERROR HERE**
});
}
});
It is highly possible you've forgot to add the new keyword before your model for example you have:
var user = UserModel();
// instead of
var user = new UserModel();
Have you tried using a closure?
CostModel = Backbone.Model.extend({
defaults: {
totalSale: 0,
totalTax: 0
},
initialize: function(attrs, options) {
var self = this;
if(options) {
if(options.vent) {
this.vent = options.vent;
}
}
this.vent.on("update:TotalCost", function(value) {
self.set({ "totalSale": value});
});
}
});
Perhaps you want this to refer to current CostModel instance, to do so you need to pass this to this.vent.on call so event callback will be executed in context of model:
this.vent.on("update:TotalCost", function(value) {
this.set({ "totalSale": value});
}, this);
it may be due to 'set' works on model not on object. so you can, first convert your object in to model then try..
in example:
new Backbone.Model(your_object).set('val', var);
Another cause of this error can be if you try to create a new model without using the "new" keyword
I was getting this mysterious error when using it with Parse. I had:
Parse.User().current().escape("facebookID")
... when I should have had:
Parse.User.current().escape("facebookID")
Removed the extra () and it works fine now.
Another cause:
// render() method in view object
setInterval(this.model.showName, 3000);
// showName() method in model object
showName: function(){
console.log(this.get('name')); // this.get is not a function
}
I am creating my "Hello world" app in backbone js. I am stuck at the very basic.
var gs = {
documentRoot: ""
}; // create namespace for our app
gs.Test = Backbone.Model.extend({
url: gs.documentRoot+'/test.php',
initialize: function(){
this.fetch();
}
});
gs.TestView = Backbone.View.extend({
render: function(){
console.log(this.model);
console.log(this.model.get('testId'));
}
});
var testM = new gs.Test();
var test = new gs.TestView({model: testM});
test.render();
Here when I log model in the console, it shows fetched attributes from the server but I can't access those attributes from test.get('attribute'). I tried logging test.attributes, it gives empty object but when I log test, it shows those attributes in attributes object.
model#fetch method has a success and error callback options that can be passed to fetch. The success callback gets called when the response from the server has come.
Right way to test the fetched attributes of a model is
test.fetch({
success: function(model){
// model here and test are same
console.log(model);
console.log(test.toJSON());
// access your attribute with name `attributeName`
console.log(test.get('attributeName'));
}
});
fetch is async method, so you have to wait some time.
The best solution in this case is promises:
test.fetch().done(function() {
console.log(test);
});
Your updated model:
initialize: function() {
// save link to promise
this.deferred = this.fetch();
}
And your render function:
render: function() {
// use promise to render view after model will be fetched
// use `bind` to save context of this view
this.model.deferred.done(_.bind(function () {
// model is fetched
// all operations goes here
console.log(this.model.get('testId')); // <- proper value
}, this));
console.log(this.model.get('testId')); // <- undefined
}
More about ajax you can read here http://api.jquery.com/jQuery.ajax
var TestModel = Backbone.Model.extend({
url : '/test.php'
});
var test = new TestModel();
// `context` context to be passed to any callback function
test.fetch({context:test}).done(function () {
// `this` is equals to `test` (`context` option)
// In case if you want to get all model data:
// the best way to get model data for read-only mode.
// this metod return a copy of the model's attributes
console.log(this.toJSON());
// you can also use `this.attributes` but this is not recommended
console.log(this.attributes());
// In case if you want to get some model data:
console.log(this.get('some_attribute'));
// If you want to get `c` from this model ({a:{b:{c:1}}}):
console.log(this.get('a').b.c);
});
For those who are stuck with the same problem, here is the solution from the library itself.
Use model's in-built 'sync' event to get the model attributes after fetch()/save() calls.
testM.on('sync',function(){
test.render();
});
suppose I have a model and a view ,ths view have two method:one is bind the document mousemove event and the other is unbind method,defalut I give the document mousemove event, once the model's enable value changed I will call the view's unbind method:
window.ConfigModel = Backbone.Model.extend({
defaults: {
'enable':0
},
initialize: function(){
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
},
change:function () {
this.set('enable', 9);
}
})
window.PortView = Backbone.View.extend({
viewOn: function () {
$(document).on('mousemove', function () {
console.log('move')
})
},
viewOff: function () {
$(document).off('mousemove');
}
})
then I put an input on the document to call the model changed:
$('input').click(function () {
var configModel = new ConfigModel();
configModel.change();
})
the boot script is :
var portView1 = new PortView();
portView1.viewOn();
The problem is once I call the click the input button ,the chrome would tell me an error:Maximum call stack size exceeded it seems the change be invoke many times.So what's the problem with my problem ,how can I solve this problem
Backbone models already have a change method:
change model.change()
Manually trigger the "change" event and a "change:attribute" event for each attribute that has changed. If you've been passing {silent: true} to the set function in order to aggregate rapid changes to a model, you'll want to call model.change() when you're all finished.
Presumably something inside Backbone is trying to call configModel.change() and getting your version of change which triggers another change() call inside Backbone which runs your change which ... until the stack blows up.
You should use a different name for your change method.
That said, your code structure is somewhat bizarre. A model listening to events on itself is well and good but a model creating a view is odd:
initialize: function() {
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
}
And instantiating a view simply to call a single method and then throw it away is strange as is creating a new model just to trigger an event.
I think you probably want to have a single ConfigModel instance as part of your application state, say app.config. Then your click handler would talk to that model:
$('input').click(function () {
app.config.enable_level_9(); // or whatever your 'change' gets renamed to
});
Then you'd have some other part of your application (not necessarily a view) that listens for changes to app.config and acts appropriately:
app.viewOn = function() {
$(document).on('mousemove', function() {
console.log('move')
});
};
app.viewOff = function() {
$(document).off('mousemove');
};
app.init = function() {
app.config = new ConfigModel();
app.viewOn();
$('input').click(function () {
app.config.enable_level_9();
});
// ...
};
And then start the application with a single app.init() call:
$(function() {
app.init();
});
I have the following problem…
MyView which is connected to two views: TaskModel and UserModel
TaskModel = {id: 1, taskName: "myTask", creatorName: "myName", creator_id: 2 },
UserModel = {id: 2, avatar: "someAvatar"}
The view should display
{{taskName}}, {{creatorName}}, {{someAvatar}}
As you can see the fetch of TaskModel and UserModel should be synchronized, because the userModel.fetch needs of taskModel.get("creator_id")
Which approach do you recommend me to display/handle the view and the two models?
You could make the view smart enough to not render until it has everything it needs.
Suppose you have a user and a task and you pass them both to the view's constructor:
initialize: function(user, task) {
_.bindAll(this, 'render');
this.user = user;
this.task = task;
this.user.on('change', this.render);
this.task.on('change', this.render);
}
Now you have a view that has references to both the user and the task and is listening for "change" events on both. Then, the render method can ask the models if they have everything they're supposed to have, for example:
render: function() {
if(this.user.has('name')
&& this.task.has('name')) {
this.$el.append(this.template({
task: this.task.toJSON(),
user: this.user.toJSON()
}));
}
return this;
}
So render will wait until both the this.user and this.task are fully loaded before it fills in the proper HTML; if it is called before its models have been loaded, then it renders nothing and returns an empty placeholder. This approach keeps all of the view's logic nicely hidden away inside the view where it belongs and it easily generalizes.
Demo: http://jsfiddle.net/ambiguous/rreu5jd8/
You could also use Underscore's isEmpty (which is mixed into Backbone models) instead of checking a specific property:
render: function() {
if(!this.user.isEmpty()
&& !this.task.isEmpty()) {
this.$el.append(this.template({
task: this.task.toJSON(),
user: this.user.toJSON()
}));
}
return this;
}
That assumes that you don't have any defaults of course.
Demo: http://jsfiddle.net/ambiguous/4q07budc/
jQuery's Deferreds work well here. As a crude example:
var succesFunction = function () {
console.log('success');
};
var errorFunction = function () {
console.log('error');
};
$.when(taskModel.fetch(), userModel.fetch()).then(successFunction, errorFunction);
You could also pipe the request through using the crude data (remember that fetch, save, create are really just wrappers around jQuery's $.ajax object.
var taskModelDeferred = taskModel.fetch();
var userModelDeferred = taskModelDeferred.pipe(function( data ) {
return userModel.fetch({ data: { user: data.userId }});
});
note: Backbone returns the collection and model in the success / error functions by default on collections and models so if you need this be sure have a reference handy.
I've run into this very same issue with a complex layout that used two models and multiple views. For that, instead of trying to synchronize the fetches, I simply used the "success" function of one model to invoke the fetch of the other. My views would listen only to the change of the second model. For instance:
var model1 = Backbone.Model.extend({
...
});
var model2 = Backbone.Model.extend({
...
});
var view1 = Backbone.View.extend({
...
});
var view2 = Backbone.View.extend({
...
});
model2.on("change",view1.render, view1);
model2.on("change",view2.render, view2);
Then...
model1.fetch({
success : function() {
model2.fetch();
}
});
The point to this is you don't have to do any sophisticated synchronization. You simply cascade the fetches and respond to the last model's fetch.