Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Which jQuery plugin design pattern should I use?

Tags:

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.

like image 218
ddinchev Avatar asked Aug 19 '11 21:08

ddinchev


2 Answers

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 
like image 149
David Hellsing Avatar answered Sep 18 '22 15:09

David Hellsing


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.

like image 26
gislikonrad Avatar answered Sep 20 '22 15:09

gislikonrad