I am trying around with Backbone.js and get along so far, but I have got a problem.
Lets say I got a root element and a child element.
When the document loads, I create 3 "root" instances. The root instance appends a tag.
Each root instance creates one child instance which creates a tag in the ul tag.
Now I would like the child instance to attach and onclick event to the tag. Unfortunately, it won't work.
I created a fiddle:
http://jsfiddle.net/Fluxo/sEjE5/17/
var child = Backbone.View.extend({
template: _.template('<li>Item '+count+'</li>'),
events: {
'click li': function() {
alert('listitem Click Child Element');
}
},
initialize: function() {
_.bindAll('render');
this.render();
}, render: function() {
this.$el.html(this.template())
}
});
var root = Backbone.View.extend({
template: _.template('<div><h3>List</h3><p /><ul></ul><hr />'),
events: {
'click li': function() {
alert('listitem Click - Root Element');
}
},
initialize: function() {
_.bindAll('render');
this.render();
},
render: function() {
this.$el.html(this.template());
$('body').append(this.el);
var item = new child();
this.$el.find('ul').append(item.$el.html());
}
});
The events created in the root element will fire, but not the ones in the child element.
Am I doing anything wrong?
You're doing two things wrong.
First of all, your child is an <li>, it doesn't contain an <li>:
template: _.template('<li>Item '+count+'</li>'),
events: {
'click li': ...
},
so your click li event won't do anything. Events are bound to the view's el using delegate:
delegateEvents delegateEvents([events])
Uses jQuery's delegate function to provide declarative callbacks for DOM events within a view. [...] Omitting the selector causes the event to be bound to the view's root element (this.el).
So if you want to bind a click handler directly to the view's el rather than one of its children, you want to leave out the selector:
events: {
'click': ...
}
The next problem is that you're not actually inserting the child element into the DOM, you're copying the HTML:
this.$el.find('ul').append(item.$el.html());
By appending item.$el.html() instead of item.el, you're grabbing the correct HTML as a string and inserting that HTML but you lose the events in the process; the events are bound to the DOM object, item.el, not to the HTML string. You can fix this by appending item.el:
this.$el.find('ul').append(item.el);
// Or you could say, the two approaches are the same
this.$('ul').append(item.el);
Demo: http://jsfiddle.net/ambiguous/K76JM/ (or http://jsfiddle.net/ambiguous/kFxHQ/)
Related
I need to attach an event to the main view element, this.$el. In this case its an 'LI'. Then I need to re render this view sometimes. The problem is if i re render it, it attaches any events in the onRender method that is attached to this.$el each time its rendered. So if i call this.render() 3 times the handler gets attached 3 times. However, if i attach the event to a childNode of this.$el, this does not happen and the events seem to be automatically undelegated and added back on each render. The problem is I NEED to use the main this.$el element in this case.
Is this a bug? Shouldn't this.$el function like the childNodes? Should I not be attaching things to this.$el?
inside the view:
onRender: function(){
this.$el.on('click', function(){
// do something
});
If you're able to use the view's event hash, you could do the following:
var Bookmark = Backbone.View.extend({
events: {
'click': function() {
console.log('bound once')
}
}
...});
If for some reason that's not an option, you could explicitly remove any existing event listeners for this event in the render method, which will prevent the listener from being attached multiple times:
var Bookmark = Backbone.View.extend({
...
render: function(x) {
this.$el.off('click.render-click');
this.$el.html(this.template());
this.$el.on('click.render-click', function () {
console.log('only ever bound once');
});
return this;
}
});
I searched info on this topic but found only info about getting event element.
Yes, I can get an element of clicked div, but why it's fired all 19 times? (it's the number of views total). Info of clicked event is same - of the clicked div.
Here is what divs look like: http://d.pr/i/AbJP
Here is console.log: http://d.pr/i/zncs
Here is the code of index.js
$(function () {
var Table = new Backbone.Collection;
var TrModel = Backbone.Model.extend({
defaults: {
id: '0',
name: 'defaultName'
},
initialize: function () {
this.view = new Tr({model: this, collection: Table});
this.on('destroy', function () {
this.view.remove();
})
}
});
var Tr = Backbone.View.extend({
el: $('.pop-tags').find('.container'),
template: _.template($('#td_template').html()),
events: {
'click .tag': 'clicked'
},
clicked: function (event) {
event.preventDefault();
event.stopPropagation();
console.log(event.currentTarget);
},
initialize: function () {
this.render();
},
render: function () {
this.$el.append(this.template(this.model.attributes));
return this;
}
});
for (var i = 0, size = 19; i < size; i++) {
var trModel = new TrModel({id: i, name: 'name_' + i});
Table.add(trModel);
}
});
How can I avoid all elements from firing an event and fire only one that clicked and only 1 time?
el: $('.pop-tags').find('.container'),
Don't do that. You are attaching every view instance to the same DOM node. Each backbone view needs a distinct DOM node or, as you see, delegate events become complete chaos. In your view, set tagName: 'tr', then when creating your views, create them, call .render() and then append them to the DOM with something like $('.table-where-views-go').append(trView.el);.
You also may want to brush up on the basic MVC concept because Tables and Rows are view-related notions, not model-related, so a class called TrModel is a code smell that you aren't clear on Model vs View.
I would use a slightly different approach to solve your problem.
Instead for one view for every tr I would create one view for the entire table.
When I create the view I would pass the collection containing the 19 models to the view and in view.initialize use the collection to render the rows.
I created a jsbin with a working example.
I have a problem when I dynamically load content with the following code.
$(document).ready(function() {
$("#tags").keyup(function(){
var q = $(this).val();
$.ajax({
url: '/AnswerMedia/utilities/autoSearch/model/suggest.php?q='+q,
success: function (data) {
$("#ajaxDiv").html(data);
},
error: function (request, status, error) {
alert(request.responseText);
}
});
});
});
After the content loads, this code was intended to trigger an event when one of the loaded div tags is clicked, but did not.
$(".pdiv").click(function(){
var val = $(this).text();
$('#tags').val(val);
$('.mncontr').hide();
});
$("#closeSearch").click(function(){
$('.mncontr').hide();
});
Then I tried the following code:
$("body").delegate(".pdiv", "click", function(){
var val = $(this).text();
$('#tags').val(val);
$('.mncontr').hide();
});
$("body").delegate("#closeSearch", "click", function(){
$('.mncontr').hide();
});
It works well in Firefox, but in Chrome the problem persists. Please help me.
From this SO post:
If you want the click handler to work for an element that gets loaded
dynamically, then you set the event handler on a parent object (that
does not get loaded dynamically) and give it a selector that matches
your dynamic object like this:
$('#parent').on("click", "#child", function() {});
The event handler
will be attached to the #parent object and anytime a click event
bubbles up to it that originated on #child, it will fire your click
handler. This is called delegated event handling (the event handling
is delegated to a parent object).
It's done this way because you can attach the event to the #parent
object even when the #child object does not exist yet, but when it
later exists and gets clicked on, the click event will bubble up to
the #parent object, it will see that it originated on #child and there
is an event handler for a click on #child and fire your event.
I have really weird problem. I am trying to implement "root" view which also works as some namespace structure. Same principle introduced in codeschool.com course part II. In this root view, I want to catch event "click button", but that's the problem. When I click on button nothing happened.
window.App = new (Backbone.View.extend({
el: $("#app"),
Collections: {},
Models: {},
Views: {},
Routers: {},
events: {
'click button' : function(e) {
alert("Thank god!");
}
},
render: function(){
//for test purposes
console.log($("#app").find("button"));
console.log(this.$el.find('button'));
},
start: function(){
this.render();
new App.Routers.PostsRouter();
Backbone.history.start({pushState: true});
}
}))();
$(document).ready(function() { App.start() });
The HTML look like this
<body>
<div id="app">
<div id="posts"></div>
<button>Click me</button>
</div>
</body>
And what's really weird is output from console.log in render function. Both selectors are same, even the context is same, so where is problem?
console.log($("#app").find("button")); //this returns correct button
console.log(this.$el.find('button')); //this returns 0 occurences (WTF?)
EDIT:
After little change at el: "#app", still same problem. Problem was (thanks to #nemesv) in instantiating this class before DOM is loaded. But however, it's not possible to instantiating after DOM is loaded, because then it's not possible to use that namespace structure (eg. App.Model.Post = Backbone.Model.extend() ). But this "main view with namespace structure" is introduced in codeschool.com course as some sort of good practice. Solution can be found there http://jsfiddle.net/BckAe
You have specified your el as a jquery selector but because you are inside an object literal it evaluates immediately so before the DOM has been loaded.
So the el: $("#app"), won't select anything.
You can solve this by using one of the backbone features that you can initilaize the el as a string containing a selector.
So change your el declaration to:
el: "#app"
Your click event is not triggered because you instantiate your view before the DOM is loaded so backbone cannot do the event delegation your you.
So you need separate your view declaration and creation into two steps. And only instantiate your view when the DOM is loaded:
var AppView = Backbone.View.extend({
el: "#app",
//...
});
$(document).ready(function()
{
window.App = new AppView();
App.start()
});
Demo: JSFiddle.
I'm fiddling with a view and related model that look like that:
App.Views.Addresses = App.Views.Addresses || {};
App.Views.Addresses.Address = Backbone.View.extend({
events: {
"click button#foo" : "clear"
},
initialize: function(model){
this.address = model.model;
this.address.view = this;
_.extend(this, Backbone.Events);
this.render();
},
render: function(){
... rendering stuff
},
clear: function(){
this.address.clear();
}
});
and
var Address = Backbone.Model.extend({
url: function() {
... url stuff
},
clear: function(){
this.destroy();
this.view.remove();
}
});
I'm facing two problems here. The first one:
I have a button with id="foo" in my source and would like the view to catch the 'click' event of this very button and fire the 'clear' event. Problem: This does not work.
Anyway calling 'clear' on my model by hand cleanly removes the data on the server but does not remove the view itself. Thats the second problem. Hopefully someone more experienced can enlighten me.
Thx in advance
Felix
First problem:
Your button must be inside the element rendered by the view.
backbone scope events to inner elements only
You must render your view within this.el element
backbone use that element for delegation
Second problem:
Use events to destroy your view
You should not store the view in the model. This is kind of a "no no" in MVC. Your model already emits a "remove" event when deleted. Your view should listen to it and behave accordingly.
You must remove your view element from the DOM yourself
This is not handled by backbone.
Other general comments:
Views already are extending Backbone.Events
Use this.model instead of this.address