Using prototyping to alter functionality of existing jQuery functions - javascript

I would like to alter the functionality of $.prepend() (and probably $.append()) for the purpose of having an "on DOM change event".
Can I do something as simple as:
$.prepend = function() { alert('Hello World'); };
Or do I need to use the $.extend() function or $.prototype.prepend or $.fn.prepend?
[I realise I'll need to include the original source for the prepend() function in my new one otherwise jQuery will break!]
EDIT :: Final Solution
For those who are interested:
$.extend($, {
domChangeStack: [],
onDomChange: function(selector, fn, unbind, removeFromStack) {
/* Needs to store: selector, function, unbind flag, removeFromStack flag */
jQuery.domChangeStack.push([selector, fn, unbind, removeFromStack]);
},
domChangeEvent: function() {
/* Ideally should only affect inserted HTML/altered DOM, but this doesn't */
var stackItem, newStack = [];
while (stackItem = jQuery.domChangeStack.pop()) {
var selector = stackItem[0],
fn = stackItem[1],
unbind = stackItem[2],
remove = stackItem[3];
if (unbind) { $(selector).unbind(); }
// Need to pass the jQuery object as fn is anonymous
fn($(selector));
if (!remove) { newStack.push(stackItem); }
}
jQuery.domChangeStack = newStack;
// Show something happened!
console.log("domChangeEvent: stack size = " + newStack.length);
}
});
$.fn.prepend = function() {
var result = this.domManip(arguments, true, function( elem ) {
if ( this.nodeType === 1 ) {
this.insertBefore( elem, this.firstChild );
}
});
// Need to actually alter DOM above before calling the DOMChange event
$.domChangeEvent();
return result;
};
And usage:
/* Run the given function on the elements found by the selector,
* don't run unbind() beforehand and don't pop this DOMChange
* event off the stack.
*/
$.onDomChange(".element_class", function(jObj) {
jObj.do_something_awesome();
}, false, false);

Which method you want to use depends on how much you need to change. Since .prepend is merely a method that resides in .fn you don't have to mess with the prototype.
I most cases its enough to rename the original method, create your own function that does what you want and end with a call to the original function, like this:
var orgPrepend = $.fn.prepend;
$.fn.prepend = function(){
// Do what you want here
// Call org prepend and return
return orgPrepend.apply(this, arguments);
}
Note: .apply and .call are more or less identical. The only difference is that .apply passes arguments by reference while .call passes them by value, so I prefer to use .apply before .call where possible. See MDC for reference
But if you look at the source of jQuery (see src/manipulation.js) you'll see that this method is very small so you can just implement it directly.
In the next example I will use .extend instead, but it's not a must; you could just replace it like in the first example.
$.extend($.fn, {
prepend: function() {
// Do what you want here
console.log("prepend was called");
// Execute domManip
return this.domManip(arguments, true, function( elem ) {
if ( this.nodeType === 1 ) {
this.insertBefore( elem, this.firstChild );
}
});
}
});
You can override .domManip or any other method in the same way, like in the following example. You'll probably see why I prefer to use .extend here.
var _domManip = $.fn.domManip;
$.extend($.fn, {
domManip: function() {
// Do what you want here
console.log("domManip was called");
_domManip.apply(this, arguments);
},
prepend: function() {
// Do what you want here
console.log("prepend was called");
// Execute domManip
return this.domManip(arguments, true, function( elem ) {
if ( this.nodeType === 1 ) {
this.insertBefore( elem, this.firstChild );
}
});
}
});
See test case on jsFiddle

I've made a little test and it seems that you must replace $.fn.prepend. Anyway if you want you can do something like:
var old=$.fn.prepend;
$.fn.prepend=function(){
//Trigger DOMchange event
old.apply(this, arguments);
};
In this way you can wrap the old prepend function in a new one without rewrite the function code.

Related

How to return "addEventListener()" from another function - Javascript

I am trying to make my code shorter and more optimized, and want to make it look clearer.
So far I did this :
function id(a) {
return document.getElementById(a);
}
function cl(a) {
return document.getElementsByClassName(a);
}
function tg(a) {
return document.getElementsByTagName(a);
}
function qs(a) {
return document.querySelector(a);
}
function qa(a) {
return document.querySelectorAll(a);
}
Now I have the possibility to call qs("#myElement"). Now I want to attach a event to the specified element just like qs("#myElement").addEventListener("click", callBack). It works great for me. But when I try to make this :
function ev(e, call) {
return addEventListener(e, callback);
}
And then try to call qs("#init-scrap").ev("click", someFunction) then it pops up the following error :
Uncaught (in promise) TypeError: qs(...).ev is not a function.. I don't know what is the problem, do I have to try method chaining ? or any other way I can resolve this problem.
Note : I don't want to use any libraries or frameworks liek Jquery etc.
If you wish to use syntax qs("#init-scrap").ev("click", someFunction), you need to wrap object returned by querySelector into another object that has ev function.
class jQueryLite {
constructor(el) {
this.el = el;
}
ev(e, callback) {
this.el.addEventListener(e, callback);
return this;
}
}
qs(a) {
return new jQueryLite(document.querySelector(a));
}
It's called Fluent interface, if you wish to look it up.
Just pass the element/nodelist in as the first argument and attached the listener to it.
function ev(el, e, call) {
return el.addEventListener(e, callback);
}
As an alternative, but not something I would recommend, you could add ev as a new Node prototype function:
function qs(selector) {
return document.querySelector(selector);
}
if (!Node.prototype.ev) {
Node.prototype.ev = function(e, cb) {
return this.addEventListener(e, cb);
};
}
qs('button').ev('click', handleClick);
let count = 0;
function handleClick() {
console.log(count++);
}
<button>Count+=1</button>
Note I've only tested this with document.querySelector. You might have to alter the code to work with document.querySelectorAll etc as they don't return single elements.
There is an error in your ev method. It should be
const ev = document.addEventListener.bind(document);
So instead of creating new functions that wrap the original, you can alias the actual function itself.
You should do the same for your other aliases if you want to go with this approach.
const qs = document.querySelector.bind(document);
const qa = document.querySelectorAll.bind(document);
My final word of advise would be to not alias these methods at all. The abbreviated method names hurt the readability of your code. Readability almost always trumps brevity as it comes to code.
I looked into the previous answers as an inspiration and created my take on it.
Core
const $ = (selector, base = document) => {
return base.querySelector(selector);
};
Node.prototype.on = function(type, listener) {
return this.addEventListener(type, listener);
};
It supports a base value in case you have another element than document but it's optional.
I like $ and on so that's what I use, just like jQuery.
Call it like below
$('button').on('click', (e) => {
console.log(e.currentTarget);
});

how to override a returned nested method in javascript?

Say I'm using a library with the code that looks like below:
(function($)
{
function Library(el, options)
{
return new Library.prototype.init(el, options);
}
Library.fn = $.Library.prototype = {
init: function(el, options) {
this.$elm.on('keydown.library', $.proxy(this.keydown.init, this));
}
keydown: function() {
return {
init: function(e) {
... somecode
},
checkStuff: function(arg1, arg2) {
...someCode
}
}
};
}
})(jQuery);
It has a plugin system that provides access to this where this is an Object {init: function, keydown: function...}. I want to override the keydown.init function. Normally I could see using something like _.wrap to do it:
somefunc = _.wrap(somefuc, function(oldfunc, args) {
donewstuff();
oldfunc.call(this.args);
});
but that doesn't seem to work on the returned nested method e.g.:
this.keydown.init = _.wrap(this.keydown.init, function(oldfunc, args) {
donewstuff();
oldfunc.call(this.args);
});
The question might be answered on here but I don't really know the right words to use to describe this style of coding so its hard to search. Bonus points if you let me know if it is even correct to call it a nested returned method?
This pattern is called a module. The best thing you can do here is cache the method you want to override and call the cached method inside your override:
somefunc._init = somefunc.init;
somefunc.init = function () {
doStuff();
this._init();
};
I checked _.wrap and it does the same thing, what your missing as pointed out by another answer is you're losing the context of somefunc. In order to prevent that you can do:
somefunc.init = _.wrap(_.bind(somefunc.init, somefunc), function (oldRef, args) {
doStuff();
oldRef.call(this.args);
});
You will need to decorate (read: wrap) the keydown function so that you can wrap the init method of the object it returns:
somefunc.keydown = _.wrap(somefunc.keydown, function(orig) {
var module = orig(); // it doesn't seem to take arguments or rely on `this` context
module.init = _.wrap(module.init, function(orig, e) {
donewstuff();
return orig.call(this, e);
});
return module;
});
The problem is that your method is run out of context.
You need to set its this context (use .bind() for this)
somefunc.init = _.wrap(somefuc.init.bind(somefunc), function(oldfunc, args) {
donewstuff();
oldfunc.call(this.args);
});

returnTrue and returnFalse functions in jQuery source

I can't help but notice there are two seemingly useless functions in the source code of jQuery (For v1.9.1, it's line 2702 and line 2706):
function returnTrue() {
return true;
}
function returnFalse() {
return false;
}
Which both are called quite often within jQuery. Is there a reason why they don't simply substitute the function call with a boolean true or false?
If an object property, function argument, etc expects a function you should provide a function not a boolean.
For example in vanilla JavaScript:
var a = document.createElement("a");
a.href = "http://www.google.com/";
/*
* see https://developer.mozilla.org/en-US/docs/DOM/element.onclick
* element.onclick = functionRef;
* where functionRef is a function - often a name of a function declared
* elsewhere or a function expression.
*/
a.onclick = true; // wrong
a.onclick = returnTrue; // correct
a.onclick = function() { return true; }; // correct
Also, writing:
someProperty: returnTrue,
Is more convenient than writing:
someProperty: function(){
return true;
},
Especially since they are called quite often.
it was used like this:
stopImmediatePropagation: function() {
this.isImmediatePropagationStopped = returnTrue;
this.stopPropagation();
}
here isImmediatePropagationStopped is a query method. used like this event.isImmediatePropagationStopped()
of course, you can define a instance method, like:
event.prototyoe.isImmediatePropagationStopped = function() { return this._isImmediatePropagationStopped };
stopImmediatePropagation: function() {
this._isImmediatePropagationStopped = true; //or false at other place.
this.stopPropagation();
}
but you have to introduce a new instance property _isImmediatePropagationStopped to store the status.
with this trick, you can cut off bunch of instance properties for hold true/false status here, like _isImmediatePropagationStopped, _isDefaultPrevented etc.
so that, in my opinion, this is just a matter of code style, not right or wrong.
PS: the query methods on event, like isDefaultPrevented , isPropagationStopped, isImmediatePropagationStopped are defined in DOM event level 3 sepc.
spec: http://www.w3.org/TR/2003/NOTE-DOM-Level-3-Events-20031107/events.html#Events-Event-isImmediatePropagationStopped

Unbind an Anonymous Function

Can somebody tell how to "unbind" an anonymous function?
In jQuery it's capable to do that, but how can I implement this Functionality in my own script.
This is the scenario:
The following code attach a onclick event to the Div which have someDivId as ID, now when you click the DIV, it's showing 'clicked!'.
var a = document.getElementById('someDivId');
bindEvent(a,'click',function(){alert('clicked!');});
That's all great, the problem is how to "un-attach" the Function to the DIV if the function is anonymous or how to "un-attach" all attached events to the 'a' Element?
unBind(a,'click'); //Not necessarily the given params, it's just an example.
This is the code for bindEvent Method:
function bindEvent (el,evtType,fn){
if ( el.attachEvent ) {
el['e'+evtType+fn] = fn;
el[evtType+fn] = function(){
fn.call(el,window.event);
}
el.attachEvent( 'on'+evtType, el[evtType+fn] );
} else {
el.addEventListener( evtType, fn, false );
}
}
Finally, and after hours of Test&Errors i have found a solution, maybe it's not the best or most efficient but... IT WORKS! (Tested on IE9, Firefox 12, Chrome 18)
First all I'v create two cross-browser and auxiliary addEvent() and removeEvent() methods. (Idea taken from Jquery's source code!)
HELPERS.removeEvent = document.removeEventListener ?
function( type, handle,el ) {
if ( el.removeEventListener ) {
//W3C Standard
el.removeEventListener( type, handle, true );
}
} :
function( type, handle,el ) {
if ( el.detachEvent ) {
//The IE way
el.detachEvent( 'on'+type, el[type+handle] );
el[type+handle] = null;
}
};
HELPERS.addEvent = document.addEventListener ?
function( type, handle,el ) {
if ( el.addEventListener ) {
//W3C Standard
el.addEventListener( type, handle, true );
}
} :
function( type, handle,el ) {
if ( el.attachEvent ) {
//The IE way
el['e'+type+handle] = handle;
el[type+handle] = function(){
handle.call(el,window.event);
};
el.attachEvent( 'on'+type, el[type+handle] );
}
}
Also we need some kind of 'container' to store the attached events to elements, like this:
HELPERS.EVTS = {};
And finally the two callable and exposed to the users Methods:
The next one to add an Event(event) and associate this Event to a Method (handler) for a specific Element (el).
function bindEvent(event, handler,el) {
if(!(el in HELPERS.EVT)) {
// HELPERS.EVT stores references to nodes
HELPERS.EVT[el] = {};
}
if(!(event in HELPERS.EVT[el])) {
// each entry contains another entry for each event type
HELPERS.EVT[el][event] = [];
}
// capture reference
HELPERS.EVT[el][event].push([handler, true]);
//Finally call the aux. Method
HELPERS.addEvent(event,handler,el);
return;
}
Lastly the method that un-attach every pre-attached events (event) for an specific Element (el)
function removeAllEvent(event,el) {
if(el in HELPERS.EVT) {
var handlers = HELPERS.EVT[el];
if(event in handlers) {
var eventHandlers = handlers[event];
for(var i = eventHandlers.length; i--;) {
var handler = eventHandlers[i];
HELPERS.removeEvent(event,handler[0],el);
}
}
}
return;
}
By the way, to call this methods you must do the following:
Capture a DOM Node
var a = document.getElementById('some_id');
Call the method 'bindEvent()' with the corresponding parameters.
bindEvent('click',function(){alert('say hi');},a);
And to de-attach it:
removeAllEvent('click',a);
That's all, hope will be useful for somebody one day.
Personally (and I know this isn't the "best" way, as it does require me to think about what I'm doing), I like to just use the on* event properties of the element I'm working with.
This has the convenient upside of being able to quickly and easily detach events.
var a = document.getElementById('someDivId');
a.onclick = function() {alert("Clicked!");};
// later...
a.onclick = null;
However, you do have to be careful with this because if you try to add a second event handler it will overwrite the first. Keep that in mind and you should be all fine.
I'm not sure if you can unbind an anonymous function attached via javascript. If possible you can simple remove the element from the DOM and recreate it. This will get rid of any event handlers previously attached.
JavaScript provides no list of event listeners attached to a node.
You can remove all event listeners of a node but using the Node.cloneNode method, see here: https://developer.mozilla.org/En/DOM/Node.cloneNode
This clones the node (obviously) but it does not clone the event listeners attached to it.
You could also just bind empty functions as event listeners:
function noop() {}
bindEvent(myElement, "click", noop);
This is from jquery's source:
jQuery.removeEvent = document.removeEventListener ?
function( elem, type, handle ) {
if ( elem.removeEventListener ) {
elem.removeEventListener( type, handle, false );
}
} :
function( elem, type, handle ) {
if ( elem.detachEvent ) {
elem.detachEvent( "on" + type, handle );
}
};

Override jQuery functions

Is there way to override jQuery's core functions ?
Say I wanted to add an alert(this.length) in size: function()
Instead of adding it in the source
size: function() {
alert(this.length)
return this.length;
},
I was wondering if it would be possible to do something like this :
if (console)
{
console.log("Size of div = " + $("div").size());
var oSize = jQuery.fn.size;
jQuery.fn.size = function()
{
alert(this.length);
// Now go back to jQuery's original size()
return oSize(this);
}
console.log("Size of div = " + $("div").size());
}
You almost had it, you need to set the this reference inside of the old size function to be the this reference in the override function, like this:
var oSize = jQuery.fn.size;
jQuery.fn.size = function() {
alert(this.length);
// Now go back to jQuery's original size()
return oSize.apply(this, arguments);
};
The way this works is Function instances have a method called apply, whose purpose is to arbitrarily override the inner this reference inside of the function's body.
So, as an example:
var f = function() { console.log(this); }
f.apply("Hello World", null); //prints "Hello World" to the console
You can override plugins method by prototype it in a separate file without modifying original source file as below::
(function ($) {
$.ui.draggable.prototype._mouseDrag = function(event, noPropagation) {
// Your Code
},
$.ui.resizable.prototype._mouseDrag = function(event) {
// Your code
}
}(jQuery));
Now put your logic here or original code with your new idea that is needed in your project.

Categories