Working through the setElement part of Addy Osmani's backbone.js tutorial.
He presents this example:
// We create two DOM elements representing buttons
// which could easily be containers or something else
var button1 = $('<button></button>');
var button2 = $('<button></button>');
// Define a new view
var View = Backbone.View.extend({
events: {
click: function(e) {
console.log(view.el === e.target);
}
}
});
// Create a new instance of the view, applying it
// to button1
var view = new View({el: button1});
// Apply the view to button2 using setElement
view.setElement(button2);
button1.trigger('click');
button2.trigger('click');
However, he doesn't explain why there is different output for button1.trigger('click'); vs. button2.trigger('click'); -- possibly a dumb question, and I know that these are different ways of attaching the view to the button elements, but why does button2.trigger('click'); also return true?
button1.trigger('click'); shouldn't produce any output at all from that code.
setElement is fairly simple:
setElement view.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use setElement, which will also create the cached $el reference and move the view's delegated events from the old element to the new one.
So view.setElement(e) does four things:
Unbinds the view's events from view.el.
Sets view.el = e.
Sets view.$el = Backbone.$(e).
Binds the view's events to the new view.el.
The result is that nothing will be left listening to events from button1 and view will be listening to events from button2.
A more thorough example might help so let us attach some more click event handlers:
var button1 = $('<button id="button1"></button>').click(function() {
console.log('button1 jQuery', this);
});
var button2 = $('<button id="button2"></button>').click(function() {
console.log('button2 jQuery', this);
});
var View = Backbone.View.extend({
events: {
click: function(e) {
console.log('Backbone ', view.el, view.el === e.target);
}
}
});
var view = new View({el: button1});
view.setElement(button2);
button1.trigger('click');
button2.trigger('click');
Demo: http://jsfiddle.net/ambiguous/S7A9z/
That should give you something like this in the console:
button1 jQuery <button id="button1"></button>
button2 jQuery <button id="button2"></button>
Backbone <button id="button2"></button> true
Both raw jQuery event handlers will be triggered as expected but we'll only get the button2 event through Backbone because the setElement call happened before the trigger calls.
So why is view.el === e.target true? Well, you're clicking on button2 so e.target will be button2 and the view.setElement(button2) call replaces view.el so this.el inside the Backbone click handler will also be button2.
Related
I have a button appended to a parent button:
var parent_button = document.createElement("button");
var child_button = document.createElement("button");
parent_button.appendChild(child_button);
I want to create functionality for the child_button that's independent from that of the parent_button:
parent_button.onclick = function () {
//do stuff
};
child_button.onclick = function () {
//do some other stuff
};
But given this code, whenever I click on child_button, I am necessarily triggering parent_button.onclick(). How do I separate the two?
The overlapping buttons look like this:
Use stopPropagation() on the event object of the child button. It will prevent the event from propagating from the child to the parent.
You can see an example here:
var pb = document.getElementById("pb");
var cb = document.getElementById("cb");
pb.addEventListener("click", function(e) {
console.log("Parent");
});
cb.addEventListener("click", function(e) {
console.log("Child");
e.stopPropagation();
})
<button id="pb">Parent Button <button id="cb">Child Button</button></button>
Note: I think that the default behavior is that when you click on a child element, the event of both the child and the parent should trigger, but in this example, this doesn't happen; maybe it's something particular to buttons being parents.
Edit: I think I got the solution! I want to try and fix this myself before I ask for further help = )
First script inhibits the second one from functioning as the click event from the first one overides the second one. Because the second one does not function it is impossible to open the drop down menu to select a list item to trigger the first scripts click.
What I tried was replacing all return false statements with event.stopPropagation(). Didnt work however. Tried re-ordering my scripts but that failed as well. I was thinking of making my second script target another parent div but that didnt work either.I also tried event.stopImmediatePropagation() and .bind methods.
Any idea?
First script that makes the drop down function. Contains click event.
function DropDown(el) {
this.f = el;
this.placeholder = this.f.children('span');
this.opts = this.f.find('ul.dropdown > li');
this.val = '';
this.index = -1;
this.initEvents();
}
DropDown.prototype = {
initEvents : function() {
var obj = this;
obj.f.on('click', function(event){
$(this).toggleClass('active');
return false;
});
obj.opts.on('click',function(){
var opt = $(this);
obj.val = opt.text();
obj.index = opt.index();
obj.placeholder.text(obj.val);
});
},
getValue : function() {
return this.val;
},
getIndex : function() {
return this.index;
}
}
$(function() {
var f = new DropDown( $('#f') );
$(document).click(function() {
// all dropdowns
$('.filter-buttons').removeClass('active');
});
});
Second script that does the filtering, also contains click event:
jQuery(document).ready(function(e) {
var t = $(".filter-container");
t.imagesLoaded(function() {
t.isotope({
itemSelector: "figure",
filter: "*",
resizable: false,
animationEngine: "jquery"
})
});
$(".filter-buttons a").click(function(evt) {
var n = $(this).parents(".filter-buttons");
n.find(".selected").removeClass("selected");
$(this).addClass("selected");
var r = $(this).attr("data-filter");
t.isotope({
filter: r
});
evt.preventDefault();
});
$(window).resize(function() {
var n = $(window).width();
t.isotope("reLayout")
}).trigger("resize")
});
html structure
<div id="f" class="filter-buttons" tabindex="1">
<span>Choose Genre</span>
<ul class="dropdown">
<li>All</li>
<li>Electronic</li>
<li>Popular</a></li>
</ul>
</div>
This doesn't really solve your problem but I was bored while drinking my coffee and felt like helping you write your dropdown plugin a little nicer
My comments below are inline with code. For uninterrupted code, see DropDown complete paste.
We start with your standard jQuery wrapper (function($){ ... })(jQuery)
(function($) {
// dropdown constructor
function DropDown($elem) {
First we'll make some private vars to store information. By using this.foo = ... we expose things (probably) unnecessarily. If you need access to these vars, you can always create functions to read them. This is much better encapsulation imo.
// private vars
var $placeholder = $elem.children("span");
var $opts = $elem.find("ul.dropdown > li")
var value = "";
var index = -1;
Now we'll define our event listeners and functions those event listeners might depend on. What's nice here is that these functions don't have to access everything via this.* or as you were writing obj.f.* etc.
// private functions
function onParentClick(event) {
$elem.toggleClass("active");
event.preventDefault();
}
function onChildClick(event) {
setValue($(this));
event.preventDefault();
}
function setValue($opt) {
value = $opt.text();
index = $opt.index();
$placeholder.text(value);
}
Here's some property descriptors to read the index and value
// properties for reading .index and .value
Object.defineProperty(this, "value", {
get: function() { return value; }
});
Object.defineProperty(this, "index", {
get: function() { return index; }
});
Lastly, let's track each instance of DropDown in an array so that the user doesn't have to define a special listener to deactivate each
// track each instance of
DropDown._instances.push(this);
}
This is the array we'll use to track instances
// store all instances in array
DropDown._instances = [];
This event listener deactivate each "registered" instance of DropDown
// deactivate all
DropDown.deactiveAll = function deactiveAll(event) {
$.each(DropDown._instances, function(idx, $elem) {
$elem.removeClass("active");
});
}
Here's the document listener defined right in the plugin! The user no longer has to set this up
// listener to deactiveAll dropdowns
$(document).click(DropDown.deactiveAll);
Might as well make it a jQuery plugin since everything in our DropDown constructor relies upon jQuery. This let's the user do var x = $("foo").dropdown();
// jQuery plugin
$.fn.dropdown = function dropdown() {
return new DropDown($(this));
};
Close the wrapper
})(jQuery);
Now here's how you use it
$(function() {
var x = $('#f').dropdown();
// get the value
f.value;
// get the index
f.index;
});
Anyway, yeah I know this doesn't really help you with your click listeners, but I hope this is still useful information to you. Off to the Post Office now!
I think you're going to need to simplify this to figure out what's going on. There's actually not enough information to see what elements the events are being attached to here.
For argument's sake, open the console and try the following:
$(document).on('click', function() { console.log('first'); return false; });
$(document).on('click', function() { console.log('second'); return false; });
Then click in the page. You'll see that both events are triggered. It might well be that your code is actually attaching the events to different elements (you don't say anywhere). If that's the case then you need to understand how event bubbling works in the DOM.
When you trigger an event, say a click on an element, that event will fire on that element, and then on it's parent, then grandparent etc all the way to the root node at the top.
You can change this behaviour by calling functions in the event itself. evt.stopPropagation tells the event to not bubble up to the ancestor nodes. evt.preventDefault tells the browser not to carry out the default behaviour for a node (eg, moving to the page specified in the href for an A tag).
In jQuery, return false from an event handler is a shortcut for, evt.preventDefault and evt.stopPropagation. So that will stop the event dead in its tracks.
I imagine you have something like:
<div event_two_on_here>
<a event_one_on_here>
</div>
If the thing that handles event_one_on_here calls stopPropagation then event_two_on_here will never even know it has happened. Calling stopPropagation explicitly, or implicitly (return false) will kill the event before it travels to the parent node/event handler.
UPDATE: In your case the issue is that the handler on .filter-buttons a is stopping the propagation (so #f doesn't get to run its handler).
$(".filter-buttons a").click(function(evt) {
// your code here...
// Don't do this - it stops the event from bubbling up to the #f div
// return false;
// instead, you'll probably just want to prevent the browser default
// behaviour so it doesn't jump to the top of the page ('url/#')
evt.preventDefault();
});
Stack.
I'm using Backbone's event map in my View.
JS:
events: {
"click .edit-info-button": "pullEdits"
},
pullEdits: function(e){
// Get the value of the button clicked
e.preventDefault();
$(".edit-info-button").click(function(){parseEdits(this.value);});
}
HTML:
<button class="button edit-info-button" value="edit address">EDIT</button>
When edit-info-button is a class, the event listener does not work. pullEdits() never fires.
When I change edit-info-button into an id ("click #edit-info-button", "button id='edit-info-button', etc.) pullEdit() and all functions after it run successfully.
The issue is, the page I'm working on needs multiple edit buttons and I'd like to give them the same class and pull the value instead of giving them all unique ids.
Am I missing something? Thanks.
Try this instead.
...
events: {
"click .edit-info-button": "pullEdits"
},
pullEdits: function(e){
var val = $(e.target).attr('value')
return parseEdits( val );
}
...
How would you "save" a page somewhere (on the client), then restore it, with all its event listeners intact?
I have a sample at http://jsfiddle.net/KellyCline/Fhd55/ that demonstrates how I create a "page", save it and go to a "next" page on a button click, and then restore the first page on another button click, but now the first page's click listeners are gone.
I understand that this is due to the serialization that html() performs, so, obviously, that's what I am doing wrong, but what I'd like is a clue to doing it right.
This is the code:
var history = [];
$(document).ready(function () {
var page1 = $(document.createElement('div'))
.attr({
'id': 'Page 1'
}).append("Page ONE text");
var nextButton = $(document.createElement('button'));
nextButton.append('NEXT');
nextButton.on('click', function () {
CreateNextPage();
});
page1.append(nextButton);
$("#content").append(page1);
});
function CreateNextPage() {
history.push( $("#content").html( ) );
$("#content").html( 'Click Me!');
var page1 = $(document.createElement('div'))
.attr({
'id': 'Page 2'
}).append("Page TWO text");
var nextButton = $(document.createElement('button'));
nextButton.append('NEXT');
nextButton.on('click', function () {
CreateNextPage();
});
var prevButton = $(document.createElement('button'));
prevButton.append('PREVIOUS');
prevButton.on('click', function () {
GoBack();
});
page1.append(nextButton);
page1.append(prevButton);
$("#content").append(page1);
}
function GoBack() {
$("#content").html( history[history.length - 1]);
history.pop( );
}
and this is the html:
Click Me!
I think this is best solved via event delegation. Basically, you encapsulate content that you know you are going to refresh within a wrapper element and bind your listeners to that. The jQuery .on() method allows you to pass a selector string as a filter and will only trigger the handler if the originating element matches. Give your buttons a class or something to get a handle on them and then bind them up above. This article has some examples.
$( '#content' ).on( 'click', 'button.next', createNextPage )
for instance.
Here's the simplified code:
<html>
<head>
<script type="text/javascript">
var ParentDiv = new Class ({
initialize : function(htmlElement) {
console.log('debug: parent object created');
$$('.childDiv').each(function(childDiv){
childDiv = new ChildDiv(childDiv);
})
htmlElement.addEvents({
'click': function (){
console.log('debug: clicked parent');
},
'testEvent' : function(){
console.log('debug: complex logic altering inner HTML of the element');
}
})
}
});
function initParent () {
$$('.parentDiv').each(function(parentDiv){parentDiv = new ParentDiv(parentDiv);})
}
var ChildDiv = new Class ({
initialize : function(htmlElement) {
console.log('debug: child object created');
htmlElement.addEvent('click', function (){
console.log('debug: clicked child');
this.addEvent('testEvent', function(){console.log('debug: grabbed an event in child element, fired by child element')})
this.fireEvent('testEvent');
})
}
});
document.addEvent('domready', function(){
initParent();
});
</script>
</head>
<body>
<div class="parentDiv">
<div>
<div>
<div>
<div class="childDiv">Clicky Thingy</div>
</div>
</div>
</div>
</div>
</body>
</html>
The outcome is the following: neither of the events are grabbed by parent object's event listeners, while capturing the events is something I'm trying to achieve. A possible occurance of such structure: we have a bunch of control elements on the page, they all fire their own events, while the document or any other kind of container manipulates the content within based on the captured event's type.
So, is there a way to make 'debug: complex logic altering inner HTML of the element' appear in the console box with the use of custom events?
There is a lot wrong with this. To make you life easier, dont use so many nested anonymous functions, they work, but make it hard to untangle sometimes. I cleaned it up and made you a fiddle so you can see how this may work.
http://jsfiddle.net/WQCDd/
You need to learn about event binding to do what you want: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
Also, you were not implementing the "events" on the class so you cant do this.fireevent
A good start to almost any mootools class will look like this:
var ClassName = new Class({
Implements: [Options, Events],
options: {
},
initialize: function(options){
this.setOptions(options);
}
});