Backbone listener doesn't appear to be firing - backbone.js

I've got a copule of files that I'm working with and I'm trying to create a listener on this method I have in my backbone view
appView.js.coffee
namespace "happiness_kpi", (exports) ->
exports.appView = Backbone.View.extend
events:
"click #happy" : "selection"
selection: ->
console.log "selection was called"
index.html.haml
%input{ :type => "image", :src => "/assets/smiley.jpg", :alt => "happy", :id => "happy" }
%input{ :type => "image", :src => "/assets/undecided.jpg", :alt => "undecided", :id => "undecided" }
%input{ :type => "image", :src => "/assets/sad.jpg", :alt => "sad", :id => "sad" }
and here is my spec:
app_view_spec.js.coffee
it "is called when one of the faces is clicked", ->
$("body").append('<input alt="happy" id="happy" src="/assets/smiley.jpg" type="image">')
$("body").append('<input alt="undecided" id="undecided" src="/assets/undecided.jpg" type="image">')
$("body").append('<input alt="sad" id="sad" src="/assets/sad.jpg" type="image">')
#subject.selection = sinon.spy()
$("#happy").click
sinon.assert.calledOnce(#subject.selection)
I'm getting the error 'Error: expected spy to be called once but was called 0 times' Anyone have any ideas as to why the event is not being triggered when the input is clicked?
Thanks in advance.

If the problem is not the obvious typo of the brackets when calling click() then the other thing I'm thinking is that the events in Backbone are scoped within the View element. That means that a View cannot list to events happening outside itself (it can, of course, but not by simply using the events object).
Try doing this at the beginning of your test:
#subject.setElement($("body"));
This will set the View el and $el to the body tag, so the images you are appending are actually going inside your View and the events will be triggered.

So after much research and tinkering, I seem to have come to an answer. The problem was definitely the way that I was appending the images to the iframe. Problem was that the test seemed to be creating a new element outside of the iframe, and was not visible to the page. The $('#happy').click() was executing correctly, however, it was executing on something that didn't exist in the iframe.
So my solution:
app_view_spec.js.coffee
beforeEach ->
$("body").append('<div id="display"></div>')
afterEach ->
$("body").append('<div id ="display"></div>').remove()
it "is called when one of the faces is clicked", ->
#subject.emotionSelection = sinon.spy()
#subject.delegateEvents()
$("input#happy").click()
sinon.assert.calledOnce #subject.emotionSelection
appView.js.coffee
el: '#display'
initialize: ->
#render()
events: ->
'click #happy': #emotionSelection
render: ->
#$el.html HandlebarsTemplates.faces()
emotionSelection: ->
console.log "hello"
I found doing it this way was a much nicer approach, I just appended my HAML to the iframe, rather than trying to append each image on its own. Lesson learned. Thanks for the help!

Related

Backbone Marionette Slow Composite Views (200+ collections)

When showing a dropdown composite view collection of around 200 countries my application gets far too slow.
What is the best way to increase performance when dealing with large collections in marionette composite views?
Here is the function in the controller that is very slow to load. It is fast with only the following lines removed:
#layout.shippingCountryRegion.show shippingCountryView
#layout.billingCountryRegion.show billingCountryView
So it appears to be a very slow rendering issue.
Show.Controller =
showProfile: ->
#layout = #getLayoutView()
#layout.on "show", =>
headerView = #getHeaderView()
#layout.headerRegion.show headerView
accessView = #getAccessView()
#layout.accessRegion.show accessView
billingReadmeView = #getBillingReadmeView()
#layout.billingReadmeRegion.show billingReadmeView
billingFieldsView = #getBillingFieldsView()
#layout.billingFieldRegion.show billingFieldsView
shippingReadmeView = #getShippingReadmeView()
#layout.shippingReadmeRegion.show shippingReadmeView
shippingFieldsView = #getShippingFieldsView()
#layout.shippingFieldRegion.show shippingFieldsView
MyApp.request "location:get_countries", (countries) =>
billingCountryView = #getBillingCountryView(countries)
#layout.billingCountryRegion.show billingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('billing_country_id'), (states) =>
billingStateView = #getBillingStatesView(states)
#layout.billingStateRegion.show billingStateView
MyApp.request "location:get_countries", (countries) =>
shippingCountryView = #getShippingCountryView(countries)
#layout.shippingCountryRegion.show shippingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('shipping_country_id'), (states) =>
shippingStateView = #getShippingStatesView(states)
#layout.shippingStateRegion.show shippingStateView
MyApp.mainRegion.show #layout
The billing country view:
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
onRender: ->
this.$el.attr('value', this.model.get('id'));
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
this.$el.attr('selected', 'selected');
class View.BillingCountryDropdown extends MyApp.Views.CompositeView
template: billingCountryTpl
itemView: View.BillingCountryDropdownItem
itemViewContainer: "select"
The template, simply:
<label>Country
<select id="billing_country_id" name="billing_country_id">
<%- name %>
</select>
</label>
Your code can be optimized. Just move content of onRender method to the ItemView attributes.
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
attributes: ->
var id = this.model.get('id');
var attributes = { 'value': id };
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
attributes['selected'] = 'selected';
return attributes
The difference between this method and onRender case is that, on render will execute when collection already rendered and 200+ operations will be done with DOM nodes, which will bring performance issues.
In case of attributes method, it executes upon view creation.
There are few advices you can follow:
1) Ask your self do you really need to render all items at once? Maybe you can render part of collection and render other items on scroll or use pagination or use 'virtual scrioll' with SlickGrid or Webix for example.
2) Checkout how often you re-render your view. Try to minify num of events cause re-render
3) Try to minify num of event listeners of ItemView. Its good practice to delegate context events to CollectionView
4) You can use setTimeout to render collection by parts. For example you divide you coll in 4 parts by 50 items and raise 4 timeouts to render it.
5) You can optimize underscore templating and get rid of with {} operator. http://underscorejs.org/#template
What's in your 'billingCountryItemTpl' varible? If it's just string with template ID then you could precompile your template using Marionette.TemplateCache.
So you'll have:
template: Marionette.TemplateCache.get(billingCountryItemTpl)

Backbone collection and view sorting

I set 'comparator' method in my collection so that I can sort list in collection.
but it doesn't effect to view.
'/api/note/getList', it returns (and it called when collection be initialized by view)
[{"id":22,"name":"Test2","isPublic":1},{"id":11,"name":"Test1","isPublic":1},{"id":33,"name":"Test3","isPublic":1}]
This is my collection,
define [
'models/Note'
],
(Note) ->
class NoteCollection extends Backbone.Collection
model : Note
url : '/api/note/getList'
initialize: () =>
_.bindAll #
#.on 'sort', #onSort
comparator : (item) ->
id = item.get 'id'
return id * -1
onSort: () =>
#.each (model) =>
console.log model.get 'id'
onSort method prints,
11
22
33
correctly, but on view, it shows
22
11
33
This is my view,
define [
'hbs!./noteCollection_tpl'
'./noteItemView'
'collections/NoteCollection'
],
(noteCollection_tpl, noteItemView, NoteCollection) ->
class NoteCollectionView extends Backbone.Marionette.CompositeView
template : noteCollection_tpl
itemView : noteItemView
itemViewContainer : '.noteListContainer'
className : 'noteWrap'
initialize : (options) ->
#collection = new NoteCollection()
Do I have to re-render view? if so, how can I catch the (what) event when after url be loaded and all list be into collection?
please advice what I am doing wrong.
Need to render when collection be sorted.
initialize : (options) ->
#collection = new NoteCollection()
#collection.on 'sort', () =>
#render()
Aww, better using 'listenTo' because,
When the view is destroyed, the listenTo call will automatically remove the event handler. This prevents memory leaks and zombie event listeners.
I'v believed, using marionette will solve this zombie event listeners.
Thanks Yurui Ray Zhang.

angular directive: infinite nesting using ng-include, model update does mess up view

I just found out, that when using the infinite structure in a directive on every model update, the template will get messed up on too many ng-changes. This is caused by the ng-include (I figured out). I use somekind of this in my directive's template via templateUrl, which is written in HAML:
.btn-group{ :name => "FIXME", "ng-required" => "true" }
%a.btn.btn-default.dropdown-toggle{ "data-toggle" => "dropdown", href: "#", "ng-disabled"=>"ngDisabled" }
{{currentValue.name || dropdownPlaceholder }}
%span.caret
%ul.dropdown-menu{ "ng-show" => "!ngDisabled" }
%div{ "ng-repeat" => "model in ngModel", "ng-include" => "'node.html'" }
%script{ :type => "text/ng-template", :id => "node.html" }
%li{ "ng-click" => "selectValue(model)" }
%a
{{model.name}}
%ul{ "ng-repeat" => "model in model.children", :style => "margin-left: 10px;" }
%div{ "ng-include" => "'node.html'" }
on many clicks on the dropdown the view seems to keeps the old items and seems to push the new ones additionally, even though the debug states the model is updating correctly and clean (checked also in directives model). I get an buggy output like this:
https://dl.dropboxusercontent.com/u/21600359/Bildschirmfoto%202013-10-13%20um%2001.31.30.png
Does anybody can give me a hint on how to have ng-include refresh and rebuild the directives template tree like structure?
I really appreciate!
kind regards,
Alex

Binding event to function Coffeescript. Backbone.js

I am trying to fetch a JSON on OnChange event. And making changes to the existing haml page.
My aim is to populate another select field with the json values. At the moment I am just trying to print a message that json was fetched. But I get no change in the html.
In the network tab of the console, I see that the URL request is being made, and a 304 response is returned. I can also see the json in the response.
events:
"submit form" : "onSubmit"
"change [name=repository]" : "onChange"
onChange: (e) ->
e.preventDefault()
name = #$("[name=repository]").val()
localStorage['new_issue_last_repo'] = name
token = localStorage['oauth2_github']['accessToken']
#model = new Assignees(name)
console.log 'Printttttt'
#model.fetch {headers : #token},
success: (model) =>
#$('.assignee').html("<span>Fetched assignees</span>")
error: =>
#$('.assignee').html("<span>Failed to fetch assignees :(</span>")
The haml file looks like this.
.message
.assignee
%form
%section.repo-select
%select{name: 'repository'}
- for repository in #repositories.models
- full_name = repository.get('full_name')
- if localStorage['new_issue_last_repo'] == repository.get('full_name')
%option{value: full_name, selected: 'selected'}= repository.get('full_name')
- else
%option{value: full_name}= repository.get('full_name')
How can I get the .assignee to change once the json is fetched. Also how can I access the json data?
I have a similar function that works. I dont know what I am doing wrong in the onChange function.
onSubmit: (e) ->
e.preventDefault()
name = #$("[name=repository]").val()
localStorage['new_issue_last_repo'] = name
repository = #repositories.find (r) -> r.get('full_name') == name
model = new IssueModel({
body: #$("[name=body]").val()
title: #$("[name=title]").val()
assignee: #$("[name=assignee]").val()
milestone: #$("[name=milestone]").val()
}, {repository: repository})
model.save {},
success: (model) =>
#badge = new Badge()
#badge.addIssues(1)
#$('.message').html("<span>Issue ##{model.get('number')} was created!</span>")
error: =>
#$('.message').html("<span>Failed to create issue :(</span>")
I'm not that big on HAML but this:
%form
%section.repo-select
%select{name: 'repository'}
should become this HTML:
<form>
<section class="repo-select">
<select name="repository">
<!-- ... -->
</select>
</section>
</form>
right? That means that there is nothing that will match the ID-selector #repo-select so of course the handler bound to those events, onChange, will never be called.
If you want to get change-events from that <select>, then you'll want something like this in your events:
'change [name=repository]'
See Backbone's View#delegateEvents and jQuery's Selectors document for details.
As far as your messages go, I think you're a little confused about the difference between the Model#save arguments:
save model.save([attributes], [options])
and the Model#fetch arguments:
fetch model.save([options])
save takes two arguments with the callbacks in the second so this works:
model.save {},
success: (model) => ...
error: => ...
but fetch only takes one argument and the callbacks should be in that argument so this:
#model.fetch {headers : #token},
success: (model) => ...
error: => ...
won't work as fetch won't even see the success or error callbacks. You'd want to say this:
#model.fetch
headers: #token
success: (model) => ...
error: => ...
to get all three arguments to fetch.

Backbone RequireJS and delegateEvents

In my router I require a view like this:
require(['views/business-detail-view'],
function(BusinessDetailView) {
var businessDetailView = new BusinessDetailView({collection: businessOverviewCollection.models[id], id: id});
businessDetailView.render();
}
);
and in the view I'm binding events like this:
events: {
'click #about-btn' : 'aboutHandler',
'click #contact-btn' : 'contactHandler',
'click #deals-btn' : 'dealsHandler',
'click #map-btn' : 'mapHandler'
},
Now the issue is that if the view gets rendered the first times the callbacks are invoked ones. But if the view needs to be rendered again in some other place the callbacks are invoked twice and so on.
How can I prevent this or am I doing something wrong?
UPDATE:
In the meantime I have changed the code in my router to:
if ( !businessDetailView ) {
require(['views/business-detail-view'],
function(BusinessDetailView) {
businessDetailView = new BusinessDetailView({collection: businessOverviewCollection.models[id]});
businessDetailView.render();
}
);
}
else {
businessDetailView.collection = businessOverviewCollection.models[id];
businessDetailView.render();
}
which seem to solve the issue, but I'm still to new to backbone this know whether this is a valid pattern.
At some point in your view you probably clear out the existing HTML on the page and replace it with new HTML. When you clear out old HTML you should also clear out old event handlers so they aren't laying around. For example, in your view when you want to render your newHtml you could do:
#$el.off().html(newHtml)

Resources