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.
Related
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 have large collection of json objects which I retrieve through a search function, though depends on the search string, the output can go up to more than thousand of arrays which I populate into a list. Within mobile environment this become a hassle and memory consuming once I add touchmove, touchstart and touchend to each object. I found solution to this that there's a minimal way of showing object using backbone.js and with trigger such as button this could become robust. though I don't know how to go foward with it. This is working example without the button. And how I shoud do this?
<script>
//model - define value objects.
var Client = Backbone.Model.extend({
defaults: {
name: 'cole',
age: '12'
}
});
//collection - load json
var ClientCollection = Backbone.Collection.extend({
defaults: {
model: Client
},
model: Client,
url: './json/test.json',
//override parse due to json format. point to "items"
parse: function (response, xhr) {
return response.items;
}
});
//view. init collection. listen for data to be loaded. render.
var ClientView = Backbone.View.extend({
initialize: function () {
this.collection = new ClientCollection();
this.collection.bind("reset", this.render, this);
this.collection.fetch();
},
render: function () {
//append to html here ...
//alert(this.collection.at(0).get("name"));
//alert(this.collection.length)
for (var i = 0; i < this.collection.length; i++) {
$('#append-el').append('<li>' + this.collection.at([i]).get("name") + '; ' + this.collection.at([i]).get("age") + '</li>')
}
}
});
var clientView = new ClientView();
</script>
<div id = "append-el"></div>
Add an event listener to your view pointing to your button with the events hash, something like this
,events {
"click #buttonID" : "fillCollection"// <- this is a method name
}
and then create this method and trigger a collection.fetch, like this
,fillCollection: function(){
this.collection.fetch();
}
If i understood you well then this should work:
var ClientView = Backbone.View.extend({
el: '#append-el',
events: {
'click button': 'onButtonClick'
},
initialize: function() {
_.bindAll(this);
this.collection = new ClientCollection();
this.collection.bind("reset", this.renderClients);
this.render();
},
render: function() {
//append to html here ...
this.$el.append('<button type="button">Fetch clients</button><ul class="clients"></ul>');
},
renderClients: function() {
var $ul = this.$('ul.clients').empty();
this.collection.each(function(client) {
$ul.append('<li>' + client.get("name") + '; ' + client.get("age") + '</li>');
});
},
onButtonClick: function(e) {
this.collection.fetch();
}
});
I would suggest not to fetch thousands of items at once. Limit it to 100 max 200 hundred. Then I would start listening to scrolling on the list a fetch the rest of the items on as needed basis (you can automatically load them once the user approaches the end of the scrolled area or just place a "Load more" button at the bottom).
There are several paginator plugins for Backbone or you can simply limit the number of rendered element within the render() function.
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 :)
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.
My events aren't working as I'd hoped, and I think I know why. When the perpage span is clicked, everything renders correctly. But I realized - maybe the events aren't reattached to the new markup? Could that be why it only works once? (If I click the span with the number 10 in it, 10 items appear like it should be. But afterwards, anything I click doesn't change anything)
What's a better way to organize this? Should the template not include the pagination portion? How do I attach backbone events to markup after it has rendered again?
var ListView = Backbone.View.extend({
initialize: function() {
var self = this;
this.collection.bind("refresh", function(){self.render();});
this.render();
},
events: {
'click ul#perpage span': 'setperpage'
},
setperpage: function(event) {
this.collection.perpageurl = '/perpage/' + $(event.target).text();
this.collection.fetch();
this.collection.refresh();
},
render: function() {
template = _.template('\
<table>\
<% _(collection).each(function(model){%>\
<tr><td><%=model.id%></td><td><%=model.name%></td><td><%=model.email%></td></tr>\
<%}); %>\
</table>\
<ul id="perpage">\
<li><span>5</span></li>\
<li><span>10</span></li>\
</ul>\
');
var context = {collection: this.collection.toJSON()};
$(this.el).html(template(context));
$('#app').html(this.el);
return this;
}
});
try:
render: function()
{
// …
this.delegateEvents();
return this;
}
For debugging events in JavaScript use Visual Event. It will tell you which elements have events attached to them.