Backbone.js firing other view's event - javascript

I'm working with an API and Backbone.js at the moment.
I have two views, both render to the same document element #viewContainer. Both of these views render a table with a couple strings to decribe them and a button that opens a form in a modal.
View 1
App.Views.TaskList = Backbone.View.extend({
el: "#viewContainer",
tagName: 'tr',
events: {
"click button": "showTaskForm"
},
showTaskForm: function (event) {
event.preventDefault();
var id = $(event.currentTarget).data("id");
var item = this.collection.get(id);
var formView = new App.Views.Form({
model: item
});
formView.render();
},
render: function () {
changeActive($('#tasksLink'));
var template = _.template($("#taskList").html(), {});
$('#viewContainer').html(template);
// loop and render individual tasks.
this.collection.each(function (model) {
var variables = {
name: model.get('name'),
button: model.getButton()
};
var template = _.template($("#task").html(), variables);
$("#taskTable tbody").append(template);
});
},
collection: App.Collections.Tasks,
});
View 2
App.Views.ProcessList = Backbone.View.extend({
el: "#viewContainer",
tagName: 'tr',
events: {
"click button": "showStartForm"
},
showStartForm: function (event) {
event.preventDefault();
var id = $(event.currentTarget).data("id");
var item = this.collection.get(id);
var formView = new App.Views.Form({
model: item
});
formView.render();
},
collection: App.Collections.Processes,
render: function () {
changeActive($('#processLink'));
var template = _.template($("#processList").html(), {});
$('#viewContainer').html(template);
this.collection.each(function (model) {
var variables = {
processId: model.get('id'),
processName: model.get('name'),
button: model.getButton()
};
var template = _.template($('#process').html(), variables);
$('#processList tbody').append(template);
});
} });
Neither of these views are rendered by default, both need to be activated by a button on the page and they over-write each other in the DOM. However, which ever view is rendered first, the click event of the buttons in that view are the ones that are always fired.
If there is any more information needed from me let me know and I will edit the question.

Be sure to call undelegateEvents() in the first view when you render the second.

Since you're listening for events on the same elements, essentially you attached two listeners for click events on the same button, and when you change your views you are not cleaning up these listeners.
Here's an article that talks about managing events on view change, which should be really helpful to you.
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/

As other posters have pointed out, you need to watch out for 'zombie' views (i.e. making sure you undelegate events). If you're building even a moderately complex app, you'll want something that can scale. I find this pattern useful:
var BaseView = Backbone.View.extend({
render: function () {
this.$el.html(this.template());
return this;
},
close: function () {
if (this.onClose) this.onClose();
this.undelegateEvents();
this.$el.off();
this.$el.remove();
}
});
Then whenever you build a view you can do:
var view = BaseView.extend({
//your code
//now the .close() method is available whenever you need to close
//a view (no more zombies!).
});

Related

Storing view class names on Backbone models

I'm struggling with a design decision and looking for some feedback. I don't think this question is necessarily specific to Backbone, but that's the framework I'm currently using.
I'm wondering if it's considered bad practice to store the classname of a view as part of a model. For example, let's say you have a parent view with multiple subviews of different types. Each subview contains an edit link, and when that edit link is clicked, the parent view should update it's contents to contain the edit view for that model. I'm using an "event bus" to orchestrate events.
For example:
var E = _.extend({}, Backbone.Events);
var ParentView = Backbone.View.extend({
initialize: function(options) {
this.apples = options.apples; // Backbone Collection of Apple models
this.oranges = options.oranges; // Backbone Collection of Orange models
this.$appleList = this.$('#apples');
this.$orangeList = this.$('#oranges');
this.$editScreen = this.$('#edit-screen');
// listen to edit events for models and render the
// edit screen for using the appropriate view
this.listenTo(E, 'edit', this.showEditScreen);
},
template: 'templates/parent',
render: function() {
this.$el.html(this.model.toJSON());
this.renderAppleViews();
this.renderOrangeViews();
},
renderAppleViews: function() {
var view = new AppleListView({collection: this.apples});
this.$appleList.html(view.render().el);
},
renderOrangeViews: function() {
var view = new OrangeListView({collection: this.oranges});
this.$orangeList.html(view.render().el);
},
// Show the edit screen for a particular model
showEditScreen: function(model) {
var view = new window[model.editScreenViewClass]({model: model}):
this.$editScreen.html(view.render().el);
}
});
AppleListView and OrangeListView simply loop through their respective collections and append a view to the list.
AppleListView adds AppleItemViews, and OrangeListView adds OrangeItemViews. I'm showing the relevant parts of those views below:
var AppleItemView = Backbone.View.extend({
events: {
'click .edit': 'onEditClick'
},
onEditClick: function(e) {
e.preventDefault();
E.trigger('edit', this.model);
}
});
var OrangeItemView = Backbone.View.extend({
events: {
'click .edit': 'onEditClick'
},
onEditClick: function(e) {
e.preventDefault();
E.trigger('edit', this.model);
}
});
Here is what the models would look like for this to work:
var Apple = Backbone.Model.extend({
editScreenViewClass: 'AppleEditView'
});
var Orange = Backbone.Model.extend({
editScreenViewClass: 'OrangeEditView'
});
I'm asking if it seems "ok" to store this editScreenViewClass on the model. That way I can retrieve it directly from the model passed into the event.

Backbone: View Event triggers multiple times

I'm novice in Backbone.
I want to show a stock list, where user can open up any stock right from the list and change stock values. After that the whole list should refresh to show changed values.
So as I found out it's better not only to create collection but create collection and a list of stock models.
For this I created a stock collection view for main table and stock model view for adding rows to the table where each row is a single model.
So this is a collection view:
App.Views.StockTable = Backbone.View.extend({
...
initialize: function() {
this.render();
},
render: function() {
this.$el.html(this.template(this.collection));
this.addAll();
return this;
},
addOne: function(stock) {
var row = new App.Views.StockRow({
model: stock,
suppliers: this.suppliers
});
return this;
},
addAll: function() {
var suppliers = new App.Collections.Suppliers();
var that = this;
suppliers.fetch({
success: function() {
_.each(that.collection.toJSON(), that.addOne, that);
}
});
return this;
}
});
And this is my stock row view:
App.Views.StockRow = Backbone.View.extend({
el: 'tbody',
templateRow: _.template($('#stockRow').html()),
templatePreview: _.template($('#stockPreview').html()),
events: {
'click #open': 'open'
...
},
initialize: function() {
this.render();
},
render: function() {
this.$el.append(this.templateRow(this.model))
.append(this.templatePreview({
stock: this.model,
suppliers: this.suppliers
}));
return this;
},
open: function(e) {
var element = $(e.currentTarget);
element.attr('id', 'hide');
$('#stock' + element.data('id')).slideToggle('fast');
}
...
});
I wrote just a piece of code. The problem is that when I click on '#open' that event triggers many times (right the quantity elements in the collection). So when I catch e.currentTarget there are many similar objects.
What i do wrong?
I suspect you have multiple things going on here.
Without seeing your template, I suspect each of your StockRow rows are rendering a tag with the id="open". Since id values should be unique, use a class in your link (example: class="open"), and then reference that class in your click handler:
events: {
'click .open': 'open'
}
Next, since each instance of the StockRow already has a model instance associated with it, just use this.model instead of trying to look it up out of the data attribute of the currentTarget.
open: function () {
$('#stock' + this.model.id).slideToggle('fast');
}
But again, instead of using an id="stock" attribute in your template, use a class… say class="stock-preview". Then just look for that in your open…
open: function () {
this.$el.find('.stock-preview').slideToggle('fast');
}
The other piece that looks questionable to me is the call to this.addAll(); in the render method of the StockTable view. It is best practice to just have your render method render state, instead of having it trigger an ajax call to fetch the state.
For example, in your initialize you can setup some event handlers that react to your collection changing state (below is an incomplete example, just hoping to get you going in the right direction):
initialize: function (options) {
…
_.bindAll(this, 'render', 'renderRow');
this.collection.on('add', this.renderRow);
this.collection.on('reset', this.render);
},
render: function () {
this.$el.html(this.tableTemplateWithEmptyTBodyTags());
this.collection.each(this.renderRow);
return this;
},
renderRow: function () {
var row = new App.Views.StockRow({
model: stock,
suppliers: this.suppliers
});
this.$el.find('tbody').append(row.render().el);
return this;
}
And then outside the table view, you can do a suppliers.fetch(). Which when the response comes back should trigger the reset.

Again on Backbone Zombie Views

I am trying to understand backbone and am currently struggling with zombie views. I have read many stack overflow posts on the matter but I still cannot figure it out.
For the sake of simplicity, I set up two views (without data) that I need to switch.
What I did so far was:
creating an object
//define application object
var app = {
vent: {},
templates: {},
views: {},
routers: {},
};
//instantiate event aggregator and attach it to app
app.vent = _.extend({}, Backbone.Events);
defining two very simple templates (stored into app.templates): the first one has some dummy text and a button (with and id of 'test-begin'), the second one just dummy text
defining two views
app.views.instructions = Backbone.View.extend({
//load underscore template
template: _.template(app.templates.instructions),
//automatically called upon instantiation
initialize: function(options) {
//bind relevant fucntions to the view
_.bindAll(this, 'render', 'testBegin', 'stillAlive', 'beforeClose');
//listen to app.vent event
this.listenTo(app.vent, 'still:alive', this.stillAlive);
},
//bind events to DOM elements
events: {
'click #test-begin' : 'testBegin',
},
//render view
render: function() {
this.$el.html(this.template());
return this;
},
//begin test
testBegin: function() {
Backbone.history.navigate('begin', {trigger: true});
},
//still alive
stillAlive: function() {
console.log('I am still alive');
},
//before closing
beforeClose: function() {
//stop listening to app.vent
this.stopListening(app.vent);
},
});
//test view
app.views.test = Backbone.View.extend({
//load underscore template
template: _.template(app.templates.test),
//automatically called upon instantiation
initialize: function(options) {
//trigger still:alive and see if removed view responds to it
app.vent.trigger('still:alive');
//bind relevant fucntions to the view
_.bindAll(this, 'render');
},
//render view
render: function() {
this.$el.html(this.template());
return this;
},
});
defining a router
//base router
app.routers.baseRouter = Backbone.Router.extend({
//routes
routes: {
'': "instructions",
'begin': "beginTest"
},
//functions (belong to object controller)
instructions: function() {baseController.instructions()},
beginTest : function() {baseController.beginTest()},
});
//baseRouter controller
var baseController = {
instructions: function() {
mainApp.viewsManager.rederView(new app.views.instructions());
},
beginTest: function(options) {
mainApp.viewsManager.rederView(new app.views.test());
},
};
defining mainApp (with a view-switcher)
//define mainApplication object
mainApp = {};
//manages views switching
mainApp.viewsManager = {
//rootEl
rootEl: '#test-container',
//close current view and show next one
rederView : function(view, rootEl) {
//if DOM el isn't passed, set it to the default RootEl
rootEl = rootEl || this.rootEl;
//close current view
if (this.currentView) this.currentView.close();
//store reference to next view
this.currentView = view;
//render next view
$(rootEl).html(this.currentView.render().el);
},
};
//render first view of app
mainApp.viewsManager.rederView(new app.views.instructions());
//initiate router and attach it to app
mainApp.baseRouter = new app.routers.baseRouter();
//start Backbone history
Backbone.history.start({silent: true
});
adding a close function to view via Backbone prototype
//add function to Backbone view prototype (available in all views)
Backbone.View.prototype.close = function () {
//call view beforeClose function if it is defined in the view
if (this.beforeClose) this.beforeClose();
//this.el is removed from the DOM & DOM element's events are cleaned up
this.remove();
//unbind any model and collection events that the view is bound to
this.stopListening();
//check whether view has subviews
if (this.hasOwnProperty('_subViews')) {
//loop thorugh current view's subviews
_(this._subViews).each(function(child){
//invoke subview's close method
child.close();
});
}
};
So, in order to check for zombie views, the second view triggers and event (still:alive) that the first view listen to and respond to it via a message sent to the console.log (although it really shouldn't).
The first view does listen to such a message (in the console log I read 'I am still alive) even when it has been replaced by the second view.
Can you help me? thank you very.
Long post, if you have any questions, please ask
A Zombie View is just a view that is not in the DOM, but listens to and reacts to events -- sometimes this behavior is expected, but not typically.
If the DOM Event handlers for the view are not properly removed, the view and it's in-memory HTML fragments will not be garbage collected. If the Backbone.Event handlers are not unbound properly, you could have all sorts of bad behavior... such as a bunch of "Zombie" view triggering AJAX requests on models. This problem was very common on older versions of Backbone prior to stopListening and listenTo especially if you shared models between views.
In your code, you don't have a Zombie View, because you are properly closing your views.
You can see the console.log because you are initializing the second view (and triggering the event still:alive) before you close the first view.
To switch views, you are calling:
mainApp.viewsManager.rederView(new app.views.test());
Calling new app.views.test() initializes the second view which triggers the event that the first listens to.
If you update your code to the following, you won't see the console.log anymore.
//baseRouter controller
var baseController = {
instructions: function() {
mainApp.viewsManager.rederView(app.views.instructions);
},
beginTest: function(options) {
mainApp.viewsManager.rederView(app.views.test);
},
};
And update rederView
rederView : function(ViewClass, rootEl) {
//if DOM el isn't passed, set it to the default RootEl
rootEl = rootEl || this.rootEl;
//close current view
if (this.currentView) this.currentView.close();
//store reference to next view
this.currentView = new ViewClass();
//render next view
$(rootEl).html(this.currentView.render().el);
},
If you remove this line from your close method, you will have a zombie view and should see the console.log.
//unbind any model and collection events that the view is bound to
this.stopListening();
Zombie View Example
In the following code, I am creating 100 views, but only displaying 1 in the DOM. Every view contains the same model and listens to it's change event. When the view's <button> element is clicked, it updates the model which causes every view's model change handler to be executed, calling fetch 100 times... 100 AJAX requests!
The view's change handlers are called 100 times, because the view close method does not call this.stopListening(), so even when the views are removed from the page, they all still listen to the model's events. Once you click the button, the model is changed, and all of the zombie views respond, even though they're not on the page.
var TestView = Backbone.View.extend({
tagName: 'h1',
initialize: function(options) {
this.i = options.i;
this.listenTo(options.model, 'change', function(model) {
model.fetch();
});
},
events: {
'click button': function() {
this.model.set("show_zombies", Date.now());
}
},
render: function() {
this.$el.append("<button>Click To Test for Zombies!</button>");
return this;
},
close: function() {
this.$el.empty(); // empty view html
// this.$el.off(); // // Whoops! Forgot to unbind Event listeners! (this view won't get garbage collected)
// this.stopListening() // Whoops! Forgot to unbind Backbone.Event listeners.
}
});
var model = new (Backbone.Model.extend({
fetch: function() {
document.body.innerHTML += "MODEL.FETCH CALLED<br />"
}
}));
var v;
for (var i = 1; i < 101; i++) {
if (v) v.close();
v = new TestView({
'i': i,
'model': model
}).render();
$('body').html(v.el);
}
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone.js"></script>

Marionette's CompositeView event not firing

I'm having trouble using Marionette's CompositeView. I render my model in my CompositeView using a template and want to add a click event to it. Somehow I can't get the events to work using the events: { "click": "function" } handler on the CompositeView... What am I doing wrong?
var FactsMenuItem = Backbone.Marionette.ItemView.extend({
template: tmpl['factsmenuitem'],
initialize: function() {
console.log('factsmenuitem');
},
onRender: function() {
console.log('factsmenuitem');
}
});
var FactsMenuView = Backbone.Marionette.CompositeView.extend({
template: tmpl['factsmenu'],
itemView: FactsMenuItem,
itemViewContainer: ".subs",
events: {
'click': 'blaat'
},
blaat: function() {
console.log('this is not working');
},
initialize: function() {
this.model.get('pages').on('sync', function () {
this.collection = this.model.get('pages');
this.render();
}, this);
},
onRender: function() {
console.log('render factsmenu');
}
});
var FactsLayout = Backbone.Marionette.Layout.extend({
template: tmpl['facts'],
regions: {
pages: ".pages",
filter: ".filter",
data: ".data"
},
initialize: function(options) {
this.currentPage = {};
this.factsMenu = new FactsMenu();
this.factsView = new FactsMenuView({model: this.factsMenu});
},
onRender: function() {
this.pages.show(this.factsView);
}
});
Edit:
I removed some code that made the question unclear...
The problem lies that the events of the non-collectionview of the compositeview (the modelView??) are not fired. I think this has something to do with the way the FactsLayoutView instantiates the compositeview...
The problem was caused by the way the region was rendered. In my FactsLayout is used this code:
initialize: function(options) {
this.currentPage = {};
this.factsMenu = new FactsMenu();
this.factsView = new FactsMenuView({model: this.factsMenu});
},
onRender: function() {
this.pages.show(this.factsView);
}
Apparently you can't show a view on a onRender function... I had to change the way the FactsLayout is initialized:
var layout = new FactsLayout({
slug: slug
});
layout.render();
var factsMenu = new FactsMenu({ slug: slug });
var factsView = new FactsMenuView({model: factsMenu});
layout.pages.show(factsView);
Maybe I did not understand your question well but if you need to listen an event fired from an item view within your composite view you should do like the following.
Within the item view test method.
this.trigger("test");
Within the composite view initialize method.
this.on("itemview:test", function() { });
Note that when an event is fired from an item of a CollectionView (a CompositeView is a CollectionView), it is prepended by itemview prefix.
Hope it helps.
Edit: Reading you question another time, I think this is not the correct answer but, about your question, I guess the click in the composite view is captured by the item view. Could you explain better your goal?

Old Backbone View Causing extra Event Triggers

OK, I've done some reading on this and I'm pretty sure I know what the problem relates to I Just don't know the best way to fix it. I've got the standard backbone router that sets me up with an item details view, then when I click on a button called "start" it creates a new view which takes me to a sort of a game that people can play with some buttons on the bottom that have "click" events attached. This second view is not called through the router but directly from the first view.
The problem is the second time someones goes back to the homescreen and does it again, this time there are two events attached to each button. The third time there are three events. Obviously the original views are still listening to these buttons. I've read about this and calling the Remove() method but is this what I need to do? If so where do I call remove? Relevant Code below:
1ST VIEW
window.GameDrillView = Backbone.View.extend({
initialize: function () {
this.render();
},
render: function () {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .start" : "startGameDrill",
},
startGameDrill: function () {
var start = $('#start').val();.
var stop = $('#stop').val();.
var StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
}
});
START VIEW
window.GameDrillStartView = Backbone.View.extend({
// declare variables
initialize: function () {
this.render();
},
events: {
"click .nextstage" : "nextstage", // 2ND TIME THROUGH GETS CALLED TWICE
},
nextstage: function () {
// Do some stuff //
this.render(); //Re-render
},
render: function () {
// Do some variables stuff
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)));..
return this;
}
});
When changing view you need to call undelegateEvents() method from the Backbone.View. It disable listening all the elements events mentioned in events { } block. Also if you need to destroy old view you can call remove() method of the view which will call undelegateEvents() internally.
update (example from official site)
var Workspace = Backbone.Router.extend({
routes: {
"help": "help", // #help
"search/:query": "search", // #search/kiwis
"search/:query/p:page": "search" // #search/kiwis/p7
},
help: function() {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new HelpView();
},
search: function(query, page) {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new SearchView();
}
});
An option is to create only one instance of the view:
if(_.isUndefined(this.StartView))
this.StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
else
this.StartView.render();
In the render method of GameDrillStartView add the empty method
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)))
In this way you won't add more event listeners but you'll update the page everytime the user presses the button.
You can manage the life cycle of StartView in GameDrillView since it seems like a better place to do so.
Got same trouble. Messy solution:
var current_view = false;
var prev_view = false;
var AppRouter = Backbone.Router.extend({
routes: {
"events/:id": "viewEvent",
}
});
var app_router = new AppRouter;
app_router.on('route:viewEvent', function (event_id) {
var _event = new Event({id:event_id});
current_view = new EventView({
model: _event,
});
});
//Will be called after route:viewEvent
app_router.on('route', function () {
if(prev_view) {
prev_view.undelegateEvents();
}
prev_view = current_view;
});
Not sure, how to make it without having current_view and prev_view out of router scope.

Categories