First jQuery plugin- How to handle events on re-initialization? - javascript

Well, this is not my first jQuery plugin but the first I think others would benefit from. So for me its the first time to make sure every possible application works as predicted.
My plugin handles modal less overlays and therefore has to listen for some events. If the plugin is re-initialized the old listeners seem to be still in place and cause malfunction.
My solution to this looks like this:
var oldSettings = $(_this).data('mlOverlaySettings');
if(oldSettings) {
$(oldSettings.target).unbind('click.mlOverlay');
$(document).unbind('click.mlOverlay');
$(document).unbind('keyup.mlOverlay');
}
For me it seems like the problem has anything todo with saving the plugin state with the $.data function and not properly released references.
$(_this).data('mlOverlaySettings', settings);
$(_this).data('mlIsOverlayVisible', false);
Additional resources
Plugin Demo
Plugin documentation and full source code
Unbinding the old event's is somewhat ugly. Am I doing something wrong here or is this always required?

You should prevent reinitialization in the jQuery boilerplate method:
(function($) {
var PLUGIN_IDENTIFIER = "my-plugin";
...plugin definition etc
//jQuery boilerplate
$.fn.myPlugin = function(opts) {
return this.each(function() {
var instance = $(this).data(PLUGIN_IDENTIFIER);
//Prevent reinit on this element
if (!instance) {
instance = new MyPlugin(this, opts);
$(this).data(PLUGIN_IDENTIFIER, instance);
}
//Method call
if (typeof opts === "string") {
instance[opts].apply(instance, [].slice.call(arguments, 1));
}
});
};
})();
You should always provide a "destroy" method that removes the .data and event listeners it added too. So it's only possible to reinitialize after calling "destroy" which conveniently removed the event listeners too.
Here's an example implementation of a pretty standard destroy method:
function MyPlugin(element, opts) {
this.element = $(element);
this.opts = $.extend(defaults, $(element).data(), opts);
//Other instance state
//absolutely do not use $.data for this, you should only occupy one $.data
//slot for your plugin for the same reason you only occupy one slot on
//$.fn
}
MyPlugin.prototype.destroy = function() {
this.element.removeData(PLUGIN_IDENTIFIER);
this.element.off(".myplugin"); //Remove all events off the element that belong to the plugin's namespace
//.remove() any helper elements created by the plugin
this.element = null;
};

Related

Access private properties and methods from event handler

I've used the jQuery Boilerplate template as starting point for a jQuery plug-in. This template provides a set up where this represents the plug-in instance and gives access to properties and methods:
init: function() {
$(this.element).css({borderColor: "red"});
this.drawMarker([100, 200]);
},
drawMarker: function(coordinates) {
if (this.settings.isAbsolute) {
// ...
}
}
Now I need to handle some mouse clicks and it's all getting really confusing because callback functions redefine the this variable to represent the clicked event so, in order to access the plugin stuff, I came up with this ugly workaround:
this.container.on("click", "." + this.settings.markerClass,
{plugin: this}, this.removeMarker);
... and:
removeMarker: function(event){
var plugin = event.data.plugin;
var marker = $(this);
if (plugin.settings.isAbsolute) {
// ...
}
}
Is this actually what I'm supposed to do or I'm overlooking a most straightforward approach?
One possibility is to use the jQuery.proxy() function (added on 1.4) to force a given context inside event handlers:
this.$container.on("click", "." + this.settings.markerClass,
$.proxy(this.removeMarker, this));
Then, the stuff you need can be reached as follows:
Plugin properties/methods: this
Clicked element: event.target (on delegated events, it's the precise element the user clicked on; the one we normally want)
removeMarker: function(event){
var $marker = $(event.target);
if (this.settings.isAbsolute) {
// ...
}
}
This technique is courtesy of Patrick Evans.
If you need to access privately scoped variables (using functions that are therefore by definition not on the plugins prototype) just create an additional variable that aliases the plugin object:
var plugin = this;
this.container.on('click', function() {
// use plugin here
...
});
If the callback function in question is on the prototype, you can access the object within the callback thus:
var plugin = $(element).data('plugin_' + pluginName);

jQuery when element becomes visible [duplicate]

This question already has answers here:
How to check if element is visible after scrolling?
(46 answers)
Closed 1 year ago.
Basically, I am wondering if there is a way to automatically run a function when an element becomes hidden or visible, not on a user click but automatically in another script.
I don't want this to just run one time, because the elements (such as a slider) constantly change from visible to hidden.
Would this be something that jQuery can do with bind? Such as binding the element's visibility to a function (I don't know how to write this)
If you need me to elaborate more on what I'm trying to do, let me know. Thanks
Pseudocode:
$('#element').bind('display:none', function);
function(){
//do something when element is display:none
}
$('#element').bind('display:block', function2);
function2(){
//do opposite of function
}
There are no events in JQuery to detect css changes.
Refer here: onHide() type event in jQuery
It is possible:
DOM L2 Events module defines mutation events; one of them - DOMAttrModified is the one you need. Granted, these are not widely implemented, but are supported in at least Gecko and Opera browsers.
Source: Event detect when css property changed using Jquery
Without events, you can use setInterval function, like this:
var maxTime = 5000, // 5 seconds
startTime = Date.now();
var interval = setInterval(function () {
if ($('#element').is(':visible')) {
// visible, do something
clearInterval(interval);
} else {
// still hidden
if (Date.now() - startTime > maxTime) {
// hidden even after 'maxTime'. stop checking.
clearInterval(interval);
}
}
},
100 // 0.1 second (wait time between checks)
);
Note that using setInterval this way, for keeping a watch, may affect your page's performance.
7th July 2018:
Since this answer is getting some visibility and up-votes recently, here is additional update on detecting css changes:
Mutation Events have been now replaced by the more performance friendly Mutation Observer.
The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. It is designed as a replacement for the older Mutation Events feature which was part of the DOM3 Events specification.
Refer: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
(function() {
var ev = new $.Event('display'),
orig = $.fn.css;
$.fn.css = function() {
orig.apply(this, arguments);
$(this).trigger(ev);
}
})();
$('#element').bind('display', function(e) {
alert("display has changed to :" + $(this).attr('style') );
});
$('#element').css("display", "none")// i change the style in this line !!
$('#element').css("display", "block")// i change the style in this line !!
http://fiddle.jshell.net/prollygeek/gM8J2/3/
changes will be alerted.
Tried this on firefox, works http://jsfiddle.net/Tm26Q/1/
$(function(){
/** Just to mimic a blinking box on the page**/
setInterval(function(){$("div#box").hide();},2001);
setInterval(function(){$("div#box").show();},1000);
/**/
});
$("div#box").on("DOMAttrModified",
function(){if($(this).is(":visible"))console.log("visible");});
UPDATE
Currently the Mutation Events (like DOMAttrModified used in the
solution) are replaced by MutationObserver, You can use that to
detect DOM node changes like in the above case.
I just Improved ProllyGeek`s answer
Someone may find it useful.
you can access displayChanged(event, state) event when .show(), .hide() or .toggle() is called on element
(function() {
var eventDisplay = new $.Event('displayChanged'),
origShow = $.fn.show,
origHide = $.fn.hide;
//
$.fn.show = function() {
origShow.apply(this, arguments);
$(this).trigger(eventDisplay,['show']);
};
//
$.fn.hide = function() {
origHide.apply(this, arguments);
$(this).trigger(eventDisplay,['hide']);
};
//
})();
$('#header').on('displayChanged', function(e,state) {
console.log(state);
});
$('#header').toggle(); // .show() .hide() supported
A catch-all jQuery custom event based on an extension of it's core methods like it was proposed by different people in this thread:
(function() {
var ev = new $.Event('event.css.jquery'),
css = $.fn.css,
show = $.fn.show,
hide = $.fn.hide;
// extends css()
$.fn.css = function() {
css.apply(this, arguments);
$(this).trigger(ev);
};
// extends show()
$.fn.show = function() {
show.apply(this, arguments);
$(this).trigger(ev);
};
// extends hide()
$.fn.hide = function() {
hide.apply(this, arguments);
$(this).trigger(ev);
};
})();
An external library then, uses sth like $('selector').css('property', value).
As we don't want to alter the library's code but we DO want to extend it's behavior we do sth like:
$('#element').on('event.css.jquery', function(e) {
// ...more code here...
});
Example: user clicks on a panel that is built by a library. The library shows/hides elements based on user interaction. We want to add a sensor that shows that sth has been hidden/shown because of that interaction and should be called after the library's function.
Another example: jsfiddle.
I like plugin https://github.com/hazzik/livequery It works without timers!
Simple usage
$('.some:visible').livequery( function(){ ... } );
But you need to fix a mistake. Replace line
$jQlq.registerPlugin('append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove', 'html', 'prop', 'removeProp');
to
$jQlq.registerPlugin('show', 'append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove', 'html', 'prop', 'removeProp');

With JQuery, is it possible to have a function run when a DOM element calls .remove()?

As the question states, what I'm attempting to do is have a function that is called when a DOM element is removed from the DOM, much like a destructor.
I looked into unload, but from my understanding that's only called when the browser navigates away from the page.
Thanks in advance!
It is possible to use the special events to build removal tracking. I've created an example that allows to bind to an removed event. Note that this approach only works when the removal is initiated by jQuery (calling $(…).remove()) for example. For a more general solution use DOMNodeRemoved but that wouldn't work in IE.
To enable tracking for an element call $(…).trackRemoval() and the element will fire a removed event when you remove it or one of its parents.
// Create a scope so that our variables are not global
(function(){
/**
* True while unbinding removal tracking
*/
var isUntracking = false;
/**
* A reference that is only known here that nobody else can play with our special event.
*/
var dummy = function(){};
/**
* Special event to track removals. This could have any name but is invoked during jQuery's cleanup on removal to detach event handlers.
*/
jQuery.event.special.elementRemoved = {
remove: function(o){
if(o.handler===dummy && !isUntracking){
$(this).trigger('removed');
}
}
};
/**
* Starts removal tracking on an element
*/
jQuery.fn.trackRemoval = function(){
this.bind('elementRemoved', dummy);
};
/**
* Stops removal tracking on an element
*/
jQuery.fn.untrackRemoval = function(){
isUntracking = true;
this.unbind('elementRemoved', dummy);
isUntracking = false;
};
})();
The jsFiddle contains sample code for usage.
I don't know if it's that what you're looking for. Not really pretty code, just to show what I would like to use:
// Just for this script's sake, you'll want to do it differently
var body = $("body");
var element = $('#loadingBlock');
// Bind the "destructor" firing event
body.bind("elementDeleted", function (element) {
// your "destructor" code
});
// Trigger the event, delete element, can be placed in a function, overloaded, etc.
body.trigger("elementDeleted", element);
element.remove();
There are of course solutions based on watching the DOM directly but the problem is the browser compatibility. You should probably check out mutation events.
Try this:
(function($) {
var _remove = $.fn.remove;
$.fn.remove = function() {
this.trigger('remove'); // notify removal
return _remove.apply(this, arguments); // call original
};
})(jQuery);
usage:
$(element).bind('remove', callback);
...
// some time later
$(element).remove();
See a working example at http://jsfiddle.net/alnitak/25Pnn/
You could always Overload the Remove function (all javascript objects can be dynamically changes in runtime) and replace it with your own function that triggers an event you can use to react to remove.

Attach multiple jQuery plugins onto a single element

According to the jQuery plugin development guides from the Internet, the common practice of developing a jQuery plugin would be:
(function($) {
$.fn.myplugin = function(options){
//...
//Plugin common characteristic
//e.g. default settings
//...
//Attach to each desired DOM element
return this.each(function(){
//Instantiation stuff...
});
}
})(jQuery);
$(document).ready(function(){
$(".someclass").myplugin();
})
It seems to me that, if the elements with class "someclass" have been attached to another plugin, once those elements are going to attach to "myplugin", they will lose the original relationship to the previously attached plugin. I'm not sure if my thinking is completely correct. Please advise if any mis-understood.
Thank you!
William Choi
An element isn't "attached" to a plug-in. A plug-in just adds further methods to the jQuery wrapper for a matched set of elements. So just as the jQuery wrapper has parent and find, it also has the plug-in's myplugin method. These can all coexist as long as there are no naming conflicts.
It's true that if two different plug-ins both try to change something about the elements that cannot be two things at once (a plug-in that changes the foreground color to "blue" and another changing the foreground color to "red"), then they'd collide if you called both of the two plug-ins methods on the same element set. But that's just like two calls to css.
In particular, remember that there can be multiple event handlers assigned to the same event on the same element, so plug-ins that hook events need not necessarily conflict with one another (unless one of them stops the event during handling).
Here's an example of two plug-ins that act on the matched set of elements, but in non-conflicting ways:
plugin1.js:
(function($) {
$.fn.foo = function() {
this.css("background-color", "#b00");
return this;
};
})(jQuery);
plugin2.js:
(function($) {
$.fn.bar = function() {
this.css("color", "white");
return this;
};
})(jQuery);
Usage:
$("#target").foo();
$("#target").bar();
or even
$("#target").foo().bar();
Live example
Now, if both the foo and bar plug-ins tried to set the foreground color, the one called later would win.
Here's an example of a pair of plug-ins that both want to handle the click event, but do so in a cooperative way:
plugin1.js:
(function($) {
$.fn.foo = function() {
this.click(function() {
$("<p>Click received by foo</p>").appendTo(document.body);
});
return this;
};
})(jQuery);
plugin2.js:
(function($) {
$.fn.bar = function() {
this.click(function() {
$("<p>Click received by bar</p>").appendTo(document.body);
});
return this;
};
})(jQuery);
Usage:
jQuery(function($) {
$("#target").foo().bar();
});
Live example
There's no magical relationship going on. There's no central registry or snap-ins that "belong" to any one element or to any one plug-in.
Javascript objects are just hacked-up functions; when you "attach a plugin" to an element, you're just calling some third-party library function that does something to that element, and possibly stores some internal data to assist with its animation throughout the session.
So there is nothing legally stopping you from "attaching" multiple plug-ins to the same element, though of course whether they'll be logically compatible is quite another question.

Only register a function with an event once?

I have a jQuery plugin that needs to register a click event handler:
$.fn.myPlugin = function (options) {
var settings = {
// snipped
};
$.extend(settings, options || {});
$("body").click(function () {
// Do Something
});
// Rest of the plugin
});
The problem is that multiple invocations register the function more than once. Since the function needs to stay attached, I can't use .one().
Is there a way if a function is already attached? Can I give it a name or so? Or do I have to set some boolean flag using closure magic?
Namespace your events.
$('body').unbind('click.myPlugin').bind('click.myPlugin', function() {
..code...
});
More on Namespaced Events.
A very easy method with good performance would be to set a data element on the body element:
if (!$.data(document.body, 'myPluginRegistered') {
$.data(document.body, 'myPluginRegistered', true);
// do your plugin code here
}
Easiest might be the boolean plus closure magic you suggested. Alternatively, you could get the list of all functions bound to "click" object, and see if the function you're attaching is there already.
This question shows how to get the list.
List all javascript events wired up on a page using jquery
Though, the namespace suggestion that came in after I first responded is probably simpler.

Categories