Which jQuery plugin design template should I use?

I need to build a jQuery plugin that will return one instance per selector id. The plugin should and will be used only for elements with id (it is impossible to use a selector that matches many elements), so it should be used as follows:

$('#element-id').myPlugin(options); 
  • I need to have several private methods for the plugin, as well as several public methods. I can achieve this, but my main problem is that I want to get the exact same instance every time I call $ ('# element-id'). MyPlugin ().
  • And I want to have some code that should only be executed the first time the plugin is initialized for a given ID (construct).
  • The options parameter must be provided for the first time, for the construct, after which I do not want the construct to be executed so that I can access the plugin in the same way as $ ('# element-id'). MyPlugin ()
  • The plugin should be able to work with several elements (usually up to two) on one page (but each of them will require its own configuration, again - they will be initialized by identifier, and not for the usual class selector for example).
  • The above syntax, for example, is open to any suggestions on how to achieve this pattern.

I have quite some OOP experience with another language, but limited knowledge of javascript, and I'm really confused about how everything is correct.

EDIT

To develop - this plugin is a GoogleMaps v3 API (helper) API to help me get rid of code duplication, as in many places I use Google maps, usually with markers. This is the current library (lots of code removed, only the most important methods left):

 ;(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 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); 

This is similar to this (in fact, it is not, because there is a PHP shell for automation with a single call, but mostly):

 $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 save the hash map for “settings” and “maps”

$id is the identifier of the DIV element where the card is initialized. It is used as a key in .map and .settings has maps, where I store settings and a MapMobject GoogleMaps instance for each initialized such GoogleMaps on the page. $jsOptions and $data from PHP code are JSON objects.

Now I need to create an instance of GoogleMapsHelper, which contains its own settings and a GoogleMaps map object, so that after I initialize it on a specific element (by its identifier), I can reuse this instance. But if I initialize it to N elements on the page, each of them should have its own configuration, map object, etc.

I am not saying that this is implemented as a jQuery plugin! . I insist that it is flexible and extensible, because I will use it in a large project with more than a dozen currently planned different screens, where it will be used in a few months, changing the usage interface will be a nightmare to refactor the whole project .

I will add generosity for this.

+22
javascript jquery jquery-plugins design-patterns
Aug 19 2018-11-21T00:
source share
7 answers

When you say “get” the instance through $('#element').myPlugin() , I assume that you mean something like:

 var instance = $('#element').myPlugin(); instance.myMethod(); 

This might seem like a good idea at first, but it is considered bad practice to extend the jQuery prototype as you break the jQuery instance chain.

Another convenient way to do this is to save the instance in the $.data object, so you just initialize the plugin once, then you can retrieve the instance at any time, using only the DOM element as a reference, f.ex

 $('#element').myPlugin(); $('#element').data('myplugin').myMethod(); 

Here is the template that I use to support the cool structure in JavaScript and jQuery (comments are 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 extract a specific instance of the apply methods, you can do:

 $('div').eq(1).data('myclass').increaseId(3).applyIdAsClass(); 

This is a sample that I have used many times that is great for my needs.

You can also open the class to use without the jQuery prototype by adding window.MyClass = MyClass . This allows you to use the following syntax:

 var instance = new MyClass( document.getElementById('element'), { id: 5 }); instance.increaseId(5); alert( instance.options.id ); // yields 10 
+19
Aug 22 2018-11-21T00:
source share

Here is the 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 the objects in the dom element ... If you use HTML5, you can use the data attributes for the element.

EDIT

Another thought came to mind. You can use jQuery.UI widgets.

+4
Aug 19 2018-11-21T00:
source share

I think you need to solve your problem, it is basically a good OO structure to store both your setup and GoogleMap.

If you are not attached to jQuery and know OOP well, I would use YUI3 Widget .

A look at the Example Widget Template should give you an idea that the structure provides access to the OOP structure, for example:

  • It provides namespace support.
  • Supports the concept of classes and objects
  • It supports class extension neatly
  • It provides a constructor and a destructor
  • It supports the concept of instance variables.
  • It provides the binding of props and events

In your case:

  • You can create your own GoogleHelper class, which has its own instance variables along with the Google Map object, which I think is what you intended.
  • Then you will start creating an instance of this class with your own settings.
  • For each new instance, you just need to match it with an identifier that you could reference later. Comparing the identifier of the GoogleHelper instance, which has both settings and GoogleMap, you do not need to save two maps (one for storing settings and one for GoogleMap), with which I accidentally agree with you that this is not an ideal situation.

This mainly relates to basic OO programming, and a proper JS framework may give you the opportunity to do this. While other OO JS frameworks can also be used, I believe that YUI3 provides a better structure than others for a large Javascript project.

+4
Aug 23 2018-11-11T00:
source share

I will provide a link to a recent blog post that I did about something similar. http://aknosis.com/2011/05/11/jquery-pluginifier-jquery-plugin-instantiator-boilerplate/

In principle, this oberter (the plugin that I called him) will allow you to create a separate JavaScript object in which all objects will be placed (public / private methods / options, etc.), but they will allow you to quickly extract and create using total $ ('#myThing') MyPlugin () ;.

The source is also available on github: https://github.com/aknosis/jquery-pluginifier

Here is the snippet where you 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 included in the same file as your plugin code.

+3
Aug 26 2018-11-11T00:
source share

Many of your requirements are not needed. Anyway, this is a rough outline of the design template that I applied for myself - this is, in fact, directly from the documentation for creating jQuery. If you have any questions, just leave me a comment.

The described template allows you to use the following:

 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 template - only a few methods are 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); 

Please note that the code is not verified.

Happy coding :)

+2
Aug 22 '11 at 18:35
source share

This may be outside the scope of your question, but I really think that you should reorganize how you handle the PHP → JS transition (in particular, all of your last block of PHP code).

I think this is an anti-template for generating tons of JS in PHP, which then runs on the client. Instead, you should return JSON data to your client, which calls everything you need based on 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 going back and forth should be JSON. Generating dynamic JS is not a reasonable IMO practice.

 <?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> 
+1
Aug 22 2018-11-18T00:
source share

I addressed these issues in the jQuery plugin template - best practice, convention, performance, and memory impact

Part of what I posted on 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/

0
Dec 6 '13 at 12:26
source share



All Articles