Marionette is great but some things can get a bit confusing to follow. On my initial page load I show a Marionette.Layout in a region of a Marionette.Application. But for some reason the click events are delegated but are not actually responding. If I navigate to another route (removing the layout view) and then return to the route (re-rendering the view) then the events are active. If I cause the view to be rendered twice in a row, then it works. Maybe I'm initializing this system wrong.
var ListView = Backbone.Marionette.Layout.extend({
el: "#listView", // exists on DOM already
events: {
// this is what is supposed to work
// as well as events in my subviews
"click" : function() { console.log("clicked");}
});
var Router = Backbone.Router.extend({
initialize: function(options) {
this.app = options.app;
},
start: function(page) {
console.log("start...");
// with silent false this causes list() to be called
// and the view is rendered
// but the ListView does not get its events delegated
Backbone.history.start({
pushState: false,
silent: false
});
// only by running loadUrl here
// do I get the events delegated
// or by switching between other routes
// and coming back to 'list'
// but this is causing a second useless calling
// of list() re-rendering
console.log("loadUrl...");
Backbone.history.loadUrl(page);
// actually the events are not yet delegated at this point
// if I pause a debugger here.
// events only active after the Application finishes its initializers
},
routes: {
'list': 'list'
},
list: function() {
var lv = ListView({});
this.app.focus.show(lv)
}
});
var app = new Backbone.Marionette.Application();
app.addInitializer(function(options) {
this.router = new Router({app: this});
this.router.start();
});
app.addRegions({
// #focus does exist on the DOM
focus: "#focus",
});
app.start({});
An alterative start that I expected would work but sadly doesn't
start: function(page) {
console.log("start...");
Backbone.history.start({
pushState: false,
silent: true // don't trigger the router
});
// call the router here via history
console.log("loadUrl...");
Backbone.history.loadUrl(page);
// causes page to be rendered correctly
// but events are not delegated
},
I've stepped through it all with a debugger but still can't see what the magical effect of loadUrl is that causes it to work.
I've also tried just reinserting the layout into the region:
app.focus.show(app.focus.currentView)
Maybe there's a different way to structure this that would simply not run into this issue.
Not 100% sure this is the answer, but...
If you're elements exist on the DOM already, and you are trying to attach a view to them, and attach that view to a region, using the region's attach method:
list: function() {
var lv = ListView({});
this.app.focus.attach(lv)
}
the attach method was created specifically for the situation where you want to use an existing DOM element. Calling show instead, will replace what's there, which may be causing the jQuery events to be released.
in the onRender section add
this.delegateEvents();
When Marionette closes a view it unbinds events as well. Events are only delegated when the view is initialized. When you show a previously stored view you will need to call the delegateEvents method again.
https://github.com/marionettejs/backbone.marionette/issues/714
Related
I create my view with a new collection.
I fire add and sync events :
this.mapDetailsCOllection.on('add', self.onAddElement, self);
this.mapDetailsCOllection.on('sync', self.onSync, self);
Before fetch I do :
this.mapDetailsCOllection.off("add");
this.mapDetailsCOllection.fetch();
And when fetch is ok, in my sync callback :
this.mapDetailsCOllection.on('add', self.onAddElement, self);
But even if I put off the add event I everytime go in add event callback function when I fetch.
I'm going to suggest the following because I do not have any context on how you've architected your application (Backbone.js is great because it gives you a lot of rope, but how you architect your application can greatly change how you need to implement sync solutions). I'm more than happy to adjust, expand, or clarify this post if you can share a little more about how you've architected your App, View, Collection, and Model code for me to understand what you are trying to achieve.
In your code, I would listen to the update event instead of the add event (since add triggers on each new item added to the collection, compared to update which triggers after any number of items have been added/removed from a collection). Then I would remove the on/off change for add event and instead have your view listen to Collection.reset event rather than turning off and on your listeners for your fetch/sync cycle.
I've used this type of View design-pattern successfully in the past:
var _ = require('lodash');
var DocumentRow = Backbone.View.extend({
events: {
"click #someEl": "open"
},
clean: function() {
// cleans up event bindings to prevent memory leaks
},
initialize: function(options) {
_.bindAll(this, [
'initialize',
'open',
'render',
'clean'
]);
options = options || {};
this.collection = options.collection ? options.collection : SomeCollection;
this.listenTo(this.collection, "reset", this.render);
},
open: function(item) {
...
},
render: function() {
...
}
});
Based on this:
The behavior of fetch can be customized by using the available set
options. For example, to fetch a collection, getting an "add" event
for every new model, and a "change" event for every changed existing
model, without removing anything:
collection.fetch({remove: false})
collection.set(models, [options]) and this:
All of the appropriate "add", "remove", and "change" events are fired
as this happens. Returns the touched models in the collection. If
you'd like to customize the behavior, you can disable it with options:
{add: false}, {remove: false}, or {merge: false}.
you can pass
collection.fetch({add: false})
to avoid firing add event.
So Im new at backbone, and Im trying to make a single page app, Im using routes to manage certain things, and I want to remove a view when the user gets to another route
Im using this method to destroy the view
destroy_view: function() {
// COMPLETELY UNBIND THE VIEW
this.undelegateEvents();
this.$el.removeData().unbind();
// Remove view from DOM
this.remove();
Backbone.View.prototype.remove.call(this);
}
also this is my route element
Router = Backbone.Router.extend({
routes: {
'':'index',
'#':'index',
'events/*event' : 'events'
},
index: function(){
this.indexView = new VistaIndex();
},
events: function(params) {
if( this.indexView )
this.indexView.destroy_view()
this.eventView = new EventView({currentEvent: params})
}
});
the problem with this is that if I do this the app crashes, so what do you recommend me to do :)
Here’s how I do it:
Backbone.View.extend({
//some other view stuff here...
destroy: function () {
this.undelegateEvents();
this.$el.removeData().unbind();
this.remove();
//OR
this.$el.empty();
}
});
First we want to make sure we’re removing all delegated events (the ones in the events:{"event selector": "callback"} hash). We do this so we can avoid memory leaks and not have mystery bindings that will fire unexpectedly later on. undelegateEvents() is a Backbone.View prototype function that removes the view’s delegated events. Simple.
Next we want to cleanup any data in the view object that is hanging around and unbind any events that we bound outside the events hash. jQuery provides a removeData() function that allows us to to do that.
You may also have bound event listeners to your view chain unbind() with no arguments to remove all previously-attached event handlers from your $el. this.$el.removeData().unbind();
Now you may want to do one of two things here. You may want to remove your view element completely OR you just want to remove any child elements you’ve appended to it during it’s life. The latter would be appropriate if, for example, you’ve set the $el of your view to be some DOM element that should remain after your view behavior is complete
In the former case, this.remove() will obliterate your view element and it’s children from the DOM.
In the later case, this.$el.empty() will remove all child elements.
Check out this fiddle if you want to fool around with my solution.
http://jsfiddle.net/oakley349/caqLx10x/
I've been trying to learn Backbone, and I'm developing an app now. But I have a problem with a view's events: App.views.ChannelView should have a click event, but it is not firing.
Here's the code:
http://pastebin.com/GgvVHvtj
Everything get rendered fine, but events won't fire. Setting the el property in the view will work, but I can't use it, and I've seen on Backbone's todo tutorial that it is possible.
How do I make events fire without a defined el property?
You must define the el element to be an existing element in your DOM. If you do not define it, fine, it will default to a div, but when you render the view, the html generated must be appended/prepended whatever, you get the point, to an existing DOM element.
Events are scoped to the view, so something's wrong with your scope. From the code you provided I can't reproduce the problem, so if you might, please provide a live example on jsfiddle/jsbin etc in order to fully understand the issue.
Demo ( in order to demonstrate the view render )
var App = {
collections: {},
models: {},
views: {},
};
App.models.Channel = Backbone.Model.extend({
defaults: {
name: '#jucaSaoBoizinhos'
}
});
App.views.ChannelView = Backbone.View.extend({
el:$('#PlaceHolder'),
events: {
"click .channel": "myhandler"
},
render: function() {
this.$el.html('<div class="channel"><button>' + this.model.get('name') + '</button></div>');
return this;
},
myhandler: function(e) {
alert(e);
console.log(this.model.get('name'));
},
});
var chView = new App.views.ChannelView({model: new App.models.Channel()});
//console.log(chView.render().el) //prints div#PlaceHolder
//without the el specified in the view it would print a div container
//but i would have to render the view into an existing DOM element
//like this
//$('#PlaceHolder').html(chView.render().el)
chView.render()
Can you try doing a
events: {
"all": "log"
}
log: function(e) {
console.log e;
}
That should log out every event that's getting fired. I find it super helpful when troubleshooting.
backbone view events can work without dom element specified. If you can't use any element at the view createion (initialization) moment, then you can use it's 'setElement' method, to attach your view to specified dom element. Here is description.
Be the way your view render method will not work also without specified 'el'.
My view should be destroyed after the current route position is left.
So in this schematic example the login view should be destroyed after the user entered his credentials:
I tried to solve this by using Backbone.Router events:
var Router = Backbone.Router.extend({
initialize: function () {
Backbone.history.start();
},
routes: {
"sample" : "sample"
},
sample: function(){
// Build view
var demoView = $("<div/>")
.appendTo(document.body)
.text("I am lost!");
// Destroy view
this.once('route', function(){
demoView.remove();
});
},
});
Unfortunately this does not work as the route events are raised after the routes are executed:
http://jsfiddle.net/hcuX9/
Is there a solution to destroy views after leaving the route position?
Do I have to hack a new event into Backbone.js?
What I use to do is to have an App.current variable pointing to the current view being rendered.
At the top of each route (or the relevant ones in your case), I remove the current view from App.current and then assign it the new view:
someRoute: function() {
if(App.current && App.current.remove) App.current.remove();
// Asign a new current page
App.current = new SomeView();
...
}
That way I only let one view live per route, getting rid of problems like yours.
If you don't like to be checking for App.current and invoking the remove method at the top of every route, you can listen for Backbone.history route event and injecting that logic there:
Backbone.history.on('route', function() {
if(App.current && App.current.remove) App.current.remove();
});
I think you are stuck with your hack, unless you can adapt .listenTo to your needs - then you will need to fire a custom event with .trigger anywhere you have a route change, which might not be possible. Note that this functionality has been requested (and denied) before in backbone:
https://github.com/documentcloud/backbone/pull/494
See that pull request for other patches that try to do the same thing you are doing.
Here, we're using on and off to listen for route events coming in instead of once because we can't rely on a single event not being the current route. When we receive a route even that is not our current route, we can destroy the view and remove the listener:
// Destroy view
var self = this;
var onRoute = function(route, params){
if(route !== 'sample'){
demoView.remove();
self.off('route', onRoute);
}
};
this.on('route', onRoute);
I've modified your test fiddle here: http://jsfiddle.net/rgthree/hcuX9/3/
Another option, as your fiddle (not in your question) navigates directly to another view. This causes the other route's event to fire after the sample2 route. Because of this the above will remove the view. Now, it's much more complete. A hackier way you could handle it is to simply defer the once in a setTimeout so it doesn't listen until after the current route has been fired:
// Destroy view
var self = this;
setTimeout(function(){
self.once('route', function(){
demoView.remove();
});
}, 0);
You can see your fiddle with this method here: http://jsfiddle.net/rgthree/hcuX9/4/
I'm making a Backbone.js app and it includes an index view and several subviews based on id. All of the views have been bound with mousedown and mouseup events. But every time I go from a subview to the index view and then go to any of subviews again, the mousedown and mouseup events in the current subview will be triggered one more time, which means when I click the subview, there will be several consecutive mousedown events triggered followed by several consecutive mouseup events triggered.
After looking through my code, I finally found that it's the router that causes this problem. Part of my code is as follows:
routes: {
"": "index",
"category/:id": "hashcategory"
},
initialize: function(options){
this._categories = new CategoriesCollection();
this._index = new CategoriesView({collection: this._categories});
},
index: function(){
this._categories.fetch();
},
hashcategory: function(id){
this._todos = new TodosCollection();
this._subtodolist = new TodosView({ collection: this._todos,
id: id
});
this._todos.fetch();
}
As you can see, I create the index collection and view in the initialize method of the router, but I create the subview collection and view in the corresponding route function of the router. And I tried to put the index collection and view in the index function and the click event in index view will behave the same way as subviews. So I think that's why the mousedown and mouseup will be triggered several times.
But the problem is, I have to use the id as one of the parameters sent to subview. So I can't create subview in the initialize method. What's more, I've already seen someone else's projects based on Backbone and some of them also create sub collection and view in the corresponding route function, but their app runs perfectly. So I don't know what is the root of my problem. Could someone give me some idea on this?
Sounds like you're having a delegate problem because:
all sub views all use the a same <div> element
Backbone views bind to events using jQuery's delegate on their el. If you have a view using a <div> as its el and then you use that same <div> for another view by replacing the contained HTML, then you'll end up with both views attached to that <div> through two different delegate calls. If you swap views again, you'll have three views receiving events through three delegates.
For example, suppose we have this HTML:
<div id="view-goes-here"></div>
and these views:
var V0 = Backbone.View.extend({
events: { 'click button': 'do_things' },
render: function() { this.$el.html('<button>V0 Button</button>'); return this },
do_things: function() { console.log('V0 clicked') }
});
var V1 = Backbone.View.extend({
events: { 'click button': 'do_things' },
render: function() { this.$el.html('<button>V1 Button</button>'); return this },
do_things: function() { console.log(V1 clicked') }
});
and we switch between them with something like this (where which starts at 0 of course):
which = (which + 1) % 2;
var v = which == 0
? new V0({el: $('#view-goes-here') })
: new V1({el: $('#view-goes-here') });
v.render();
Then you'll have the multi-delegate problem I described above and this behavior seems to match the symptoms you're describing.
Here's a demo to make it easy to see: http://jsfiddle.net/ambiguous/AtvWJ/
A quick and easy way around this problem is to call undelegateEvents on the current view before rendering the new one:
which = (which + 1) % 2;
if(current)
current.undelegateEvents(); // This detaches the `delegate` on #view-goes-here
current = which == 0
? new V0({el: $('#view-goes-here') })
: new V1({el: $('#view-goes-here') });
current.render();
Demo: http://jsfiddle.net/ambiguous/HazzN/
A better approach would be to give each view its own distinct el so that everything (including the delegate bindings) would go away when you replaced the HTML. You might end up with a lot of <div><div>real stuff</div></div> structures but that's not worth worrying about.