backbone.js: understanding browser event handling and view removing - events

I'm fiddling with a view and related model that look like that:
App.Views.Addresses = App.Views.Addresses || {};
App.Views.Addresses.Address = Backbone.View.extend({
events: {
"click button#foo" : "clear"
},
initialize: function(model){
this.address = model.model;
this.address.view = this;
_.extend(this, Backbone.Events);
this.render();
},
render: function(){
... rendering stuff
},
clear: function(){
this.address.clear();
}
});
and
var Address = Backbone.Model.extend({
url: function() {
... url stuff
},
clear: function(){
this.destroy();
this.view.remove();
}
});
I'm facing two problems here. The first one:
I have a button with id="foo" in my source and would like the view to catch the 'click' event of this very button and fire the 'clear' event. Problem: This does not work.
Anyway calling 'clear' on my model by hand cleanly removes the data on the server but does not remove the view itself. Thats the second problem. Hopefully someone more experienced can enlighten me.
Thx in advance
Felix

First problem:
Your button must be inside the element rendered by the view.
backbone scope events to inner elements only
You must render your view within this.el element
backbone use that element for delegation
Second problem:
Use events to destroy your view
You should not store the view in the model. This is kind of a "no no" in MVC. Your model already emits a "remove" event when deleted. Your view should listen to it and behave accordingly.
You must remove your view element from the DOM yourself
This is not handled by backbone.
Other general comments:
Views already are extending Backbone.Events
Use this.model instead of this.address

Related

Backbone: Attaching event to this.$el and re rendering causes multiple events to be bound

I need to attach an event to the main view element, this.$el. In this case its an 'LI'. Then I need to re render this view sometimes. The problem is if i re render it, it attaches any events in the onRender method that is attached to this.$el each time its rendered. So if i call this.render() 3 times the handler gets attached 3 times. However, if i attach the event to a childNode of this.$el, this does not happen and the events seem to be automatically undelegated and added back on each render. The problem is I NEED to use the main this.$el element in this case.
Is this a bug? Shouldn't this.$el function like the childNodes? Should I not be attaching things to this.$el?
inside the view:
onRender: function(){
this.$el.on('click', function(){
// do something
});
If you're able to use the view's event hash, you could do the following:
var Bookmark = Backbone.View.extend({
events: {
'click': function() {
console.log('bound once')
}
}
...});
If for some reason that's not an option, you could explicitly remove any existing event listeners for this event in the render method, which will prevent the listener from being attached multiple times:
var Bookmark = Backbone.View.extend({
...
render: function(x) {
this.$el.off('click.render-click');
this.$el.html(this.template());
this.$el.on('click.render-click', function () {
console.log('only ever bound once');
});
return this;
}
});

How to use SuperScrolloRama with Backbone.js Views

I am trying to understand how to use a plugin like http://johnpolacek.github.io/superscrollorama/, with Backbone.js by integrating it into my Views. I know that I need to hook into the backbone View-Events, but I want to do a horizontal scroll with the plugin, and I don't know of a horizontal scroll-event. How can I still utilize the plugin? Thanks in advance for any ideas.
Views:
var ArtistsView = Backbone.View.extend({
tagName: 'ul',
initialize: function () {
this.cleanUp();
$("body").attr('id','artists');
this.render();
},
events: {
"click div.open" : "largeArtViewOpen",
"click div.close" : "largeArtViewClose",
},
render: function () {
this.collection.each(function(model) {
var artistView = new ArtistView({ model: model });
this.$el.append(artistView.render().el);
}, this);
console.log('and a new view was rendered!')
return this;
},
cleanUp: function(){
if (this != null) {
this.remove();
this.unbind();
console.log('View was removed!');
}
},
largeArtViewOpen: function(e){
var thisArt = $(e.currentTarget).parent().attr("class");
console.log(thisArt);
$("#open-view, li."+thisArt).show();
},
largeArtViewClose: function(e){
//var thisArt = $(e.currentTarget).parent().attr("class");
console.log('clicked!');
$("#open-view, ul#large li").hide();
},
scrollFx: function(){
var controller = $.superscrollorama({
isVertical:false
});
controller.addTween('h2#fade-it', TweenMax.from( $('h2#fade-it'), .5, {css:{opacity: 0}}), 800);
//$('h2#fade-it').css({'color':'#dbdbdb'});
console.log('scroll message!');
},
});
var ArtistView = Backbone.View.extend({
tagName:'li',
className:'artistLink not-active',
render: function(){
this.id = this.model.get('idWord')+"-menu-item";
this.$el.attr('id', this.id).html(this.template(this.model.toJSON()));
return this;
},
});
So, in the past 3 days since I've asked this question, I've spent some time trying different scrollable 'targets' for Superscrollorama...Document vs. Window vs. Body vs. other DOM elements within the HTML, and the questions that I've had to consider are, should the scroll event be bound to the View's top element? Should it be bound to the body, but initialized in the view? In both cases I tried, I couldn't get the scroll events to continuously fire...this may just be due to bad code, but I couldn't make it happen.
So, what I arrived at, was, avoiding the view entirely: I instantiating and called Superscrollorama in a function called scrollFx() within a separate 'helper.js' document, and then called scrollFx() from my view's router.
I'm thinking I will just empty the target's styles and unbind any existing scroll events in the beginning of scrollFx(), before I call the Superscrollorama function so that the resulting scroll styles/animations are cleaned up, and events aren't exponentially bound.
I'm still very much working through these issues, though now the scroll events are working, so if anyone happens to read through this train of thought, please feel free to add your two sense, especially, if you have better ideas about re-implementing the Superscrollorama function within the View itself.
Thanks.

How do I add default events to Marionette's Marionette.View base type?

I am currently extending Marionette's base Marionette.View type with the method I named quickClick. I'm doing this to
config/marionette/view.js
(function() {
define([
'backbone.marionette'
],
function(Marionette){
return _.extend(Backbone.Marionette.View.prototype, {
quickClick: function(e) {
$(e.target).get(0).click();
}
});
});
}).call(this);
This allows me to call this method from any view I create without having to redefine it per view. Great!
Here's a trimmed down view with the events object still in place:
(function() {
define([
'backbone.marionette',
'app',
'templates'
],
function(Marionette, App, templates){
// Define our Sub Module under App
var List = App.module("SomeApp");
List.Lessons = Backbone.Marionette.ItemView.extend({
events: {
'tap .section-container p.title': 'quickClick'
}
});
// Return the module
return List;
});
}).call(this);
In case your wondering, tap is an event I'm using from Hammer.js because this is a mobile app. So, in order to circumvent the 300ms click event delay on iOS and Android, I'm manually triggering a click event when the tap event fires on certain elements.
Now, all of this is working just fine, and I felt it was necessary to describe this in detail, so that an answer could be given with context.
My problem is having to define the events object. I don't mind at all for elements as specific as the one above, .section-container p.title. But, I would like to register a tap event for all <a> tags within every view. It doesn't make sense to keep defining this event in each view I create
events: {
'tap .section-container p.title': 'quickClick',
// I don't want to add this to every single view manually
'tap a': 'quickClick'
}
Instead, of adding this to every view, I thought I would just add an events object to the config/marionette/view.js file where I added a method to the Marionette.View prototype.
Here's what I did
(function() {
define([
'backbone.marionette'
],
function(Marionette){
return _.extend(Backbone.Marionette.View.prototype, {
events: {
'tap a': 'quickClick'
},
quickClick: function(e) {
$(e.target).get(0).click();
}
});
});
}).call(this);
Of course, that doesn't work. The events object is overridden each time I need to add events that only apply to that view. Btw, tap a does work when my view does not have its' own events object.
So, my question is: How do I add default events to Marionette's Marionette.View base type?
"Of course, that doesn't work. The events object is overridden each time I need to add events that only apply to that view."
Yes, that seems to be the problem. Here is the part of Marionette that does the event delegation:
// internal method to delegate DOM events and triggers
_delegateDOMEvents: function(events){
events = events || this.events;
if (_.isFunction(events)){ events = events.call(this); }
var combinedEvents = {};
var triggers = this.configureTriggers();
_.extend(combinedEvents, events, triggers);
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
},
One possible solution could be overwriting this (private!) part of Marionette - but it could probably change in new versions of Marionette and you'd always have to make sure that things still work. So this is bad.
But you could do something like this in your subviews.:
events: _.extend(this.prototype.events, {
'tap .section-container p.title': 'quickClick'
})
If this makes sense for only one 'global' event is another question.
Or you could define an abstract View Class, which does something like that
events: _.extend({'tap a': 'quickClick'}, this.my_fancy_events)
and also defines the quickClick method and then use this view for all you subviews. They then define their events not in 'events' but in 'my_fancy_events'.
When extending the views I occasionally find myself in situation when I need to add some extra calls in 'initialize' as well as extend 'events' property to include some new calls.
In my abstract view I have a function:
inheritInit: function(args) {
this.constructor.__super__.initialize.apply(this, args);
this.events = _.extend(this.constructor.__super__.events, this.eventsafter);
},
Then, in an extended view, I can call
initialize: function(options) {
this.inheritInit(arguments)
//..some extra declarations...
}
and also I can use 'events' property in a regular way.

Backbone JS where to put Application main logic

Using Backbone i'm starting to build an App where i have everything cleanly separated. But now i have the following question. Where should i put the App main logic, in the views or in the model.
For example i have a view and a model, which are binded to a button and when i click that button i have to make
$.ajax(params)
do i put that in the view or the view calls a method with :
this.model.doAction(params)
which do you think is the best approach?
You can define an events property in the view which is of the format {"event selector": "callback"} for eg. {"click .collapse": "collapse"} where collapse would be a function defined as a property of the view. Then write your ajax request code in this callback function.
Also, unless I am missing something, "binding a view and model to a button" doesn't sound correct Backbone way to me. Instead you should think of one instance of model associated with one instance of the view. Whenever an attribute of the the model instance changes, a model change event will be triggered. You can bind a view function to this event so that change in the model is reflected in the view. Here is a quick example
var Book = Backbone.Model.extend({
// ...
});
var BookView = Backbone.View.extend({
initialize: function () {
this.model.bind('change', this.render, this);
},
render: function () {
// here, make changes to the dom as per changes in model
}
});
To associate a model with a view instance, you can pass the it while instantiating a new
view object..
var book = new Book({
title: "A great book"
});
var view = new BookView({model: book});
view.model.set('author', 'AGreatAuthor');
The set function call will fire change event and will result in render function of view
to be called.
Refer to the annotated source of Todos app example for a complete example.

Backbone.js - event trigger not work after rendering other views

There's a addPost function in my router. I don't want to re-create the postAddView every time the function is invoked:
addPost: function () {
var that = this;
if (!this.postAddView) {
this.postAddView = new PostAddView({
model: new Post()
});
this.postAddView.on('back', function () {
that.navigate('#/post/list', { trigger: true });
});
}
this.elms['page-content'].html(this.postAddView.render().el);
}
Here's the PostAddView:
PostAddView = backbone.View.extend({
events: {
'click #post-add-back': 'back'
}
, back: function (e) {
e.preventDefault();
this.trigger('back');
}
});
The first time the postAddView is rendered, the event trigger works well. However, after rendering other views to page-content and render postAddView back, the event trigger won't be trigger anymore. The following version of addPost works well, though.
addPost: function () {
var that = this, view;
view = new PostAddView({
model: new Post()
});
this.elms['page-content'].html(view.render().el);
view.on('back', function () {
delete view;
that.navigate('#/post/list', { trigger: true });
});
}
Somewhere you are calling jQuery's remove and that
In addition to the elements themselves, all bound events and jQuery data associated with the elements are removed.
so the delegate call that Backbone uses to bind events to your postAddView.el will be lost. Then, when you re-add your postAddView.el, there are is no delegate attached anymore and no events are triggered. Note that Backbone.View's standard remove method calls jQuery's remove; a few other things in jQuery, just as empty will do similar things to event handlers. So the actual function call that is killing your delegate could be hidden deep inside something else.
You could try calling delegateEvents manually:
this.elms['page-content'].html(this.postAddView.render().el);
this.postAddView.delegateEvents();
or better, just throw the view away and create a new one every time you need it. Your view objects should be pretty light weight so creating new ones should be cheap and a lot less hassle than trying to keep track of the existing views by hand.
If you really want to reuse the current DOM and View you do not need to set again and again the element as you are doing, everything that you call .html() you are destroying the DOM of the View and generating again and losing events. Also I prefer always to add the "el" in the DOM before render the View. I will have your function in this way:
addPost: function () {
if (!this.postAddView) {
this.postAddView = new PostAddView({
model: new Post()
});
this.postAddView.on('back', this.onBack);
this.elms['page-content'].html(this.postAddView.el);
}
this.postAddView.render();
},
onBack : function () {
this.navigate('#/post/list', { trigger: true });
}
I'm not fan of the use of local variables to refer to "this". If all of your Views uses _.bindAll(this) in the initialize method you could bind your events to your view and could use this(check how I transformed onBack).
With my code there is not a need to manually call this.delegateEvents()

Resources