create an add/remove or refresh function in a boilerplate plugin - javascript

I have this simple plugin I am building which just builds a table:
; (function ($, window, document, undefined) {
// Create the defaults once
var pluginName = "tableBuilder",
defaults = {
};
// 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();
}
String.prototype.format = function (values) {
var regex = /\{([\w-.]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g;
var getValue = function (key) {
var value = values,
arr, type;
if (values == null || typeof values === 'undefined') return null;
if (key.indexOf('.')) {
arr = key.split('.');
while (arr.length && value) {
value = value[arr.shift()];
}
} else {
value = val && val[key] || values[key];
}
type = typeof value;
return type === 'string' || type === 'number' ? value : null;
};
return this.replace(regex, function (match) {
//match will look like {sample-match}
//key will be 'sample-match';
var key = match.substr(1, match.length - 2);
var value = getValue(key);
return value != null ? value : match;
});
};
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).
this.cycle();
},
cycle: function() {
var self = this;
self.buildRow();
self.display();
},
buildRow: function () {
var self = this;
self.rows = [];
$.each(self.options.json, function (i, item) {
self.rows.push(self.options.rowTemplate.format(item));
});
console.log(self.rows);
},
display: function (el, options) {
var self = this;
$(self.element).html(self.rows.join());
}
};
// 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);
I call this from a button click event:
var row = "<tr data-id=\"{Id}\"><td>{FileName}</td><td>{Metadata.FileSize}</td><td></td><td><button type=\"button\" class=\"close\" data-id=\"{Id}\" aria-hidden=\"true\">×</button></td></tr>"
$("#assets").on("click", ".glyphicon", function () {
var $asset = $(this).parent();
var $actionBar = $("#action-bar");
var $selected = $("#selected-asset");
var $table = $(".table");
var currentSelected = parseInt($selected.text());
var assetId = parseInt($asset.attr("id"))
if ($asset.hasClass("active")) {
$selected.text(currentSelected - 1);
activeItems = $.grep(activeItems, function (obj) {
return obj.Id != assetId
});
$asset.removeClass("active");
if (activeItems.length <= 0) {
$actionBar.hide();
}
} else {
$selected.text(currentSelected + 1);
var asset = $.grep(assets, function (obj) {
return obj.Id == assetId
});
activeItems.push(asset[0]);
$asset.addClass("active");
$actionBar.show();
}
$("#assets-table").tableBuilder({
json: activeItems,
rowTemplate: row
});
});
Now, when I click add the first time, the table is created. But each click after does nothing. I put a console.log on the buildRows function and it only gets called once, which is expected because we only instantiated the plugin on that element.
So, I need to add a refresh function or an add/remove function that is available to the client.
Can anyone give me a hand?

Ok, so I was not so impressed with my last answer.
With the help of this video:
Head first into plugin development
I was able to work out that all the functions are actually part of the plugin instance.
So, here is my new plugin :)
String.prototype.format = function (values) {
var regex = /\{([\w-.]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g;
var getValue = function (key) {
var value = values,
arr, type;
if (values == null || typeof values === 'undefined') return null;
if (key.indexOf('.')) {
arr = key.split('.');
while (arr.length && value) {
value = value[arr.shift()];
}
} else {
value = val && val[key] || values[key];
}
type = typeof value;
return type === 'string' || type === 'number' ? value : null;
};
return this.replace(regex, function (match) {
//match will look like {sample-match}
//key will be 'sample-match';
var key = match.substr(1, match.length - 2);
var value = getValue(key);
return value != null ? value : match;
});
};
; (function ($, window, document, undefined) {
var pluginName = "tableBuilder",
defaults = {
};
function Plugin(element, options) {
this.element = element;
this.$element = $(element);
this.rows = [];
this.rowTemplate = (typeof options === "string") ? options : options.rowTemplate;
this.options = $.extend({}, defaults, options);
this._defaults = defaults;
this._name = pluginName;
this.init();
}
Plugin.prototype = {
init: function () {
this.cycle();
},
cycle: function () {
var self = this;
if (self.options.json != null) {
self.buildRow();
self.display();
}
if (typeof self.options.onComplete === "function") {
self.options.onComplete.apply(self.element, arguments);
}
},
buildRow: function () {
var self = this;
$.each(self.options.json, function (i, item) {
self.rows.push(self.rowTemplate.format(item));
});
},
display: function (el, options) {
this.$element.html(this.rows.join());
},
add: function (row) {
console.log("moo");
this.rows.push(this.options.rowTemplate.format(row));
this.display();
},
remove: function(row) {
var match = this.options.rowTemplate.format(row);
this.rows = $.grep(this.rows, function (obj) {
return obj != match;
});
this.display();
}
};
$.fn[pluginName] = function (options) {
return this.each(function () {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName,
new Plugin(this, options));
}
});
};
})(jQuery, window, document);
now, the functions I needed access to are add() and remove() so if you look at these lines:
$.fn[pluginName] = function (options) {
return this.each(function () {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName,
new Plugin(this, options));
}
});
};
they are actually passing the instance to the $.data array which allows me to call my instance with a line of code:
$("#assets-table").data("plugin_tableBuilder")
and because of this, I am able to call any function that is a part of that instance, like this:
$("#assets-table").data("plugin_tableBuilder").add(asset[0]); // Add a row to our widget
I hope this helps someone else :D
/r3plica

I am going to answer this myself :)
Basically I decided that this was not the best way to handle my widget, so I used the widget factory boilerplate to sort out my issue. I modified my click event to this:
$("#assets").on("click", ".glyphicon", function () {
var $asset = $(this).parent(); // Get our asset element
var $actionBar = $("#action-bar"); // Get the action bar
var $selected = $("#selected-asset");// Get our selected asset counter
var currentSelected = parseInt($selected.text()); // Get our current counter value
var assetId = parseInt($asset.attr("id")); // Get the asset id
var asset = $.grep(assets, function (obj) { // Find our asset from our array
return obj.Id == assetId;
});
if ($asset.hasClass("active")) { // If our asset is already selected, then we must unselect it
$selected.text(currentSelected - 1); // First, decrease our counter
tableWidget.tableBuilder("remove", asset[0]); // Then call our widget and remove the current asset from the table
activeItems = $.grep(activeItems, function (obj) { // Repopulate our array of active assets
return obj != asset;
});
$asset.removeClass("active"); // And remove the active class from our element
if (activeItems.length <= 0) { // Finally, if this is the only selected asset
$actionBar.hide(); // Hide our actionbar
}
} else { // Else, we are selecting an asset
$selected.text(currentSelected + 1); // Increase our counter
tableWidget.tableBuilder("add", asset[0]); // Add a row to our widget
activeItems.push(asset[0]); // Add the asset to our array of active assets
$asset.addClass("active"); // Add our active alss to our element
$actionBar.show(); // And show our actionbar
}
});
And I instantiated my plugin on the page load like this:
var row = "<tr data-id=\"{Id}\"><td>{FileName}</td><td>{Metadata.FileSize}</td><td></td><td><button type=\"button\" class=\"close\" data-id=\"{Id}\" aria-hidden=\"true\">×</button></td></tr>"
var tableWidget;
$(function () {
tableWidget = $("#assets-table").tableBuilder({
rowTemplate: row
});
});
and then, my script I rewrote to this:
/*!
* jQuery UI Widget-factory plugin boilerplate (for 1.8/9+)
* Author: #addyosmani
* Further changes: #peolanha
* Licensed under the MIT license
*/
String.prototype.format = function (values) {
var regex = /\{([\w-.]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g;
var getValue = function (key) {
var value = values,
arr, type;
if (values == null || typeof values === 'undefined') return null;
if (key.indexOf('.')) {
arr = key.split('.');
while (arr.length && value) {
value = value[arr.shift()];
}
} else {
value = val && val[key] || values[key];
}
type = typeof value;
return type === 'string' || type === 'number' ? value : null;
};
return this.replace(regex, function (match) {
//match will look like {sample-match}
//key will be 'sample-match';
var key = match.substr(1, match.length - 2);
var value = getValue(key);
return value != null ? value : match;
});
};
; (function ($, window, document, undefined) {
// define your widget under a namespace of your choice
// with additional parameters e.g.
// $.widget( "namespace.widgetname", (optional) - an
// existing widget prototype to inherit from, an object
// literal to become the widget's prototype );
$.widget("skipstone.tableBuilder", {
//Options to be used as defaults
options: {
json: null,
rowTemplate: null
},
//Setup widget (eg. element creation, apply theming
// , bind events etc.)
_create: function () {
// _create will automatically run the first time
// this widget is called. Put the initial widget
// setup code here, then you can access the element
// on which the widget was called via this.element.
// The options defined above can be accessed
// via this.options this.element.addStuff();
this.rows = [];
if (this.options.json != null) {
this._buildRow();
this._display();
}
},
_buildRow: function () {
var self = this;
$.each(self.options.json, function (i, item) {
self.rows.push(self.options.rowTemplate.format(item));
});
},
_display: function (el, options) {
$(this.element).html(this.rows.join());
},
add: function (row) {
this.rows.push(this.options.rowTemplate.format(row));
this._display();
},
remove: function(row) {
var match = this.options.rowTemplate.format(row);
this.rows = $.grep(this.rows, function (obj) {
return obj != match;
});
this._display();
},
// Destroy an instantiated plugin and clean up
// modifications the widget has made to the DOM
destroy: function () {
// this.element.removeStuff();
// For UI 1.8, destroy must be invoked from the
// base widget
$.Widget.prototype.destroy.call(this);
// For UI 1.9, define _destroy instead and don't
// worry about
// calling the base widget
}
});
})(jQuery, window, document);
This how now fixed my issue. You can see that I add rows by calling
tableWidget.tableBuilder("add", asset[0]);
and remove items by calling:
tableWidget.tableBuilder("remove", asset[0]);
I really hope that helps someone else :D
Cheers,
r3plica

Related

Merge two custom javascript functions

This is the first function:
jQuery.fn.serializeObject = function(){
var self = this,
json = {},
push_counters = {},
patterns = {
"validate": /^[a-zA-Z][a-zA-Z0-9_]*(?:\[(?:\d*|[a-zA-Z0-9_]+)\])*$/,
"key": /[a-zA-Z0-9_]+|(?=\[\])/g,
"push": /^$/,
"fixed": /^\d+$/,
"named": /^[a-zA-Z0-9_]+$/
};
this.build = function(base, key, value){
base[key] = value;
return base;
};
this.push_counter = function(key){
if(push_counters[key] === undefined){
push_counters[key] = 0;
}
return push_counters[key]++;
};
jQuery.each(jQuery(this).serializeArray(), function(){
// skip invalid keys
if(!patterns.validate.test(this.name)){
return;
}
var k,
keys = this.name.match(patterns.key),
merge = this.value,
reverse_key = this.name;
while((k = keys.pop()) !== undefined){
// adjust reverse_key
reverse_key = reverse_key.replace(new RegExp("\\[" + k + "\\]$"), '');
// push
if(k.match(patterns.push)){
merge = self.build([], self.push_counter(reverse_key), merge);
}
// fixed
else if(k.match(patterns.fixed)){
merge = self.build([], k, merge);
}
// named
else if(k.match(patterns.named)){
merge = self.build({}, k, merge);
}
}
json = jQuery.extend(true, json, merge);
});
return json;
};
This function to serialize the form's values but the problem of this function that it's not getting the value of the unchecked inputs because that I make this function:
jQuery.fn.mySerialize = function() {
var $container = jQuery(this),
$checkboxes = $container.find("input[type='checkbox']").each(function() {
jQuery(this).val(this.checked ? 1 : 0).prop('checked', true);
});
var serialized = ($container.serializeObject());
$checkboxes.each(function() {
jQuery(this).prop('checked', jQuery(this).val() == 1);
});
return serialized;
};
And it's working as I need by directly using mySerialize function now I want to merge the edits that I make at mySerialize function to the serializeObject function because I failed to do that directly into serializeObject function because that I created the mySerialize function.
Thanks in advance and really hope anyone helps me on this.

Knockout JS setting optionsValue destroys my code

The code below is simplified, see the fiddle: http://jsfiddle.net/QTUqD/7/
Basically I'm setting the device name under the data-bind, but I also need to specify the optionsValue for sending off to the database, but when I set it, the display data-bind is blank.
<script id="extItems" type="text/html">
<tr>
<td data-bind="text: device() && device().name"></td>
</tr>
</script>
<script id="editExts" type="text/html">
<tr>
<td>
<select data-bind="options: $root.devicesForItem($data), optionsText: 'name', value: device, optionsValue: 'id'"></select>
</td>
</tr>
</script>
window.ExtListViewModel = new function () {
var self = this;
window.viewModel = self;
self.list = ko.observableArray();
self.pageSize = ko.observable(10);
self.pageIndex = ko.observable(0);
self.selectedItem = ko.observable();
self.extQty = ko.observable();
self.devices = ko.observableArray();
self.addressList = ko.observableArray(['addressList']);
self.availableDevices = ko.computed(function() {
var usedQuantities = {}; // for each device id, store the used quantity
self.list().forEach(function(item) {
var device = item.device();
if (device) {
usedQuantities[device.id] = 1 + (usedQuantities[device.id] || 0);
}
});
return self.devices().filter(function(device) {
var usedQuantity = usedQuantities[device.id] || 0;
return device.qty > usedQuantity;
});
});
// need this to add back item's selected device to its device-options,
// and to maintain original order of devices
self.devicesForItem = function(item) {
var availableDevices = self.availableDevices();
return self.devices().filter(function(device) {
return device === item.device() || availableDevices.indexOf(device) !== -1;
});
}
self.edit = function (item) {
if($('#extMngForm').valid()) {
self.selectedItem(item);
}
};
self.cancel = function () {
self.selectedItem(null);
};
self.add = function () {
if($('#extMngForm').valid()) {
var newItem = new Extension();
self.list.push(newItem);
self.selectedItem(newItem);
self.moveToPage(self.maxPageIndex());
}
};
self.remove = function (item) {
if (confirm('Are you sure you wish to delete this item?')) {
self.list.remove(item);
if (self.pageIndex() > self.maxPageIndex()) {
self.moveToPage(self.maxPageIndex());
}
}
$('.error').hide();
};
self.save = function () {
if($('#extMngForm').valid()) {
self.selectedItem(null);
};
};
self.templateToUse = function (item) {
return self.selectedItem() === item ? 'editExts' : 'extItems';
};
self.pagedList = ko.dependentObservable(function () {
var size = self.pageSize();
var start = self.pageIndex() * size;
return self.list.slice(start, start + size);
});
self.maxPageIndex = ko.dependentObservable(function () {
return Math.ceil(self.list().length / self.pageSize()) - 1;
});
self.previousPage = function () {
if (self.pageIndex() > 0) {
self.pageIndex(self.pageIndex() - 1);
}
};
self.nextPage = function () {
if (self.pageIndex() < self.maxPageIndex()) {
self.pageIndex(self.pageIndex() + 1);
}
};
self.allPages = ko.dependentObservable(function () {
var pages = [];
for (i = 0; i <= self.maxPageIndex() ; i++) {
pages.push({ pageNumber: (i + 1) });
}
return pages;
});
self.moveToPage = function (index) {
self.pageIndex(index);
};
};
ko.applyBindings(ExtListViewModel, document.getElementById('extMngForm'));
function Extension(extension, name, email, vmpin, device, macAddress, shipTo){
this.extension = ko.observable(extension);
this.name = ko.observable(name);
this.email = ko.observable(email);
this.vmpin = ko.observable(vmpin);
this.device = ko.observable(device);
this.macAddress = ko.observable(macAddress);
this.shipTo = ko.observable(shipTo);
}
When you use optionsValue, KO writes the property value to whatever you have bound against value. So, it would now populate value with the id rather than the object.
There are a couple of ways to tackle this scenario where you want both the value (for sending to the DB) and the object (for binding other parts of the UI against).
A pretty typical solution is to create a computed observable on your object that takes the currently selected object and returns the id.
So, in your Extension you would do something like:
this.device = ko.computed({
read: function() {
var device = this.device.asObject();
return device && device.id;
},
deferEvaluation: true, //deferring evaluation, as device.asObject has not been created yet
}, this);
//create this as a sub-observable, so it just disappears when we turn this into JSON and we are just left with the id to send to the DB
this.device.asObject = ko.observable(device);
Then remove the optionsValue and bind value against device.asObject
In this case, I added the asObject sub-observable, so it will just drop off when you turn this into JSON (ko.toJSON) to send to the server. The only tricky part about this technique is that if you are loading existing data from the server, then you would need to populate asObject with the appropriate choice from your options.
Here is a sample: http://jsfiddle.net/rniemeyer/Q3PEv/
Another option that I have used is to continue to use optionsValue, but then to create a custom binding that tracks the object in a separate observable. Here is a custom binding that creates an asObject sub-observable for whatever is bound against value. This way you really don't need to mess with it at all in your view model.
//when using optionsValue, still track the select object in a different observable
ko.bindingHandlers.valueAsObject = {
init: function(element, valueAccessor, allBindingsAccessor) {
var value = allBindingsAccessor().value,
prop = valueAccessor() || 'asObject';
//add an "asObject" sub-observable to the observable bound against "value"
if (ko.isObservable(value) && !value[prop]) {
value[prop] = ko.observable();
}
},
//whenever the value or options are updated, populated the "asObject" observable
update: function(element, valueAccessor, allBindingsAccessor) {
var prop = valueAccessor(),
all = allBindingsAccessor(),
options = ko.utils.unwrapObservable(all.options),
value = all.value,
key = ko.utils.unwrapObservable(value),
keyProp = all.optionsValue,
//loop through the options, find a match based on the current "value"
match = ko.utils.arrayFirst(options, function(option) {
return option[keyProp] === key;
});
//set the "asObject" observable to our match
value[prop](match);
}
};
Sample here: http://jsfiddle.net/rniemeyer/E2kvM/

How to "new" a returned function in Javascript

I am trying to simulate a namespace feature in Javascript.
var com = {};
com.domain = {};
com.domain.system = {};
com.domain.net = {};
com.domain.net.ip = {};
com.domain.net.ip.tcp = {};
com.domain.net.ip.udp = {};
com.domain.net.ip.ssl = {};
com.domain.util = {};
com.domain.util.timer = {};
com.domain.plugins = {};
com.domain.session = {};
com.domain.io = {};
com.domain.algorithm = {};
com.domain.debug = {};
This is the namespaces declaration. Later I will add functions to these namespaces.
This is my selector function:
For a convenient way to use namespaces, I add a function named $. This function will walk all namespaces in com. If the selected name exists, return the object.
function $ (selector) {
function digger (namespace, selector) {
for (var prop in namespace) {
if (typeof namespace[prop] == "array" || typeof namespace[prop] == "object") {
if (prop == selector) {
return namespace[prop];
}
var dig = digger(namespace[prop], selector);
if (dig != null) {
return dig;
}
} else {
if (prop == selector) {
return namespace[prop];
}
}
}
}
return digger (com, selector);
}
After that, I add a timer to namespace com.doamin.util.
com.domain.util.timer = function () {
this._handle = new InnerObj.SystemTimer(io);
return this;
};
com.domain.util.timer.prototype.expiresFromNow = function (seconds, cbHandler) {
this._handle.ExpiresFromNow (seconds, cbHandler);
};
com.domain.util.timer.prototype.wait = function (seconds, cbHandler) {
this._handle.Wait (seconds, cbHandler);
};
com.domain.util.timer.prototype.expiresAt = function (seconds, cbHandler) {
this._handle.Wait (seconds, cbHandler);
};
com.domain.util.timer.prototype.cancel = function () {
this._handle.Cancel ();
};
Usage:
1. var timer = new com.domain.util.timer (); OK
timer.expiresAt (1, {}); OK
2. var func = $("timer"); OK
var timer = new func (); OK
timer.expiresAt (1, {}); OK
But but but but but
var timer = new $("timer") (); NG
Can anyone tell me why the last new function is not working?
Try var timer = new ($("timer"))();.
Your question is not clear but I guess since $("timer") returns a function, you want a new instance of the result of $("timer") and not a new instance of $().

apply parameter into function within parameter array

http://jsbin.com/ukizof/1/
how do you call a function which is part of a array and set a paramater to it , as in the example below i want the script to return a function in order to call a function parameter which as below in the example is set below.
var bQuery = {
one: function(elem, options) {
options = this.extend({
method: 'html',
event: 'test2',
func:null
}, options || {});
var element = elem;
if (options.method == 'html') {
element.innerHTML = options.event;
} else if (options.method == 'append') {
element.innerHTML = element.innerHTML + options.event;
} else if (options.method == 'prepend') {
element.innerHTML = options.event + element.innerHTML;
}
return // return method to apply string to func: parameter function as below is "e"
},
extend: function(a, b) {
for (var prop in b) {
a[prop] = b[prop];
}
return a;
}
};
$ = bQuery;
$.one(document.getElementById("log"), {
method: 'append',
event: 'rjfjfjjffj',
func: function(e){
alert(e);
}
});
If I understood you right, you want
var options, element;
…
return function() {
if (typeof options.func == 'function')
return options.func(element.innerHTML);
};

Javascript: What is the preferred design for nested (inner) types?

I use Resig's makeClass() approach for constructors:
// makeClass - By John Resig (MIT Licensed)
// Allows either new User() or User() to be employed for construction.
function makeClass(){
return function(args){
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" )
this.init.apply( this, (args && args.callee) ? args : arguments );
} else
return new arguments.callee( arguments );
};
}
// usage:
// ------
// class implementer:
// var MyType = makeClass();
// MyType.prototype.init = function(a,b,c) {/* ... */};
// ------
// class user:
// var instance = new MyType("cats", 17, "September");
// -or-
// var instance = MyType("cats", 17, "September");
//
var MyType = makeClass();
MyType.prototype.init = function(a,b,c) {
say("MyType init: hello");
};
MyType.prototype.Method1 = function() {
say("MyType.Method1: hello");
};
MyType.prototype.Subtype1 = makeClass();
MyType.prototype.Subtype1.prototype.init = function(name) {
say("MyType.Subtype1.init: (" + name + ")");
}
In that code, MyType() is a toplevel type, and MyType.Subtype1 is a nested type.
To use it, I can do:
var x = new MyType();
x.Method1();
var y = new x.Subtype1("y");
Can I get a reference to the instance of the parent type, within the init() for Subtype1() ?
How?
Nope, not unless you write a class implementation that tracks this "outer" class explicitly, Javascript won't be able to give this to you.
For example:
function Class(def) {
var rv = function(args) {
for(var key in def) {
if(typeof def[key] == "function" && typeof def[key].__isClassDefinition == "boolean")
def[key].prototype.outer = this;
this[key] = def[key];
}
if(typeof this.init == "function")
this.init.apply( this, (args && args.callee) ? args : arguments );
};
rv.prototype.outer = null;
rv.__isClassDefinition = true;
return rv;
}
var MyType = new Class({
init: function(a) {
say("MyType init: " + a);
say(this.outer);
},
Method1: function() {
say("MyType.Method1");
},
Subtype1: new Class({
init: function(b) {
say("Subtype1: " + b);
},
Method1: function() {
say("Subtype1.Method1");
this.outer.Method1();
}
})
});
var m = new MyType("test");
m.Method1();
var sub = new m.Subtype1("cheese");
sub.Method1();

Categories

Resources