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);
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()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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With