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
Related
I have a javascript code
Html5Template_300x250 = function(config) {
this.config = config;
var self = this;
adkit.onReady(this.init());
};
Html5Template_300x250.prototype = {
// Function That Creates Element Var
d: function(id) {
return document.getElementById(id);
},
// Initialize DCO HTML5 template
init: function() {
adkit.onReady(this.handleSVData);
},
handleSVData: function() {
var myData = adkit.getSVData("varName");
alert(myData);
this.startAd();
},
startAd: function(data) {
alert("test2");
}
}
In the above code i have used an external javascript adkit.js and using that method in my code. The initial method is started as
adkit.onReady(this.init());
It is calling a init function and which is then calling other methods including handleSVData which is getting a value from the json file which is in the root folder as
var myData = adkit.getSVData("varName");
The part of the code is working fine but after that line i am calling another method
this.startAd();
But this method is not working and i am getting error
TypeError: this.startAd is not a function
I am not good in javascript and giving me headaches can someone explain me why it is so complicated and what i am doing wrong here ??
When handleSVData is called by adkit it is called in the scope that is not an instance of Html5Template_300x250 - that is why this does not have startAd method.
As for adkit.onReady(this.init()); line.
adkit.onReady expects a function as a parameter. It stores this function variable and calls when it is time for onReady event. This is set correctly by adkit.onReady(this.handleSVData); line. this.init(), however, is a call to init function and your line adkit.onReady(this.init()); passes to adkit.onReady whatever init returns. But it does not return anything - you are passing undefined as parameter.
init: function() {
var template = this;
adkit.onReady(function(){
template.handleSVData();
});
},
And change line
adkit.onReady(this.init());
to
this.init();
I fear this is something as embarrassing as a typo, but since I´m stuck on this and quite desperate I´m willing to pay with pride. ;)
This is my case:
Task = function (data) {
var self = this;
self.TaskId = data.TaskId;
self.TaskName = ko.observable(data.TaskName);
}
ViewModel = function () {
var self = this;
self.Tasks = ko.observableArray();
self.SelectedTask = ko.observable();
}
$.getJSON("/myService/GetAllTasks",
function (tData) {
var mappedTasks = $.map(tData, function (item) {
return new Task(item)
});
self.Tasks(mappedTasks); // Populate Tasks-array...
});
self.newTaskItem = function () {
var newitem = new Task({
TaskId: -1,
TaskName: "enter taskname here"
});
self.Tasks.push(newitem); // THIS ONE CRASH
self.Tasks().push(newitem); // BUT SUBSTITUTED WITH THIS ONE IT RUNS ON...
self.editTaskItem(newitem);
};
self.editTaskItem = function (item) {
self.SelectedTask(item); // UNTIL TIL LINE WHERE IT CRASHES FOR GOOD...
self.showEditor(true); // makes Task-edior visible in HTML
};
I also hava an "self.SelectedTask.subscription" in my file, but leaving it out of the code makes no difference.
I also should mention that my database table is empty, so the getJSON returns no data to the mappedTasks, leaving self.Tasks() = [ ] (according to Firebug)
I have fixed the incorrectly closed tags in my code.
Part 2:
Decided after a while to redo my code from the starting point. It got me one step further.
The code now stops on the second of these lines (in "self.newTaskItem"):
self.Tasks.push(newitem);
self.SelectedTask(newitem); // Here it fails.
These two observables are connected in my HTML like this:
<select data-bind="options: Tasks, optionsText: '$root.TaskName', value: SelectedTask"</select>
It looks like your ViewModel() function never gets closed. Add a closing } to wherever you want that function declaration to end. It looks to me (based on your formatting) that you want this:
ViewModel = function () {
var self = this;
self.Tasks = ko.observableArray();
self.SelectedTask = ko.observable();
}
Additionally, you need to close your$.getJson call with a );:
$.getJSON("/myService/GetAllTasks",
function (tData) {
var mappedTasks = $.map(tData, function (item) {
return new Task(item)
});
self.Tasks(mappedTasks); // Populate Tasks-array...
});
I am not 100% sure what your problem is or what error you are getting but this is what I would do - change your Task = function to function Task -
function Task(data) {
var self = this;
self.TaskId = data.TaskId;
}
By saying Task = function without using a var in front of it you are registering Task in the global namespace, not a good idea. Same thing with your view model... Fix it if you can still...
self.newTaskItem = function () {
var newitem = new Task({
// Your Task is looking for a TaskId, not a TextBatchId
TaskId: 1
});
self.Tasks.push(newitem);
self.editTaskItem(newitem);
};
Also, you are creating a TextBatchId where I think your Task object is looking for a TaskId. Fix that, or if you are doing it on purpose for some reason please show your view code and give a better explanation of what is going wrong and what errors you see.
(assuming the unclosed stuff isn't present in your real code)
In Task, TaskId isn't an observable, so when you set SelectedTask to a particular task your editor fields won't properly update (it's a fairly common mistake to assume that the elements of an observableArray are themselves observable, but they aren't unless you explicitly make them so).
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));
I am authoring a plugin which instantiates a map. The map would then provide a function to move to another place on the earth.
The script makes the map just fine. However I can't "tack" the function on the element, to be used by another plugin in a callback.
Here's the approach I tried; in plugin:
(function($){
$.fn.mapDo(options){
map = new BlahMap(this.get(0));
this.moveTheMap = function(place){
map.moveItToThat(place);
}; // nope.
}
})(jQuery);
Then, in view:
$(map).mapDo();
$(otherElement).otherControl({
callback: function(place){
$(map).moveTheMap(place); // moveTheMap is not there on $(map)!
}
};
The Question
How do I add a function to the map jQuery or DOM element, if possible? If not, how can I provide that kind of functionality?
More importantly, am I going the right way here by separating the things that way? I'm a bit of a neophyte to Javascript, how are these tasks usually done while still keeping the components apart?
While that's the stab I took at it, more generally, I struggled with the concept of outputting things from a jQuery plugin while maintaining chainability. In this case, what I am trying to do is to output a callback from the plugin that will work on the called element later in the execution.
Plugins normally only add one method to the jQuery prototype, and the method calls to the plugin's instances are done with strings.
(function($) {
$.fn.mapDo = function(options) {
var args = [].slice.call(arguments, 1); //Get all the arguments starting from 2nd argument as an array
return this.each(function() {
var $this = $(this),
instance = $this.data("map-instance");
if (!instance) {
$this.data("map-instance", (instance = new BlahMap(this, options)));
}
if (typeof options == "string") {
instance[options].apply(instance, args);
}
});
};
})(jQuery);
$(elem).mapDo( "moveTheMap", place ); //This would also instantiate the plugin if it wasn't instantiated
Here's jsfiddle showing it in action:
http://jsfiddle.net/X8YA8/1/
You could store the map with .data method.
(function($){
$.fn.mapDo = funciont(options) {
this.data('map', new BlahMap(this.get(0)));
return this;
};
$.fn.moveTheMap = function(place) {
var map = this.data('map');
if (map) {
map.moveItToThat(place);
}
return this;
};
})(jQuery);
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.