I am attempting to trigger a d3 event using Jasmine. In particular, I want to check that my event listener is being called using a Jasmine spy.
For example, if I attach the d3 zoom behavior to an svg element (I am using Backbone.js for my front end):
Code (a):
class MyView extends Backbone.View
initialize: ->
zoom = d3.behavior.zoom().on("zoom", this.zoom_listener)
d3.select(this.el).append("svg").attr("class", "viewport").call(zoom)
zoom_listener: ->
console.log("zoom called")
The following test in Jasmine fails:
Code (b):
it "calls zoom listener on dblclick", ->
zoom_spy = spyOn(MyView.prototype, "zoom_listener").andCallThrough()
view = new MyView()
view.$(".viewport").trigger("dblclick")
waitsFor((-> zoom_spy.callCount == 1), "Call zoom", 1000)
On the other hand, (just as a sanity check) if I binded a 'dblclick' event to my view as shown below, the above test i.e. Code (b) will pass:
Code (c):
class MyView extends Backbone.View
events:
"dblclick" : "zoom_listener"
initialize: ->
zoom = d3.behavior.zoom().on("zoom", this.zoom_listener)
d3.select(this.el).append("svg").attr("class", "viewport")
# .call(zoom) # commented off this line for the sanity check
zoom_listener: ->
console.log("zoom called")
Can anyone give me some insight as to why I can't seem to get the D3 zoom event triggered within the Jasmine test i.e. Code (b) using my original view above i.e. Code (a)?
Backbone triggers jQuery events, which doesn't seem to register outside of the jQuery world. Some workarounds are explained here. But here is a general way to test D3 events with Jasmine.
it('should trigger a callback on custom events', function() {
fixture.datum(dataset)
.call(barChart);
var callback = jasmine.createSpy("filterCallback");
barChart.on('customHover', callback);
var bars = fixture.selectAll('.bar');
bars[0][0].__onmouseover();
var callBackArguments = callback.argsForCall[0][0];
expect(callback).toHaveBeenCalled();
expect(callBackArguments).toBe(dataset[0]);
});
D3 exposes events on the DOM object. So you can attach a spy and trigger it.
As stated by M. Bostock in https://github.com/mbostock/d3/issues/906: "jQuery trigger doesn't dispatch real events; it only calls its own listeners."
One way to dispatch a real event using vanilla JS is (based on the reply from user "handler" in How to invoke "click" event programmatically in d3?):
Code (b-modified)
it "calls zoom listener on dblclick", ->
zoom_spy = spyOn(MyView.prototype, "zoom_listener").andCallThrough()
view = new MyView()
jQuery.fn.custom_mouse_dblclick = (->
this.each(((i, element) ->
evt = document.createEvent("MouseEvent")
evt.initMouseEvent(
"dblclick",
true,
true,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null)
element.dispatchEvent(evt)
))
)
view.$(".viewport").custom_mouse_dblclick()
waitsFor((-> zoom_spy.callCount == 1), "Call zoom", 1000)
So the Jasmine test in Code (b-modified) passes when performed on Code (a) in the original question i.e. svg element with d3.zoom() behavior.
A variation on the example by #Biovisualize:
fixture.datum(dataset).call(barChart);
var callback = jasmine.createSpy("filterCallback");
barChart.on('customHover', callback);
//to trigger all elements
fixture.selectAll('.bar').each(function() {
this.__onmouseover();
});
expect(callback).toHaveBeenCalled();
Related
For some reason, I can't get preventDestroy: true to work.
In my example, the loading view is removed, when the applicationsListView is showing - even though I pass in preventDestroy true.
var loadingView = new App.Common.Loading.View();
App.layout.mainRegion.show(loadingView);
// Fetch the applications
var fetchingApplications = App.request('application:entities');
$.when(fetchingApplications).done(function(applications) {
var applicationsListView = new List.Applications({
collection: applications
});
App.layout.mainRegion.show(applicationsListView, { preventDestroy: true });
});
It is removed from region but not destroyed.
When you pass preventDestroy : true, it means that marionette doesn't itself call destroy method and event on previous view. Destroy method provides ubinding events and calls destroy on subviews and so on (https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#view-destroy).
But call show with preventDestroy : true still clear innerHTML and put new view in region (look at attachHtml method in backbone.marionette/src/marionette.region.js).
It can be helpfull if you reuse your existing loading view.
I am having a hard time getting my trigger to respond properly. I have plenty that are working but one of them isn't and I can't understand why.
Here is my AppController class
class ProjectOrder.View.AppController extends Backbone.View
initialize: ->
#promptOfficeSearch()
promptOfficeSearch: ->
officeSearch = new ProjectOrder.View.OfficeSearch
officeSearch.on 'createOffice', #promptOfficeCreate
officeSearch.on 'createTicket', #promptTicketCreate
officeSearch.on 'accountAndTicketExist', #killProcessAccountExists
promptOfficeCreate: (serial) ->
#officeModel = new ProjectOrder.Model.OfficeModel()
#officeModel.set('serial_number', serial)
officeCreate = new ProjectOrder.View.OfficeCreator({model: #officeModel})
officeCreate.on 'createTicketOffAccount', #promptTicketCreate
promptTicketCreate: (model) ->
console.log 'promptTicketCreate'
model = model || #officeModel
ticketModel = new ProjectOrder.Model.TicketModel()
new ProjectOrder.View.TicketCreator({ticketModel: ticketModel, officeModel: model})
killProcessAccountExists: (ticket_id) ->
msg = document.createElement 'div'
msg.className = 'account-exists-msg'
msg.innerHTML = "Account already exists. Redirecting to ticket #{ticket_id}..."
$('#create-order-div').append(msg)
setTimeout((->
window.location = "/pto/#{ticket_id}"
), 2000)
All of the triggers from the officeSearch object in the promptOfficeSearch function work properly. They are all triggered as follows, respectively:
#trigger 'createOffice', serial
#trigger 'createTicket', data.model[0]
#trigger 'accountAndTicketExist', data.model
But with the officeCreate object in the promptOfficeCreate, it does not respond to the createTicketOffAccount event which is registered in the submitOffice ajax success callback in my OfficeCreator class:
class ProjectOrder.View.OfficeCreator extends Backbone.View
template: _.template($("#OfficeCreator").html())
id: 'office-creator'
events:
'click .submit' : 'submitOffice'
initialize: ->
#render()
render: ->
#$el.html(#template(#model.toJSON()))
$('#create-order-div').append(#$el)
submitOffice: ->
#setModelData()
#model.save(null,{
success: (model) =>
#trigger 'createTicketOffAccount', model
##$el.remove()
error: ->
alert 'error'
})
setModelData: ->
#model.set({
office_name: $('#office').val()
doctor_name: $('#doctor').val()
emr: $('#has-emr').is(':checked')
forms_builder: $('#has-forms').is(':checked')
iehr: $('#has-iehr').is(':checked')
clipboard: $('#has-clip').is(':checked')
specialty_id: $('#specialty').val()
})
any ideas why my trigger is not working?
I think you need fat arrows on all the methods in your AppController class.
When this event fires:
officeSearch.on 'createOffice', #promptOfficeCreate
the promptOfficeCreate function gets invoked as a normal function as opposed to a method bound to your controller instance as this, so when this happens:
officeCreate.on 'createTicketOffAccount', #promptTicketCreate
#promptTicketCreate is undefined and the event binding doesn't wire up properly.
I have a backbone model like -
ModelA = Backbone.Model.extend({
this.set("prop1",true);
})
and View like -
ViewA = Backbone.View.extend({
initialize : function(){
this.listenTo(this.model,"change:prop1",this.changeProp1)l;
this.model.set("prop1",true);
},
changeProp1 : function(){
// callback doesn't call because I'm setting the same value
}
});
var model1 = new ModelA();
var view1 = new ViewA({model:model1});
Here the callback changeProp1 triggers whenever prop1 changes from true -> false -> true .
But I want to listen everytime whenever I'm setting the same value or different value.
I'd say it's best to leave the change event alone, and implement a new set event (or whatever you want to call it). After all, you want to be notified about things that aren't strictly 'changes'.
You could implement your own version of set() in your model which fires a custom 'set' event and then calls backbone's usual set method afterwards.
var MyModel = Backbone.Model.extend({
set: function(key, val, options) {
// Deal with single name/value or object being passed in
var changes;
if (typeof key === 'object') {
changes = key;
options = val;
} else {
(changes = {})[key] = val;
}
options || (options = {});
// Trigger 'set' event on each property passed in
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('set:' + changes[i], this, this.attributes[changes[i]], options);
}
// Call the usual backbone 'set' method
Backbone.Model.prototype.set.apply(this, arguments);
}
});
and then listen for your new event instead of (or as well as) 'change', where appropriate:
ViewA = Backbone.View.extend({
initialize : function(){
this.listenTo(this.model,"set:prop1",this.changeProp1)l;
this.model.set("prop1",true);
},
However, most of this code is just lifted from Backbone's default set method, and doesn't deal with some other issues such as some option flags and nested events. If you wanted to change the Backbone source itself, the line you want to look for is:
if (!_.isEqual(current[attr], val)) changes.push(attr);
(line 347 in version 1.0.0) and try removing that if clause.
(Code above isn't tested, sorry for any syntax errors)
For implementing above , you have to make changes in change function in backbone.js . Change checks whether the value of the property changed if yes than only it calls the binded function.
I'm creating an ajax upload component which consists of a progress bar for each backbone view, this is how my view template looks like.
<script id="view-template-dropped-file" type="text/html">
<a><%=name %></a><span><%=fileSize%></span>
<div class="ui-progress-bar">
<div class="ui-progress"></div>
</div>
</script>
When I drop files on my drop area I create a view for each file like this
for (i = 0; i < files.length; i++) {
var view = new DroppedFileView({
model: new DroppedFile({
name: files[i].name,
fileSize: files[i].size
})
});
var $li = view.render().$el;
$('#droparea ul').append($li);
});
The drop area with some files added showing a progress bar for each file. http://cl.ly/Lf4v
Now when I press upload I need to show the progress for each file individually.
What I tried to do was to bind to an event in my DroppedFileView like this
initialize: function() {
var app = myapp.app;
app.bind('showProgress', this._progress, this);
}
and the _progress function
_progress: function(percentComplete) {
this.$el.find('.ui-progress').animateProgress((percentComplete * 100), function () { }, 2000);
}
and this is how I trigger the event from the drop area view
xhr: function () {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
app.trigger('showProgress', percentComplete);
}
}, false);
return xhr;
}
of course this will not work because I listen to the same showProgress event in all views which will cause all progress bars to show the same progress.
So, is it possible to bind an event to a specified view so the progress can be updated individually or is events not a good approach?
You might want to consider making the DroppedFile model emit the progress events. So simply instead of triggering the event on app, trigger it on the model instance which is being uploaded.
Your sample code doesn't mention which class holds the xhr method, but it would make sense to define it on the model itself. In which case the event triggering is trivial:
xhr: function () {
var model = this;
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
model.trigger('showProgress', percentComplete);
}
}, false);
return xhr;
}
And in view constructor:
initialize: function() {
this.model.bind('showProgress', this._progress, this);
}
Edit based on comments:
Even if your view structure is a bit more complicated than I assumed above, in my opinion using the DroppedFile model as event emitter is the way to go. If one DroppedFileView represents DroppedFile, it should reflect the state of the model it makes sense.
Just keep track of the models in DropzoneView, just like (or instead of how) you do now with the files in the DropzoneView.files. Whether you want to have the actual AJAX request to be the responsibility of the view or refactor it to the individual models doesn't really matter.
I have my view set up with some events and I want to reference, for example, the button element that was clicked because it has a data attribute I need. I want to do it like so:
events: {
'click #testGadget': 'fireEvent',
...
},
fireEvent: function(){
var x = $(this).data('iCanHaz')
}
But the 'this' variable is scoped to the view itself. I know there's a way to accomplish what I'm looking to do but I can't seem to word my question in a way that returns any google hits.
Can simply be done with the event.target property:
fireEvent: function(e){
var x = $(e.target).data('iCanHaz')
}
see Event Object doc