Calling a Jquery Function in outside javascript (Jquery Boilerplate Plugin) - javascript

I'm using https://github.com/jquery-boilerplate/jquery-boilerplate
I created a method "fillLoginForm" inside that plugin and trying to access it outside
// the semi-colon before function invocation is a safety net against concatenated
// scripts and/or other plugins which may not be closed properly.
// TODO : Write public methods here above
;
(function ($, window, document, undefined) {
// undefined is used here as the undefined global variable in ECMAScript 3 is
// mutable (ie. it can be changed by someone else). undefined isn't really being
// passed in so we can ensure the value of it is truly undefined. In ES5, undefined
// can no longer be modified.
// window and document are passed through as local variable rather than global
// as this (slightly) quickens the resolution process and can be more efficiently
// minified (especially when both are regularly referenced in your plugin).
// Create the defaults once
var pluginName = "messageDashboard",
defaults = {
propertyName: "value",
};
// The actual plugin constructor
function Plugin(element, options) {
var widget = this;
widget.element = element;
// jQuery has an extend method which merges the contents of two or
// more objects, storing the result in the first object. The first object
// is generally empty as we don't want to alter the default options for
// future instances of the plugin
widget.settings = $.extend({}, defaults, options);
widget._defaults = defaults;
widget._name = pluginName;
widget.initLoginPage(); // FIX : To be decided whether its login page or message dashboard
$.each(widget.settings, function (key, value) {
if (typeof value === 'function') {
console.log(' adding Handler');
console.log(' For :' + key);
console.log(' Value :' + value);
console.log(widget.element);
$('body').on(key + '_' + pluginName,
function (e) {
console.log(' Event Handled On:' + widget.element);
value(e, widget.element);
}
);
}
});
}
// Avoid Plugin.prototype conflicts
$.extend(Plugin.prototype, {
/**
* To initialize Login Widget
*
*/
initLoginPage: function () {
// Place initialization logic for Login page here
// You already have access to the DOM element and
// the options via the instance, e.g. this.element
// and this.settings
// you can add more functions like the one below and
// call them like so: this.yourOtherFunction(this.element, this.settings).
},
/**
* To initialize Message Dashboard Widget Widget
*
*/
initDashboard: function () {
// Place initialization logic for Login page here
// You already have access to the DOM element and
// the options via the instance, e.g. this.element
// and this.settings
},
/**
*
* To fill the login form when the remember me option is enabled
*
*/
fillLoginForm : function(username, password) {
$("#id").val(username);
$("#pwd").val(password);
}
});
// A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations
$.fn[pluginName] = function (options) {
this.each(function () {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName, new Plugin(this, options));
}
});
// chain jQuery functions
return this;
};
})(jQuery, window, document);
In a different place, I'm trying to access it like
$('body').on('loadEvent',
function (e) {
var $dashboard = $('body').messageDashboard();
$dashboard.fillLoginForm("id","pwd");
}
);
But it throws undefined error !!!
Or how do i create public method which can be accessed outside directly ?

I'm able to get it working by using data method like as follows.
$('body').on('loadEvent',
function (e) {
var $dashboard = $('body').messageDashboard().data('plugin_messageDashboard');
$dashboard.fillLoginForm("4089032912", "2912");
}
);
But I hope this is not a better way to solve it !!

Related

Create an instance of a jQuery plugin

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

Access plugin instance using the id of the DOM Element

The concept is to have 2 plugins one for form and another for button. I want to bind all forms in my page to JQuery plugin that will handle some jobs let say that this is my plugin
$.fn.PluginForm = function (Options) {
var o = jQuery.extend({
SomeOption: 1
}, Options);
var Validate = function(){
if(o.SomeOption == 1) return true;
else return false;
};
$(this).on('submit', function(e) {
e.preventDefault();
//some code here
});
};
The form actually doesn’t have button in my case the post is triggered from another control. This is because of the structure of the application I want to build. The button plugin is:
$.fn.PluginButton = function (Options) {
var o = jQuery.extend({
Actions: [],
FormID: ''
}, Options);
$(this).click(function(){
var Form = $('#' + o.FormID);
if(Form.length > 0 && Form.PluginForm.Validate()) {
Form.submit();
//do something
}
else{
//do something else
}
});
};
What I want to succeed is to invoke the validation function on the Form element but I don’t want to invoke another instance of the PluginForm. Something like $('#' + o.FormID).PluginForm.Validate()
All this must be as plugin because there will be a lot of forms in the same page and a lot of buttons. Also there will be a lot of buttons that can invoke submit on the same form but with different options. That’s why I want to invoke one time the instance of the form. Also the controls that will be validated will be passed as parameter in the options of the PluginForm. Something like this $('#' + o.FormID).PluginForm({ Action: ‘Validate’ }) is not an option because will lose the initial parameters of the PluginForm.
You can save the plugin instance in the .data() structure on the element, and then call it back. Most of plugins use it that way.
/*!
* jQuery lightweight plugin boilerplate
* Original author: #ajpiano
* Further changes, comments: #addyosmani
* Licensed under the MIT license
*/
// the semi-colon before the function invocation is a safety
// net against concatenated scripts and/or other plugins
// that are not closed properly.
;(function ( $, window, document, undefined ) {
// undefined is used here as the undefined global
// variable in ECMAScript 3 and is mutable (i.e. it can
// be changed by someone else). undefined isn't really
// being passed in so we can ensure that its value is
// truly undefined. In ES5, undefined can no longer be
// modified.
// window and document are passed through as local
// variables rather than as globals, because this (slightly)
// quickens the resolution process and can be more
// efficiently minified (especially when both are
// regularly referenced in your plugin).
// Create the defaults once
var pluginName = "defaultPluginName",
defaults = {
propertyName: "value"
};
// The actual plugin constructor
function Plugin( element, options ) {
this.element = element;
// jQuery has an extend method that merges the
// contents of two or more objects, storing the
// result in the first object. The first object
// is generally empty because we don't want to alter
// the default options for future instances of the plugin
this.options = $.extend( {}, defaults, options) ;
this._defaults = defaults;
this._name = pluginName;
this.init();
}
Plugin.prototype = {
init: function() {
// Place initialization logic here
// You already have access to the DOM element and
// the options via the instance, e.g. this.element
// and this.options
// you can add more functions like the one below and
// call them like so: this.yourOtherFunction(this.element, this.options).
},
yourOtherFunction: function(el, options) {
// some logic
}
};
// A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations
$.fn[pluginName] = function ( options ) {
return this.each(function () {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName,
new Plugin( this, options ));
}
});
};
})( jQuery, window, document );
taken from: https://github.com/jquery-boilerplate/jquery-patterns/blob/master/patterns/jquery.basic.plugin-boilerplate.js
also there are more jquery plugin design patterns that may fit more for your plugin at http://jqueryboilerplate.com/.

How do I access declared functions the done() scope of a jQuery plugin that makes JSON requests like this?

I'm trying to keep a jquery plugin I'm working on configurable, and someone what maintainable, by keeping the various functions I use short and relatively easy to test.
To this I'm using some jQuery plugin code, based around the jQuery boilerplate, Addy Osmani's Lightweight Start, to have a plugin where I can pass in overrides, and compose existing functions from a series of small ones.
However, I'm having some trouble working out how to access functions I've declared from within a deferred done() callback, without declaring all the function code inside the done() function call again.
Is there a recommended pattern for making these functions available when using a prototype based approach like outlined in the boilerplate?
(function($, window, document, undefined) {
var pluginName = 'myModule';
function myModule(element, options) {
this.element = element;
// allow override of defaults
this.options = $.extend({}, defaults, options);
this._defaults = defaults;
this._name = pluginName;
// calling the init() function defined below
this.init();
}
myModule.prototype = {
init: function() {
// add listeners for clicks on the element, and trigger some
// behaviour defined in fetchScore()
$(this.element).click(function() {
that.fetchScore();
return false;
});
},
handySuccessFunction: function() {
// some handy DOM manipulation stuff,
// kept out the main fetchScore function,
// ideally to make it more testable and readable
},
handyFailingFunction: function() {
// same again for failing case
},
fetchScore: function(authToken) {
$.getJSON(this.options.endpoint, {
apiKey: this.options.apiKey,
otherParam: this.options.otherParam,
token: authToken
})
.done(function(json) {
// I want to call the handySuccessFunction() here,
// but I have no access to myModule
})
.fail(function(jqxhr, textStatus, error) {
// Likewise I want to call the handyFailingFunction() here
});
}
}
// A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations.
// We store a reference to the
$.fn[pluginName] = function(options) {
return this.each(function() {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName,
new pluginName(this, options));
}
});
}
})(jQuery, window, document);
Here's my expected usage:
jQuery(document).ready(function($) {
// console.log('clicking the popup');
$('#elementToAttachTo').myModule();
// clicking on this to trigger the fetchScore
// behaviour in myModule
$('#elementToAttachTo').click();
})
You should use "bind" to callback function in "done", set it context ("this") to myModule instance, where this function was declarated.
There are several ways.
You can use navtive Function.prototype.bind() method, which works in modern browsers
You can use jQuery $.proxy function.
So
myModule.prototype.fetchScore = function(authToken) {
$.getJSON(this.options.endpoint, {
apiKey: this.options.apiKey,
otherParam: this.options.otherParam,
token: authToken
})
.done(function(json) {
this.handySuccessFunction();
}.bind(this))
.fail($.proxy(function(json) {
this.handyFailingFunction();
}, this))
;
};

Yielding a variable in a jQuery plugin / Javascript

I want to create a jQuery plugin which can be attached to a text box, and after the user enters a certain key combination, a callback function can be called, with a variable that is set based on the entered key combo. I'm coming from a Ruby background, and I'm not sure if this is even possible in Javascript/jQuery. Here's an example:
$('textbox').attach_my_plugin(function(){|key_combo_var|
// do something with key_combo_var...
});
How would I achieve this? Plan B is to stick key_combo_var into the .data() of the element. Would there be a better way than this?
This is totally possible. Although you don't give much details (what certain action ?).
A good start is this jQuery plugin boilerplate
The site provides a way to start creating your own plugin. The thing is pretty well documented so if you can read javascript/jquery code, it should not be too difficult.
If you provide a bit more details on what you'd like to do, I can help you further implementing it but right now it's a bit too blurry.
As example
I have created using the boilerplate an example of a plugin that should do what you're looking after. At least this will give you a good start.
It basically will execute the callback when you press ctrl-shift-a.
You can test it live on jsfiddle.
;(function ( $, window, document, undefined ) {
var pluginName = 'callbackOnKey',
defaults = {
// define a default empty callback as default
callback: function() {}
};
function Plugin( element, options ) {
this.element = element;
this.options = $.extend( {}, defaults, options) ;
this._defaults = defaults;
this._name = pluginName;
this.init();
}
Plugin.prototype.init = function () {
var $this = $(this.element),
keydownHandler = function(e) {
// in here, 'this' is the plugin instance thanks to $.proxy
// so i can access the options property (this.options.callback)
// if key combination is CTRL-SHIFT-a
if (e.ctrlKey && e.shiftKey && e.which === 65 && this.options.callback) {
// execute the callback
this.options.callback.apply(this);
}
};
// bind the handler on keydown
// i use $.proxy to change the context the handler will be executed
// with (what will be 'this' in the handler). by default it would
// have been the input element, now it will be the plugin instance
$this.bind('keydown', $.proxy(keydownHandler, this));
};
$.fn[pluginName] = function ( options ) {
return this.each(function () {
if (!$.data(this, 'plugin_' + pluginName)) {
$.data(this, 'plugin_' + pluginName, new Plugin( this, options ));
}
});
}
})(jQuery, window, document);
// use the plugin and pass a callback function
$('#myinput').callbackOnKey({
callback: function() { alert("It's working :o)"); }
});

Which jQuery plugin design pattern should I use?

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/

Categories