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

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.

Related

Webshims - Show invalid form fields on initial load

(Follow on questions from Placeholder Hidden)
I'd like my form to validate existing data when it is loaded. I can't seem to get that to happen
I jQuery.each of my controls and call focus() and blur(), is there a better way than this? I tried to call ctrl.checkValidity(), but it wasn't always defined yet. When it was, it still didn't mark the controls.
I seem to have a timing issue too, while the focus and blur() fire, the UI does not update. It's as if the Webshims are not fully loaded yet, even though this fires in the $.webshims.ready event.
I also tried to call $('#form').submit(), but this doesn't fire the events as I expected. The only way I could make that happen was to include an input type='submit'. How can I pragmatically case a form validation like clicking a submit button would?
Here's a jsFiddle that demonstrates the problem. When the form loads, I want the invalid email to be marked as such. If you click the add button it will be marked then, but not when initially loaded. Why?
Focus and blur in the control will cause it to be marked.
BUT, clicking ADD will too (which runs the same method that ran when it was loaded). Why does it work the 2nd time, but not when initially loaded?
updateValidation : function () {
this.$el.find('[placeholder]').each(function (index, ctrl) {
var $ctrl = $(ctrl);
if( $ctrl.val() !== "" && (ctrl.checkValidity && !ctrl.checkValidity()) ) {
// alert('Do validity check!');
$ctrl.focus();
$ctrl.blur();
}
});
}
I see this in FF 17.0.5. The problem is worse in IE9, sometimes taking 2 or 3 clicks of ADD before the fields show in error. However, I get errors on some of the js files I've liked 'due to mime type mismatch'.
This has to do with the fact, that you are trying to reuse the .user-error class, which is a "shim" for the CSS4 :user-error and shouldn't be triggered from script. The user-error scripts are loaded after onload or as soon as a user seems to interact with an invalid from.
From my point of view, you shouldn't use user-error and instead create your own class. You can simply check for validity using the ':invalid' selector:
$(this)[ $(this).is(':invalid') ? 'addClass' : 'removeClass']('invalid-value');
Simply write a function with similar code and bind them to events like change, input and so on and call it on start.
In case you still want to use user-error, you could do the following, but I would not recommend:
$.webshims.polyfill('forms');
//force webshims to load form-validation module as soon as possible
$.webshims.loader.loadList(['form-validation']);
//wait until form-validation is loaded
$.webshims.ready('DOM form-validation', function(){
$('input:invalid')
.filter(function(){
return !!$(this).val();
})
.trigger('refreshvalidityui')
;
});

Backbone-route click doesn't not respond on 2nd attempt

I am doing form validation in a function when a user wants to preview an invoice which is called by a route:
routes: {
"new" : "newInvoice",
"new/:assignmentid" : "newInvoiceAssignment",
"edit/:invoiceid" : "editInvoice",
"preview/:invoiceid" : "previewInvoice",
"preview" : "preview",
"delete/:invoiceid" : "deleteInvoiceModal",
"whyCant" : "whyCant",
"whatsThis" : "whatsThis"
},
And here is my two buttons (actually, a button and an href) on the form:
<div class="span8 alignRight">
<button id="saveInvoiceDraft" type="submit" class="btn btn-warning">Save Draft</button>
<a id="previewInvoice" class="btn btn-primary">Preview & Send</a>
</div>
When this invoice is created, the URL for the tag is set with:
var url = '#preview';
$('#previewInvoice').attr('href',url);
And finally, when I click on the "Preview & Send" button, the previewInvoice(invoiceid) function below runs, properly catches the one form field missing and displays the error message. At that point even if I populate the form field, that button is dead and no longer responds. However, the "Save Draft" button works perfectly and mimic's the same code as in the previewInvoice() function.
I know there is probably a better way to do this, but I was following that way it was done in another section of the app I inherited. Actually, as I am typeing this I am wondering since the sendDraft() function works and its a button and the previewInvoice() function does not, the fact that it is a href might have something to do with it.
function previewInvoice(invoiceid) {
var invoice = new Invoice({"invoiceid": invoiceid});
invoice.set({"invoiceid": invoiceid,"invoicestatus": "draft"});
formGetter(invoice);
validateInvoiceForm(invoice);
if (window.errors.length == 0) {
//business logic here
if (window.panel == undefined) {
// business logic here
}
else {
//save business logic here
}
}
else {
showInvoiceErrors();
}
}
Any ideas why the button no longer responds? I am not seeing any error's in the console. I added a consol.log inside the function to display the value of a different form element, and it displays the first time in the console, but if I change the data and click the button again, that log does not update, which to me is another clue that it is just not firing.
Backbone.History listens to hashchange events to trigger navigation to routes. When you first click the previewInvoice button, the URL hash fragment is set to #preview, and the matching route is triggered.
When you click the same button the second time, the hash doesn't actually change, and therefore the router doesn't catch it.
I'm having a hard time recommending a good solution to this problem. Normally I would recommend catching the click event and calling router.navigate("preview", {trigger:true}); manually. However, based on your code sample it looks like your application is built around the Router, and there isn't a View layer for DOM event handling as you would expect in most Backbone applications.
On the Router level this is a bit trickier to solve. You could use router.navigate to set a dummy hash after the preview route has been executed. This would cause the link to trigger a hashchange on the second time as well. Unfortunately this would mean that the preview page would not be bookmarkable, and since you're not using pushState, would leave an extraneous history entry.
I'm afraid this issue will have to either be solved with a hacky fix (as outlined above) or a major refactoring.
I have this same problem, my solution is to put in a handler that does a fake "in-between" route that is hidden from history, so that Backbone.history will register the navigation as a change and trigger the action.
Put a class on links that you need the route action to trigger regardless if the URL is the same:
do it
In your Backbone view, put in an event handler:
events: {
"click .js-ensureNav": "_ensureNav",
},
Actual handler:
_ensureNav: function (event) {
var route_name = $(event.target).attr('href').slice(1);
Backbone.history.navigate("fake");
Backbone.history.navigate(route_name, {trigger: true, replace: true});
},
Instead of
var url = '#preview';
$('#previewInvoice').attr('href',url);
Try this
var date = new Date();
var url = '#preview?date:' + date ;
$('#previewInvoice').attr('href',url);
So Every time new request will be generated and this will solve your problem.

Debugging events that are not loaded in view

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.

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

Display n time ago on various items using jquery, [issue with new elements generated after the loading the DOM]

I am using Jquery plugin http://timeago.yarp.com/ for showing time.
Issue is timeago will not take effect for dynamically generated items.
$(document).ready(function() {
$(".timeago").timeago(); // works perfectly fine for the items which are loaded on page load
//$(".timeago").live(timeago()); // gives me an error ie timeago is not defined
//$(".timeago").live($(".timeago").timeago()); // gives me an error too much recursion.
jQuery.timeago.settings.allowFuture = true;
});
From some google search I got to know something ie:
Using live is the same as using bind, except that it is limited only to the events click, dblclick, keydown, keypress, keyup, mousedown, mousemove, mouseout, mouseover, and mouseup.
Now how can do it cause I dont have any click event? How can I bind this?
.live() and .bind() assign callbacks to event. In your case, you don't have an event to assign the function to and so it fails.
You can, in theory, assign your callback to a custom event. You will however have to manually trigger the event (using .trigger()) whenever your item is generated. For example:
$("abbr.timeago").live("timeago", function() {
$(this).timeago();
});
// ... and in the bit that generates your item
$new_item.trigger("timeago")
Demo: http://jsfiddle.net/ZjuW4/9
Of course, using .live() in this situation is purely academic and does not really serve a good purpose.
If you do have access to the code that's generating the items, you can simply chain in the call to .timeago() as the items are generated, i.e. http://jsfiddle.net/ZjuW4/3/
take a look in this topic
here was discussed how to put timeago on dynamically loaded items
for example the results of an ajax request.
Activate timeago on newly added elements only
PS: allowFuture does not have anything to do with putting timeago on newly created items on your page. it just allows dates in the future (f.e. "in 3 days", "next week")

Resources