I'm using mootools and working on popup menu:
document.getElement('.cart a').toggle(
function() {
this.getParent('div').removeClass('open');
this.getNext('.cart_contents').hide();
},
function() {
this.getParent('div').addClass('open');
this.getNext('.cart_contents').show();
})
);
The toggle function implementation:
Element.implement({
toggle: function(fn1,fn2){
this.store('toggled',false);
return this.addEvent('click',function(event){
event.stop();
if(this.retrieve('toggled')){
fn1.call(this);
}else{
fn2.call(this);
}
this.store('toggled',!(this.retrieve('toggled')));
});
}
});
The outer click function:
Element.Events.outerClick = {
base : 'click',
condition : function(event){
event.stopPropagation();
return false;
},
onAdd : function(fn){
this.getDocument().addEvent('click', fn);
},
onRemove : function(fn){
this.getDocument().removeEvent('click', fn);
}
};
I would like to add outerclick event to close my popup menu:
document.getElement('.cart a').toggle(
function() {
this.getParent('div').removeClass('open');
this.getNext('.cart_contents').hide();
},
function() {
this.getParent('div').addClass('open');
this.getNext('.cart_contents').show();
}).addEvent('outerClick',function() {
// Error: Wrong code below
// Uncaught TypeError: Object #<HTMLDocument> has no method 'getParent'
this.getParent('div').removeClass('open');
this.getNext('.cart_contents').hide();
});
Error: Uncaught TypeError: Object # has no method 'getParent'
Thanks.
This is a problem (or a deficiency) to do with the outerClick implementation by Darren. It's not a bug - it's built to work as fast as possible. you just need to understand what it does when it binds the actual event to document.
Element.Events.outerClick = {
base : 'click',
condition : function(event){
event.stopPropagation();
return false;
},
onAdd : function(fn){
// the event actually gets added to document!
// hence scope in fn will be document as delegator.
this.getDocument().addEvent('click', fn);
},
onRemove : function(fn){
this.getDocument().removeEvent('click', fn);
}
};
So the functions will run with context this === document.
One way to fix it is to bind the callback specifically to the element. Problem is, removing it won't work as .bind will return a unique new function that won't match the same function again.
(function(){
var Element = this.Element,
Elements = this.Elements;
[Element, Elements].invoke('implement', {
toggle: function(){
var args = Array.prototype.slice.call(arguments),
count = args.length-1,
// start at 0
index = 0;
return this.addEvent('click', function(){
var fn = args[index];
typeof fn === 'function' && fn.apply(this, arguments);
// loop args.
index = count > index ? index+1 : 0;
});
}
});
Element.Events.outerClick = {
base : 'click',
condition : function(event){
event.stopPropagation();
return false;
},
onAdd : function(fn){
this.getDocument().addEvent('click', fn.bind(this));
},
onRemove : function(fn){
// WARNING: fn.bind(this) !== fn.bind(this) so the following
// will not work. you need to keep track of bound fns or
// do it upstream before you add the event.
this.getDocument().removeEvent('click', fn.bind(this));
}
};
}());
document.id('myp').toggle(
function(e){
console.log(e); // event.
this.set('html', 'new text');
},
function(){
console.log(this); // element
this.set('html', 'old text');
},
function(){
this.set("html", "function 3!");
}
).addEvent('outerClick', function(e){
console.log(this, e);
});
http://jsfiddle.net/dimitar/UZRx5/ - this will work for now - depends if you have a destructor that removes it.
Another approach is when you add the event to do so:
var el = document.getElement('.cart a');
el.addEvent('outerClick', function(){ el.getParent(); // etc });
// or bind it
el.addEvent('outerClick', function(){ this.getParent(); }.bind(el));
it can then be removes if you save a ref
var el = document.getElement('.cart a'),
bound = function(){
this.getParent('div');
}.bind(el);
el.addEvent('outerClick', bound);
// later
el.removeEvent('outerClick', bound);
that's about it. an alternative outerClick is here: https://github.com/yearofmoo/MooTools-Event.outerClick/blob/master/Source/Event.outerClick.js - not tried it but looks like it tries to do the right thing by changing scope to element and keeping a reference of the memoized functions - though multiple events will likely cause an issue, needs event ids to identify the precise function to remove. Also, it looks quite heavy - considering the event is on document, you want to NOT do too much logic on each click that bubbles to that.
Related
I have a function, simplified like this:
var fooFunction = function($container, data) {
$container.data('foobarData', data);
$container.on('click', 'a', function(e) {
var data = $(e.delegateTarget).data('foobarData');
var $target = $(e.currentTarget);
if (typeof data.validateFunction === 'function' && !data.validateFunction(e)) {
return;
}
e.preventDefault();
e.stopPropagation();
// Do stuff
console.log(data.returnText);
});
};
fooFunction('.same-container', {
validateFunction: function(event) {
return $(e.currentTarget).closest('.type-companies').length ? true : false;
},
returnText: 'Hello Company!',
});
fooFunction('.same-container', {
validateFunction: function(event) {
return $(e.currentTarget).closest('.type-humans').length ? true : false;
},
returnText: 'Hello Human!',
})
I am using event delegation on the same container (.same-container) with a custom validateFunction() to validate if the code in // Do stuff should run.
For each fooFunction() initiation, I have some different logic that will get called on // Do stuff. The issue is that those two event delegations conflict. It seems that only one of them is called and overwrites the other one.
How can I have multiple event delegations with the option to define via a custom validateFunction which one should be called. I use preventDefault() + stopPropagation() so on click on a <a>, nothing happens as long as validateFunction() returns true.
The problem is that you're overwriting $(e.delegateTarget).data('foobarData') every time.
Instead, you could add the options to an array, which you loop over until a match is found.
var fooFunction = function($container, data) {
var oldData = $container.data('foobarData', data);
if (oldData) { // Already delegated, add the new data
oldData.push(data);
$container.data('foobarData', oldData);
} else { // First time, create initial data and delegate handler
$container.data('foobarData', [data]);
$container.on('click', 'a', function(e) {
var data = $(e.delegateTarget).data('foobarData');
var $target = $(e.currentTarget);
var index = data.find(data => typeof data.validateFunction === 'function' && !data.validateFunction(e));
if (index > -1) {
var foundData = data[index]
e.preventDefault();
e.stopPropagation();
// Do stuff
console.log(foundData.returnText);
}
});
}
}
I am creating a mini-library, sort of trying to reconstruct, at least partly, the way jQuery works for learning purposes and to understand better how object-oriented programming works.
I have recreated the jQuery methods click and addClass, but when I call them like:
$(".class1").click(function() {
$(".class1").addClass("class2"); // Works, but it adds class2 to all elements
this.addClass("class2"); // Doesn't work
});
I get an Uncaught Error saying this.addClass is not a function, which is normal, since I shouldn't be able to access another object's methods.
How is $(this) made in jQuery to mean the DOM element that triggered an event, so that in my case I can use it to add class2 only to the element clicked and not all elements that have the class class1?
P.S: I tried reading the jQuery file, but I feel like these waters are currently too deep for me.
Edit:
I always appreciate all the answers and the help I get on Stack Overflow, but telling me to use $(this) instead of this doesn't solve my issue, because $(this) doesn't exist in my code. I'm trying to learn how to create something like jQuery's $(this) & what's the logic behind it.
The click method is defined as follows:
$.prototype.click = function(callback) {
for (var i = 0; i < this.length; i++) {
this[i].onclick = function(event) {
callback.call(this, event);
}
}
};
With an extra 1.5 years of experience, this question becomes rather easy.
Alter $, so that, except string selectors, it can accept HTML elements.
Create a new instance of the object containing the HTML element given.
Call addClass with that as the context.
Code:
;(function() {
/* The object constructor. */
function ElementList(arg) {
/* Cache the context. */
var that = this;
/* Save the length of the object. */
this.length = 0;
/* Check whether the argument is a string. */
if (typeof arg == "string") {
/* Fetch the elements matching the selector and inject them in 'this'. */
[].forEach.call(document.querySelectorAll(arg), function(element, index) {
that[index] = element;
that.length++;
});
}
/* Check whether the argument is an HTML element and inject it into 'this'. */
else if (arg instanceof Element) {
this[0] = arg;
this.length = 1;
}
}
/* The 'click' method of the prototype. */
ElementList.prototype.click = function(callback) {
/* Iterate over every element and set the 'click' event. */
[].forEach.call(this, function(element) {
element.addEventListener("click", function(event) {
callback.call(this, event);
});
});
}
/* The 'addClass' method of the prototype. */
ElementList.prototype.addClass = function(className) {
/* Iterate over every element. */
[].forEach.call(this, function(element) {
/* Cache the classList of the element. */
var list = element.classList;
/* Add the specified className, if it doesn't already exist. */
if (!list.contains(className)) list.add(className);
});
}
/* The global callable. */
window.$ = function(arg) {
return new ElementList(arg);
}
})();
/* Example */
$("#b1").click(function() {
$(this).addClass("clicked");
console.log(this);
});
<button id="b1">Click</button>
You need to use call, apply, bind or some combination of those to set the callback's context to the DOM Node. Here is a contrived example of jquery's each method that sets the context of the callback using call:
var $ = {
each: function(selector, callback) {
var collection = Array.from(document.querySelectorAll(selector));
collection.forEach(function(element, index) {
// the magic...
callback.call(element, index, element);
});
}
}
$.each('.foo', function(idx, el) {
console.log(this.textContent);
});
this is the native JavaScript element and only exposes the native API. You need to pass it to the jQuery constructor in order to access jQuery's API
$(this).addClass("class2"); // This will work
One possible way (only selectors are accepted):
$ = function(selector) {
this.elements = '';//Select the element(s) based on your selector
this.addClass = function(klass) {
//apply your klass to you element(s)
return this;
};
this.click= function(handler) {
//Attach click event to your element(s)
return this;
};
return this;
};
Please keep in mind it's just an example.
Edit 1:
In your click method you are calling the handler in the wrong scope (the anonymous function scope). You need to use the outer scope:
$.prototype = {
click: function(callback) {
console.log(this.length);
var _self = this;
for (var i = 0; i < this.length; i++) {
this[i].onclick = function(event) {
//this here presents the anonymous function scope
//You need to call the handler in the outer scope
callback.call(_self, event);
//If you want to call the handler in the Element scope:
//callback.call(_self[i], event);
}
}
}
}
Note: In your example, this.addClass("class2"); doesn't work because jQuery calls the click handler in the Element scope not jQuery scope. Therefore, this presents the Element which dosen't have the addClass method;
Ok, I understand now your question. Let me try to help you again.
jQuery doesn't knows what DOM element do you use when you give it to selector. It doesn't parsing it or something else. Just save it to the internal property.
Very simplified code to understand:
$ = function(e) {
// finding object. For example "this" is object
if (typeof e !== 'object' || typeof e.className === 'undefined') {
if (typeof e == 'string') {
if (e[0] == '#') {
e = document.getElementById(e.substring(1));
} else if (e[0] == '.') {
e = document.getElementsByClassName(e.substring(1))[0];
} else {
// ... etc
}
}
// ... etc
}
var manager = {
elem: e,
addClass: function(newClass) {
manager.elem.className = manager.elem.className + ' ' + newClass;
return manager;
},
click: function(callback) {
// here is just simple way without queues
manager.elem.onclick = function(event) {
callback.call(manager, event);
}
}
}
return manager;
}
I've got a variable timekeep.
var timeKeep;
and I define it thusly:
timeKeep = Class.create({
initialize: function() {
this.initObservers();
},
initObservers: function() {
$$('input').each( function(el) {
el.observe('keypress', function(ev) {
// the key code for 'enter/return' is 13
if(ev.keyCode === 13){
timeKeep.submit();
// Uncaught TypeError: Object function klass() {
// this.initialize.apply(this, arguments);
// } has no method 'submit'
}
});
});
},
submit: function() {
alert('Submitted!');
}
})
The error I am getting is commented out below the line that it occurs. It's got something to do with calling a timeKeep method within a different scope I think?
Is there a problem calling timeKeep.method() inside a foreach statement?
Problem is with you OOP style. Use a closure so you call the current instance of your class.
initObservers: function () {
var that = this;
$$('input')
.each(function (el) {
el.observe('keypress', function (ev) {
// the key code for 'enter/return' is 13
if (ev.keyCode === 13) {
that.submit();
}
});
});
},
You could also look at bind
initObservers: function() {
var submit = this.submit.bind(this);
$$('input')
.each(function (el) {
el.observe('keypress', function (ev) {
// the key code for 'enter/return' is 13
if (ev.keyCode === 13) {
submit();
}
});
});
},
You are assuming that Class.create returns an instance of an object of the type you are defining, but no, it returns a constructor function for creating instances of the class you are defining.
You can add the new keyword to the assignment and then you will have in timeKeep what you want to:
timeKeep = new Class.create({
...
})()
As suggested - using Function#bind() will solve your problem but there is a cleaner way so that you can continue to use this inside the class scope.
Also look into the invoke() method as this is a perfect opportunity to use it. $$() returns a list of elements and if you want to perform the same function on all of the elements invoke() will handle iterating over the list.
timeKeep = Class.create({
initialize: function() {
this.initObservers();
},
initObservers: function() {
$$('input').invoke('observe','keypress',function(ev) {
// the key code for 'enter/return' is 13
if(ev.keyCode === 13){
timeKeep.submit();
// Uncaught TypeError: Object function klass() {
// this.initialize.apply(this, arguments);
// } has no method 'submit'
}
//Add the bind method to the closure to bind 'this' inside that function scope
//to 'this' of the class
//the binding will allow you to call this.submit() instead of timeKeep.submit
//as well as access any of the class properties and methods inside this closure
}.bind(this));
} ,
submit: function() {
alert('Submitted!');
}
});
PrototypeJS documentation on observers http://api.prototypejs.org/dom/Event/observe/ - look at the heading Using an Instance Method as a Handler
I have this code to set "State Machine" in view of a javascript application:
var Events = {
bind: function(){
if ( !this.o ) this.o = $({});
this.o.bind(arguments[0], arguments[1])
},
trigger: function(){
if ( !this.o ) this.o = $({});
this.o.trigger(arguments[0], arguments[1])
}
};
var StateMachine = function(){};
StateMachine.fn = StateMachine.prototype;
$.extend(StateMachine.fn, Events);
StateMachine.fn.add = function(controller){
this.bind("change", function(e, current){
console.log(current);
if (controller == current)
controller.activate();
else
controller.deactivate();
});
controller.active = $.proxy(function(){
this.trigger("change", controller);
}, this);
};
var con1 = {
activate: function(){
console.log("controller 1 activated");
},
deactivate: function(){
console.log("controller 1 deactivated");
}
};
var sm = new StateMachine;
sm.add(con1);
con1.active();
What I don't understand at this point is where the current parameter in bind function comes from (That is: this.bind("change", function(e, current){...}). I try to log it on firebug console panel and it seems to be the controller parameter in StateMachine.fn.add function. Could you tell me where this parameter comes from?
Thank you.
As far as I understand, you specified the second argument to be passed to you event callback here:
this.trigger("change", controller);
jQuery's trigger method will call all binded functions, passing the Event object as the first argument (always), and then, after it, all the arguments you passed to .trigger() method after the name of event.
I am writing a jQuery event plugin and I need to pass some data to a argument. So if I use it like this:
$(element).on('myevent', function(event, myargument) { console.log(myargument); });
I wan't to get the myargument object and it's properties (e.g. name), which is set in the handler.
So how would this work with the code below?
SmartScroll.myevent = {
setup: function() {
var myargumentData,
handler = function(evt) {
var _self = this,
_args = arguments;
// The data I wan't to get
myargumentData = {
name: "Hello"
};
evt.type = 'myevent';
jQuery.event.handle.apply(_self, _args);
};
jQuery(this).bind('scroll', handler).data(myargumentData, handler);
},
teardown: function() {
jQuery(this).unbind('scroll', jQuery(this).data(myargumentData));
}
};
You can modify an object passed to add.
$.event.special.foo = { add: function(h) {
var hh = h.handler;
h.handler = function(e, a) {
hh.call(this, e, a * 2);
};
} };
Now, let's bind the event:
$("body").on("foo", function(e, a) {
console.log(a);
});
Fire the event and see what happens:
$("body").trigger("foo", [ 10 ]); // 20
on: function( types, selector, data, fn, /*INTERNAL*/ one )
According to the source.
so if i understand your problem , just do :
$(element).on('myevent',{my_data:"foo"},function(event) {
console.log(event.data.my_data);
});
prints "foo"