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;
}
Related
I am writing a micro-library instead of using jQuery. I need only 3-4 methods ( for DOM traversal, Adding Eventlisteners etc). So I decided to write them myself instead of bloating the site with jQuery.
Here is the snippet from the code:
lib.js
window.twentyFourJS = (function() {
let elements;
const Constructor = function(selector) {
elements = document.querySelectorAll(selector);
this.elements = elements;
};
Constructor.prototype.addClass = function(className) {
elements.forEach( item => item.classList.add(className));
return this;
};
Constructor.prototype.on = function(event, callback, useCapture = false){
elements.forEach((element) => {
element.addEventListener(event, callback, useCapture);
});
return this;
}
const initFunction = function(selector){
return new Constructor(selector);
}
return initFunction;
})(twentyFourJS);
script.js
(function($){
$('.tab-menu li a').on('click', function(event) {
console.log('I am clicked'); // This works
this.addClass('MyClass'); // This does NOT work (as expected)
// I want to be able to do this
$(this).addClass('MyClass');
event.preventDefault();
});
})(twentyFourJS);
Basically I want to be able to use $(this) like we use it in jQuery.
this.addClass('MyClass') and $(this).addClass('MyClass') won't work and this is the expected behaviour.
As per my understanding this is referring to the plain HTML element. So it does not have access to any Constructor methods. it won't work.
And I have not written any code that will wrap element in Constructor object in $(this). I will have to do some changes to my Constructor so that I can access the Constructor functions using $(this). What are those changes/addition to it?
Kindly recommend only Vanilla JS ways instead of libraries.
in your constructor you need to see what you have and handle it in different ways.
const Constructor = function(selector) {
if (typeof selector === 'string') {
elements = document.querySelectorAll(selector);
} else {
// need some sort of check to see if collection or single element
// This could be improved since it could fail when someone would add a length property/attribute
elements = selector.length ? selector : [selector];
}
this.elements = elements;
};
All you really need to do is make sure your Constructor argument can distinguish between a string selector being passed in, and an object.
const Constructor = function(selector) {
if(typeof selector == "string"){
elements = document.querySelectorAll(selector);
this.elements = elements;
}
else{
this.elements = selector;
}
};
You can go further than this, but at a very minimum for the example given that works.
Live example below:
window.twentyFourJS = (function() {
let elements;
const Constructor = function(selector) {
if(typeof selector == "string"){
elements = document.querySelectorAll(selector);
this.elements = elements;
}
else{
this.elements = selector;
}
};
Constructor.prototype.addClass = function(className) {
elements.forEach( item => item.classList.add(className));
return this;
};
Constructor.prototype.on = function(event, callback, useCapture = false){
elements.forEach((element) => {
element.addEventListener(event, callback, useCapture);
});
return this;
}
const initFunction = function(selector){
return new Constructor(selector);
}
return initFunction;
})();
(function($){
$('.btn').on('click', function(event) {
console.log('I am clicked'); // This works
// I want to be able to do this
$(this).addClass('MyClass');
event.preventDefault();
});
})(twentyFourJS);
.MyClass{
background-color:red
}
<button class="btn">Click me</btn>
first You Need to Check for a string
case 1. $("div")
Then You need to Check for it's NodeType and for a window
case 1. var elm = document .getElementById("ID")
$(elm)
case 2. $(this) -- window
function $(selector){
var element;
if (typeof selector === 'string') {
element = document.querySelectorAll(selector)
}
if (element.nodeType || element=== window) element= [selector];
return element ;
}
I'm trying to replicate jQuery's element manipulation to a certain extent. Now what I have found to be very useful is the .first() selector. I would like to be able to chain functions like this;
getElement(selector).first().hasClass(className);
Now, there are 2 issues with how far I've gotten (Do note that my code example is minimised, so please, no comments about error-handling.)
var getElement = function(selector, parent) {
ret = document.querySelectorAll(selector);
this.element = ret;
this.hasClass = function(className) {
className.replace('.', '');
if(this.multiple())
{
console.log('Cannot use hasClass function on multiple elements');
return false;
}
};
this.first = function() {
this.element = this.element[0];
return this;
};
return this;
};
My current problem
If I call my function;
var $test = getElement('.something');
//result: nodelist with .something divs
If I call for the first element within the result;
$test.first();
//Result: First div, perfect!
However, now if I call $test again, it will replace the elements property with the result of first(), meaning I have "lost" my old values. I don't want to lose them, I only want the first() functions for that specific functionality. Then I want $test to return all elements again.
Also, recalling first() will now end up undefined, since there is only 1 element left within this as it has deleted the old elements from within the object.
Another attempt
Now, I've also tried to turn it around a bit by returning the first-child instead of the entire class object;
this.first = function() {
return this.element[0];
};
However, I will
$test.first().hasClass(className);
//Returns ERROR, method hasClass undefined
this is because .hasClass exists on the original this, which doesn't get returned anymore since I'm now returning the element.
I have tried to get something out of jQuery's library, though that just confused me more...
I have googled this subject, but with all the 'chaining methods' solutions I'm finding, all of them seem to be overwriting the original values of the object, which is not what I want to happen. One other solution actually required me to re-initiate the object over and over again, which did not seem very efficient to me... Any help is appreciated. I'm assuming I'm going about this completely the wrong way.
-- If you can help me, please do explain why your solution works. I really feel like if I understand this, my understanding of javascript can expand a lot further. I just need to get past this structural(?) issue.
A method like first() should not modify this, it should create a new object and return that. You only use return this; in methods that modify an element rather than returning information derived from the element.
this.first = function() {
return new getElement(this.element[0]);
};
And note that you have to use new getElement to create an object, not just getElement.
This also requires a change to the constructor, so it can accept either a selector string or an element:
var getElement = function(selector, parent) {
var ret = typeof selector == "string" ? document.querySelectorAll(selector) : [selector];
...
}
You should also consider doing this in proper OO fashion, by putting the methods in a prototype, rather than defining them in every object.
var getElement = function(selector, parent) {
var ret = typeof selector == "string" ? document.querySelectorAll(selector) : [selector];
this.element = ret;
};
getElement.prototype.hasClass = function(className) {
className.replace('.', '');
if (this.multiple()) {
console.log('Cannot use hasClass function on multiple elements');
return false;
}
};
getElement.prototype.first = function() {
return new getElement(this.element[0])
};
this in your outer function refers to the window / global object.
Instead, return the ret variable itself.
In the inner functions (which become the object's methods), this acts the way you expect it to.
Here's an alternative solution, which allows chaining, even after you've called the first method:
var getElement = function(selector, parent) {
var ret = typeof selector == 'string' ? document.querySelectorAll(selector)
: selector;
ret.hasClass = function(className) {
if(!this.classList) {
console.log('Cannot use hasClass function on multiple elements');
return false;
} else {
return this.classList.contains(className);
}
};
ret.first = function() {
return new getElement(this[0]);
};
return ret;
};
console.log(getElement('p').length); //2
console.log(getElement('p').first().innerHTML); //abc
console.log(getElement('p').first().hasClass('test')); //true
console.log(getElement('p').first().hasClass('nope')); //fase
console.log(getElement('p').hasClass('test')); //false (multiple elements)
<p class="test">
abc
</p>
<p>
def
</p>
Here is how I would approach this:
Create a constructor, say Search, tasked to find the elements based on the input. Using a constructor is proper OO Programming and you also have the advantage of defining methods once in the prototype and they can be accessed by all instances.
Ensure that the context (this) is an array-like object, with numeric properties and a length, so that you can easily iterate over every matched element in the traditional way (using for loops, [].forEach etc).
Create a function, say getElement, that will use the constructor and return the result without having to use the new keyword all the time. Since the function returns an instance of our constructor, you can chain the methods you want as you would normally do.
The method first uses the constructor to create a new instance instead of modifying the original, since its role is to return the first element, not delete everything but the first element.
Each time you come up with a new method you want your object to have, you can simply add it to the prototype of the constructor.
Snippet:
;(function () {
function Search (value) {
var elements = [];
/* Check whether the value is a string or an HTML element. */
if (typeof value == "string") {
/* Save the selector to the context and use it to get the elements. */
this.selector = value;
elements = document.querySelectorAll(value);
}
else if (value instanceof Element) elements.push(value);
/* Give a length to the context. */
this.length = elements.length;
/* Iterate over every element and inject it to the context. */
for (var i = 0, l = this.length; i < l; i++) this[i] = elements[i];
}
/* The method that returns the first element in a Search instance. */
Object.defineProperty(Search.prototype, "first", {
value: function () {
return new Search(this[0]);
}
});
/* The global function that uses the Search constructor to fetch the elements. */
window.getElement = (value) => new Search(value);
/* Create a reference to the prototype of the constructor for easy access. */
window.getElement.fn = Search.prototype;
})();
/* Get all elements matching the class, the first one, and the first's plain form. */
console.log(getElement(".cls1"));
console.log(getElement(".cls1").first());
console.log(getElement(".cls1").first()[0]);
/* ----- CSS ----- */
.as-console-wrapper {
max-height: 100%!important;
}
<!----- HTML ----->
<div id = "a1" class = "cls1"></div>
<div id = "a2" class = "cls1"></div>
<div id = "a3" class = "cls1"></div>
Example:
In this example, I'm adding a new method called hasClass to the prototype of the constructor.
/* The method that returns whether the first element has a given class. */
Object.defineProperty(getElement.fn, "hasClass", {
value: function (value) {
return this[0].classList.contains(value);
}
});
/* Check whether the first element has the 'cls2' class. */
console.log(getElement(".cls1").first().hasClass("cls2"));
<!----- HTML ----->
<script src="//pastebin.com/raw/e0TM5aYC"></script>
<div id = "a1" class = "cls1 cls2"></div>
<div id = "a2" class = "cls1"></div>
<div id = "a3" class = "cls1"></div>
I think the easiest would be to return a new class that contains the nodes you have selected. That would be the easiest solution, as you don't really want to mutate any of your previous selectors.
I made a small example, using some ES6 that makes a few things easier to work with, which also has a $ to initiate the selections being made.
You would notice that first of all, any selection that is made, is just calling the native document.querySelectorAll but returns a new Node class. Both first and last methods also return those elements.
Lastly, hasClass should work on all elements in the current nodes selections, so it will iterate the current node, and check all classes in there, this one returns a simple bool, so you cannot continue with the method chaining there.
Any method you wish to chain, should either:
return this object (the current node)
return an element of the this object as a new node so any further manipulations can be done there
const $ = (function(global) {
class Node extends Array {
constructor( ...nodes ) {
super();
nodes.forEach( (node, key) => {
this[key] = node;
});
this.length = nodes.length;
}
first() {
return new Node( this[0] );
}
last() {
return new Node( this[this.length-1] );
}
hasClass( ...classes ) {
const set = classes.reduce( (current, cls) => {
current[cls] = true;
return current;
}, {} );
for (let el of this) {
for (let cls of el.classList) {
if (set[cls]) {
return true;
}
}
}
return false;
}
}
global.$ = function( selector ) {
return new Node( ...document.querySelectorAll( selector ) );
};
return global.$;
}(window));
let selector = $('.foo');
let first = selector.first(); // expect 1
console.log(first[0].innerHTML);
let last = selector.last();
console.log(last[0].innerHTML); // expect 4
console.log( first.hasClass('foo') ); // expect true
console.log( first.hasClass('bar') ); // expect false
console.log( selector.hasClass('foo') ); // expect true
console.log( selector.hasClass('bar') ); // expect true
<div class="foo">1</div>
<div class="foo">2</div>
<div class="foo bar">3</div>
<div class="foo">4</div>
You can update getElement so it returns back again when you send it an element.
var getElement = function(selector, parent) {
var ret = null
if (typeof selector === "string") {
ret = document.querySelectorAll(selector);
} else {
ret = selector
}
this.element = ret;
this.hasClass = function(className) {
className.replace('.', '');
if (this.multiple()) {
console.log('Cannot use hasClass function on multiple elements');
return false;
}
};
this.first = function() {
this.element = getElement(this.element[0]);
return this;
};
return this;
};
var test = getElement(".foo");
console.log(test.first())
console.log(test.first().hasClass)
<div class="foo">1</div>
<div class="foo">2</div>
<div class="foo">3</div>
<div class="foo">4</div>
You can use .querySelectorAll(), spread element and Array.prototype.find(), which returns the first match within an array or undefined
const getElement = (selector = "", {prop = "", value = "", first = false} = {}) => {
const el = [...document.querySelectorAll(selector)];
if (first) return el.find(({[prop]:match}) => match && match === value)
else return el;
};
let first = getElement("span", {prop: "className", value: "abc", first: true});
console.log(first);
let last = getElement("span");
console.log(all);
<span class="abc">123</span>
<span class="abc">456</span>
How Jquery can chain and return multiple values?
I know you can chain:
const $ = {
a() {
return this;
}
b() {
return this;
}
}
$.a().b()
The example below will speaks by itself:
$('div').find('p').hide().css() // find an apply style
$('div').find('p') // return all the "p"
See my example
How jQuery return all the p and keep the plugin instance?
How can I achieve the same behavior?
How to know if there is another call after find()?
Thanks.
All of these API members are returning a jQuery object. It just so happens that the jQuery API has all of these members on it. That is how you are able to chain calls on the same object.
Taking a look at the docs:
In API calls that return jQuery, the value returned will be the original jQuery object unless otherwise documented by that AP
show() returns a jQuery type
hide() returns a jQuery type
find() returns a jQuery type
Note that css() doesn't return a jQuery type, so you can't chain off that.
jQuery follows an approach like this one:
function $(param) {
if(!(this instanceof $)) // if $ is called without using 'new'
return new $(param); // then return a new instance
this.param = param; // ...
}
$.prototype.a = function() { // the function a has to...
// a's logic
console.log("this is a");
return this; // return this so there will be chaining
}
$.prototype.b = function() { // the same goes for b
// b's logic
console.log("this is b");
return this;
}
$.c = function() {
// c's logic
console.log(c);
// not returning this as c can't be chained
}
$("some param").a().b().a();
In the example, a and b are methods (attached to the prototype) that return this so they can be chained (example of $(...).val(), $(...).addClass()). c however is attached as a property to the function object $ directly, thus it can't be chained (example of $.isArray() and $.each() not to be confused with $(...).each()).
A mini-jQuery example:
function $(selector) {
if(!(this instanceof $))
return new $(selector);
if(selector instanceof $) // if selector is already an instance of $ then just return it to be used as it is
return selector;
else if(typeof selector === "string") // otherwise, if selector is a string, then select the elements
this.elems = document.querySelectorAll(selector);
else if($.isArray(selector)) // otherwise, if the selector is an array, then the elements will be those in the array ( this is used by find )
this.elems = selector;
else if(!selector) // otherwise, selector is a DOM element
this.elems = [selector];
}
$.prototype.each = function(callback) { // take a function (callback) and call it on all elements
for(var i = 0; i < this.elems.length; i++) // for each element in this.elems
callback.call(this.elems[i], i, this.elems[i]); // call callback, setting its this to the current element and passing to it two parameters: the index and the element itself
return this;
}
$.prototype.css = function(prop, value) { // a function that take a CSS property and probably a value and either set or return the value of that CSS property
if(value === undefined) // if the value is not provided, then
return this.elems[0].style[prop]; // return the value of the first element in the selection (some checking if the element exist should be here)
return this.each(function() { // if a value is provided, then
this.style[prop] = value; // set the style of each element in the selection, and then return this (this.each returns this so just return this.each())
});
}
$.prototype.text = function(val) { // follows the same as css above
if(val === undefined)
return this.elems[0].textContent;
return this.each(function() {
this.textContent = val;
});
}
$.isArray = function(obj) { // is not a method (this is not the instance of the class)
return Object.prototype.toString.call(obj) === "[object Array]";
}
// FIND:
$.prototype.find = function(selector) {
var newElems = []; // the array of new elements
this.each(function() { // for each element in the current selection
var thisElems = this.querySelectorAll(selector); // select it descendants that match the selector
for(var i = 0; i < thisElems.length; i++) // add them to the result array
newElems.push(thisElems[i]);
});
// remove duplicates // this is important as elements could be selected twice (elements inside a parent inside a grand parent where both parent and grand parent are in the old selection), check jQuery's source code for how they remove duplicats using sort
return $(newElems); // return a new $ object using the new array of elements as the wrapped elements (check the constructor for more informations)
}
// usage:
$("div").css("background", "#fd8") // selects all divs and change their background
.find("span") // now find all spans inside those divs
.css("background", "#0ff"); // change their background
<div>a</div>
<div class="even"><span>b</span></div>
<div>c</div>
<div class="even">d</div>
<div><span>e</span></div>
<span>outer span</span>
I am writing a vanilla JavaScript tool, that when enabled adds event listeners to each of the elements passed into it.
I would like to do something like this:
var do_something = function (obj) {
// do something
};
for (var i = 0; i < arr.length; i++) {
arr[i].el.addEventListener('click', do_something(arr[i]));
}
Unfortunately this doesn't work, because as far as I know, when adding an event listener, parameters can only be passed into anonymous functions:
for (var i = 0; i < arr.length; i++) {
arr[i].el.addEventListener('click', function (arr[i]) {
// do something
});
}
The problem is that I need to be able to remove the event listener when the tool is disabled, but I don't think it is possible to remove event listeners with anonymous functions.
for (var i = 0; i < arr.length; i++) {
arr[i].el.removeEventListener('click', do_something);
}
I know I could easily use jQuery to solve my problem, but I am trying to minimise dependencies. jQuery must get round this somehow, but the code is a bit of a jungle!
This is invalid:
arr[i].el.addEventListener('click', do_something(arr[i]));
The listener must be a function reference. When you invoke a function as an argument to addEventListener, the function's return value will be considered the event handler. You cannot specify arguments at the time of listener assignment. A handler function will always be called with the event being passed as the first argument. To pass other arguments, you can wrap the handler into an anonymous event listener function like so:
elem.addEventListener('click', function(event) {
do_something( ... )
}
To be able to remove via removeEventListener you just name the handler function:
function myListener(event) {
do_something( ... );
}
elem.addEventListener('click', myListener);
// ...
elem.removeEventListener('click', myListener);
To have access to other variables in the handler function, you can use closures. E.g.:
function someFunc() {
var a = 1,
b = 2;
function myListener(event) {
do_something(a, b);
}
elem.addEventListener('click', myListener);
}
// Define a wrapping function
function wrappingFunction(e) {
// Call the real function, using parameters
functionWithParameters(e.target, ' Nice!')
}
// Add the listener for a wrapping function, with no parameters
element.addEventListener('click', wrappingFunction);
// Save a reference to the listener as an attribute for later use
element.cleanUpMyListener = ()=>{element.removeEventListener('click', wrappingFunction);}
// ...
element.cleanUpMyListener ()
Step 1) Name your function.
Step 2) Save a reference to your function (in this case, save the reference as an attribute on the element itself)
Step 3) Use the function reference to remove the listener
// Because this function requires parameters, we need this solution
function addText(element, text) {
element.innerHTML += text
}
// Add the listener
function addListener() {
let element = document.querySelector('div')
if (element.removeHoverEventListener){
// If there is already a listener, remove it so we don't have 2
element.removeHoverEventListener()
}
// Name the wrapping function
function hoverDiv(e) {
// Call the real function, using parameters
addText(e.target, ' Nice!')
}
// When the event is fired, call the wrapping function
element.addEventListener('click', hoverDiv);
// Save a reference to the wrapping function as an attribute for later use
element.removeHoverEventListener = ()=>{element.removeEventListener('click', hoverDiv);}
}
// Remove the listener
function removeListener() {
let element = document.querySelector('div')
if (element.removeHoverEventListener){
// Use the reference saved before to remove the wrapping function
element.removeHoverEventListener()
}
}
<button onclick="addListener()">Turn Listener on</button>
<button onclick="removeListener()">Turn Listener off</button>
<div>Click me to test the event listener.</div>
To pass arguments to event handlers bind can be used or handler returning a function can be used
// using bind
var do_something = function (obj) {
// do something
}
for (var i = 0; i < arr.length; i++) {
arr[i].el.addEventListener('click', do_something.bind(this, arr[i]))
}
// using returning function
var do_something = obj => e {
// do something
}
for (var i = 0; i < arr.length; i++) {
arr[i].el.addEventListener('click', do_something(arr[i]))
}
But in both the cases to remove the event handlers it is not possible as bind will give a new referenced function and returning function also does return a new function every time for loop is executed.
To handle this problem we need to store the references of the functions in an Array and remove from that.
// using bind
var do_something = function (obj) {
// do something
}
var handlers = []
for (var i = 0; i < arr.length; i++) {
const wrappedFunc = do_something.bind(this, arr[i])
handlers.push(wrappedFunc)
arr[i].el.addEventListener('click', wrappedFunc);
}
//removing handlers
function removeHandlers() {
for (var i = 0; i < arr.length; i++) {
arr[i].el.removeEventListener('click', handlers[i]);
}
handlers = []
}
This can be done quite easily, just not as you have it right now.
Instead of trying to add and remove random anonymouse functions, you need to add or remove a function that handles the execution of your other functions.
var
// Here we are going to save references to our events to execute
cache = {},
// Create a unique string to mark our elements with
expando = String( Math.random() ).split( '.' )[ 1 ],
// Global unique ID; we use this to keep track of what events to fire on what elements
guid = 1,
// The function to add or remove. We use this to handler all of other
handler = function ( event ) {
// Grab the list of functions to fire
var handlers = ( cache[ this[ expando ] ] && cache[ this[ expando ] ][ event.type ] ) || false;
// Make sure the list of functions we have is valid
if ( !handlers || !handlers.length ) {
return;
}
// Iterate over our individual handlers and call them as we go. Make sure we remeber to pass in the event Object
handlers.forEach( function ( handler ) {
handler.call( this, event );
});
},
// If we want to add an event to an element, we use this function
add = function ( element, type, fn ) {
// We test if an element already has a guid assigned
if ( !element[ expando ] ) {
element[ expando ] = guid++;
}
// Grab the guid number
var id = element[ expando ];
// Make sure the element exists in our global cache
cache[ id ] = cache[ id ] || {};
// Grab the Array that we are going to store our handles in
var handlers = cache[id ][ type ] = cache[ id ][ type ] || [];
// Make sure the handle that was passed in is actually a function
if ( typeof fn === 'function' ) {
handlers.push( fn );
}
// Bind our master handler function to the element
element.addEventListener( type, handler, false );
};
// Add a click event to the body element
add( document.body, 'click', function ( event ) {
console.log( 1 );
});
This is just a cut down version of what I've written before, but you can get the gist of it I hope.
Maybe its not perfect solution, but near to ideal, in addition I dont see other ways
Thanks to Kostas Bariotis
Solution key here is:
So what do we do when we need to remove our attached event handlers at some point at runtime? Meet handleEvent, the default function that JavaScript looks for when tries to find a handler that has been attached to an event.
In cas link is broken (I placed first way)
let Button = function () {
this.el = document.createElement('button');
this.addEvents();
}
Button.prototype.addEvents = function () {
this.el.addEventListener('click', this);
}
Button.prototype.removeEvents = function () {
this.el.removeEventListener('click', this);
}
Button.prototype.handleEvent = function (e) {
switch(e.type) {
case 'click': {
this.clickHandler(e);
}
}
}
Button.prototype.clickHandler = function () {
/* do something with this */
}
P.S:
Same tehnics in JS class implementation.
If you develop in typescript you have to implement handleEvent method from EventListenerObject interface
To 'addEventListener' with some parameters, you can use the following code:
{
myButton.addEventListener("click",myFunction.bind(null,event,myParameter1,myParameter2));
}
And the function 'myFunction' should be something like this:
{
function myFunction(event, para1, para2){...}
}
I'm trying to understand jQuery classes but it is not going very well.
My goal is to use a class this way (or to learn a better way to do it):
var player = new Player($("playerElement"));
player.InitEvents();
Using other people's examples, this is what I tried:
$.Player = function ($) {
};
$.Player.prototype.InitEvents = function () {
$(this).keypress(function (e) {
var key = e.which;
if (key == 100) {
MoveRight();
}
if (key == 97) {
MoveLeft();
}
});
};
$.Player.prototype.MoveRight = function () {
$(this).css("right", this.playerX += 10);
}
$.Player.prototype.MoveLeft = function () {
$(this).css("right", this.playerX -= 10);
}
$.Player.defaultOptions = {
playerX: 0,
playerY: 0
};
The end goal is to have a character moving on the screen left and right using the keyboard letters A and D.
I have a feeling that I'm doing something very wrong with this "class"
but I'm not sure why.
(sorry for my English)
An important issue is that you have to assign the passed jQuery object/element to a this.element - or another this.propertyName - so you can access it later inside the instance's methods.
You also cannot call MoveRight()/MoveLeft() directly like that because those functions are not defined up in the scope chain, but rather in the prototype of your instance's Constructor, hence you need a reference to the instance itself to call these.
Updated and commented code below:
(function ($) { //an IIFE so safely alias jQuery to $
$.Player = function (element) { //renamed arg for readability
//stores the passed element as a property of the created instance.
//This way we can access it later
this.element = (element instanceof $) ? element : $(element);
//instanceof is an extremely simple method to handle passed jQuery objects,
//DOM elements and selector strings.
//This one doesn't check if the passed element is valid
//nor if a passed selector string matches any elements.
};
//assigning an object literal to the prototype is a shorter syntax
//than assigning one property at a time
$.Player.prototype = {
InitEvents: function () {
//`this` references the instance object inside of an instace's method,
//however `this` is set to reference a DOM element inside jQuery event
//handler functions' scope. So we take advantage of JS's lexical scope
//and assign the `this` reference to another variable that we can access
//inside the jQuery handlers
var that = this;
//I'm using `document` instead of `this` so it will catch arrow keys
//on the whole document and not just when the element is focused.
//Also, Firefox doesn't fire the keypress event for non-printable
//characters so we use a keydown handler
$(document).keydown(function (e) {
var key = e.which;
if (key == 39) {
that.moveRight();
} else if (key == 37) {
that.moveLeft();
}
});
this.element.css({
//either absolute or relative position is necessary
//for the `left` property to have effect
position: 'absolute',
left: $.Player.defaultOptions.playerX
});
},
//renamed your method to start with lowercase, convention is to use
//Capitalized names for instanceables only
moveRight: function () {
this.element.css("left", '+=' + 10);
},
moveLeft: function () {
this.element.css("left", '-=' + 10);
}
};
$.Player.defaultOptions = {
playerX: 0,
playerY: 0
};
}(jQuery));
//so you can use it as:
var player = new $.Player($("#playerElement"));
player.InitEvents();
Fiddle
Also note that JavaScript does not have actual "classes" (at least not until ES6 gets implemented) nor Methods (which by definition are associated exclusively to Classes), but rather Constructors which provide a sweet syntax that resembles classes. Here's an awesome article written by TJ Crowder regarding JS's "fake" methods, it is a little advanced but everyone should be able to learn something new from reading it:
http://blog.niftysnippets.org/2008/03/mythical-methods.html
When you use this inside your Player prototype functions, this points to the current Player object.
But when you use $(this).keypress it requires that this points to an HTML element.
The two simply are incompatible. There is only one this and it points to the current Player object, not to an HTML element.
To fix your problem, you will need to pass the HTML element into the Player object upon its creation or into the relevant function calls.
You can pass the element into the Player object upon construction like this:
$.Player = function ($, element) {
this.element = element;
};
$.Player.prototype.InitEvents = function () {
$(this.element).keypress(function (e) {
var key = e.which;
if (key == 100) {
MoveRight();
}
if (key == 97) {
MoveLeft();
}
});
};
$.Player.prototype.MoveRight = function () {
$(this.element).css("right", this.playerX += 10);
}
$.Player.prototype.MoveLeft = function () {
$(this.element).css("right", this.playerX -= 10);
}
$.Player.defaultOptions = {
playerX: 0,
playerY: 0
};