I have been trying to learn how to build jQuery plugins, so I'm still new at this. Since this is my first plugin, I decided I would try a simple collapsible panel/box (you can only read so much, right?). I'm having difficulty accessing my reference to my Javascript class object when the click event triggers. I know that this inside of the event refers to the element that triggered the event. I also know that I can do something like _self = this; before the event, but that will only cache the last object from the collection. Does anyone have any suggestions on how I can keep a reference to the class object?
Thanks!
Here is my code.
HTML code
<div class="mypanel" title="Panel 1">Testing panel 1</div>
<div class="mypanel" title="Panel 2">Testing panel 2</div>
$('.mypanel').collapsiblePanel();
Plugin code
var contentVisible = 'showContent', contentNotVisible = 'hideContent';
;(function($) {
var pluginName = 'collapsibleBox';
function Plugin(element, options) {
this.ele = element;
this.$ele = $(element);
var _self = this;
this.options = $.extend({}, $.fn[pluginName].defaults, options);
this.init();
/* Expose methods of Plugin we wish to be public.
* This gets stored when the plugin is created
*/
return {
option: this.option,
destroy: this.destroy
/* Other public methods here */
}
}
$.fn[pluginName] = function(options) {
/* If the first parameter is a string, treat this as a call to a public method. */
if(typeof arguments[0] === 'string') {
var methodName = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
var returnVal;
this.each(function() {
/* Check that the element has a plugin instance, and that
* the requrested public method exists.
*/
if($.data(this, 'plugin_' + pluginName) && typeof $.data(this, 'plugin_' + pluginName)[methodName] === 'function') {
/* Call the method of the Plugin instance, and Pass it the supplied arguments */
returnVal = $.data(this, 'plugin_' + pluginName)[methodName].appy(this, args);
}
else {
$.error('Method ' + methodName + ' does not exist on jQuery.' + pluginName);
}
});
if(returnVal !== undefined) {
/* If the method returned something, return it */
return returnVal;
}
else {
/* Otherwise, returning 'this' preserves chainability */
return this;
}
}
/* If the first parameter is an object (options), or was omitted,
* instantiate a new instance of the plugin
*/
else if(typeof options === 'object' || !options) {
return this.each(function() {
/* Only allow the plugin to be instantiated once */
if(!$.data(this, 'plugin_' + pluginName)) {
/* Pass options to Plugin constructor, and store Plugin
* instance in the element's jQuery data object
*/
$.data(this, 'plugin_' + pluginName, new Plugin(this, options));
}
});
}
};
$.fn[pluginName].defaults = {
onInit: function() {},
onDestroy: function() {}
};
Plugin.prototype = {
init: function() {
this.createContentArea();
this.createTitleBar();
this.hook('onInit');
},
createContentArea: function() {
/* ... */
},
createTitleBar: function() {
/* ... */
this.$title.click(function() {
if(this.$ele.data('state') == contentVisible) { // The problem is here
this.collapse();
}
else {
this.expand();
}
});
},
expand: function() {
this.$content.slideDown();
this.$ele.data('state', contentVisible);
},
collapse: function() {
console.log(this);
this.$content.slideUp();
this.$ele.data('state', contentNotVisible);
},
/* Use this function to get/set variables */
option: function (key, val) {
if(val) {
this.options[key] = val;
}
else {
return this.options[key];
}
},
destroy: function () {
/* ... */
},
hook: function (hookName) {
if(this.options[hookName] !== undefined) {
this.options[hookName].call(this.ele);
}
}
};
})(jQuery);
I have managed to fix this issue using a combination of the .data() method and the .call() method to ensure that the context of the function call is always the parent element. I managed to get this working using the pattern I had as well as using the pattern suggested by the jQuery Plugin Authoring Tutorial. Whether this was the correct way to do it or not, I don't know.
var methods = {
init: function(options) {
var $this = $(this), data = {};
/* Set a local copy of the options */
data.options = $.extend({}, defaults, options);
/* Create the content area */
data.$content = createContentArea(this);
/* Create the title bar */
data.$title = createTitleBar(this, data.options.header, data.options.title);
/* Show/Hide the content based on the default state passed in */
if(data.options.defaultState == contentVisible) {
data.$content.show();
data.state = contentVisible;
}
else {
data.$content.hide();
data.state = contentNotVisible;
}
/* Add the title click event */
data.$title.click(function() {
methods.toggle.call($(this).parent());
});
/* Add the public methods */
data.methods = {
toggle: methods.toggle,
expand: methods.expand,
collapse: methods.collapse
};
return data;
},
toggle: function() {
var $this = $(this), data = $this.data('plugin_' + pluginName);
if(data.state == contentVisible) {
methods.collapse.call(this);
}
else if(data.state == contentNotVisible) {
methods.expand.call(this);
}
},
expand: function() {
var $this = $(this), data = $this.data('plugin_' + pluginName);
data.$content.slideDown();
data.state = contentVisible;
$this.data('plugin_' + pluginName, data);
},
collapse: function() {
var $this = $(this), data = $this.data('plugin_' + pluginName);
data.$content.slideUp();
data.state = contentNotVisible;
$this.data('plugin_' + pluginName, data);
}
};
Related
Here's my plugin code:
( function() {
this.Modal = function( selector, options ) {
// Define option defaults
var defaults = {
open: false
}
this.options = extendDefaults( defaults, options );
alert();
}
function extendDefaults( source, properties ) {
var property;
for ( property in properties ) {
if ( properties.hasOwnProperty( property ) ) {
source[ property ] = properties[ property ];
}
}
return source;
}
}() );
Simply I need a way to prevent call the plugin again for the SAME selector if it has already called.
To be more clear if i try to initialize the plugin by doing this:
var firstSeelctor = new Modal( '.button' );
var secondSeelctor = new Modal( '.button' );
I need to call the first one and ignore the second one because it's already called for the same selector at the first one.
You need to store selectors somewhere (directly in the function constructor for example) that you have already created and then check it during every instance creation.
(function() {
this.Modal = function modal(selector, options) {
if (!modal.instances) {
modal.instances = {};
}
if (modal.instances[selector]) {
return modal.instances[selector];
}
modal.instances[selector] = this;
var defaults = {
open: false
};
this.options = extendDefaults(defaults, options);
console.log('created for selector: ' + selector);
// alert();
}
function extendDefaults(source, properties) {
var property;
for (property in properties) {
if (properties.hasOwnProperty(property)) {
source[property] = properties[property];
}
}
return source;
}
var firstSeelctor = new Modal('.button');
var secondSeelctor = new Modal('.button');
var thirdSeelctor = new Modal('.button-2');
console.log(firstSeelctor === secondSeelctor); // true, because it's the same instances
}());
I have a jQuery plugin, and I want to be able to change options on the fly, like this example: $('.element').pwstabs('options','effect',scale) or something simular to it. I tried adding update: function, tried adding Plugin.prototype.update, but still cant figure out how to do that :)
Here's the structure of the plugin:
;(function ($, window, document, undefined) {
var pluginName = "pwstabs",
defaults = {
effect: 'scaleout',
defaultTab: 1,
containerWidth: '100%',
tabsPosition: 'horizontal',
horizontalPosition: 'top',
verticalPosition: 'left',
responsive: false,
theme: '',
rtl: false,
controlls: false,
next: '',
prev: '',
first: '',
last: '',
auto: false,
play: '',
pause: ''
};
function Plugin(element, options) {
this.element = $(element);
this.$elem = $(this.element);
this.settings = $.extend({}, defaults, options);
this._defaults = defaults;
this._name = pluginName;
this.init();
}
Plugin.prototype = {
init: function(){
// Here's the code for the plugin
}
};
$.fn[pluginName] = function ( options ) {
return this.each(function () {
new Plugin( this, options );
});
};
})(jQuery, window, document);
So now I use the plugin like:
$('.element').pwstabs({
effect: 'scalein',
defaultTab: 2
});
And when I click a button, i want to change effect to lets say scaleout. With code like:
$('.button').click(function(){
$('.element').pwstabs('options','effect','scalein');
});
So how do I implement this in the plugin?
Currently the only supported invocation pattern in that plugin is to send in an object literal containing the settings to overwrite the defaults. E.g.:
$('.element').pwstabs({
effect: 'scalein',
defaultTab: 2
});
That invocation pattern is defined in the following method:
$.fn[pluginName] = function ( options ) {
return this.each(function () {
new Plugin( this, options );
});
};
As you see, a dictionary of options is sent as the only parameter to the constructor function Plugin() to build the plugin and initialize it.
To support the invocation pattern you need, you would have to modify this method to support both invocation patterns (initialization with an object literal, but also invoking any method with more params, like your options setting method).
Here is an improved function that will handle both invocation patterns. In addition it will also store the instance of a plugin on an element, so you can access the existing settings etc. on subsequent invocations (e.g. settings changes) on the same element.
$.fn[pluginName] = function (options) {
// get the arguments
var args = $.makeArray(arguments),
after = args.slice(1);
return this.each(function () {
// check if there is an existing instance related to element
var instance = $.data(this, pluginName);
if (instance) {
if (instance[options]) {
instance[options].apply(instance, after);
} else {
$.error('Method ' + options + ' does not exist on Plugin');
}
} else {
// create the plugin
var plugin = new Plugin(this, options);
// Store the plugin instance on the element
$.data(this, pluginName, plugin);
return plugin;
}
});
}
This would allow you to invoke the plugin as requested:
$('.element').pwstabs('options','effect','slidedown');
However, this implies you have an 'options' method in the Plugin prototype, so make sure to add one:
Plugin.prototype = {
options: function (option, val) {
this.settings[option] = val;
},
// Constructing Tabs Plugin
init: function () {
// omitted code for brevity
}
}
As you see the options settings just sets the new option on the existing instance. Very simple and efficient. The new setting will be picked up by the click method handler and voila!
Here is a jsFiddle with example code in case you have trouble implementing what i was describing so far:
http://jsfiddle.net/7whs3u1n/6/
Update: I have much improved my answer to get rid of unneeded stuff, include more details and a full implementation that works (check the fiddle above) ;) i hope that this answers your question!
Adding statefulness to your plugin wasn't hard, but when you have spare time also check the alternative mechanism for writing stateful jQuery stateful plugins called jQuery widget factory:
http://learn.jquery.com/plugins/stateful-plugins-with-widget-factory/
In the future you can consider rewriting your plugin to use the widget factory. It would certainly make your code simpler ;)
Try this pattern
(function ($) {
var defaults = {
"text": "abcdefg",
}
, options = $.extend({}, defaults, options);
$.fn.plugin = function (options) {
var options = (function (opts, def) {
var _opts = {};
if (typeof opts[0] !== "object") {
_opts[opts[0]] = opts[1];
};
return opts.length === 0
? def
: typeof opts[0] === "object"
? opts[0] : _opts
}([].slice.call(arguments), defaults));
return $(this).text(options.text)
}
}(jQuery));
$(".results:eq(0)").plugin(); // return `defaults`
$(".results:eq(1)").plugin({"text":"gfedcba"}); // return `options`
$(".results:eq(2)").plugin("text", 123); // return `arguments` as `options`
(function ($) {
var defaults = {
"text": "abcdefg",
}
, options = $.extend({}, defaults, options);
$.fn.plugin = function (options) {
var options = (function (opts, def) {
var _opts = {};
if (typeof opts[0] !== "object") {
_opts[opts[0]] = opts[1];
};
return opts.length === 0
? def
: typeof opts[0] === "object"
? opts[0] : _opts
}([].slice.call(arguments), defaults));
return $(this).text(options.text)
}
}(jQuery));
$(".results:eq(0)").plugin(); // return `defaults`
$(".results:eq(1)").plugin({"text":"gfedcba"}); // return `options`
$(".results:eq(2)").plugin("text", 123); // return `arguments` as `options`
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div class="results"></div><br />
<div class="results"></div><br />
<div class="results"></div>
I have this class made in javascript :
function MyClass(id)
{
this.attribID = this.getID(id);
// ==========================================================================
this.getID = function(id) { return ("tooltip_" + id); }
// ==========================================================================
}
When I call it from my HTML with :
<script>
var a = new MyClass("onecell");
</script>
it generates this error : this.getID is not a function
How may I init my attribute with a generic function defined inside MyClass, function that can also be called from many other places than the init one ?
Just clarifying what was sugested by #RobG
There are two ways of doing it.
Change the order of assignments:
function MyClass(id)
{
this.getID = function(id) { return ("tooltip_" + id); }
this.attribID = this.getID(id);
}
Or using the class prototype:
function MyClass(id)
{
this.attribID = this.getID(id);
}
MyClass.prototype.getID = function(id) { return ("tooltip_" + id); };
Just move the this.getID function like so: JSFiddle
function MyClass(id)
{
// ==========================================================================
this.getID = function(id) { return ("tooltip_" + id); }
// ==========================================================================
this.attribID = this.getID(id);
}
I have a jQuery Plugin which accept multiple elements and some methods to be called like:
(function($){
methods = {
init : function( options, callbacks) {
$.fn.myPlugin.settings = $.extend({
'userDefinedMethod': function() {}
}, options);
return this.each(function(){
$.fn.myPlugin.settings.userDefinedMethod();
}
}
}
$.fn.myPlugin = function(method) {
if ( methods[method] ) {
return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exists on jQuery.myPlugin' );
}
}
})(jQuery);
An simple example which will make you understand what I want to achieve:
$(document).ready(function(){
$('#myElement1, #myElement2, #myElement3').myPlugin({
userDefinedMethod: function() {
// I want here to use the elements in selector
$(this).css('color', 'black');
}
});
});
I know that $(this) in the example above will represent the jQuery Plugin Object but I want somehow to use each element in the provided selector.
$(document).ready(function () {
$('#myElement1, #myElement2, #myElement3').myPlugin({
userDefinedMethod: function () {
// I want here to use the elements in selector
$(this).css('color', 'red');
}
});
});
(function ($) {
methods = {
init: function (options, callbacks) {
//don't set the settings to shared object
this.settings = $.extend({
userDefinedMethod: $.noop
}, options);
return this.each($.proxy(function (idx, el) {
//use Function.call() to set a custom execution context
this.settings.userDefinedMethod.call(el);
}, this))
}
}
$.fn.myPlugin = function (method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exists on jQuery.myPlugin');
}
}
})(jQuery);
Demo: Fiddle
In methods.init function this will be the jQuery object obtained by quering the selector. So, if you want to send this to userDefinedMethod just use apply or call when you call that function:
...
var methods = {
init : function( options, callbacks) {
$.fn.myPlugin.settings = $.extend({
'userDefinedMethod': function() {}
}, options);
$.fn.myPlugin.settings.userDefinedMethod.call(this);
// or if you want to send the arguments
// $.fn.myPlugin.settings.userDefinedMethod.apply(this, arguments);
return this;
}
}
...
Also, don't forget that you didn't use var for declaring methods. methods will become a magic global variable...
I also corrected the missing ) that was generating a syntax error.
JSFIDDLE
I'm trying to write a simple jQuery plugin that follows similar structure to the one below. The problem I'm having is that when I initialize the plugin the value plugin.myValue is set to 1 however when I try to access that value in the talk function it's undefined. Can anyone help me refine this plugin structure so when I set values on the plugin object they can be accessed in different methods.
Please keep in mind that the plugin below is not my actual use case it is just a simple example to illustrate the problem I'm having.
My actual use case would be very long because it is a jQuery carousel that I'm writing. If it will help I can provide that code however it's much longer and the below example follows the same flow. I would initialize this plugin with the following code:
$('#Div1').talk({ message: "test message" });
$('#Div1').talk('talk');
(function ($) {
$.fn.talk = function(method) {
var settings = {};
var $el = $(this);
var plugin = this;
var methods = {
init: function(options){
settings = $.extend(true, settings, this.talk.defaults, options);
return this.each(function() {
plugin.myValue = 1;
});
},
talk: function(){
helpers.talk(settings.message);
}
}
var helpers = {
talk: function(message){
if (plugin.myValue == 1)
{
alert(message);
}
}
}
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error( 'Method "' + method + '" does not exist in talk plugin!');
}
}
$.fn.talk.defaults = {
message: "default message"
}
})(jQuery);
When you define a plugin, this refers already to the jquery object (not the dom element), so I think your plugin var should go en each element, like this:
(function ($) {
$.fn.talk = function(method) {
var settings = {};
var $el = this; //Not need of $() here
var methods = {
init: function(options){
settings = $.extend(true, settings, this.talk.defaults, options);
return this.each(function(index, item) {
item.myValue = 1; //check this
});
},
talk: function(){
helpers.talk.call(this, settings.message); //talk needs scope
}
}
var helpers = {
talk: function(message){
var $elems = this;
if ($elems[0] && $elems[0].myValue == 1) //Something like this
{
alert(message);
}
}
}
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error( 'Method "' + method + '" does not exist in talk plugin!');
}
}
$.fn.talk.defaults = {
message: "default message"
}
})(jQuery);
I hope you get my idea. Don't forget that your selector might match to more than one element. You were stroring the data to the jquery object, and don't forget that it's a different one each time you call $("#yourDiv"), so your data was lost.
Note: It'd be cleaner to do $(item).data('myValue', 1); instead of item.myValue = 1; (and its proper retrieval), but it's a matter of choice
EDIT Option 2. This may look more similar to your code, but will work only when your selector only matched a single element
(function ($) {
$.fn.talk = function(method) {
var settings = {};
var $el = this; //Not need of $() here
var methods = {
init: function(options){
settings = $.extend(true, settings, this.talk.defaults, options);
$el.data("myValue", 1);
},
talk: function(){
helpers.talk(settings.message);
}
}
var helpers = {
talk: function(message){
if ($el.data("myValue") == 1)
{
alert(message);
}
}
}
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error( 'Method "' + method + '" does not exist in talk plugin!');
}
}
$.fn.talk.defaults = {
message: "default message"
}
})(jQuery);
Hope this helps. Cheers