I have a section element with id = wrapper. In my router I add .delegate() jQuery method to delegate events to dynamically created buttons (as traditional events:{"click button" : "gotoSomeMethod"} is not working for me).
$(document).ready(function() {
window.App = new window.Routers.Package;
Backbone.history.start();
$('#wrapper').delegate("button", "click", function(ev){
alert ($(ev.target).id);
});
});
Here is my view,
window.Views.Actions = Backbone.View.extend({
tag: 'nav',
initialize: function() {
_.bindAll(this, 'render', 'gotoNode');
},
render:function(){
this.model.each(function(action){
var buttonTemplate = "<button id = '" + action.toNodeId + "'>" + action.name + " </button>";
$(this.el).append(buttonTemplate)
}, this);
console.log(this.el); // when I do this I get `<div><button id = 'something'></button></div>`
return this;
},
events:{
"click button":"gotoNode"
},
gotoNode:function() {
alert("inside gotoNode");
},
});
So the first issue here is why I get undefined as an for id? Secondly, how do I make backbone to call gotoNode() method?
The jQuery function $ returns a jQuery object, not the DOMElement. If you want to access the id of that element you need to use $(ev.target).attr("id"). Also you shouldn't use ev.target. It returns the DOMElement that was clicked and it can be the button, but it can also be the TextNode inside the button. In the second case you won't have the id attribute. What you need to use is $(this).attr("id").
For the gotoNode part, it would make more sense if you where doing the event binding in your view (move the delegate call inside your initialize method).
initialize : function () {
_.bindAll(this, 'render', 'gotoNode');
var self = this;
$('#wrapper').delegate("button", "click", function(ev){
self.gotoNode();
});
}
If you want gotoNode() method to work you must place your button into this.el. Only then delegate will work.
Related
I created a view for table row, that i want to listen to any click on any cells in this row.
This is how I implemented:
let ListItem = Backbone.View.extend({
events: {
'click tr': 'showDetails',
'click': 'showDetails'
},
template: function() {
return "<tr><td><img src=<%= imageUrl %> /></td><td class='name'><%= firstName %></td><td><%= lastName %></td><td><%= homeTown %></td><td><button type='button' id='ddd' class='btn btn-danger'>Delete</button></td></tr>";
},
render: function() {
var oTemplate = _.template(this.template());
this.$el.html(oTemplate(this.model.toJSON()));
$('table > tbody:last-child').append(this.$el.html());
return this;
},
showDetails: function() {
alert("*****");
},
hide: function(bToHide, index) {
index++;
if (!bToHide) {
$($('tr')[index]).show();
return;
}
$($('tr')[index]).hide();
this.rendered = true;
},
openActorView: function() {
window.open('/#actor/' + window.actorsCollection.indexOf(this.model), '_self');
}
});
After clicking the row\cells within, nothing happens.
I listen to click on this view + click on the tr, but nothing happens.
Also, when add event to tr td nothing happens ether.
Thanks for helping
The reason it doesn't work
$('table > tbody:last-child').append(this.$el.html());
The jQuery .html() function returns a string.
So you're creating a nice Backbone view, with easy to define event listeners, then you don't use it, you just take its string representation and dump it in the DOM, losing any event listeners previously bound.
The improvements
First, there's no need to make the template a function, just use _.template directly. I wrote another answer which provides additional information on how to use _.template.
After that, don't use global selectors inside a view. It kills the purpose of a scoped component view. Let the parent view class deals with this.
let ListItem = Backbone.View.extend({
tagName: 'tr', // the list item is a `<tr>`
events: {
'click': 'showDetails'
},
template: _.template("<td><img src=<%= imageUrl %> /></td><td class='name'><%= firstName %></td><td><%= lastName %></td><td><%= homeTown %></td><td><button type='button' id='ddd' class='btn btn-danger'>Delete</button></td>"),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
showDetails: function() {
console.log("show details event");
}
});
Make a list view that manages the table. This is the parent class that will put the list items into the DOM.
Instead of using the jQuery core function, use the Backbone view scoped alias this.$() to cache the table body.
let ListView = Backbone.View.extend({
initialize: function() {
this.$body = this.$('tbody');
},
render: function() {
this.$body.empty();
this.collection.each(this.renderItem, this);
return this;
},
renderItem: function(model) {
var view = new ListItem({ model: model });
this.$body.append(view.render().el); // use the DOMElement
}
});
Then pass an existing table from outside the view.
new ListView({ el: 'table.my-table' }).render();
This is a super basic example and you should be aware that performance could be improved further and that it could leak memory if used as-is.
See how to improve list rendering performance with Backbone.
I'm having an issue when I am trying to switch the view after a model save on a click event.
The flow I am trying to create is a reorder process, the user will have a confirmation page to reorder. On clicking submit an api call will execute and the invoice page will load on success.
Currently when I click the submit button the first time nothing happens and when i click again I can get an invoice page. no such issue for the cancel button.
var confirmView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template( $("#confirmReorder_template").html());
this.$el.html(template);
},
events: {
"click #submitButton": "submitReorder",
"click #cancelButton": "cancelReorder"
},
submitReorder: function(event){
var URI='<config property="api.url.itemReorder"/>';
var ItemReorderModel = new itemReorderModel({url:URI});
$("#submitButton").click(function(event) {
event.preventDefault();
ItemReorderModel.set('id','1');
ItemReorderModel.save( {}, {
success : function() {
var response = ItemReorderModel.toJSON();
var InvoiceView = new invoiceView({el: $("#itemData")});
},
error : function(model, xhr, options) {
}
});
});
},
cancelReorder: function(event){
document.location.href = "items_list.ctl";
}
});
second view
var invoiceView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template( $("#reorderInvoice_template").html());
this.$el.html(template);
},
events: {
"click #returnButton": "itemlist",
"click #printButton": "print"
},
itemlist: function(event){
document.location.href = "items_list.ctl";
},
print: function(event){
}
});
loading of first view
$(document).ready(function() {
var ConfirmView = new confirmView({el:$('#itemData')});
});
I'm new to backbone so not sure if I should be using a route, I also have read something about binding, but still trying to get my head around how it all works.
any advice is much appreciated.
You are binding a new event handler in submitReorder method, and your actual functionality is inside that event handler.
So the fist time you click the button, the event handler delegated toview via backbone event hash will trigger submitReorder, which binds a new event handler with actual functionality directly to the button element.
Next time when you click it, this new direct handler will also trigger and fire the functionality you expect.
Each time you click the button you're adding a new event handler.
Your code should be simply:
submitReorder: function(event){
event.preventDefault();
var URI='<config property="api.url.itemReorder"/>';
//-----^------ if this is hardcoded, why not specify this in the model itself..?
var ItemReorderModel = new itemReorderModel({url:URI});
//-------------^----------- why not do this just once while initializing view..?
ItemReorderModel.set('id','1');
//-------------^----------- if this is hardcoded, why not set specify it in model..?
ItemReorderModel.save( {}, {
success : function() {
var response = ItemReorderModel.toJSON();
var InvoiceView = new invoiceView({el: $("#itemData")});
},
error : function(model, xhr, options) {
}
});
},
I also suggest initializing the model in the view's initialize method and caching it as it's property rather than initializing a new model on every click.
I have a simple backbone view as follows:
/**
* Renders a form view for an event object.
*/
APP.EventFormView = Backbone.View.extend({
tagName: 'form',
events: {
'keydown': 'keyPressed',
'focus input': 'inputChanged',
'change select': 'selectChanged',
'change textarea': 'textareaChanged'
},
initialize: function() {
this.template = _.template($('#newevent-form').html());
this.listenTo(this.model, 'change', this.render);
this.listenTo(APP.eventTypes, 'update', this.render);
this.listenTo(APP.selectedEvent, 'update', this.render);
},
render: function() {
var modelJSON = this.model.toJSON();
if ('id' in modelJSON && modelJSON.id !== "") {
this.loadForm();
} else if (!('id' in modelJSON) || modelJSON.id === "") {
this.loadForm();
} else {
this.$el.html('');
}
return this;
},
loadForm: function() {
var templateData = $.extend(this.model.toJSON(),
{"event_types":APP.eventTypes.toJSON()});
this.$el.html('');
this.$el.html(this.template($.extend(this.model.toJSON(),
{event_types: APP.eventTypes.toJSON()})));
$('.ev-main-container').html('').html(this.el);
},
inputChanged: function(e) {
console.log('inputChanged');
},
selectChanged: function(e) {
console.log('selectChanged');
},
textareaChanged: function(e) {
console.log('textareaChanged');
},
keyPressed: function(e) {
console.log('key pressed');
}
});
I initialize this view as follows under document.ready:
// Initialize the form view
APP.selectedEvent = APP.selectedEvent || new APP.Event();
APP.eventFormView = new APP.EventFormView({model: APP.selectedEvent});
APP.eventFormView.render();
But none of the events I have defined are firing for some reason, What is it that I am doing wrong here ?
Update:
Ok, I fugred out if i remove $('.ev-main-container').html('').html(this.el); from the loadForm method and instead intialize the view as follows, it works:
APP.eventFormView = new APP.EventFormView({
model: APP.selectedEvent,
el: $('.ev-main-container'),
});
I was able to resolve it but I still don't understand why this happens, could anyone throw a little light on what's going on and how this works.
jQuery's html function has a side effect that many people seem to forget about, from the fine manual:
jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.
Consider what that means when you do something like this:
container.html(view.el);
container.html(view.el);
Everything will be fine after the first container.html() call. But the second will "remove ... event handlers from child elements" (such as view.el) before adding the new content. So after the second container.html() call, all the events on view.el are gone. Sound familiar?
You have lots of things that will call render on your view and render will eventually do this:
$('.ev-main-container').html('').html(this.el);
Your events will silently disappear the second time that gets called but the HTML will look just fine.
Consider this simplified example (http://jsfiddle.net/ambiguous/otnyv93e/):
var V = Backbone.View.extend({
tagName: 'form',
events: {
'click button': 'clicked'
},
initialize: function() {
this.template = _.template($('#t').html());
},
render: function() {
this.$el.html('');
this.$el.html(this.template());
$('.ev-main-container').html('').html(this.el);
return this;
},
clicked: function() {
console.log('clicked');
}
});
var v = new V;
v.render();
$('#re-render').click(function() {
v.render();
console.log('Re-rendered');
});
and you'll see exactly your problem.
If you make the view's el the .ev-main-container then you'll be using html() to alter the contents of el rather than altering the contents of the element that contains el. Once you're working entirely inside the el you're no longer accidentally re-using an element and no longer accidentally removing the event bindings from that element.
My rules of thumb for preventing event problems with Backbone:
Never attach views to existing DOM nodes, always let views create and own their own el and let the caller put that el in a container.
Call remove on views to dispose of them when they're no longer needed.
Don't try to re-use views, create them when you need them and remove them when you don't need them.
No view references anything outside its el.
There are exceptions (of course) and this approach won't solve everything but it is a good starting point and avoids most of the common problems.
I'm trying to manually trigger a click event right after the html has been rendered but it's not working.
To simplify and verify that it's not working I tried this code:
var _testView = Backbone.View.extend({
events : {
'click a' : 'sayHi'
},
initialize : function() {
this.render();
this.$el.find('a').trigger('click');
},
render : function() {
$(document.body).html(
this.$el.html('alert hi')
);
},
sayHi : function() {
alert('Hi');
return false;
}
});
var y = new _testView;
I'm trying to manually trigger the click event but it's not being triggered. If I'm going to put the trigger in a setTimeout with a delay of 500 it will work. I don't know why.... thx
I found the answer. I looked at the Backbone core and I see that initialize method is being called first before attaching the events to the view.
View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
You are calling the click event on the element which haven't been yet created. You should call the function when the render is finished or you can just call this.sayHi() instead of triggering the click.
hjuster is correct. the $(document.body) wait for the 'document ready' event, and you are calling new _testView before the document is ready. You can change your intialize to this - it queues up the trigger to execute after the document is ready.
initialize : function() {
this.render();
var self = this;
$(function(){
self.$el.find('a').trigger('click');
});
},
I added the var 'self' since you can't reference 'this' to get your _testView object in the function.
It works in this fiddle
The 2nd answer to this question nicely explains how event declarations in Backbone.js views are scoped to the view's el element.
It seems like a reasonable use case to want to bind an event to an element outside the scope of el, e.g. a button on a different part of the page.
What is the best way of achieving this?
there is not really a reason you would want to bind to an element outside the view,
there are other methods for that.
that element is most likely in it's own view, (if not, think about giving it a view!)
since it is in it's own view, why don't you just do the binding there, and in the callback Function,
use .trigger(); to trigger an event.
subscribe to that event in your current view, and fire the right code when the event is triggered.
take a look at this example in JSFiddle, http://jsfiddle.net/xsvUJ/2/
this is the code used:
var app = {views: {}};
app.user = Backbone.Model.extend({
defaults: { name: 'Sander' },
promptName: function(){
var newname = prompt("Please may i have your name?:");
this.set({name: newname});
}
});
app.views.user = Backbone.View.extend({
el: '#user',
initialize: function(){
_.bindAll(this, "render", "myEventCatcher", "updateName");
this.model.bind("myEvent", this.myEventCatcher);
this.model.bind("change:name", this.updateName);
this.el = $(this.el);
},
render: function () {
$('h1',this.el).html('Welcome,<span class="name"> </span>');
return this;
},
updateName: function() {
var newname = this.model.get('name');
console.log(this.el, newname);
$('span.name', this.el).text(newname);
},
myEventCatcher: function(e) {
// event is caught, now do something... lets ask the user for it's name and add it in the view...
var color = this.el.hasClass('eventHappened') ? 'black' : 'red';
alert('directly subscribed to a custom event ... changing background color to ' + color);
this.el.toggleClass('eventHappened');
}
});
app.views.sidebar = Backbone.View.extend({
el: '#sidebar',
events: {
"click #fireEvent" : "myClickHandler"
},
initialize: function(){
_.bindAll(this, "myClickHandler");
},
myClickHandler: function(e) {
window.user.trigger("myEvent");
window.user.promptName();
}
});
$(function(){
window.user = new app.user({name: "sander houttekier"});
var userView = new app.views.user({model: window.user}).render();
var sidebarView = new app.views.sidebar({});
});
Update: This answer is no longer valid/right. Please see other answers below!
Why do you want to do this?
Apart from that, you could always just bind it using regular jQuery handlers. E.g.
$("#outside-element").click(this.myViewFunction);
IIRC, Backbone.js just uses the regular jQuery handlers, so you're essentially doing the same thing, but breaking the scope :)