Marionette Itemview click is triggered multiple times - backbone.js

I am seeing the click event is triggered multiple times for each row on the itemview
return Marionette.ItemView.extend( {
template: ItemViewTemplate,
tagName: 'tr',
className: 'ItemRow',
templateHelpers: viewHelpers,
events: {
'click .editRowItem': 'editRowItem'
The editRowItem() function is triggered multiple times. What is the correct way to trigger the click just on that particular row?
Thanks!

Usually it shouldn't trigger multiple times. It can happen however, for example when:
Nested elements with the same class in a view. Event bubbling will cause multiple events to be fired.
Parent views listening to click events on a class which is present in some or all children views.
An example (stripped non-relevant parts):
<script type="text/template" id="movie-list-item">
<button type="button" class="btn btn-success some-button">
<span class="some-button">Click here</span>
</button>
</script>
// Itemview
var movieItemView = Marionette.ItemView.extend({
template: "#movie-list-item",
ui: {
viewButton: '.some-button'
},
events: {
'click #ui.viewButton': 'clickHandler'
},
clickHandler: function(ev) {
// Log the click
console.log('There was a click in an itemView!');
// Uncomment the following to prevent multiple events:
//ev.stopPropagation();
}
});
// Composite view
var movieCompView = Marionette.CompositeView.extend({
template: "#movie-list",
itemView: movieItemView,
ui: {
viewButton: '.some-button'
},
events: {
'click #ui.viewButton': 'clickHandler'
},
clickHandler: function(ev) {
// Log the click
console.log('There was a click in a collectionView!');
// Uncomment the following to prevent multiple events:
//ev.stopPropagation();
}
});
Demo here: http://jsfiddle.net/Cardiff/7d3fC/2/
Note the following, if we don't use ev.stopPropagation() in this case to prevent the event from bubbling, the console will log 4 entries; being two for the itemView and two for the collectionView. To prevent this behaviour (and you shouldn't use a click event in the collectionView in this case) and thus receive one instead of two events we use ev.stopPropagation().
Also keep in mind that using the ui attribute of a view to describe the components is considered good practice and can make your life a little easier.

Just try following if you want to apply click event to each row item in the template:
events: {
'click' : 'editRowItem'
}

Related

Detect click on itemview container in Backbone/Marionette

I have the following item view:
return Marionette.ItemView.extend({
template:tpl,
tagName: 'div',
className: 'v_itv_record_type_item',
events:{
'click #ui.item':'itemClicked'
},
ui:{
item:'.v_itv_record_type_item'
},
itemClicked:function(e){
console.log('clicked');
}
});
that uses the following handlebars template:
<div class="clicktarget">
Stuff Goes Here
</div>
If you click on one of these item views, it does not register the click event. I understand that Backbone restricts access to just the views slice of the DOM, but apparently this does not extend to the containing div itself, even though that containing div is not part of any template, parent view or otherwise.
If we change the ui hash and point item at .clicktarget the click is registered. But this gives me me a <div><div>stuff goes here</div></div> structure for seemingly no reason. Is this the only way to detect a click on the entirety of an item views DOM element?
You can register a click event on the view element by omitting the selector:
events:{
'click' :'itemClicked'
}
Note that if you have an event handler at view level, all the clicks inside the view will bubble up and trigger it's handler, unless it was stopped (event.stopPropagation()) on the way. This is the expected behavior.

events hash for dynamically generated buttons in backbonejs

I have a template in underscore having a button inside it. I want a click event to be mentioned in the event hash in the backbone view.
template code is:
<script type="text/template" id="ledgerListing">
<button class="btn btn-danger pull-right" id="addLedgerButton">Add Ledger</button>
</script>
View Code is:
app.ledgerView=Backbone.View.extend({
el:"#container",
template:_.template($("#ledgerListing").html()),
events: {},
initialize: function(){
},
render: function()
{
this.$el.html(template())
}
});
Now how to specify the click event in the event hash for the button with id addLedgerButton
Your can add events as the following, (format should be event type and a space and the element
events: {
'click #addLedgerButton': 'myclick'
},
and define a function named myclick:
myclick: function () {
alert(1);
}
Here is jsfiddle. http://jsfiddle.net/w2jm7/

Backbone.js per attribute rendering (multiple small views vs multiple templates per view )

I have a model and a view. The view displays attributes of a model and allows the user to manipulate these attributes. The problem is that when an attribute is modified it re-renders the whole view which causes a lot of problems for me.
Example blur event on a text input saves the new input to an attribute and thus fires render. Which means that if the user clicked from that text input straight to a button on the same view that event will never fire as the first event that fires will be blur causing the whole view to re-render and thus losing the button click event.
I have two ideas:
Have a single view where every attribute is in a separate template. Then I bind to a particular attribute change event and in render I update only the html of the changed attribute. This seems like a hack, as there is a lot of work to force the view to update only the changed attribute. It will add a lot of unnecessary complexity to an already complex view.
Create a master view which consists of views, where each of them represents a model's attribute. This will create a lot of views, with nearly no functionality.
I seem to prefer the 2. option. What do you think? What are the best practices? Is there any better way to handle this?
I think you can do this quite easily.
Take a step back and think about where you are binding your events. It seems that you are binding them directly on top of each individual element instead of using a parent delegate.
Here's an example
Backbone.View.extend({
el: $("div.parent"),
events: function() {
this.$el.on("click", "input[type=button]", function(){});
// jquery cross browser on this
this.$el.on("blur", "input[type=text]", function(){});
},
initialize: function() {
this.model.bind("change", this.render, this);
},
render: function() {
this.$el.html('<input type="text" /><input type="button" />');
}
});
Here's what el and it's structure looks like
<div class="parent">
<input type="text" />
<input type="button" />
</div>
So this.$el points to div.parent. I can constantly rerender the contents of this.$el, and as long as the html structure dosen't change, I don't have to worry about events getting unbound. The other solution is that if I really cannot do delegation, I would just call the events method whenever I render again.
Like you said yourself, both of your options seem very complex. But sometimes additionaly complexity is a necessary evil. However, if the updated fields are something relatively simple (like binding a value to an element or an input field), I would simply update the DOM elements without creating additional View/Template abstractions on top of them.
Say you have a model:
var person = new Person({ firstName: 'John', lastName: 'Lennon', instrument:'Guitar' });
And a view which renders the following template:
<div>First Name: <span class="firstName">{{firstName}}</span></div>
<div>Last Name: <span class="lastName">{{lastName}}</span></div>
<div>Instrument: <input class="instrument" value="{{instrument}}"></input></div>
You could declare in the view which property change should update which element, and bind the model change event to a function which updates them:
var PersonView = Backbone.View.extend({
//convention: propertyName+"Changed"
//specify handler as map of selector->method or a function.
firstNameChanged: { '.firstName': 'text' },
lastNameChanged: { '.lastName': 'text' },
instrumentChanged: { '.instrument': 'val' },
otherFieldChanged: function(val) { //do something else },
initialize: function (opts) {
this.model.on('change', this.update, this);
},
//called when change event is fired
update: function(state) {
_.each(state.changed, function(val, key) {
var handler = this[key + "Changed"];
//handler specified for property?
if(handler) {
//if its a function execute it
if(_.isFunction(handler)) {
handler(val);
//if its an object assume it's a selector->method map
} else if(_.isObject(handler)) {
_.each(handler, function(prop, selector) {
this.$(selector)[prop](val);
}, this);
}
}
}, this);
}
A solution like this doesn't scale to very complex views, because you have to add classed elements to the DOM and maintain them in the View code. But for simpler cases this might work quite well.
In addition it's always good to try to compose views of multiple, smaller views, if they naturally divide into sections. That way you can avoid the need to update single fields separately.

Tabs in Backbone

I'm new with Backbone and I'm making an example app in which I have to include tabs. The thing is that I have a collection of cities and I want to create one tab for each city (the collection fetchs from the server). I made a view called TabsView, which in the render function passes the collection to a template, and this one loops through the collection and renders the tabs.
What I want to do is that the first tab appears as 'active'. What I've done for the moment is that each tab has a href to a route in the router which changes it's class to active using jquery. Don't know if this is the best way to do this but it works. Maybe there's a better way. Also, when the user clicks a tab, I want to be able to render other view.
Hope I made myself clear. Thanks, cheers,
Martin
Ok I solved this problem doing something like the following:
var Tabs = Backbone.View.extend({
template: JST['tabs'],
events: {
'click li' : 'switchTab'
},
tagName: 'ul',
className: 'nav-tabs',
render: function() {
this.renderTabs();
return this;
},
renderTabs: function() {
this.$el.html(this.template({ cities: this.cities }));
this.$('li:first').addClass('active');
},
switchTab: function(event) {
var selectedTab = event.currentTarget;
this.$('li.active').removeClass('active');
this.$(selectedTab).addClass('active');
}
});
It works fine, maybe it can be improved.

Attach ExtJS MVC controllers to DOM elements, not components

Is there a way to use the Ext.app.Controller control() method, but pass in a DOM query? I have a page that contains standard links and would like to add a click handler to them even though they were not created as Ext Buttons.
I've tried
Ext.define('app.controller.TabController', {
extend: 'Ext.app.Controller',
init: function() {
console.log("init");
this.control({
'a': {
click: this.changeTab
}
});
},
changeTab: function() {
alert("new tab!");
}
});
But clicking on links does not fire the alert.
Is there a way to specify a CSS selector with this.control? Or does it only work with components?
I asked this question at SenchaCon this year, the Sencha developers stated that their intent is that DOM listeners should be attached within your view, and the view should abstract them into more meaningful component events and refire them.
For example, suppose you're creating a view called UserGallery that shows a grid of people's faces. Within your UserGallery view class, you would listen for the DOM click event on the <img> tag to receive event and target, and then the view might fire a component event called "userselected" and pass the model instance for the clicked user instead of the DOM target.
The end goal is that only your views should be concerned with things like interface events and DOM elements while the application-level controller only deals with meaningful user intents. Your application and controller code shouldn't be coupled to your markup structure or interface implementation at all.
Sample View
Ext.define('MyApp.view.UserGallery', {
extend: 'Ext.Component'
,xtype: 'usergallery'
,tpl: '<tpl for="users"><img src="{avatar_src}" data-ID="{id}"></tpl>'
,initComponent: function() {
this.addEvents('userselected');
this.callParent(arguments);
}
,afterRender: function() {
this.mon(this.el, 'click', this.onUserClick, this, {delegate: 'img'});
this.callParent(arguments);
}
,onUserClick: function(ev, t) {
ev.stopEvent();
var userId = Ext.fly(t).getAttribute('data-ID');
this.fireEvent('userselected', this, userId, ev);
}
});
Notes on views
Extend "Ext.Component" when all you want is a managed <div>, Ext.Panel is a lot heavier to support things like titlebars, toolbars, collapsing, etc.
Use "managed" listeners when attaching listeners to DOM elements from a component (see Component.mon). Listeners managed by a components will be automatically released when that component gets destroyed
When listening for the same event from multiple DOM elements, use the "delegate" event option and attach the listener to their common parent rather than to individual elements. This performs better and lets you create / destroy child elements arbitrarily without worrying about continuously attaching/removing event listeners to each child. Avoid using something like .select('img').on('click', handler)
When firing an event from a view, Sencha's convention is that the first parameter to the event be scope -- a reference to the view that fired the event. This is convenient when the event is being handled from a controller where you'll need the actual scope of the event handler to be the controller.
Sample Controller
Ext.define('app.controller.myController', {
extend: 'Ext.app.Controller'
,init: function() {
this.control({
'usergallery': {
userselected: function(galleryView, userId, ev) {
this.openUserProfile(userID);
}
}
});
}
,openUserProfile: function(userId) {
alert('load another view here');
}
});
I have found a work around for this problem. It isn't as direct as one may hope, but it leaves all of your "action" code in the controller.
requirment: Wrap the html section of your page in an actual Ext.Component. This will likely be the case either way. So for instance, you may have a simple view that contains your HTML as follows:
Ext.define('app.view.myView', {
extend: 'Ext.panel.Panel',
alias: 'widget.myView',
title: 'My Cool Panel',
html: '<div>This link will open a window</div><br /> <label for="myInput">Type here: </label><input name="myInput" type="text" value="" />',
initComponent: function(){
var me = this;
me.callParent(arguments);
}
});
Then in the controller you use the afterrender event to apply listeners to your DOM elements. In the example below I illustrate both links (a element) and input elements:
Ext.define('app.controller.myController', {
extend: 'Ext.app.Controller',
init: function() {
this.control({
'myView': {
afterrender: function(cmp){
var me = this; //the controller
var inputs = cmp.getEl().select('input'); // will grab all DOM inputs
inputs.on('keyup', function(evt, el, o){
me.testFunction(el); //you can call a function here
});
var links = cmp.getEl().select('a'); //will grab all DOM a elements (links)
links.on('click', function(evt, el, o){
//or you can write your code inline here
Ext.Msg.show({
title: 'OMG!',
msg: 'The controller handled the "a" element! OMG!'
});
});
}
}
});
},
testFunction: function(el) {
var str = 'You typed ' + el.value;
Ext.Msg.show({
title: 'WOW!',
msg: str
});
}
});
And there you have it, DOM elements handled within the controller and adhering to the MVC architecture!
No, this seems not to be possible. The Ext.EventBus listens to events fired by ExtJS components. Your standard DOM elements do not fire those events. Additionally the query is checked with the ExtJS componets is( String selector ) method, wich can't be called by DOM elements. Someone might correct me if i'm wrong, but so i'm quite sure it's not possible, unfortunately.
I also have a solution that works around this problem. I use this technique regardless though as it has other benefits: I created an application wide messaging bus. Its an object in my application that extends Observable, and defines a few events. I can then trigger those events from anywere in my app, including html a links. Any component that wants to listen to those events can relay them and they'll fire as if fired from that component.
Ext.define('Lib.MessageBus', {
extend: 'Ext.util.Observable',
constructor: function() {
this.addEvents(
"event1",
"event2"
);
this.callParent(arguments);
}
});
Then, each other compnent can add this after initialisation:
this.relayEvents('Lib.MessageBus', ['event1','event2']);
and then listen to those events.
You can trigger the events from anything by doing:
Lib.MessageBus.fireEvent('event1', 'param a', 'param b')
and you can do that from anything including html links.
Very handy to fire off events from one part of the app to another.
I had the same problem recently (mixing mvc with some non components).
Just thought I'd throw this in as an answer as it seems pretty simple and works for me :)
Ext.define('app.controller.TabController', {
extend: 'Ext.app.Controller',
init: function() {
console.log("init");
this.control({
/* 'a': {
click: this.changeTab
} */
});
var link = Ext.dom.Query.selectNode('a');
Ext.get(link).on('click', this.changeTab);
},
changeTab: function() {
alert("new tab!");
}
});

Resources