Debugging events that are not loaded in view - backbone.js

My events are not loading automatically. When I added delegateEvents() at the end of the render() method, it worked for a while. I do not want to use delegateEvents, but now, even with delegateEvents the events are not loading.
I reckon the DOM is not known at the time, so the events aren't bound, but how do I check (debug) that?
View:
class EditGroup extends BaseView
initialize: ->
#render()
render: ->
html = _.template tpl, #model.toJSON()
#$el.html html
for own key, options of FormConfig[#model.type]
options.key = key
options.value = #model.get key
input = new Input options
input.on 'valuechanged', (key, value) => #model.set key, value
#$('section.'+key).html input.$el
#delegateEvents() # doesn't work
#
DOM:
h2 Edit Group
section.title
section.type
section.members
button.save.btn.btn-primary(onclick="return false") Save changes
In the sections type and member there are typeaheads and selects rendered (Backbone views). One works without delegateEvents and the other works with. The events in the parent view (shown above) don't work at all. Removing the for-loop doesn't make any difference.

Ok, I solved it, but I don't understand the workings.
I'm using a 'view manager', which registers all views and shows (read: attaches the html to the DOM) the parent view. The view manager's show function is triggered when a new route is fired, but the route was fired double, once as "route:edit" and once as "route". I catch them with router.on "all", (eventName) -> etc.. I reckon the events are bound to the html, but the html is overridden by the second router event without the bindings and attached to the DOM. Question remains why the route is fired twice.

Related

Backbone.js 'swallows' click event if another event triggers a re-render

What I want to achieve is that on form changes, the whole view should be re-rendered. This is to provide a preview of the data just edited, and to hide certain elements in the form when check boxes are ticked.
When the user edits the field and clicks on the button without leaving the filed first two events are fired at the same time: change, click. The change handler first updates the model, which triggers a re-render of the form. When it's the click events turn, nothing happens. I guess it has to do with the re-render because when I comment out the
#model.on 'change', #render, #
Both event handlers are executed as it should be.
Maybe the click handler is not executed because the click target has been removed from dom and a new button has been added? How would I fix this? I was thinking the code I wrote was 'idiomatic' Backbone.js, but I'm still learning :-)
Here is a simplified version of my code showing the problem:
jsbin
Let us add a few things so that we can see what's going on. First we'll mark the Save button with a unique ID:
render: ->
id = "b#{Math.floor(Math.random() * 1000)}"
console.log('button id = ', id)
#...
And then we can see which button was hit:
save: ->
console.log('pressed = ', #$('button').attr('id'))
#...
We'll also add a global click handler to watch the <button> outside of the Backbone stuff:
$(document).on('click', 'button', ->
console.log('global click = ', #id)
)
Live version: http://jsbin.com/oviruz/6/edit
Play around with that version a bit and you might see what is going on:
Change the content of the <input>.
Try to click Save.
As soon as the <input> loses focus, the change event is triggered.
That event calls fieldChanged which does #model.set(...).
The #model.set call triggers Backbone's events, in particular, the #model.on(...) from the view's initialize.
The Backbone event sends us into render which does a #$el.html(...) which replaces both the <input> and the <button>.
The html call kills all the DOM elements inside the view's el. But, and this is a big but, the browser needs to get control again before this process finishes.
Now we're back into the event queue to deal with the click on Save. But the <button> we're clicking is a zombie as the browser's work queue looks like this: deal with the click event, replace the DOM elements from 3.4. Here the work from 3.4 isn't complete so the <button> that you're clicking is half in the DOM and half dead and won't respond to any events.
You have two event queues fighting each other; your Backbone events are changing the DOM behind the browser's back and, since JavaScript is single threaded, the browser is losing and getting confused.
If you delay the #$el.html call long enough to let the browser catch up:
set_html = =>
#$el.html """
<input type="text" id="text" value="#{#model.get('foo')}"/>
<button class="save" id="#{id}">Save</button>
"""
setTimeout(set_html, 1000) # Go higher if necessary.
You'll get the behavior you're expecting. But that's an awful, horrific, nasty, and shameful kludge.
Messing around with the DOM while you're still processing events on those DOM elements is fraught with danger and is little more than a complicated way to hurt yourself.
If you want to validate the field when it changes and bind the view's render to "change" events on the model, then I think you'll have to do the validation by hand and use a silent set call:
fieldChanged: (e) ->
field = #$(e.currentTarget)
#model.set({ foo: field.val() }, { silent: true })
// #model.validate(#model.attributes) and do something with the return value
If you do a #model.save() in the Save button's callback, the silent changes will be validated en mass and sent to the server. Something like this: http://jsbin.com/oviruz/7/edit
Or you skip the #model.set inside fieldChanged and just use #model.validate:
fieldChanged: (e) ->
val = #$(e.currentTarget).val()
// #model.validate(foo: val) and do something with the return value
and leave all the setting stuff for save:
save: ->
#model.save(foo: #$('#text').val())
Something like this: http://jsbin.com/oviruz/8/edit
You can add a little delay before update model in fieldChange, you can replace change event with keyup. There might be many workarounds, but probably best was would be not to re-render whole view on model change.

Backbone.js event handler triggers multiple times

I have a View called Form that renders either a form to edit a list, or the list itself, depending on what argument is passed to render. I've added event handlers so that the show/edit mode can be toggled. I've taken this out from the code below to keep it simple, but this just gives a bit of context to what the View does in context.
I can instantiate this Form view as a child in another view that requires a form, or the list to be rendered, which I've done in the New view, where it would be rendered as a form.
When I need to save, I call the form:save event, which triggers a routine in the Form view that saves the form, I've just made it call a console.log here to show it works. In my code, I call form:save through an $('a#submit').click binding which binds to navigation buttons that are inserted by an ApplicationView (but I don't think that matters for the purposes of this question.)
Lets say I navigate away from the New view, and I go back to it a number of times. When I hit save, the method runs the number of times I have instantiated and rendered a new Form view.
So far:
I've tried doing unbind() and remove() in a close method on the Form view from the New view with no luck.
I think I may have problems with scoping, but I'm unsure.
I know this isn't related to my navigation bindings.
I think this may be to do with zombie views.
Any pointers to make it only run once?
App.Views.New = Support.CompositeView.extend
initialize: (options) ->
_.bindAll this, 'render'
#model = new App.Models.Item()
render: ->
self = this
form = new App.Views.Form model: #model, collection: #collection
#$el.append form.render().el
setTimeout (->
$('a#submit').click (e) ->
e.preventDefault()
App.eventHandler.trigger 'form:save'
), 0
this
App.Views.Form = Support.CompositeView.extend
initialize: ->
_.bindAll this, 'render', 'save'
App.eventHandler.on 'form:save', #save
render: ->
self = this
# RENDER TEMPLATE HERE
this
save: ->
console.log 'form saved'
I believe your issue is that you are creating a new view each time you want to render the form, but you aren't getting rid of your old view. What you can do is either destroy your old view, or keep a reference to it and instead of creating a new view each time, just pass in the model to the existing view and refresh/rerender the display

Backbonejs: How to unbind collection bindings when a view is removed?

I've got a backbonejs application with two view. It kind of looks like this:
<body>
<header></header>
<div id="content"></div>
</body>
Every time a view is loaded the app overwrites the current view by completely overwriting the contents of #content.
// Like this...
$('#content').html( new primaryView().render() );
// ...and this.
$('#content').html( new secondaryView().render() );
The application has a global collection.
App.Collection();
The secondary view modifies itself depending on the global collection. Therefor it binds a function to the 'add' event' on App.Collection in the views initialize function;
App.Collection.bind('add', function(){
console.log('Item added');
});
Which result in my problem. Every time the secondary view is loaded a new function is binded to App.Collection's add event. If I go from the primary view to the secondary view three times, the function will fire three times everytime an item is added to App.Collection.
What am I doing wrong?
I can see how I would do it if there was an uninitialize function on views.
I can see how I could do it if I never removed a view once it was loaded.
I can see how I would do it if I could namespace events like in Jquery. (by unbinding before binding).
You can generalize your problem quite a bit. Basically, you are writing an event-driven app, and in such app events should be taken care of.
Check out this post to see a recommended way to work with event handlers in backbone.
Depending on the situation, you can use initialize and render methods to handle different aspects of creating a view. For instance, you can put your binding inside the initialize
initialize: function() {
App.Collection.bind('add', function(){
this.view.render()
});
}
which only fires when the view is created. This binds your render method to the add event. Then in your render method you can actually create the html.
This prevents the binding from happening every time you need to re-render.

testing backbone.js view events with jasmine

I'm trying to implement view tests for a Coffeescript implementation of the ubiquitous backbone.js 'todo' example (see github.com/rsim/backbone_coffeescript_demo.)
My jasmine tests of the above demo work pretty well, except for view events. I expect I am stuck on one or both of the following i) I do not understand the event binding in the view code, ii) I do not understand how to properly set up the Jasmine test of the view code events.
Here is an example of the 'edit' event...
class TodoApp.TodoView extends Backbone.View
tagName: "li"
template: TodoApp.template '#item-template'
events:
"dblclick div.todo-content" : "edit"
...
initialize: ->
_.bindAll this, 'render', 'close'
#model.bind 'change', #render
#model.bind 'destroy', => #remove()
render: ->
$(#el).html #template #model.toJSON()
#setContent()
this
edit: ->
$(#el).addClass "editing"
#input.focus()
...
...now here's a test of whether focus was gained upon double clicking:
describe "edit state", ->
li = null
beforeEach ->
setFixtures('<ul id="todo-list"></ul>')
model = new Backbone.Model id: 1, content: todoValue, done: false
view = new TodoApp.TodoView model: model, template: readFixtures("_item_template.html")
$("ul#todo-list").append(view.render().el)
li = $('ul#todo-list li:first')
target = li.find('div.todo-content')
expect(target).toExist()
target.trigger('dblclick') # here's the event!
it "input takes focus", ->
expect(li.find('.todo-input').is(':focus')).toBe(true)
The expectation on neither i) the spy nor ii) the focus is met.
Is there a peculiarity to testing backbone.js event code about which I should be aware in Jasmine?
you're spying on the view's edit method. this replaces the method with a spy object, which means the actual edit method won't get called. therefore, you're #input.focus will never fire.
since you want the test to actually call your edit method, i would remove the spy for it.
side note: don't call expect methods in your beforeEach. if you truly need to set an expectation on those, create an it block for them.
I'm not great with coffescript so I might be missing something but where are you setting up your spy?
In order to test event calling you may need to refresh the view's events once you've set up the spy.
spyOn(view, 'edit');
view.delegateEvents();
target.trigger('dblclick');
it("should call edit when target is double clicked", function() {
expect(view.edit).toHaveBeenCalled()
});
The issue with this is that Backbone.View events object is using event delegation. For the events to be able to be called work the element has to be part of DOM, you can do this by doing something like $('body').append(someView.el) in your beforeEach. Personally, I try not to test if Backbone is correctly setting the events and triggering clicks manually, is more practical for unit tests to call the callback handlers directly avoiding the DOM completely which can slow down your tests a lot.
For :focus is the same problem, there has to be an element in the DOM so that jQuery can tell if an element is focused. In this case it's better to set some state as part of your component and not checking for state via querying the DOM, e.g.: someView.hasFocus === true. Alternatively you can spy on the elements focus implementation and check if it was called.
I did not write my test in coffeescript, but I did have the same problem, so I hope you will forgive me for answering in javadcript. I ended up breaking down your test into two different tests. First, I tested if calling the view's edit function set the focus on the input box. After that, I tested whether the edit was called when the label was double-clicked, and have not yet gotten that test to pass. But here's how I tested if the edit function worked.
describe ("A todo item view", function() {
var my_model;
var todo_view;
beforeEach(function() {
my_model = new Todo({content:"todo value", done:false});
todo_view = new TodoView({model:my_model});
});
it("should set the focus on the input box when the edit function is called", function(){
$('body').append( todo_view.$el ); //append the view to Specrunner.html
todo_view.edit(); //call the view's edit function
var focus= document.activeElement; //finds what element on the page has focus
expect(focus).toBe('.todo-input'); //jasmine-jquery matcher checks if focused element has the .todo-input class
});
Something that might be causing problems is that your model And your view are declared inside beforeEach. Declaring them inside beforeEach means they only exist inside beforeEach's scope, and no longer exist when you run your it.
Also, does setFixtures do what you think it does? The focus cannot be set on an element that is not part of the DOM tree, so I appended the view's el to the body of the jasmine spec itself. (I'm using the HTML specrunner, not the command-line version) That makes it part of the dom tree and therefore allows it to have focus, and also makes whether it has focus testable.

Can you trigger action/events in Sencha Touch from html elements?

I have a Sencha tab panel, each tab loads html content via ajax. One of the components is a post/list that visitors can use to drill down once more to read the entire post.
My question is, can I somehow trigger a view switch through the html? Or should I be loading the post data via JSON, and styling a listpanel in Sencha?
Thank you!
You can add listeners to elements within your HTML which would then be able to trigger the view switch. For example,
// simple HTML snippet contained in Panel
<a class="my-link">Click Me!</a>
// on after load/after render (need to ensure that the elements exists in the page!)
// get reference to the containing panel (tab)
var panel = this.items.get(0);
panel.getEl().on({
tap: function(e){
console.log('i was clicked!');
},
delegate: 'a.my-link'
});
The delegate option allows you to pass a selector that means the event will only fire when an element matching that selector is in the event target's chain (i.e. returns something in an e.getTarget(delegate) call).
EDIT
You can access attributes of the tapped element using either the DOM node tapped or use Ext.fly to wrap an Ext.Element instance around it and use the helper methods.
console.log(e.getTarget('a.my-link')); // logs DOM node
console.log(Ext.fly(e.getTarget('a.my-link'))); // logs Ext.Element wrapping DOM node
console.log(e.getTarget('a.my-link').href); // logs href via DOM node property
console.log(Ext.fly(e.getTarget('a.my-link')).getAttribute('href')); // logs href via Ext.Element getAttribute() method
Depending on the nesting you may be able to remove the selector from the getTarget() call (i.e. if you're always tapping on the element your listening on then you can remove it, but if there are children in the element you're listening on then you will need it. In the second case the 'target' will be the child that the event bubbled from so the href etc will be wrong. If that makes sense... :) )
The solution for 2.2.1:
initialize: function() {
this.element.on({
tap: <function>,
delegate: <query_expression>
});
this.callParent(arguments);
}
<query_expression> can be anything fitting in to Ext.query() too.

Resources