It's been a while since I've done a jQuery plugin and I'm working off a pretty common boilerplate with options and internal events. In one of the internal methods I need to trigger a custom event so that other pages can capture the event and work with it. More specifically in this usage, at the end of drawing on a canvas element I want to be able to capture the event outside of the plugin in order to grab the canvas content for sending elsewhere agnostic of the plugin itself.
However, it doesn't appear that the trigger call is either firing or finding the bound event from the other page. None of the console messages show up in Firebug.
Here's my sample plugin (simplified):
; (function ($, window, document, undefined) {
"use strict";
var $canvas,
context,
defaults = {
capStyle: "round",
lineJoin: "round",
lineWidth: 5,
strokeStyle: "black"
},
imgElement,
options,
pluginName = "myPlugin";
function MyPlugin(element, opts) {
this.imgElement = element;
this.options = $.extend({}, defaults, opts);
this._defaults = defaults;
this._name = pluginName;
this.init();
}
$.extend(MyPlugin.prototype, {
init: function () {
var $imgElement = $(this.imgElement);
$canvas = $(document.createElement("canvas")).addClass("myPluginInstances");
$imgElement.after($canvas);
context = $canvas[0].getContext("2d");
$canvas.on("mousedown touchstart", inputStart);
$canvas.on("mousemove touchmove", inputMove);
$canvas.on("mouseup touchend", inputEnd);
}
});
$.fn.myPlugin = function (opts) {
return this.each(function () {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName, new MyPlugin(this, opts));
}
});
};
function inputStart(event) {
//...processing code
}
function inputMove(event) {
//...processing code
}
function inputEnd(event) {
//...processing code
// Trigger custom event
$(this.imgElement).trigger("mydrawevent", [this.toDataURL()]);
}
}(jQuery, window, document));
Then from a separate page in the document.ready the event is bound:
$(".myPluginInstances").myPlugin().on("mydrawevent", function (e, data) {
console.log("mydrawevent");
console.log(data);
});
From Firebug I am seeing that the imgElement does have the listener bound:
mydrawevent
-> function(a)
-> function(e, data)
I've tried quite a few things such as calling the trigger on different DOM elements, passing the event parameter data in and out of the array, defining a callback method instead (which had its own issues), and more. I have a feeling the problem is something dumb and right in front of me but I could use more sets of eyes to double check my sanity.
As further explanation for my response to Vikk above, the problem was indeed scoping and understanding which objects were bound to what parts of the plugin. In my case, the internal methods that are private event handlers were bound to my canvas element but the plugin itself was being instantiated on the img element which is at least a temporary requirement of this particular implementation.
Based on this, from the internal event handler the use of $(this) meant that it was trying to use trigger on my canvas element and not the img element which had the mydrawevent listener attached to it from outside the plugin.
Related
i've a little problem with an event throwing from a plugin. My plugin is throwing an event calling 'initNewHeight' and this is working fine. And now i want to send the changed value in the plugin to the function whitch called this plugin...
like so:
(function($) {
$.fn.meAutoSize = function (options, callback){
var $this = $(this);
var $options = options || {};
/* Optionen welche übergeben wurden in eine Variable speichern und mit den Standardwerten verbinden */
$options = $.extend($.fn.meAutoSize.defaults, options);
/* Event an den EventHandler binden */
$this.bind("initEvent", function() { options.init.call(); });
/* Initialize Event auslösen, neu Inhalte laden */
$this.trigger("initEvent", $this);
/* Initialize Event auslösen, neu Inhalte laden */
$this.bind("initEventNewHeight", function() { options.initNewHeight.call() });
var newHeight = 12
$this.trigger("initEventNewHeight", $this, newHeight);
};
// Standard-Optionen für das Plugin
$.fn.meAutoSize.defaults = {
init: function() {},
initNewHeight: function(el, newHeight) {}
}
})(jQuery);
$('.autoheight').meAutoSize({
init: function() {
$('#Init').html('init function')
},
initNewHeight: function(el, newHeight) {
$('#NewHeight').html('NewHeight: ' + el +', ' + newHeight)
}
});
but this way doesn't work.
so you can see it here: http://jsfiddle.net/Tabes/qRPfm/
try changing, this:
$this.bind("initEventNewHeight", function() { options.initNewHeight.call() });
to:
$this.bind("initEventNewHeight", function() { options.initNewHeight.call($this, $this, 120) });
There are a lot of issues with your plugin and I strongly advise you to read at least this tutorial.
Updated Fiddle Note that I did not fixed the points mentionned below, I just made it work like you expected
If you pass multiple arguments to trigger, you need to wrap them in an array.
$this.trigger("initEventNewHeight", [$this, newHeight]);
Within a jQuery object prototype function (your plugin function), this points to the jQuery object, not a DOM node. Therefore, var $this = $(this); doesn't do what you think it does. Also, the way your code is structured will cause issues if the jQuery set on which your plugin is called encapsulates multiple DOM elements.
Your plugin function should look like:
return this.each(function {
/*apply plugin to 'this', which now is a DOM node*/
});
bind is deprecated, use on.
Arguments do not fall of the sky, I do not see how you expected to get any value at all since you were not passing any.
$this.bind("initEventNewHeight", function(e, el, newHeight) {
//passing el is not necessary since the context will already be $this
options.initNewHeight.call($this, el, newHeight);
});
You should consider using the DOM element itself as an event emitter and leverage the jQuery event architecture to publish your events rather than relying on callbacks functions like you do.
//E.g.
var someEvent = $.Event('myCustomEvent');
someEvent.someParam = 'some value';
$(domElOfYourPlugin).trigger(someEvent);
Now clients can register handlers like $(domElOfYourPlugin).on('myCustomEvent', ...)
I'm building a simple jQuery plugin called magicForm (How ridiculous is this?). Now face to a problem that I think I'm not figuring out properly.
My plugin is supposed to be applied on a container element, that will show each of its inputs one by one as user fills them. That's not the exact purpose of my problem.
Each time I initialize the container, I declare an event click callback. Let me show an example.
(function($){
var methods = {
init: function(options){
return this.each(function(){
var form, inputs;
var settings = {
debug: false
};
settings = $.extend(settings, options);
form = $(this);
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
});
},
reset: function() {
}
}
$.fn.magicForm = 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.' );
}
};
})($);
I'm focusing on a specific part of this code :
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
Because each time the init method is called, that poor callback is registered.
I was experiencing this painfully, when I invoked my plugin on an element nested in a twitter bootstrap 'tab', nested itself in a bootstrap modal :
I was calling init each time the event 'shown' of my bootstrap modal was triggered.
So, this is how I fixed it in my init method :
// Prevent callback cumulation
if (!$(this).data('form_initialized')) {
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
$(this).data('form_initialized', true);
}
And I'm far from feeling sure about this.
Thank your for your time !
Many jquery plugins use data to know if their plugins were initialized. Most often, they use the name of their own plugin as a part (or in whole) as the data. For example:
$(this).data('magicForm')
So your approach of using that to signal is not a bad one.
However, you have two other options:
1) Pull the event handler out so the handler is a single instance. Above your methods, do var fnOnSubmit = function() { ... } Then you can simply ensure proper binding by calling $('a.submit', form).unbind('click', fnOnSubmit) before rebinding it the way you are already doing it.
2) Another option is to use event namespaces.
$('a.submit', form).unbind('click.magicForm'); then rebinding it with .on('click.magicForm') This namespace approach ensures that when you unbind it only unbinds in the context of your namespace magicForm, thus leaving all other click events (e.g. from other plugins) intact.
I hope this helps.
You could first explicitely remove the click-handler:
$('a.submit', form).off('click').on('click', function(event){ ... })
However, I would suggest you use event namespacing to prevent all click handlers (even those perhaps set by code not your own) from being removed:
$('a.submit', form).off('click.magicForm').on('click.magicForm', function(event){ ... })
I have a problem with event object passed to the function in drop event. In my code, div#dropArea has it's drop event handled by firstDrop function which does some animations and then calls the proper function dropFromDesktop which handles the e.dataTransfer.files object. I need this approach in two separate functions because the latter is also used further by some other divs in the HTML document (no need to duplicate the code). First one is used only once, to hide some 'welcome' texts.
Generally, this mechanism lets you drag files from desktop and drop them into an area on my website.
Here's, how it looks (in a shortcut):
function firstDrop(ev) {
var $this = $(this);
//when I call the function here, it passes the event with files inside it
//dropFromDesktop.call($this, ev);
$this.children('.welcomeText').animate({
opacity: '0',
height: '0'
}, 700, function() {
$('#raw .menu').first().slideDown('fast', function() {
//when I call the function here, it passes the event, but 'files' object is empty
dropFromDesktop.call($this, ev);
});
});
}
function dropFromDesktop(ev) {
var files = ev.originalEvent.dataTransfer.files;
(...) //handling the files
}
$('#dropArea').one('drop', firstDrop);
$('some_other_div').on('drop', dropFromDesktop);
The problem is somewhere in jQuery.animation's callback - when I call my function inside it, the event object is passed correctly, but files object from dataTransfer is empty!
Whole script is put inside $(document).ready(function() { ... }); so the order of function declarations doesn't matter, I guess.
I suspect your problem is related with the lifetime of the Event object. Unfortunately, I have no clue about the cause of it. But, there is a way to workaround it that I can think of and it is keeping a reference to Event.dataTransfer.files instead.
var handleFileList = function(fn) {
return function(evt) {
evt.preventDefault();
return fn.call(this, evt.originalEvent.dataTransfer.files);
};
};
var firstDrop = function(fileList) { ... }
var dropFromDesktop = function(fileList) { ... }
$('#dropArea').one('drop', handleFileList(firstDrop));
$('some_other_div').on('drop', handleFileList(dropFromDesktop));
I'm learning javascript and have a question about listening and dispatching events with jQuery.
In my Model, I have a function that triggers a change event:
Model.prototype.setCurrentID = function(currentID) {
this.currentID = currentID;
$('body').trigger('change');
}
The trigger event requires an element, so I bound it to the 'body'. Is this good practice or bad practice?
In AS3, which I'm more familiar, I would simply dispatch a global event from the model, passing in a const value, listening for this event with an instance of the Model:
var model:Model = new Model();
model.addEventListener(CONST_VALUE, handlerFunction);
In jQuery, within my View object, I need to attach an element to the listener as well, so I bound it to the 'body' once again:
var View = function(model, controller) {
var model;
var controller;
this.model = model;
this.controller = controller;
$('body').change(function(evt) { updateSomething(evt); });
function updateSomething(evt){console.log('updating...')};
}
It's working, but I'm interested in your take on the subject.
I recommend using a private dispatcher, something that isn't exposed to the public.
For instance, your logic may fail if the user or a plugin unbinds all the events on the body(your dispatcher) :
$('body').unbind();
This can be avoided by creating a dom node and not expose it to the end user (do not append it to the dom) :
var dispatcher = $('<div />');
Model.prototype.setCurrentID = function(currentID) {
this.currentID = currentID;
dispatcher.trigger('change');
}
var View = function(model, controller) {
this.model = model;
this.controller = controller;
dispatcher.bind('change',function(evt) { updateSomething(evt); });
function updateSomething(evt){console.log('updating...')}
}
Another good thing to have in mind when developing event-programming app with jQuery is that jQuery allows you to bind/trigger custom events and also allows you to namespace your events. This way you can control more efficiently the event binding and triggering :
Model.prototype.setCurrentID = function(currentID) {
this.currentID = currentID;
dispatcher.trigger('modelIdChange.' + this.currentID);
}
Model.prototype.destroy = function() {
// unbind all the event handlers for this particular model
dispatcher.unbind('.'+this.currentID);
}
var View = function(model, controller) {
/*...*/
// this will be triggered for all the changes
dispatcher.bind('modelIdChange',function(evt) { updateSomething(evt); });
// this will be triggered only for the model with the id "id1"
dispatcher.bind('modelIdChange.id1',function(evt) { updateSomething(evt); });
/*...*/
}
I'd go a step further and create custom global events. With jQuery you can trigger a global custom event like so:
$.event.trigger('change');
Any element can subscribe to that event:
$('#myDiv').bind('change', function() {
console.log($(this));
});
The this keyword in the event handler is the DOM element which subscribed to the triggered event.
My objections are:
I wouldn't bind events that have the same name as broswer events, there might be interferences.
Your code works if you have one model, but if you have 2 or more, you'd want to separate them, and not bind/trigger both on the same element.
How about:
Model.prototype.bind = function(event, func) {
if (!this._element) this._element = $('<div>');
this._element.bind(this.name+'_'+event, $.proxy(func, this));
return this;
};
Model.prototype.trigger = function(event) {
if (!this._element) this._element = $('<div>');
this._element.trigger(this.name+'_'+event);
return this;
};
This way you solve both. Note I'm appending this.name+'_' to event names (which assume each model has some sort of name, and makes sure events won't match with browser events), but you can also drop the the prefix.
I'm also using $.proxy in bind so that the this in the event handler refers to the model.
var View = function(model, controller) {
....
model.bind('change', function() {...});
}
I have created a class in mootools that when the initialize() method is first called it creates a div elements which is then appended to the document.body. I then attach a context menu handler which will call functions when an option from the context menu is selected in the browser.
The trouble I am having is that the context menu handler will not actually call any functions and I can't quite figure out why and was wondering if anyone here could spot the problem?
Here is the class I have created and the attached context-menu handler, some of the other code has been removed for brevity:
var uml_Canvas = new Class({
initialize: function()
{
this.mainCanvasDiv = document.createElement("div");
this.mainCanvasDiv.id = "mainCanvas";
this.mainAppDiv.appendChild(this.mainCanvasDiv);
this.paper = Raphael(this.mainCanvasDiv.id, 500, 400);
this.paper.draggable.enable();
$("#"+this.mainCanvasDiv.id).contextMenu('canvasPanel_Menu',
{
bindings:
{
'clear': function(t)
{
this.clearPaper();
}
}
});
},
clearPaper : function()
{
this.paper.clear();
}
});
So a quick overview, an object is created which creates a div and then appends it to the body. The div then has a context-menu assigned. When the 'clear' option is called the method clearPaper() should be called be for some reason it is not. If, however, I replace the this.clearPaper(); line with a simple alert() call, it does indeed run.
Can anyone see a reason why it is not possible to call a method?
BTW the error I get is this.clearPaper is not a function if that helps.
Try binding "this" to your clear function:
'clear': function(t)
{
this.clearPaper();
}.bind(this)
This takes the "this" scope and allows the anonymous function to use it as if it were a member of that class.
Note that you have to do this whenever you try to use "this." inside of any anonymous function. For instance, if you have inside a class:
method: function() {
button.addEvent('click', function(e) {
new Request({
onComplete: function(res) {
this.process_result(res);
}
}).send();
});
},
process_results: function(res) {...}
You have to bind all the way down:
method: function() {
button.addEvent('click', function(e) {
new Request({
onComplete: function(res) {
this.process_result(res);
}.bind(this)
}).send();
}.bind(this));
},
process_results: function(res) {...}
Notice the new bind()s on the event function and the onComplete function. It may seem like an annoying extra step, but without doing this, you'd have scope free-for-all. Mootools makes it extremely easy to take your class scope and attach it to an anonymous function.