Backbone view updated unexpected - backbone.js

I am newin Backbone, and try to write some simple applications to learn backbone.
The application contains a list of items, and user can add/delete/ items, when user hover one of the item view, the item should changed its color to red,othewise, its color should be black.
var ItemView = Backbone.View.extend({
tagName: 'li',
events: {
'click a.delete': 'onDelete',
'mouseover': 'onHoverIn',
'mouseout': 'onHoverOut'
},
template: _.template($('#tpl-item').html()),
initialize: function () {
this.model.on("change", this.render, this);
},
render: function () {
var item = this.model;
this.$el.css("color", item.get("hover") ? "red" : "black").html(this.template(item.toJSON()));
return this;
},
onDelete: function () {
this.model.destroy();
},
onHoverIn: function () {
this.model.set("hover", true);
},
onHoverOut: function () {
this.model.set("hover", false);
}
});
This is the full codes of the app: jfiddle live example
However, it works while I move the mouse over the items one by one slowly, once I move quickly, I found that there may be more than one items are colored with red, which is not expected.
What's going on?

because re-render is a heavy and expansive task for browser, and somehow (honestly i dont know the magic behind) it mis-triggered the event.
To reduce the browser reflow, i turn the color manipulation to repaint by adding and remove class >> see here the update code
onHoverIn: function() {
this.$el.addClass('hover');
console.log('in: ' + this.model.get('name'));
},
onHoverOut: function() {
this.$el.removeClass('hover');
console.log('out: ' + this.model.get('name'));
}

Related

Undelegate view events when browser back button pressed

Could anyone explain please how to remove events in order to prevent triggering duplication when clicking browser back button. Or is there any way to undelegate events when initalizing view again. Really stuck how to deal with it.
Pressing back button and then back again causes firing events for multiple times. When saving model form data for instance. Thank you.
var App = {};
// extending models, collections etc.
App.SamplesCollectionView = Backbone.View.extend({
el: '#samples',
template: _.template($('#sample-edit-template').html()),
events: {
'click a.sample-item': 'onEdit'
},
render: function(){
this.$el.append(this.template());
var $sample_list = this.$el.find('ul#sample-list');
this.collection.each(function(sample) {
var rendered = new App.CategoryView({model: sample}).render().el;
$sample_list.append(rendered);
});
},
onEdit: function(e) {
this.undelegateEvents();
// go to edit view
Backbone.history.navigate(e.target.getAttribute('href'), {trigger: true});
return false;
}
});
App.SampleEditView = Backbone.View.extend({
el: '#samples',
template: _.template($('#sample-edit-template').html()),
events: {
'click button.save': 'onSave',
'click button.cancel': 'onCancel',
},
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this;
},
onSave: function() {
this.undelegateEvents();
var data = Helpers.getFormData(this.$el.find('form'));
this.model.save(data);
// go back to index view
Backbone.history.navigate('/samples', {trigger: true});
return false;
}
});
App.SamplesRouter = Backbone.Router.extend({
routes: {
'samples': 'index',
'samples/edit/:id': 'edit'
},
index: function() {
App.samples = new App.SamplesCollection;
App.samplessView = new App.SamplesCollectionView({collection: App.samples});
},
edit: function(id) {
App.sampleEdit = new App.SampleEdit({id: id});
App.sampleEditView = new App.SampleEditView({model: App.sampleEdit})
}
});
App.samplesRouter = new App.SamplesRouter;
Backbone.history.start({pushState: true, hashChange: false});
The problem is that you have many views pointing to same element #samples. You can't remove one view because if you call view.remove() your other view's element is gone.
And as long as that that element exists in DOM, the view you thought to be gone will exist in memory since the shared element has event handlers referring the view instance.
If you want to delegate display functionality and edit functionality under same element, do it in same view using something like show/hide techniques without creating a new view instance.
Otherwise they should have it's own elements, you shouldn't have two view instances pointing to same element. While switching to a different view, make sure you call it's remove() method which removes the element from DOM and invokes undelegateEvents so that it get's garbage collected properly.

Event handling between views

Ok I have a layout like the one in this pic:
The table in the upper part of the screen is made by:
MessageListView
define(['backbone','collections/messages','views/message'], function(Backbone, MessageCollection, MessageView) {
var MessageListView = Backbone.View.extend({
el: '#messagesContainer',
initialize: function() {
this.collection = new MessageCollection();
this.collection.fetch({reset:true});
this.listenTo( this.collection, 'reset', this.render );
this.table = this.$el.find("table tbody");
this.render();
},
render: function() {
this.collection.each( function(message, index) {
this.renderMessage(message, index);
}, this);
},
renderMessage: function(message, index) {
var view = new MessageView({
model:message,
className: (index % 2 == 0) ? "even" : "odd"
});
this.table.append( view.render().el );
}
});
return MessageListView;
});
MessageView
define(['backbone','models/message'], function(Backbone, MessageCollection, MessageView) {
var MessageView = Backbone.View.extend({
template: _.template( $("#messageTemplate").html() ),
render: function() {
this.setElement( this.template(this.model.toJSON()) );
return this;
},
events:{
'click':'select'
},
select: function() {
// WHAT TO DO HERE?
}
});
return MessageView;
});
AppView
define(['backbone','views/messages'], function(Backbone, MessageList) {
var App = Backbone.View.extend({
initialize: function() {
new MessageList();
}
});
return App;
});
I will soon add a new view (maybe "PreviewView") in the lower part of the screen.
I want to make something happen inside the "PreviewView" when user clicks a row.
For example, it could be interesting to display other model's attributes (details, e.g.) inside the PreviewView.
What is the best practice?
holding a reference to PreviewView inside each MessageView ?
triggering events inside select method, and listening to them using on() inside the preview view.
using a transient "selected" attribute in my model, and make PreviewView listen to collection "change" events?
Thank you, if you need more details tell me please, I'll edit the question.
Not sure about the best practice but I found this solution trivial to implement. I created a global messaging object, bus, whatever:
window.App = {};
window.App.vent = _.extend({}, Backbone.Events);
You have to register the "triggerable" functions of PreviewView on the previously created event bus (according to your example, this should be in the PreviewView):
initialize: function () {
App.vent.on('PreviewView.show', this.show, this);
}
Now you should be able to trigger any of registered events from anywhere within your application by calling: App.vent.trigger. For example when the user click on a row you will have something similar:
App.vent.trigger('PreviewView.show');
in case if you have to send and object along with the triggered event use:
App.vent.trigger('PreviewView.show', data);

Backbone Layout Manager subview's events doesn't work after reload

I'm using the Backbone Layout Manager Boilerplate. Unfortunately, a quite frustrating bug occurred. I like render a list of items as subviews inserted by insertView function. At the first load everthing works fine. But after a reload the the click events doesn't work anymore :(. I already tried to call delegateEvents() on the TableItem View manually but nothing changed. I hope anyone can give me a clue.
App.Views.Item = Backbone.View.extend({
template: "templates/item",
tagName: "li",
events: {
"click .applyButton" : "apply",
"click .viewDetailsButton" : "showDetail"
},
serialize: function() {
return { table : this.model.toJSON() };
},
apply: function(ev) {
ev.preventDefault();
alert("apply button clicked");
},
showDetail: function(ev) {
ev.preventDefault();
var id = this.model.get("_id");
app.router.navigate("#events/"+ id, {trigger : true})
}
});
/*
* List View
*/
App.Views.List = Backbone.View.extend({
template: "templates/list",
tagNam: "ul",
className: "tableList",
beforeRender: function() {
var events = this.model.get("userEvents").get("hosting");
events.each(function(model) {
this.insertView(new App.Views.Item({ model : model }));
}, this);
},
serialize: function() {
return {};
}
});
I think you might want to add a cleanup function on your Item view to undelegate the events when layoutmanager removes the view. I don't know if this will fix your problem, but it seems like good practise.
When you say after a reload, do you mean reloading the page with the browser reload button? if so, how do you get it to work in the first place?
It would help if you could provide a jsfiddle of your setup, or point us to a repo so we can test it on our machines. Make sure you include the router so that we can have a look at how the view and the layout that contains it are initialised.

Display a view using an existing rendered HTML with Backbone Marionette

I am having an application layout like the one attached. The upper panel is already to the page (i.e. in the server's HTML response). While the user interacts with the elements in that panel the content of the dynamic panel below changes accordingly.
I've studied Backbone Marionette various View types and Region Manager. But I still can't figure out a way to implement this. I need to capture events from the already rendered elements and change the dynamic content accordingly. As I understand, every time a region is created to show a specific Marionette view, the region's content is replaced by that view's el. And with that I cannot have a Layout view for the container of the whole thing.
So can this be done in anyway using Marionette?
You can certainly support what I would call a "pre rendered" or partial view. In fact, here's a Marionette View that I use quite a bit, as I'm working under with an app that includes server side partial views:
My.PartialView = Backbone.Marionette.Layout.extend({
render: function () {
//noop
if (this.onRender) {
this.onRender();
}
return this;
},
onShow: function () {
// make sure events are ready
this.delegateEvents();
}
});
It's simple to use:
My.NavBar = My.PartialView.extend({
events: {
"change #search-input": "searchRequested",
"click #faq-link": "faqRequested",
"click #home-link": "homeRequested",
},
searchRequested: function (e) {
// search
},
faqRequested: function (e) {
// show the faq
},
homeRequested:function () {
// go home
}
});
var navbar = new main.views.NavBar({ el: ".my-nav" });
someRegion.show();
// or, just wire up the events manually
navbar.delegateEvents();
I think the better way is using constructor.
Make your rendered layout class.
App.RenderedLayout = Marionette.Layout.extend({
render: function () {
if (this.onRender) {
this.onRender();
}
return this;
},
constructor: function(){
this._ensureElement();
this.bindUIElements();
Marionette.Layout.prototype.constructor.apply(this, slice(arguments));
}
});
Then you can use full of Marionette capabilities.
App.module('Some.Page', function (Mod, App, Backbone, Marionette, $, _) {
Mod.SomeLayout = App.RenderedLayout.extend({
el: '#renderedDiv',
events: {
'click .something': 'onSomethingClick'
},
regions: {
'innerRegion': '#innerRegion'
},
ui: {
something: '.something div'
},
initialize: function () {
},
onSomethingClick: function(e){
return false;
}
});
Mod.addInitializer(function(){
App.addRegions({renderedRegion: '#renderedDiv'});
Mod.someLayout = new Mod.SomeLayout();
App.renderedRegion.attachView(Mod.someLayout);
});
});

Event delegation failing to attach events in Backbone.js app

My Backbone.js app has a popup editor view that will be repeatedly closed and reopened as the user makes changes. I'm trying to figure out the cleanest way to implement this, and I'm stuck on an event delegation issue.
I believe the problem lies in the way I'm applying the template in my render method. I haven't had any issues with getting events to fire in other views, but those views differed in that they had a model. The view I'm having issues with is more of an application view that contains sub-views, so I'm not sure how to pass the view's context to the MyApp view.
Here's my code:
MyApp = Backbone.View.extend({
tagName: 'div',
template: _.template($('#app-template').html()),
initialize: function() {
_.bindAll(this);
this.render();
},
render: function() {
$('#container').html(this.template);
return this;
},
events: {
"click .save" : "onSaveClicked"
},
onSaveClicked: function () {
console.log("Save clicked.");
this.$el.remove();
}
});
$('#show').click(function () {
var myapp = new MyApp;
});
I've also posted it as a jsFiddle.
I stepped through the Backbone.js source, and it appears that render is called first, then events are assigned, which is what I'd expect. Everything looks OK from what I can tell, but onSaveClicked never fires when I click Save.
The desired functionality is that clicking Show displays the form and Save removes it. If there's a better way to do this that's more inline with Backbone's underlying philosophy I'm open to that as well. You'll notice that I'm nesting an unnamed div inside container, and that's because I wanted to maintain a consistent anchor point for my popup.
The events are bound to the view el, but you never append the el to the DOM. Try
MyApp = Backbone.View.extend({
tagName: 'div',
template: _.template($('#app-template').html()),
initialize: function() {
_.bindAll(this);
this.render();
},
render: function() {
this.$el.html(this.template);
return this;
},
events: {
"click .save" : "onSaveClicked"
},
onSaveClicked: function () {
console.log("Save clicked.");
this.$el.remove();
}
});
$('#show').click(function () {
var myapp = new MyApp;
$("#container").append(myapp.$el);
});
http://jsfiddle.net/WBPqk/18/
Note that in your Fiddle you bound the click event to .save where your template uses a done class.

Resources