I have several pages which I wish to allow the the user to inline edit many fields and update the server DB. To implement this, my intent is to create a jQuery plugin which I can do the typical passing of the configuration options and uses ajax to save the results.
(function($){
var methods = {
init : function (options) {return this.each(function () {/* ... */});},
method1 : function () {return this.each(function () {/* ... */});},
method2 : function () {return this.each(function () {/* ... */});}
};
$.fn.myEditPlugin= function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); //Line 10
} else if (typeof method === 'object' || ! method) {
return methods.init.apply(this, arguments); //Line 12
} else {
$.error('Method ' + method + ' does not exist on jQuery.myEditPlugin');
}
};
}(jQuery)
);
For each individual page, there are several options which are common to all (i.e. the url endpoint, the record's primary key, etc) and I would rather not duplicate each when applying the plugin.
Originally, I was just going to define a function on each page which takes some input and applies the common options to each.
function wrapEdit(e,options) {
options.url='/page1/etc';
options.pk=document.getElementById('pk').value;
return $(e).myEditPlugin(options);
}
wrapEdit('.someclass',{foo:123});
It doesn't seem all that professional to me, so in my obsessive quest, thought I would make a class which I could pass the common options to and it would apply the plugin.
class WrapEdit(options)
{
constructor(options) {
this.options = options;
}
this.applyIndividualOptions=function(e, options) {
return $(e).myEditPlugin(Object.assign({}, this->options, options));
}
}
var wrapEdit=new WrapEdit({url: '/page1/etc', pk: document.getElementById('pk').value});
wrapEdit.applyIndividualOptions('.someclass',{foo:123});
Better, but not very jQueryish as I will be passing the select element instead of directly applying the plugin to elements typical of jQuery.
Is it possible to create an instance of a jQuery plugin which keeps previously defined data? Maybe something like the following:
$.myEditPlugin({url: '/page1/etc', pk: document.getElementById('pk').value});
$('.someclass').myEditPlugin({foo:123}); //Will also pass previously defined url and pk to myEditPlugin
Or maybe best to create a custom jQuery plugin per page which just adds the extra options and initiates the real plugin...
$.fn.myEditPluginInstance = function(options) {
return this.myEditPlugin(Object.assign({url: '/page1/etc', pk: document.getElementById('pk').value}, options));
};
Creating a function to be called against a jquery collection
The basic idea is to define a new property (function) in jQuery.fn, before any call to your plugin is made (In other words, any code related to the application is executed). You can use an "Immediately Invoked Function Expressions" (a.k.a. IIFEs) to fence your plugin API in. Then you have to loop over the collection and execute any code your plugin needs to apply on the collection items.
Basic skeleton:
(function ($) {
// Enclosed scope (IIFE)
// You can define private API/variables in here
// …
// Once your plugin API is ready, you have to apply the magic to each item
// in the collection in some ways. You must add a property to jQuery.fn object.
$.fn.myAwesomePlugin = function(Opt) {
var defaultConfig = {option1: 'someValue' /*, …*/};
// Eval supplied Opt object (Validate, reject, etc.)
// If all goes well, eventually merge the object with defaults.
$.extend(defaultConfig, Opt);
// Apply the magic against each item in the jQuery collection
// (Your plugin may not need to use "each" function though)
// Return the jQuery collection anyway to keep chaining possible.
// Once again, this is not required, your plugin may return something else depending on the options passed earlier for instance.
return this.each(function(el, idx) {
// Your plugin magic applied to collection items…
});
}
})(jQuery);
You should be able to call your plugin $('someSelector').myAwesomePlugin(); right after the declaration.
Simple implementation example:
(function ($) {
let required = {url: null, pk: null}
// Function to be executed upon first call to the plugin
, populateCommons = () => {
let ep = $('#someNode').data('endpoint')
, pk = document.querySelector('#pk')
;
// Basic tests to alert in case the page
// doesn't comply with the plugin requirements
if( typeof ep !== 'string' || !/^\/[a-z]+/.test(ep) || !pk) {
throw ` "myEditPlugin" init phase error:
Detected endpoint: '${ep}'
Is PK value found: ${!!pk}
`;
}
[required.url, required.pk] = [ep, +pk.value];
};
$.fn.myEditPlugin = function(Opt) {
let allOpts;
// First call will trigger the retrival of common data
// that should be available as static data somewhere every page source.
!required.url && populateCommons();
allOpts = $.extend({}, Opt, required);
return this.each(function(el, idx) {
// Your logic here, request
console.log("Payload is", allOpts);
});
}
})(jQuery);
function debounce(fn, time) {
debounce.timer && (clearTimeout(debounce.timer));
debounce.timer = setTimeout(() => (fn(), debounce.timer = null), time);
}
$('[type="text"]').keydown(function(e){
debounce(() => this.value && $(this).myEditPlugin({foo:this.value, bar: 'Contextual value'}), 2000);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="pk" type="hidden" value="5">
<div id="someNode" data-endpoint="/api/endpoint">
Editing the below input will trigger the plug-in code
</div>
<input type="text" title="Edit me"/>
Related documentation here
I want to create jQuery plugin with config (for example plugin myplugin).
Than call $(elem).myplugin(config); After that I want to call methods from this plugin like $(elem).myplugin().method() with already stored config.
My offer is something like that:
(function($) {
$.fn.myplugin = function(options) {
var $this = $(this);
var getOptions = function() {
return $this.data('myplugin');
};
var initOptions = function(opt) {
$this.data('myplugin', opt);
};
var setOption = function(key, value) {
$this.data('myplugin')[key] = value;
}
var updateBorderWidth = function() {
$this.css('border-width',
getOptions().borderWidth * getOptions().coeficient);
};
var init = function(opt) {
initOptions(opt);
updateBorderWidth();
}
function changeBorder(width) {
setOption('borderWidth', width)
updateBorderWidth();
}
if(options) {
init(options);
}
return {
changeBorder : changeBorder
};
}
})(jQuery);
And usage:
$(function() {
var item1 = $('#test1').myplugin({ coeficient: 1, borderWidth: 1 });
var item1 = $('#test2').myplugin({ coeficient: 2, borderWidth: 1 });
$('#btn').click(updateBorder);
});
function updateBorder() {
$('#test1').myplugin().changeBorder($('#inpt').val());
$('#test2').myplugin().changeBorder($('#inpt').val());
}
Example: http://jsfiddle.net/inser/zQumX/4/
My question: is it a good practice to do that?
May be it's incorrect approach. Can you offer better solution?
Edit:
After searching for threads on jQuery plugin template I found these Boilerplate templates (updated) which are more versatile and extensive designs than what I've offered below. Ultimately what you choose all depends on what your needs are. The Boilerplate templates cover more use cases than my offering, but each has its own benefits and caveats depending on the requirements.
Typically jQuery plugins either return a jQuery object when a value is passed to them as in:
.wrap(html) // returns a jQuery object
or they return a value when no parameter is passed in
.width() // returns a value
.height() // also returns a value
To read your example calling convention:
$('#test1').myplugin().changeBorder($('#inpt').val());
it would appear, to any developer who uses jQuery, as though two separate plugins are being utilized in tandem, first .myplugin() which one would assume will return a jQuery object with some default DOM maniplulation performed on #test1, then followed by .changeBorder($('#inpt').val()) which may also return a jQuery object but in the case of your example the whole line is not assigned to a variable so any return value is not used - again it looks like a DOM manipulation. But your design does not follow the standard calling convention that I've described, so there may be some confusion to anyone looking at your code as to what it actually does if they are not familiar with your plugin.
I have, in the past, considered a similar problem and use case to the one you are describing and I like the idea of having a convenient convention for calling separate functions associated with a plugin. The choice is totally up to you - it is your plugin and you will need to decide based on who will be using it, but the way that I have settled on is to simply pass the name of the function and it's parameters either as a separate .myplugin(name, parameters) or in an object as .myplugin(object).
I typically do it like so:
(function($) {
$.fn.myplugin = function(fn, o) { // both fn and o are [optional]
return this.each(function(){ // each() allows you to keep internal data separate for each DOM object that's being manipulated in case the jQuery object (from the original selector that generated this jQuery) is being referenced for later use
var $this = $(this); // in case $this is referenced in the short cuts
// short cut methods
if(fn==="method1") {
if ($this.data("method1")) // if not initialized method invocation fails
$this.data("method1")() // the () invokes the method passing user options
} else if(fn==="method2") {
if ($this.data("method2"))
$this.data("method2")()
} else if(fn==="method3") {
if ($this.data("method3"))
$this.data("method3")(o) // passing the user options to the method
} else if(fn==="destroy") {
if ($this.data("destroy"))
$this.data("destroy")()
}
// continue with initial configuration
var _data1,
_data2,
_default = { // contains all default parameters for any functions that may be called
param1: "value #1",
param2: "value #2",
},
_options = {
param1: (o===undefined) ? _default.param1 : (o.param1===undefined) ? _default.param1 : o.param1,
param2: (o===undefined) ? _default.param2 : (o.param2===undefined) ? _default.param2 : o.param2,
}
method1 = function(){
// do something that requires no parameters
return;
},
method2 = function(){
// do some other thing that requires no parameters
return;
},
method3 = function(){
// does something with param1
// _options can be reset from the user options parameter - (o) - from within any of these methods as is done above
return;
},
initialize = function(){
// may or may not use data1, data2, param1 and param2
$this
.data("method1", method1)
.data("method2", method2)
.data("method3", method3)
.data("destroy", destroy);
},
destroy = function(){
// be sure to unbind any events that were bound in initialize(), then:
$this
.removeData("method1", method1)
.removeData("method2", method2)
.removeData("method3", method3)
.removeData("destroy", destroy);
}
initialize();
}) // end of each()
} // end of function
})(jQuery);
And the usage:
var $test = $('#test').myplugin(false, {param1: 'first value', param2: 'second value'}); // initializes the object
$test.myplugin('method3', {param1: 'some new value', param2: 'second new value'}); // change some values (method invocation with params)
or you could just say:
$('#test').myplugin(); // assume defaults and initialize the selector
Passing parameters to javascript via data attributes is a great pattern, as it effectively decouples the Javascript code and the server-side code. It also does not have a negative effect on the testability of the Javascript code, which is a side-effect of a lot of other approaches to the problem.
I'd go as far as to say it is the best way for server-side code to communicate with client-side code in a web application.
I am in process of learning more about javascript plugins and found one that is of interest to me. I am willing to get my feet dirty and see how this thing can be modified...
(function( $ ){
var methods = {
init : function( options ) {
return this.each(function(){
var $this = $(this),
data = $this.data('tooltip'),
tooltip = $('<div />', {
text : $this.attr('title')
});
// If the plugin hasn't been initialized yet
if ( ! data ) {
console.log('still working..' );
/*
Do more setup stuff here
*/
$(this).data('tooltip', {
target : $this,
tooltip : tooltip
});
}
});
},
show : function( ) {
console.log('this is the show');
},
hide : function( ) {
// GOOD
},
update : function( content ) {
console.log('this is the update');
// !!!
}
};
$.fn.tooltip = function( method ) {
// Method calling logic
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 on jQuery.tooltip' );
}
};
})( jQuery );
ok I have 4 questions...
1.how do you initialize this plugin? i keep getting 'still working..' with my console log when i try to run a random div element, $('#mtest').tooltip();.
2 the init: is inside the var method, which is private, meaning I can't access init: from outside of this plugin? right? where would i put initializing logic at since it appears to be returning options...?
3.
I am confused about this part of the code...
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 on jQuery.tooltip' );
}
I know its returning all the methods, but...
3a. why write methods[method]// is looks like [method] is an array, and that looks confusing to me because I don't see an array, its a bunch of methods...
3b. what is the else checking for? or why would an error occur?
thanks for any advice on helping me fully understand this plugin!
I don't know what your getting at with the first question. But the other questions can be solved pretty easily.
First, lets go over 3.
The code you have, and what jQuery provides in their docs, is merely a sort of "getter" between you and your methods. Instead of clustering up a namespace with all of your methods, you put your methods into an object title methods (which is instantiated on the second line of your first block of code.
If you look at the jQuery provided code you are asking about, its not returning methods as you've thought. Its calling the method of the key in your methods object. The first if statement says that if you call your plugin (in your case, tooltip) with a string variable, it will look up that index in the methods object and Call the function.
The second else if block says that if you pass a object as a parameter OR no parameter, it will call your init method. This is sort of like a custom built getter/initializer for your plugin.
So now, to answer your second question, the init method can be accessed by either calling your tooltip plugin with..
1) no parameters
2) a object parameter (usually options such as {"someOption":true,"anotherOption":400})
3) the string 'init' as in $('#id').tooltip('init')
This way you can also access your show and hide methods with...
$('#id).tooltip('hide') ... and so forth.
You can read up on this in the jQuery docs for much more detail. This is me merely putting it into layman's terms.
I need to build a jQuery plugin that would return a single instance per selector id. The plugin should and will only be used on elements with id (not possible to use selector that matches many elements), so it should be used like this:
$('#element-id').myPlugin(options);
I need to be able to have few private methods for the plugin as well as few public methods. I can achieve that but my main issue is that I want to get the very same instance every time I call $('#element-id').myPlugin().
And I want to have some code that should be executed only the first time the plugin is initialized for a given ID (construct).
The options parameter should be supplied the first time, for the construct, after that I do not want the construct to be executed, so that I can access the plugin just like $('#element-id').myPlugin()
The plugin should be able to work with multiple elements (usually up to 2) on the same page (but each and every one of them will need own config, again - they will be initialized by ID, not common class selector for example).
The above syntax is just for example - I'm open for any suggestions on how to achieve that pattern
I have quite some OOP experience with other language, but limited knowledge of javascript and I'm really confused on how do it right.
EDIT
To elaborate - this plugin is a GoogleMaps v3 API wrapper (helper) to help me get rid of code duplication as I use google maps on many places, usually with markers. This is the current library (lots of code removed, just most important methods are left to see):
;(function($) {
/**
* csGoogleMapsHelper set function.
* #param options map settings for the google maps helper. Available options are as follows:
* - mapTypeId: constant, http://code.google.com/apis/maps/documentation/javascript/reference.html#MapTypeId
* - mapTypeControlPosition: constant, http://code.google.com/apis/maps/documentation/javascript/reference.html#ControlPosition
* - mapTypeControlStyle: constant, http://code.google.com/apis/maps/documentation/javascript/reference.html#MapTypeControlStyle
* - mapCenterLatitude: decimal, -180 to +180 latitude of the map initial center
* - mapCenterLongitude: decimal, -90 to +90 latitude of the map initial center
* - mapDefaultZoomLevel: integer, map zoom level
*
* - clusterEnabled: bool
* - clusterMaxZoom: integer, beyond this zoom level there will be no clustering
*/
$.fn.csGoogleMapsHelper = function(options) {
var id = $(this).attr('id');
var settings = $.extend(true, $.fn.csGoogleMapsHelper.defaults, options);
$.fn.csGoogleMapsHelper.settings[id] = settings;
var mapOptions = {
mapTypeId: settings.mapTypeId,
center: new google.maps.LatLng(settings.mapCenterLatitude, settings.mapCenterLongitude),
zoom: settings.mapDefaultZoomLevel,
mapTypeControlOptions: {
position: settings.mapTypeControlPosition,
style: settings.mapTypeControlStyle
}
};
$.fn.csGoogleMapsHelper.map[id] = new google.maps.Map(document.getElementById(id), mapOptions);
};
/**
*
*
* #param options settings object for the marker, available settings:
*
* - VenueID: int
* - VenueLatitude: decimal
* - VenueLongitude: decimal
* - VenueMapIconImg: optional, url to icon img
* - VenueMapIconWidth: int, icon img width in pixels
* - VenueMapIconHeight: int, icon img height in pixels
*
* - title: string, marker title
* - draggable: bool
*
*/
$.fn.csGoogleMapsHelper.createMarker = function(id, options, pushToMarkersArray) {
var settings = $.fn.csGoogleMapsHelper.settings[id];
markerOptions = {
map: $.fn.csGoogleMapsHelper.map[id],
position: options.position || new google.maps.LatLng(options.VenueLatitude, options.VenueLongitude),
title: options.title,
VenueID: options.VenueID,
draggable: options.draggable
};
if (options.VenueMapIconImg)
markerOptions.icon = new google.maps.MarkerImage(options.VenueMapIconImg, new google.maps.Size(options.VenueMapIconWidth, options.VenueMapIconHeight));
var marker = new google.maps.Marker(markerOptions);
// lets have the VenueID as marker property
if (!marker.VenueID)
marker.VenueID = null;
google.maps.event.addListener(marker, 'click', function() {
$.fn.csGoogleMapsHelper.loadMarkerInfoWindowContent(id, this);
});
if (pushToMarkersArray) {
// let's collect the markers as array in order to be loop them and set event handlers and other common stuff
$.fn.csGoogleMapsHelper.markers.push(marker);
}
return marker;
};
// this loads the marker info window content with ajax
$.fn.csGoogleMapsHelper.loadMarkerInfoWindowContent = function(id, marker) {
var settings = $.fn.csGoogleMapsHelper.settings[id];
var infoWindowContent = null;
if (!marker.infoWindow) {
$.ajax({
async: false,
type: 'GET',
url: settings.mapMarkersInfoWindowAjaxUrl,
data: { 'VenueID': marker.VenueID },
success: function(data) {
var infoWindowContent = data;
infoWindowOptions = { content: infoWindowContent };
marker.infoWindow = new google.maps.InfoWindow(infoWindowOptions);
}
});
}
// close the existing opened info window on the map (if such)
if ($.fn.csGoogleMapsHelper.infoWindow)
$.fn.csGoogleMapsHelper.infoWindow.close();
if (marker.infoWindow) {
$.fn.csGoogleMapsHelper.infoWindow = marker.infoWindow;
marker.infoWindow.open(marker.map, marker);
}
};
$.fn.csGoogleMapsHelper.finalize = function(id) {
var settings = $.fn.csGoogleMapsHelper.settings[id];
if (settings.clusterEnabled) {
var clusterOptions = {
cluster: true,
maxZoom: settings.clusterMaxZoom
};
$.fn.csGoogleMapsHelper.showClustered(id, clusterOptions);
var venue = $.fn.csGoogleMapsHelper.findMarkerByVenueId(settings.selectedVenueId);
if (venue) {
google.maps.event.trigger(venue, 'click');
}
}
$.fn.csGoogleMapsHelper.setVenueEvents(id);
};
// set the common click event to all the venues
$.fn.csGoogleMapsHelper.setVenueEvents = function(id) {
for (var i in $.fn.csGoogleMapsHelper.markers) {
google.maps.event.addListener($.fn.csGoogleMapsHelper.markers[i], 'click', function(event){
$.fn.csGoogleMapsHelper.setVenueInput(id, this);
});
}
};
// show the clustering (grouping of markers)
$.fn.csGoogleMapsHelper.showClustered = function(id, options) {
// show clustered
var clustered = new MarkerClusterer($.fn.csGoogleMapsHelper.map[id], $.fn.csGoogleMapsHelper.markers, options);
return clustered;
};
$.fn.csGoogleMapsHelper.settings = {};
$.fn.csGoogleMapsHelper.map = {};
$.fn.csGoogleMapsHelper.infoWindow = null;
$.fn.csGoogleMapsHelper.markers = [];
})(jQuery);
It's usage looks like this (not actually exactly like this, because there is a PHP wrapper to automate it with one call, but basically):
$js = "$('#$id').csGoogleMapsHelper($jsOptions);\n";
if ($this->venues !== null) {
foreach ($this->venues as $row) {
$data = GoogleMapsHelper::getVenueMarkerOptionsJs($row);
$js .= "$.fn.csGoogleMapsHelper.createMarker('$id', $data, true);\n";
}
}
$js .= "$.fn.csGoogleMapsHelper.finalize('$id');\n";
echo $js;
The problems of the above implementation are that I don't like to keep a hash-map for "settings" and "maps"
The $id is the DIV element ID where the map is initialized. It's used as a key in the .map and .settings has maps where I hold the settings and GoogleMaps MapObject instance for each initialized such GoogleMaps on the page. The $jsOptions and $data from the PHP code are JSON objects.
Now I need to be able to create a GoogleMapsHelper instance that holds its own settings and GoogleMaps map object so that after I initialize it on certain element (by its ID), I can reuse that instance. But if I initialize it on N elements on the page, each and every of them should have own configuration, map object, etc.
I do not insist that this is implemented as a jQuery plugin! I insist that it's flexible and extendable, because I will be using it in a large project with over dozen currently planned different screens where it will be used so in few months, changing it's usage interface would be a nightmare to refactor on the whole project.
I will add a bounty for this.
When you say "get" the instance via $('#element').myPlugin() I assume you mean something like:
var instance = $('#element').myPlugin();
instance.myMethod();
This might seem to be a good idea at first, but it’s considered bad practice for extending the jQuery prototype, since you break the jQuery instance chain.
Another handy way to do this is to save the instance in the $.data object, so you just initialize the plugin once, then you can fetch the instance at any time with just the DOM element as a reference, f.ex:
$('#element').myPlugin();
$('#element').data('myplugin').myMethod();
Here is a pattern I use to maintain a class-like structure in JavaScript and jQuery (comments included, hope you can follow):
(function($) {
// the constructor
var MyClass = function( node, options ) {
// node is the target
this.node = node;
// options is the options passed from jQuery
this.options = $.extend({
// default options here
id: 0
}, options);
};
// A singleton for private stuff
var Private = {
increaseId: function( val ) {
// private method, no access to instance
// use a bridge or bring it as an argument
this.options.id += val;
}
};
// public methods
MyClass.prototype = {
// bring back constructor
constructor: MyClass,
// not necessary, just my preference.
// a simple bridge to the Private singleton
Private: function( /* fn, arguments */ ) {
var args = Array.prototype.slice.call( arguments ),
fn = args.shift();
if ( typeof Private[ fn ] == 'function' ) {
Private[ fn ].apply( this, args );
}
},
// public method, access to instance via this
increaseId: function( val ) {
alert( this.options.id );
// call a private method via the bridge
this.Private( 'increaseId', val );
alert( this.options.id );
// return the instance for class chaining
return this;
},
// another public method that adds a class to the node
applyIdAsClass: function() {
this.node.className = 'id' + this.options.id;
return this;
}
};
// the jQuery prototype
$.fn.myClass = function( options ) {
// loop though elements and return the jQuery instance
return this.each( function() {
// initialize and insert instance into $.data
$(this).data('myclass', new MyClass( this, options ) );
});
};
}( jQuery ));
Now, you can do:
$('div').myClass();
This will add a new instance for each div found, and save it inside $.data. Now, to retrive a certain instance an apply methods, you can do:
$('div').eq(1).data('myclass').increaseId(3).applyIdAsClass();
This is a pattern I have used many times that works great for my needs.
You can also expose the class so you can use it without the jQuery prototyp by adding window.MyClass = MyClass. This allows the following syntax:
var instance = new MyClass( document.getElementById('element'), {
id: 5
});
instance.increaseId(5);
alert( instance.options.id ); // yields 10
Here's an idea...
(function($){
var _private = {
init: function(element, args){
if(!element.isInitialized) {
... initialization code ...
element.isInitialized = true;
}
}
}
$.fn.myPlugin(args){
_private.init(this, args);
}
})(jQuery);
...and then you can add more private methods. If you want to 'save' more data, you can use the element passed to the init function and save objects to the dom element... If you're using HTML5, you can use data- attributes on the element instead.
EDIT
Another thing came to mind. You could use jQuery.UI widgets.
I think what you need to solve your problem is basically a good OO structure to hold both your setting and GoogleMap.
If you are not tied to jQuery and know OOP pretty well, I would use YUI3 Widget.
A glance at the Sample Widget Template should give you an idea that the framework provide access to the OOP structure such as:
It provides Namespace support.
It support notion of classes and objects
It supports class extension neatly
It provides constructor and destructor
It supports the concept of instance variables
It provides render and event binding
In your case:
You can create your GoogleHelper class which has its own instance variables along with the Google Map object which I think is what you intended.
You would then start creating the instance of this class with its own settings.
For each new instance, you will just have to map it with an ID that you could refer it later. By referencing the ID to the GoogleHelper instance that has both the settings and GoogleMap, you don't have to keep two maps (one to hold the setting and one for the GoogleMap) which I happen to agree with you that it is not an ideal situation.
This is basically goes back to basic OO programming and the right JS framework can empower you to do that. While other OO JS framework can be used as well, I find that YUI3 provide better structure than others for large Javascript project.
I will provide a link to a recent blog post I did about something similar. http://aknosis.com/2011/05/11/jquery-pluginifier-jquery-plugin-instantiator-boilerplate/
Basically this wrapper (pluginifier I've called it) will allow you to create a seperate JavaScript object that will house everything (public/private methods/options objects etc.) but allow for quick retrieval and cretation with common $('#myThing').myPlugin();
The source is available on github as well: https://github.com/aknosis/jquery-pluginifier
Here's a snippet where you would put your code:
//This should be available somewhere, doesn't have to be here explicitly
var namespace = {
//This will hold all of the plugins
plugins : {}
};
//Wrap in a closure to secure $ for jQuery
(function( $ ){
//Constructor - This is what is called when we create call new namspace.plugins.pluginNameHere( this , options );
namespace.plugins.pluginNameHere = function( ele , options ){
this.$this = $( ele );
this.options = $.extend( {} , this.defaults , options );
};
//These prototype items get assigned to every instance of namespace.plugins.pluginNameHere
namespace.plugins.pluginNameHere.prototype = {
//This is the default option all instances get, can be overridden by incoming options argument
defaults : {
opt: "tion"
},
//private init method - This is called immediately after the constructor
_init : function(){
//useful code here
return this; //This is very important if you want to call into your plugin after the initial setup
},
//private method - We filter out method names that start with an underscore this won't work outside
_aPrivateMethod : function(){
//Something useful here that is not needed externally
},
//public method - This method is available via $("#element").pluginNameHere("aPublicMethod","aParameter");
aPublicMethod : function(){
//Something useful here that anyone can call anytime
}
};
//Here we register the plugin - $("#ele").pluginNameHere(); now works as expected
$.pluginifier( "pluginNameHere" );
})( jQuery );
The $.pluginifier code is in a separate file but can be include in the same file as your plugin code as well.
A lot of your requirements are unnecessary. Anyhow here is a rough outline of the design pattern I have adopted for myself - which is essentially direct from the jQuery authoring documentation. If you have any questions, just leave me a comment.
The pattern described allows the following use:
var $myElements = $('#myID').myMapPlugin({
center:{
lat:174.0,
lng:-36.0
}
});
$myElements.myMapPlugin('refresh');
$myElements.myMapPlugin('addMarker', {
lat:174.1,
lng:-36.1
});
$myElements.myMapPlugin('update', {
center:{
lat:175.0,
lng:-33.0
}
});
$myElements.myMapPlugin('destroy');
And here is the general pattern - only a few method implemented.
;(function($) {
var privateFunction = function () {
//do something
}
var methods = {
init : function( options ) {
var defaults = {
center: {
lat: -36.8442,
lng: 174.7676
}
};
var t = $.extend(true, defaults, options);
return this.each(function () {
var $this = $(this),
data = $this.data('myMapPlugin');
if ( !data ) {
var map = new google.maps.Map(this, {
zoom: 8,
center: new google.maps.LatLng(t['center'][lat], t['center']['lng']),
mapTypeId: google.maps.MapTypeId.ROADMAP,
mapTypeControlOptions:{
mapTypeIds: [google.maps.MapTypeId.ROADMAP]
}
});
var geocoder = new google.maps.Geocoder();
var $form = $('form', $this.parent());
var form = $form.get(0);
var $search = $('input[data-type=search]', $form);
$form.submit(function () {
$this.myMapPlugin('search', $search.val());
return false;
});
google.maps.event.addListener(map, 'idle', function () {
// do something
});
$this.data('myMapPlugin', {
'target': $this,
'map': map,
'form':form,
'geocoder':geocoder
});
}
});
},
resize : function ( ) {
return this.each(function(){
var $this = $(this),
data = $this.data('myMapPlugin');
google.maps.event.trigger(data.map, 'resize');
});
},
search : function ( searchString ) {
return this.each(function () {
// do something with geocoder
});
},
update : function ( content ) {
// ToDo
},
destroy : function ( ) {
return this.each(function(){
var $this = $(this),
data = $this.data('myMapPlugin');
$(window).unbind('.locationmap');
data.locationmap.remove();
$this.removeData('locationmap');
});
}
};
$.fn.myMapPlugin = 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 exist on jQuery.myMapPlugin' );
}
};
})(jQuery);
Note that the code is untested.
Happy Coding :)
This may be outside the scope of your question, but I really think that you should refactor how you handle the PHP -> JS transition (specifically, your entire last PHP code block).
I think it's an anti-pattern to generate tons of JS in PHP, which is then run on the client. Instead, you should be returning JSON data to your client, which invokes whatever is needed based off of that data.
This example is incomplete, but I think it gives you an idea. ALL of your JS should actually be in JS, and the only thing being sent back & forth should be JSON. Generating dynamic JS is not a sane practice IMO.
<?php
// static example; in real use, this would be built dynamically
$data = array(
$id => array(
'options' => array(),
'venues' => array(/* 0..N venues here */),
)
);
echo json_encode($data);
?>
<script>
xhr.success = function (data) {
for (var id in data)
{
$('#' + id).csGoogleMapsHelper(data[id].options);
for (var i = 0, len = data[id].venues.length; i < len; i++)
{
$.fn.csGoogleMapsHelper.createMarker(id, data[id].venues[i], true);
}
$.fn.csGoogleMapsHelper.finalize(id);
}
}
</script>
I addressed these issues at jQuery plugin template - best practice, convention, performance and memory impact
Part of what I posted at jsfiddle.net:
;(function($, window, document, undefined){
var myPluginFactory = function(elem, options){
........
var modelState = {
options: null //collects data from user + default
};
........
function modeler(elem){
modelState.options.a = new $$.A(elem.href);
modelState.options.b = $$.B.getInstance();
};
........
return {
pluginName: 'myPlugin',
init: function(elem, options) {
init(elem, options);
},
get_a: function(){return modelState.options.a.href;},
get_b: function(){return modelState.options.b.toString();}
};
};
//extend jquery
$.fn.myPlugin = function(options) {
return this.each(function() {
var plugin = myPluginFactory(this, options);
$(this).data(plugin.pluginName, plugin);
});
};
}(jQuery, window, document));
My project: https://github.com/centurianii/jsplugin
See: http://jsfiddle.net/centurianii/s4J2H/1/
How do you manage namespace for a custom JavaScript library depends on jQuery?
Do you create your own namespace, say foo and add your objects there? e.g. foo.myClass, foo.myFunction
Or do you add your objects to jQuery's namespace? e.g. jQuery.myClass, jQuery.myFunction
Which is the more common practice and why?
This article discusses writing jQuery plugins/libraries in excruciating detail.
What NOT to do:
(function( $ ){
$.fn.tooltip = function( options ) { // THIS };
$.fn.tooltipShow = function( ) { // IS };
$.fn.tooltipHide = function( ) { // BAD };
$.fn.tooltipUpdate = function( content ) { // !!! };
})( jQuery );
What to do:
(function( $ ){
var methods = {
init : function( options ) { // THIS },
show : function( ) { // IS },
hide : function( ) { // GOOD },
update : function( content ) { // !!! }
};
$.fn.tooltip = function( method ) {
// Method calling logic
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 on jQuery.tooltip' );
}
};
})( jQuery );
I also wrote a blog post last year about various methods for namespacing in JavaScript (non-jQuery related).
It would depend on what the library does.
If you're extending the functionality of instances of jQuery objects, you'd use jQuery.fn as was described very nicely by #David Titarenco in his answer.
If you're creating utilities that are meant to be seen as additions to those provided in window.jQuery, then I don't see a problem with using that namespace (as long as you're careful with naming).
If it is really its own separate library that is not meant to be seen as an extension of jQuery, yet relies on functionality from jQuery, then definitely use your own namespace.