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/
Related
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 have a config.json that I am going to load into my app as a Backbone Model like:
var Config = Backbone.Model.extend({
defaults: {
base: ''
},
url: 'config.json'
});
Other models should be dependent on some data contained in Config like:
var ModelA = Backbone.Collection.extend({
initialize: function(){
//this.url should be set to Config.base + '/someEndpoint';
}
});
In above example, ModelA's url property is dependent on Config's base property's value.
How do I go about setting this up properly in a Backbone app?
As I see it, your basic questions are:
How will we get an instance of the configuration model?
How will we use the configuration model to set the dependent model's url?
How can we make sure we don't use the url function on the dependent model too early?
There are a lot of ways to handle this, but I'm going to suggest some specifics so that I can just provide guidance and code and "get it done," so to speak.
I think the best way to handle the first problem is to make that configuration model a singleton. I'm going to provide code from backbone-singleton GitHub page below, but I don't want the answer to be vertically long until I'm done with the explanation, so read on...
var MakeBackboneSingleton = function (BackboneClass, options) { ... }
Next, we make a singleton AppConfiguration as well as a deferred property taking advantage of jQuery. The result of fetch will provide always(callback), done(callback), etc.
var AppConfiguration = MakeBackboneSingleton(Backbone.Model.extend({
defaults: {
base: null
},
initialize: function() {
this.deferred = this.fetch();
},
url: function() {
return 'config.json'
}
}));
Now, time to define the dependent model DependentModel which looks like yours. It will call AppConfiguration() to get the instance.
Note that because of MakeBackboneSingleton the follow is all true:
var instance1 = AppConfiguration();
var instance2 = new AppConfiguration();
instance1 === instance2; // true
instance1 === AppConfiguration() // true
The model will automatically fetch when provided an id but only after we have completed the AppConfiguration's fetch. Note that you can use always, then, done, etc.
var DependentModel = Backbone.Model.extend({
initialize: function() {
AppConfiguration().deferred.then(function() {
if (this.id)
this.fetch();
});
},
url: function() {
return AppConfiguration().get('base') + '/someEndpoint';
}
});
Now finally, putting it all together, you can instantiate some models.
var newModel = new DependentModel(); // no id => no fetch
var existingModel = new DependentModel({id: 15}); // id => fetch AFTER we have an AppConfiguration
The second one will auto-fetch as long as the AppConfiguration's fetch was successful.
Here's MakeBackboneSingleton for you (again from the GitHub repository):
var MakeBackboneSingleton = function (BackboneClass, options) {
options || (options = {});
// Helper to check for arguments. Throws an error if passed in.
var checkArguments = function (args) {
if (args.length) {
throw new Error('cannot pass arguments into an already instantiated singleton');
}
};
// Wrapper around the class. Allows us to call new without generating an error.
var WrappedClass = function() {
if (!BackboneClass.instance) {
// Proxy class that allows us to pass through all arguments on singleton instantiation.
var F = function (args) {
return BackboneClass.apply(this, args);
};
// Extend the given Backbone class with a function that sets the instance for future use.
BackboneClass = BackboneClass.extend({
__setInstance: function () {
BackboneClass.instance = this;
}
});
// Connect the proxy class to its counterpart class.
F.prototype = BackboneClass.prototype;
// Instantiate the proxy, passing through any arguments, then store the instance.
(new F(arguments.length ? arguments : options.arguments)).__setInstance();
}
else {
// Make sure we're not trying to instantiate it with arguments again.
checkArguments(arguments);
}
return BackboneClass.instance;
};
// Immediately instantiate the class.
if (options.instantiate) {
var instance = WrappedClass.apply(WrappedClass, options.arguments);
// Return the instantiated class wrapped in a function so we can call it with new without generating an error.
return function () {
checkArguments(arguments);
return instance;
};
}
else {
return WrappedClass;
}
};
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 !!
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/.
I'm writing a new jQuery plugin. For the guide, I am using their recommendation:
(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 ) {
data = {
element : this,
target : $this,
tooltip : tooltip
};
$(this).data('tooltip', data);
}
methods.update.apply(data.element, 'Test');
},
update : function( content ) {
var $this = $(this),
data = $this.data('tooltip');
// check or change something important in the data.
private.test.apply( data.element );
return data.element;
}
};
var private = {
test: function() {
var $this = $(this),
data = $this.data('tooltip');
// again, do some operation with data
}
};
$.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 );
Its a little different from their version to make it shorter but also to show my differences. Basically, in the init, I am instantiating and creating data object that gets stored in the element. Part of the data object is the element itself:
element : this,
Then, after all of the initialization is done, I call a public method from the init (lets say I do it for functionality reuse purpose). To make the call, I use .apply() and provide the proper context (my element), which would match the context when the function is called externally:
return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
This is fine and understandable. However, what I am unsure about is the performance of acquiring the data of the plugin from within a private or a public method. To me, it seems that at the top of every public and private method I have to execute the following lines in order to get the data:
var $this = $(this),
data = $this.data('tooltip');
Of course, I wouldn't execute them when I have no need for whatever is stored in data. However, my plugin performs quite a bit of animations and state tracking and almost all of the functions require access to the data. As such, it seems like accessing .data() in almost every private and public call is a pretty big performance hit.
My question is whether anyone uses this plug-in structure (I'm hoping that yes since jQuery recommends it) and has found a different way of referencing the data without hitting .data() in every function call.