Backbone per instance event bindings - backbone.js

I have a view that creates a sub-view per item in the list. Generically let's call them ListView and ListItemView. I have attached an event as follows on ListItemView:
events: {
"click .remove": "removeItem"
}
I have template-generated html for ListItemView that is approximately like the following (swapped lb/rb for {/} so you can see the "illegal" html):
{div class="entry" data-id="this_list_item_id"}
SOME STUFF HERE
{div class="meta"}
{a class="remove" href="javascript:;"}[x]{/a}
{/div}
{/div}
The problem is, when the click on any of the [x]'s, ALL of the ListItemViews trigger their removeItem function. If I have it go off of this model's id, then I drop all the items on the page. If I have it go off the clicked item's parent's parent element to grab the data-id, I get a delete for EACH ListItemView instance. Is there a way to create an instance-specific event that would only trigger a single removeItem?
If I have ListView hold a single instance of ListItemView and reassign the ListItem model and render for each item in the list it works. I only end up with one action (removeItem) being triggered. The problem is, I have to find the click target's parent's parent to find the data-id attr. Personally, I think the below snippet is rather ugly and want a better way.
var that = $($(el.target).parent()).parent();
Any help anyone gives will be greatly appreciated.

It seems like your events hash is on your ListView.
If it is, then you can move the events hash to ListItemView and your removeItem function can be the following
removeItem: function() {
this.model.collection.remove(this.model);
}
If this isn't the case, can you provide your ListView and ListItemView code so I can look at it.

A wild guess but possible; check that your rendered html is valid. It might be possible that the dom is getting in a tiz due to malformed html

Related

Backbone JS - event problems adding multiple Views into same parent container

My code fetches a Collection from the server and iterates through it. For each model fetched it creates a View that is appended to the same parent element. Each of these views has a "click" event that triggers a function.
The problem is that clicking on any item in the list causes the event function to trigger for ALL elements in the list. The only workaround I have is to make the function itself dynamic and try to determine if it should run based on things like the ID of the item clicked:
'click .request_box': function(e) {
var myid = $(e.currentTarget).attr("id");
// code here which determines whether the function runs
}
}
This is a workaround, but it is also a hack, and I'm sure there has to be a better way of dealing with what must be a common problem (creating a list). Repeat searching around the web does not suggest any better way, so I am posting in the hope someone with more experience using Backbone can offer suggestions on a better way to approach this problem....
Thank you in advance.

Finding the segue that was called in a ViewController

I'm using storyboarding and have a UITableView containing events, which when clicked load another view with more details. I also have an 'add' button on that list which goes to the same page but doesn't prepopulate the information and changes the banner button.
I do it by setting the detail item with the following method, and then in the configureView method I just check if the detail item exists.
- (void)setDetailItem:(id)newDetailItem {
if (self.detailItem != newDetailItem) {
_detailItem = newDetailItem;
[self configureView];
} }
This works ok, but I thought there might be a better way to distinguish between methods, eg by getting the segue identifier in this new view controller and using that. Is there an easy way to do this or do I need to pass this information through as part of the prepareForSegue method?
Using prepareForSegue: seems right. In general, it's a bad idea for methods to care about the conditions under which they're being called if it's not explicit in their parameters.

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.

Delegating events

I've got a backbone view for an entire collection (a list of "clickable" categories). Can I delegate events on each item of the view so that I can find which category has been clicked?
Here's a post that might help. Basically you use a data-* attribute in the item view to store and then retrieve the id of item clicked:
Backbone.js: Getting The Model For A Clicked Element
If you'd rather go directly to code, here's the jsFiddle that's used in the post to demonstrate. Hope that helps.
I have no answer for your question (no, I think), but would like to share my approach: a general collection view component, which renders a collection using other view. It can be as simple as in the example below or more sophisticated (listening add/remove/reset events and react accordingly).
var CollectionView = Backbone.View.extend({
render : function() {
this.options.collection.each(function(model) {
this.$el.append((new this.options.view({model : model})).el);
}, this);
}
})

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.

Resources