I'm trying to write a test for debouncing user input in a search query. The function is defined on a Backbone View:
SearchView = Backbone.View.extend({
events: {
"input .search-input": "search"
},
// init, render, etc.
search: _.debounce(function() {
this.collection.fetch();
}, 200)
});
Originally, the Backbone library (v0.9.10) used Underscore (v1.4.4), and the test was defined as follows:
describe("SearchView", function() {
var view, $viewContainer;
beforeEach(function() {
appendSetFixtures('<div class="jasmine-container"></div>');
$viewContainer = $(".jasmine-container");
view = new SearchView({
el: $viewContainer
});
});
afterEach(function() {
view.remove();
view.cleanup();
});
//...
describe("wires the search input", function() {
var collectionStub,
fakeTimer;
beforeEach(function() {
collectionStub = sinon.stub(
SearchResultsCollection.prototype,
"fetch"
);
fakeTimer = sinon.useFakeTimers();
});
afterEach(function() {
collectionStub.restore();
fakeTimer.restore();
});
it("should not trigger a search before 200ms", function() {
fakeTimer.tick(199);
expect(collectionStub).not.toHaveBeenCalled();
});
it("should trigger a search after 200ms", function() {
fakeTimer.tick(200);
expect(collectionStub).toHaveBeenCalled();
});
});
});
However, now I want to incorporate LoDash instead of Underscore. Using the latest Underscore compatibility build on their site (LoDash 2.4.1 / Underscore 1.5.6), all my tests pass except for the one using _.debounce!
I did some research and came across these relevant issues to create a LoDash Underscore build with runInContext, but I have no idea how to use it due to lack of examples. How can I use _.runInContext() in my spec(s) to work with sinon.fakeTimer?
SearchView = Backbone.View.extend({
events: {
"input .search-input": function() {
this.search();
}
},
initialize: function() {
this.search = _.debounce(this.search, 200);
}
// init, render, etc.
search: function() {
this.collection.fetch();
}
});
describe("SearchView", function() {
var view;
var $viewContainer;
var clock;
var lodash = window._;
beforeEach(function() {
appendSetFixtures('<div class="jasmine-container"></div>');
$viewContainer = $(".jasmine-container");
clock = sinon.useFakeTimers();
window._ = _.runInContext(window);
view = new SearchView({
el: $viewContainer
});
});
afterEach(function() {
view.remove();
view.cleanup();
clock.restore();
window._ = lodash;
});
//...
describe("wires the search input", function() {
var collectionStub;
beforeEach(function() {
collectionStub = sinon.stub(
SearchResultsCollection.prototype,
"fetch"
);
});
afterEach(function() {
collectionStub.restore();
});
it("should not trigger a search before 200ms", function() {
fakeTimer.tick(199);
expect(collectionStub).not.toHaveBeenCalled();
});
it("should trigger a search after 200ms", function() {
fakeTimer.tick(200);
expect(collectionStub).toHaveBeenCalled();
});
});
});
You need add this line
_ = _.runInContext(window);
before creation (not initialization) of SearchView or any call of _.debounce(). So it should be right after including Lo-Dash.
This allows you to run lodash in global window context so you can use overridden by SinonJS setTimeout.
Related
There's a remove method on EventView. When I click on the remove button, the Event (Model) should be removed so as the EventView.
With the following code, I can remove the Model from mongodb by clicking on the remove button. But the Model View won't remove itself until I refresh the page.
I am using express, EJS and mongodb for this demo.
app.js // with express routes settings
events.init = function(req, res) { res.render('index') };
events.all = function(req, res) {
db.event.find({}, function(err, event) {
if (err) return;
res.json(event);
});
}
events.delete = function (req, res) {
var Id = db.ObjectId(req.params.id);
db.event.remove({
"_id": Id
});
}
app.get('/', events.init);
app.get('/events', events.all);
app.del('/events/:id', events.delete);
client.js // Backbone Model, Collection and View setup
var Event = Backbone.Model.extend({
idAttribute: "_id"
});
var EventCollection = Backbone.Collection.extend({
model: Event,
url: "/events"
});
var EventView = Backbone.View.extend({
events: {
"click .remove": "remove"
},
initialize: function () {
this.listenTo(this.model, 'destroy', this.remove);
},
remove: function (e) {
this.model.destroy();
},
render: function () {
var html = new EJS({url: '/partials/event-field.ejs'}).render(this.model);
this.$el.html(html);
return this
}
});
var EventCollectionView = Backbone.View.extend({
render: function () {
this.collection.each(function(event){
var eventView = new EventView({ model: event });
this.$el.append(eventView.render().$el);
}, this);
return this
}
});
init.js // Called on page load
$(function () {
var collection = new EventCollection();
collection.fetch({
success: function(data){
var collectionView = new EventCollectionView({ collection: data})
$('.upcoming .list-group').append(collectionView.render().$el);
}
});
});
Somehow I found out how to make it work.
I renamed the remove method to destroy and changed the destroy method like so.
events: {
"click .remove": "destroy"
},
initialize: function () {
this.listenTo(this.model, 'destroy', this.destroy);
},
destroy: function () {
this.remove();
this.unbind();
this.model.destroy();
},
Total newbie to Backbone so apologize if this is a simple question.
I am have successfully loaded a collection and rendered the view. However I have a dropdown with A tags that I would like to use the filter the data displayed. I'm trying to set an event listener in my VIEW and then trigger a function within the view to filter the results and re-render the view.
Here's my code:
IBA.NewsModel = Backbone.Model.extend({});
IBA.NewsCollection = Backbone.Collection.extend({
model: IBA.NewsModel,
url: "/news/api"
});
IBA.NewsView = Backbone.View.extend({
el: '#main',
template: _.template($("#news-article").html()),
events: {
"change .dropdown-item": "filterNews"
},
initialize: function () {
this.collection = new IBA.NewsCollection();
this.listenTo(this.collection, 'reset', this.render);
this.collection.fetch({
success: function() {
console.log("JSON file load was successful");
view.render();
},
error: function(){
console.log('There was some error in loading and processing the JSON file');
}
});
},
render: function () {
this.$el.html(this.template({
articles: this.collection.toJSON()
})
);
return this;
},
filterNews: function (e){
e.preventDefault();
var items = this.collection.where({cat_name: "interviews"});
console.log(items);
this.$el.html(this.template({articles: this.items.toJSON()}));
}
});
var view = new IBA.NewsView();
Easiest way to do it would be to reset the actual collection with the filtered results:
IBA.NewsModel = Backbone.Model.extend({});
IBA.NewsCollection = Backbone.Collection.extend({
model: IBA.NewsModel,
url: "/news/api"
});
IBA.NewsView = Backbone.View.extend({
el: '#main',
template: _.template($("#news-article").html()),
events: {
"change .dropdown-item": "filterNews"
},
initialize: function() {
this.collection = new IBA.NewsCollection();
this.listenTo(this.collection, 'reset', this.render);
this.fetchNews();
},
fetchNews: function() {
var view = this;
this.collection.fetch({
success: function() {
console.log("JSON file load was successful");
view.render();
},
error: function() {
console.log('There was some error in loading and processing the JSON file');
}
});
},
render: function() {
this.$el.html(this.template({
articles: this.collection.toJSON()
}));
return this;
},
filterNews: function(e) {
e.preventDefault();
this.collection.reset(this.collection.where({
cat_name: "interviews"
}));
}
});
var view = new IBA.NewsView();
When you want to go back to original data, fetch it again using the fetchNews() method.
You also had a syntax error that view was not defined in your initialize
I use the backbone to test something, but i don't know why the drawSomething just no show ##"
initialize: function() {
setInterval(function() {
//alert("Hello");
this.drawSomething();
}, 1000);
},
drawSomething: function() {
alert('hi');
},
The problem is that inside setInterval callback context this is not what you expect (it's global object window). Simplest fix is to save proper object reference in variable:
var self = this;
setInterval(function() {
//alert("Hello");
self.drawSomething();
}, 1000);
as you are using backbone, so possible you are using underscore too. Bind should help:
initialize: function () {
var foo = function () { this.drawSomething(); };
foo = _.bind(foo, this);
setInterval(foo, 1000);
}
or jQuery analog Proxy:
foo = $.proxy(foo, this);
as quick solution
initialize: function() {
setInterval(function() {
//alert("Hello");
this.drawSomething();
}.bind(this), 1000);
},
drawSomething: function() {
alert('hi');
},
but I would prefer to use additional variable as dfsq methioned, because some old browsers doesn't support bind
Expanding my original question located here
If my Userview is in Userview.js file and I want to inherit from that class in AdminView.js file, how would I go about it.
I tried this, but would not fit my need as I don't have a class.
UPDATE 1:
define([
'modules/userdetail'
],
function(UserView) {
var adminView
adminView.Views.Content = UserView.Views.Content.extend({
initialize: function() {
//looking to override the fn that is declared in UserView
console.log("AAA");
},
});
}
UPDATE 2:
So digging deep, the User Detail is
define(
[ 'modules/baseClass'],
function(BaseClass) {
// Create a new module
//Create Model
//Create View
UserDetails.Views.Content = Backbone.View
.extend({
template :
initialize : function() {
this.model = new UserDetails.Model();
},
events : {
},
render : function(LayOut) {
return LayOut(this).render().then(this.pageReady);
},
pageReady : function() {
},
});
UserDetails.activate = function() {
app.router.navigate('UserDetails', true);
};
UserDetails.configureRouting = function() {
app.router.route('UserDetails', 'UserDetails',
function() {
layoutmanager.setView('#content',
new UserDetails.Views.Content())
.render();
});
};
return UserDetails;
});
ADMIN:
define([
'modules/baseclass',
'modules/UserDetail'
],
function(BaseClass, UserDetails) {
UserDetail.Views.Content = UserDetail.Views.Content.extend({
render:function(){
console.log("rendering");
UserDetail.Views.Content.prototype.render();
}
});
//create admin model
//admin view
AdminView.Views.Content = Backbone.View.extend({
template: "admin-template",
events: {
},
initialize: function() {
this.model = new AdminModel.Model();
},
render: function(manage) {
return manage(this).render().then(this.pageReady);
},
pageReady: function() {
});
},
AdminView.activate = function() {
app.router.navigate('adminview', true);
};
AdminView.configureRouting = function() {
app.router.route('adminview', 'adminview', function() {
layoutmanager.setView('#content', new AdminView.Views.Content()).render();
layoutmanager.setView('#userDetials', new UserDetials.Views.Content()).render();
});
};
if(app.router && app.router.route) {
AdminView.configureRouting();
}
return AdminView;
});
Now if I have to call the render of the userdetails from admin view, the render method fails as the param is undefined.
I am not well versed with where the para in render is defined as I looked through my code and have not found anything
Either include the script tag for Userview.js before the script tag for AdminView.js, or using a module system like requirejs or browserify where you can specify the two modules as dependencies.
I have a backbone view which load subview. When I load a subview, I would like to show a loader when the view fetch needed datas and hide the loader when the view is ready to render.
I did something like this :
var appView = Backbone.View.extend({
showLoader: function() {
// Code to show loader
},
hideLoader: function() {
// Code to hide loader
},
loadSubView: function() {
this.showLoader();
var myView = new MySubView();
this.$el.html(myView.render().el);
this.hideLoader();
}
});
For now, my sub-view load a collection and is implemented like this :
var mySubView = Backbone.View.extend({
initialize: function() {
this.myCollection.fetch({
async: false
});
},
render: function() {
// Code to render
}
});
My sub view load the collection synchronously because it is the only way I found to know when my view is "ready" to render but I think this is not the best way to use Backbone.
What schould I do ?
There are several ways to do it.
You can explicitly use the pubsub pattern. Something like this:
var AppView = Backbone.View.extend({
showLoader: function() {
console.log('show the spinner');
},
hideLoader: function() {
console.log('hide the spinner');
},
loadSubView: function() {
this.showLoader();
var subView = new SubView();
subView.on('render', this.hideLoader);
this.$el.html(subView.render().el);
}
});
var SubView = Backbone.View.extend({
render: function() {
console.log('a subView render');
this.trigger('render');
return this;
}
});
var appView = new AppView({el: $('body')});
appView.loadSubView();
http://jsfiddle.net/theotheo/qnVhy/
You can attach a function to the ajaxStart/ajaxStop events on the
spinner itself:
var AppView = Backbone.View.extend({
initialize: function() {
var _this = this;
this.$('#spinner')
.hide()
.ajaxStart(_this.showLoader)
.ajaxStop(_this.hideLoader);
}
...
}
Or you can use jQuery.ajaxSetup:
var AppView = Backbone.View.extend({
initialize: function() {
var _this = this;
jQuery.ajaxSetup({
beforeSend: _this.showLoader,
complete: _this.hideLoader,
success: function() {}
});
}
...
}