Creating methods on the fly - javascript

Hi I'm trying to author a jQuery plugin and I need to have methods accessible to elements after they are initialized as that kind of object, e.g.:
$('.list').list({some options}); //This initializes .list as a list
//now I want it to have certain methods like:
$('.list').find('List item'); //does some logic that I need
I tried with
$.fn.list = function (options) {
return this.each(function() {
// some code here
this.find = function(test) {
//function logic
}
}
}
and several other different attempts, I just can't figure out how to do it.
EDIT:
I'll try to explain this better.
I'm trying to turn a table into a list, basically like a list on a computer with column headers and sortable items and everything inbetween. You initiate the table with a command like
$(this).list({
data: [{id: 1, name:'My First List Item', date:'2010/06/26'}, {id:2, name:'Second', date:'2010/05/20'}]
});
.list will make the <tbody> sortable and do a few other initial tasks, then add the following methods to the element:
.findItem(condition) will allow you to find a certain item by a condition (like findItem('name == "Second"')
.list(condition) will list all items that match a given condition
.sort(key) will sort all items by a given key
etc.
What's the best way to go about doing this?

If you want these methods to be available on any jQuery object, you will have to add each one of them to jQuery's prototype. The reason is every time you call $(".list") a fresh new object is created, and any methods you attached to a previous such object will get lost.
Assign each method to jQuery's prototype as:
jQuery.fn.extend({
list: function() { .. },
findItem: function() { .. },
sort: function() { .. }
});
The list method here is special as it can be invoked on two occasions. First, when initializing the list, and second when finding particular items by a condition. You would have to differentiate between these two cases somehow - either by argument type, or some other parameter.
You can also use the data API to throw an exception if these methods are called for an object that has not been initialized with the list plugin. When ('xyz').list({ .. }) is first called, store some state variable in the data cache for that object. When any of the other methods - "list", "findItem", or "sort" are later invoked, check if the object contains that state variable in its data cache.
A better approach would be to namespace your plugin so that list() will return the extended object. The three extended methods can be called on its return value. The interface would be like:
$('selector').list({ ... });
$('selector').list().findOne(..);
$('selector').list().findAll(..);
$('selector').list().sort();
Or save a reference to the returned object the first time, and call methods on it directly.
var myList = $('selector').list({ ... });
myList.findOne(..);
myList.findAll(..);
myList.sort();

I found this solution here:
http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html
This seems to do exactly what I need.
(function($) {
var TaskList = function(element, options)
{
var $elem = $(element);
var options = $.extend({
tasks: [],
folders: []
}, options || {});
this.changed = false;
this.selected = {};
$elem.sortable({
revert: true,
opacity: 0.5
});
this.findTask = function(test, look) {
var results = [];
for (var i = 0,l = options.tasks.length; i < l; i++)
{
var t = options['tasks'][i];
if (eval(test))
{
results.push(options.tasks[i]);
}
}
return results;
}
var debug = function(msg) {
if (window.console) {
console.log(msg);
}
}
}
$.fn.taskList = function(options)
{
return this.each(function() {
var element = $(this);
if (element.data('taskList')) { return; }
var taskList = new TaskList(this, options);
element.data('taskList', taskList);
});
}
})(jQuery);
Then I have
$('.task-list-table').taskList({
tasks: eval('(<?php echo mysql_real_escape_string(json_encode($tasks)); ?>)'),
folders: eval('(<?php echo mysql_real_escape_string(json_encode($folders)); ?>)')
});
var taskList = $('.task-list-table').data('taskList');
and I can use taskList.findTask(condition);
And since the constructor has $elem I can also edit the jQuery instance for methods like list(condition) etc. This works perfectly.

this.each isn't needed. This should do:
$.fn.list = function (options) {
this.find = function(test) {
//function logic
};
return this;
};
Note that you'd be overwriting jQuery's native find method, and doing so isn't recommended.
Also, for what it's worth, I don't think this is a good idea. jQuery instances are assumed to only have methods inherited from jQuery's prototype object, and as such I feel what you want to do would not be consistent with the generally accepted jQuery-plugin behaviour -- i.e. return the this object (the jQuery instance) unchanged.

Related

Custom JQuery Plugin Method error

I've been working on writing a custom jquery plugin for one of my web applications but I've been running into a strange error, I think it's due to my unfamiliarity with object-oriented programming.
The bug that I've been running into comes when I try to run the $(".list-group").updateList('template', 'some template') twice, the first time it works just fine, but the second time I run the same command, I get an object is not a function error. Here's the plugin code:
(function($){
defaultOptions = {
defaultId: 'selective_update_',
listSelector: 'li'
};
function UpdateList(item, options) {
this.options = $.extend(defaultOptions, options);
this.item = $(item);
this.init();
console.log(this.options);
}
UpdateList.prototype = {
init: function() {
console.log('initiation');
},
template: function(template) {
// this line is where the errors come
this.template = template;
},
update: function(newArray) {
//update code is here
// I can run this multiple times in a row without it breaking
}
}
// jQuery plugin interface
$.fn.updateList = function(opt) {
// slice arguments to leave only arguments after function name
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var item = $(this), instance = item.data('UpdateList');
if(!instance) {
// create plugin instance and save it in data
item.data('UpdateList', new UpdateList(this, opt));
} else {
// if instance already created call method
if(typeof opt === 'string') {
instance[opt](args);
}
}
});
}
}(jQuery));
One thing I did notice when I went to access this.template - It was in an array so I had to call this.template[0] to get the string...I don't know why it's doing that, but I suspect it has to do with the error I'm getting. Maybe it can assign the string the first time, but not the next? Any help would be appreciated!
Thanks :)
this.template = template
Is in fact your problem, as you are overwriting the function that is set on the instance. You end up overwriting it to your args array as you pass that as your argument to the initial template function. It basically will do this:
this.template = ["some template"];
Thus the next time instance[opt](args) runs it will try to execute that array as if it were a function and hence get the not a function error.
JSFiddle

Store state of a JavaScript Object

Im trying to store the stats of 'this' in my javscript object so that later on in my application I can return 'this' to a previous state. I thought I could accomplish using a closure but so far I haven't successful. My idea was to do something like this
function SavedFeature() {
var self = this;
this.savedItem;
this.storeState = function() {
this.savedItem = storeClosure();
}
function storeClosure() {
var closure = self;
return function() {
return closure;
};
};
//other things the user can change...
}
so later on in my application if I needed to return to the point when I called storeState I could just do
//return the object I put in my closure
var backToNormal = savedFeature.savedItem();
that doesn't work though because any changes to my savedFeature object after I call storeState() are being reflected in the item im retrieving from called savedItem(). I'm guessing this is happening because closure is being set to a reference of self instead of copied to a new instance.
Is there anyway to store the state of my entire object in a closure like this or do I need to store this some other way.
The issue you are running into is that in js objects are passed by reference. This means that all changes performed on your object will apply to your obj.savedItem property.
Fix: Store a deep clone into obj.savedItem
this.storeState = function() {
this.savedItem = _.cloneDeep(this); // or _.clone(this, true);
}
cloneDeep is a lodash method, most js libs supply one of their own, e.g. jQuery's $.extend, etc.
You could easily roll your own deep clone function, look up the options on this thread.
A complete example with jQuery:
function SavedFeature() {
this.savedItem;
this.clone = function() {
return $.extend(true, {}, this);
},
this.storeState = function() {
this.savedItem = this.clone();
}
}
Doing it this way allows you adapt to different environments by changing your clone method as it is facading the used library method.
There are dozens of ways how to implement it. I will do just simple one. saving property.
Take into account if you want to save entire object you need to do deep copy of the object.
this is your feature:
function SavedFeature() {
this.savedItem = {'isNew': true};
this.stateMachine = new StateMachine();
}
this is some kind of state machine:
function StateMachine () {
var state = { 'isNew' : null};
function set(newState) {
state.isNew = newState.isNew;
}
function get() {
return state.isNew;
}
return {
get : get,
set : set
};
}
which, know how to store isNew property
and a working sample:
var savedFeature = new SavedFeature();
console.log(savedFeature.savedItem); // true by default
savedFeature.stateMachine.set(savedFeature.savedItem); // saving state.
savedFeature.savedItem.isNew = false; // modifying state
console.log(savedFeature.savedItem); // return false, because of statement above
var restoredState = savedFeature.stateMachine.get(); // restoring state
console.log(restoredState); // true
savedFeature.savedItem.isNew = restoredState.isNew;
console.log(savedFeature.savedItem); // true
you can adjust that code, and reach functionality whatever you need. hope that helps

How can I make Ember.js handlebars #each iterate over objects?

I'm trying to make the {{#each}} helper to iterate over an object, like in vanilla handlebars. Unfortunately if I use #each on an object, Ember.js version gives me this error:
Assertion failed: The value that #each loops over must be an Array. You passed [object Object]
I wrote this helper in attempt to remedy this:
Ember.Handlebars.helper('every', function (context, options) {
var oArray = [];
for (var k in context) {
oArray.push({
key : k,
value : context[k]
})
}
return Ember.Handlebars.helpers.each(oArray, options);
});
Now, when I attempt to use {{#every}}, I get the following error:
Assertion failed: registerBoundHelper-generated helpers do not support use with Handlebars blocks.
This seems like a basic feature, and I know I'm probably missing something obvious. Can anyone help?
Edit:
Here's a fiddle: http://jsfiddle.net/CbV8X/
Use {{each-in}} helper. You can use it like like {{each}} helper.
Example:
{{#each-in modelWhichIsObject as |key value|}}
`{{key}}`:`{{value}}`
{{/each-in}}
JS Bin demo.
After fiddling with it for a few hours, I came up with this hacky way:
Ember.Handlebars.registerHelper('every', function(context, options) {
var oArray = [], actualData = this.get(context);
for (var k in actualData) {
oArray.push({
key: k,
value: actualData[k]
})
}
this.set(context, oArray);
return Ember.Handlebars.helpers.each.apply(this,
Array.prototype.slice.call(arguments));
});
I don't know what repercussions this.set has, but this seems to work!
Here's a fiddle: http://jsfiddle.net/CbV8X/1/
I've been after similar functionality, and since we're sharing our hacky ways, here's my fiddle for the impatient: http://jsfiddle.net/L6axcob8/1/
This fiddle is based on the one provided by #lxe, with updates by #Kingpin2k, and then myself.
Ember: 1.9.1, Handlebars: 2.0.0, jQuery 2.1.3
Here we are adding a helper called every which can iterate over objects and arrays.
For example this model:
model: function() {
return {
properties: {
foo: 'bar',
zoo: 'zar'
}
};
}
can be iterated with the following handlebars template:
<ul class="properties">
{{#every p in properties}}
<li>{{p.key}} : {{p.value}}</li>
{{/every}}
</ul>
every helper works by creating an array from the objects keys, and then coordinating changes to Ember by way of an ArrayController. Yeah, hacky. This does however, let us add/remove properties to/from an object provided that object supports observation of the [] property.
In my use case I have an Ember.Object derived class which notifies [] when properties are added/removed. I'd recommend looking at Ember.Set for this functionality, although I see that Set been recently deprecated. As this is slightly out of this questions scope I'll leave it as an exercise for the reader. Here's a tip: setUnknownProperty
To be notified of property changes we wrap non-object values in what I've called a DataValueObserver which sets up (currently one way) bindings. These bindings provide a bridge between the values held by our internal ArrayController and the object we are observing.
When dealing with objects; we wrap those in ObjectProxy's so that we can introduce a 'key' member without the need to modify the object itself. Why yes, this does imply that you could use #every recursively. Another exercise for the reader ;-)
I'd recommend having your model be based around Ember.Object to be consistent with the rest of Ember, allowing you to manipulate your model via its get & set handlers. Alternatively, as demonstrated in the fiddle, you can use Em.Get/Em.set to access models, as long as you are consistent in doing so. If you touch your model directly (no get/set), then every won't be notified of your change.
Em.set(model.properties, 'foo', 'asdfsdf');
For completeness here's my every helper:
var DataValueObserver = Ember.Object.extend({
init: function() {
this._super();
// one way binding (for now)
Em.addObserver(this.parent, this.key, this, 'valueChanged');
},
value: function() {
return Em.get(this.parent, this.key);
}.property(),
valueChanged: function() {
this.notifyPropertyChange('value');
}
});
Handlebars.registerHelper("every", function() {
var args = [].slice.call(arguments);
var options = args.pop();
var context = (options.contexts && options.contexts[0]) || this;
Ember.assert("Must be in the form #every foo in bar ", 3 == args.length && args[1] === "in");
options.hash.keyword = args[0];
var property = args[2];
// if we're dealing with an array we can just forward onto the collection helper directly
var p = this.get(property);
if (Ember.Array.detect(p)) {
options.hash.dataSource = p;
return Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
}
// create an array that we will manage with content
var array = Em.ArrayController.create();
options.hash.dataSource = array;
Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
//
var update_array = function(result) {
if (!result) {
array.clear();
return;
}
// check for proxy object
var result = (result.isProxy && result.content) ? result.content : result;
var items = result;
var keys = Ember.keys(items).sort();
// iterate through sorted array, inserting & removing any mismatches
var i = 0;
for ( ; i < keys.length; ++i) {
var key = keys[i];
var value = items[key];
while (true) {
var old_obj = array.objectAt(i);
if (old_obj) {
Ember.assert("Assume that all objects in our array have a key", undefined !== old_obj.key);
var c = key.localeCompare(old_obj.key);
if (0 === c) break; // already exists
if (c < 0) {
array.removeAt(i); // remove as no longer exists
continue;
}
}
// insert
if (typeof value === 'object') {
// wrap object so we can give it a key
value = Ember.ObjectProxy.create({
content: value,
isProxy: true,
key: key
});
array.insertAt(i, value);
} else {
// wrap raw value so we can give it a key and observe when it changes
value = DataValueObserver.create({
parent: result,
key: key,
});
array.insertAt(i, value);
}
break;
}
}
// remove any trailing items
while (array.objectAt(i)) array.removeAt(i);
};
var should_display = function() {
return true;
};
// use bind helper to call update_array if the contents of property changes
var child_properties = ["[]"];
var preserve_context = true;
return Ember.Handlebars.bind.call(context, property, options, preserve_context, should_display, update_array, child_properties);
});
Inspired by:
How can I make Ember.js handlebars #each iterate over objects?
http://mozmonkey.com/2014/03/ember-getting-the-index-in-each-loops/
https://github.com/emberjs/ember.js/issues/4365
https://gist.github.com/strathmeyer/1371586
Here's that fiddle again if you missed it:
http://jsfiddle.net/L6axcob8/1/

Referencing a parent object in callback functions with jQuery

I've a page that is generated dynamically, and that includes certain number (user-dynamically-defined) of advanced scatter plot charts. I intend to create a JavaScript object which defines the scatter plot itself, i.e. which takes some parameters, some data, and some container ID, and which will create the various elements needed to obtain the visualisation: canvas elements, toolbar, etc.. To do so, I started with the following (simplified) class:
(function () {
if (!this.namespace) { this.namespace = {};}
this._instances = { index: 0 };
this.namespace.ScatterPlot = function (containerId, file, options) {
_instances.index ++;
this.id = this.containerId+"-"+_instances.index ;
this.containerId = containerId ;
_instances [this.id] = this;
// ... Do stuffs with file and options ...
// Initialize elements once the DOM is ready
$(this.updateDOM);
}
namespace.ScatterPlot.prototype = {
updateDOM: function() {
$("<canvas>")
.click(clickCallback)
.appendTo("#"+this.containerId);
//(...)
},
clickCallback: function() {
alert("Some click: "+this.id);
}
}
})();
Each object can be created with:
var v1 = new namespace.ScatterPlot("container1", "foo", "foo");
var v2 = new namespace.ScatterPlot("container2", "foo", "foo");
There are two problems here: (1) in updateDOM, 'this' does not make reference to my initial ScatterPlot object, which means that this example will never work, and (2) similarly, the clickCallback will not be able reference the scatterplot with 'this' either.
I'm new to javascript, and I'm still struggeling to understand the logic of OO programming in javascript, so the question is: I'm I taking the wrong direction here ? After some digging, I could roughly achieve what I wanted by passing this to updateDOM:
$(this.updateDOM(this)); // This blows my eyes but does the trick, at least partially
updateDOM: function(that) {
$("<canvas>")
.click(that.clickCallback)
.appendTo("#"+that.containerId);
//(...)
},
clickCallback: function() {
// Not working either... Should pass 'that' to the function too
alert("Some click: "+this.id);
}
But I don't feel this patters to be very elegant... And the problem is not fixed either regarding the click callback.
Thoughts ?
Have a look at MDN's introduction to the this keyword.
The standard ways of dealing with that issue are using a that variable - not as an argument, but in a separate function:
var that = this;
$(function() {
that.updateDOM();
});
// or
$(this.getClickCallback());
...
namespace.ScatterPlot.prototype.getClickCallback = function() {
var that = this;
return function clickCallback(e) {
alert("Some click: "+that.id);
};
};
Alternatively, you can always use .bind() (or $.proxy for older browsers) which do quite what the second example does in a more generic way:
$(this.clickCallback.bind(this));

Render jQuery object through Backbone View

I'm rather new to Backbone.js development, and have run into a bit of a roadblock while attempting to render a subview.
Currently, I have in place several views to render a custom dropdown-button, as well as other elements. I've taken this approach based on DocumentCloud's code
Here's what I have so far:
app.ui.SelectMenu = Backbone.View.extend({
className: 'btn-group group-item',
options: {
id: null,
standalone: false
},
events: {
"click .dropdown-menu a": "setLabel"
},
constructor: function (options) {
Backbone.View.call(this, options);
this.items = [];
this.content = JST['common-select_button'];
this.itemsContainer = $('.dropdown-menu', $(this.content.render()));
// Add any items that we may have added to the object params
if (options.items) {
this.addItems(options.items);
}
},
render: function () {
this.$el.html(this.content.render({
label: this.options.label,
items: this.itemsContainer
}));
this._label = this.$('.menu-label');
return this;
},
setLabel: function (label) {
$(this._label).text(label || this.options.label);
},
addItems: function (items) {
this.items = this.items.concat(items);
var elements = _(items).map(_.bind(function (item) {
var attrs = item.attrs || {};
_.extend(attrs, { 'class': 'menu_item' + (attrs['class'] || '') });
var el = this.make('li', attrs, item.title);
return el;
}, this));
$(this.itemsContainer).append(elements);
}
});
So far I have successfully rendered my button, as well as the appropriate label, but I cannot seem to populate the .dropdown-menu when calling the addItems function.
I'm assuming that when render hits, the items variable cannot be populated due to the fact that I am passing a jQuery object and not a string, yet whenever I use items: this.itemsContainer.html(), that simply pastes the html surrounded by quotes... I could simply replace the quotes but that just feels like a hack to me.
Any help would be much appreciated. Thanks!
jQuery's append doesn't take an array:
.append( content [, content] )
content: DOM element, HTML string, or jQuery object to insert at the end of each element in the set of matched elements.
content: One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements.
If you want to append multiple elements in one call, you have to supply them as separate arguments:
$(x).append(e1, e2, e3, ...);
so you'd have to use apply to convert your array to separate arguments:
var $i = $(this.itemsContainer);
$i.append.apply($i, elements);
That sort of chicanery really isn't necessary though, you can add them one by one as you create them:
addItems: function (items) {
this.items = this.items.concat(items);
_(items).each(function (item) {
var attrs = item.attrs || {};
_.extend(attrs, { 'class': 'menu_item' + (attrs['class'] || '') });
this.itemsContainer.append(this.make('li', attrs, item.title));
}, this);
}
Also note that _.each can take a context argument so you don't need a separate _.bind call. And I'm pretty sure that this.itemsContainer is already a jQuery object so you don't need to wrap it $() again.
You might have problems with your render as well:
render: function () {
this.$el.html(this.content.render({
label: this.options.label,
items: this.itemsContainer
}));
this._label = this.$('.menu-label');
return this;
}
I suspect that items: this.itemsContainer is going to end stringifying this.itemsContainer, you might have better luck with something like this:
this.$el.html(this.content.render({ label: this.options.label });
this.$el.find('some selector').append(this.itemsContainer);
where 'some selector' would, of course, depend on the HTML structure; you'll have to adjust the template for this as well.
Your Github link is broken so I don't know what code you're adapting. I do know that your use of constructor is non-standard. Why not use the standard initialize?
constructor / initialize new View([options])
[...] If the view defines an initialize function, it will be called when the view is first created.
You should probably do it this way:
app.ui.SelectMenu = Backbone.View.extend({
// No 'constructor' in here or anywhere...
initialize: function (options) {
this.items = [];
this.content = JST['common-select_button'];
this.itemsContainer = $('.dropdown-menu', $(this.content.render()));
// Add any items that we may have added to the object params
if (options.items) {
this.addItems(options.items);
}
},
//...
});

Categories

Resources