I can't find any good resources on how custom events in jquery are actually implemented. Like how they simulate the event bubbling, etc.
This way:
// bubbling is internal
trigger: function( event, data, elem /*, bubbling */ ) {
// Event object or event type
var type = event.type || event,
bubbling = arguments[3];
// Handle a global trigger
if ( !elem ) {
// Don't bubble custom events when global (to avoid too much overhead)
event.stopPropagation();
// Only trigger if we've ever bound an event for it
if ( jQuery.event.global[ type ] ) {
jQuery.each( jQuery.cache, function() {
if ( this.events && this.events[type] ) {
jQuery.event.trigger( event, data, this.handle.elem );
}
});
}
}
// ... snip ...
// Trigger the event, it is assumed that "handle" is a function
var handle = elem.nodeType ?
jQuery.data( elem, "handle" ) :
(jQuery.data( elem, "__events__" ) || {}).handle;
if ( handle ) {
handle.apply( elem, data );
}
var parent = elem.parentNode || elem.ownerDocument;
// ... snip ....
if ( !event.isPropagationStopped() && parent ) {
jQuery.event.trigger( event, data, parent, true );
} else if ( !event.isDefaultPrevented() ) {
// ... snip ...
jQuery.event.triggered = true;
target[ targetType ]();
}
}
What's going on here is as follows:
When trigger is called jQuery checks to see if the event is being triggered globally ($.trigger("event_name");).
If it is not being triggered globally, and propagation has not been stopped and the element in question has a parent element (!event.isPropagationStopped() && parent) then jQuery calls the trigger event manually on the parent element.
jQuery.event.trigger( event, data, parent, true );
There is quite a bit more going on -- see event.js in the jQuery source code.
Check out the tutorials
$(document).bind("eventType", ...);
// This is equivalent to the plugin's $.subscribe("eventType", ...);
$(document).trigger("eventType");
// equivalent to plugin's $.publish("eventType");
Also checkout this SO question
Related
I'm testing a Backbone View in Jasmine. When I call the view's remove method, the element isn't actually removed.
I have this event handler in my view:
onModelChange: function() {
this.$el.html('');
this.render();
}
I have to have it written that way because manually setting the html is the only way to remove it. Calling remove doesn't do anything, and when the view renders itself again it just renders the new content appended to the old content. I even tried calling remove from the developer tools in Chromium but that didn't work either. However, remove does work when I manually test it in the browser, but it doesn't work in Jasmine and it's screwing up my tests.
I think the answer to the problem lies in the jQuery source:
remove: function( selector, keepData /* Internal Use Only */ ) {
var elem,
elems = selector ? jQuery.filter( selector, this ) : this,
i = 0;
for ( ; (elem = elems[i]) != null; i++ ) {
if ( !keepData && elem.nodeType === 1 ) {
jQuery.cleanData( getAll( elem ) );
}
if ( elem.parentNode ) {
if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
setGlobalEval( getAll( elem, "script" ) );
}
elem.parentNode.removeChild( elem ); // right here to be specific
}
}
return this;
},
The remove function is relying on the node's parent to do the removing. I'm guessing that when the tests run in karma, the backbone view's node has no parent. To explore a bit more, I debugged the test. In the console, if I query for a view element's child and I remove it, it works.
The CanJS documentation has an example like this:
var Todos = can.Control.extend({
init: function( element , options ) { ... },
'li click': function( li ) { ... },
'li .destroy {destroyEvent}': function( el, ev ) {
// previous destroy code here
}
});
// create Todos with this.options.destroyEvent
new Todos( '#todos', { destroyEvent: 'mouseenter' } );
However, if #todos is created after new Todos is called, no event is bound the future element, or if a method within Todos removes a pre-created #todos dummy as deemed necessary. How can I rebind custom events within a Control? After a Control instantiation call?
Just use Control.on();
http://canjs.com/docs/can.Control.prototype.on.html
You can specifiy which event to be listen to or just call the function without parameters like this the control listen to all events.
I am attempting to implement a Pub/Sub pattern in jQuery with the following code :
$.each({
trigger : 'publish',
on : 'subscribe',
off : 'unsubscribe'
}, function ( key, val) {
jQuery[val] = function() {
o[key].apply( o, arguments );
};
});
This works fine until I attempt to build something with multiple instances.
I have an activity object that is applied to each $('.activity_radio') div element. When I click on a radio button inside any $('.activity_radio') div the $.subscribe event will trigger (X) amount of times based on the number of activity_radio divs on are on the page.
How do I publish/subscribe events based only within a particular div?
Code
Radio Activity ( radio-activity.js )
var activity = {
init : function ( element ) {
// get our boilerplate code
this.activity = new util.factories.activity();
this.element = element;
this.$element = $(element);
// other init code
// gather our radio elements
this.target_element = this.$elem.find('input[type=radio]');
// send our radio elements to onSelect
this.activity.onSelect(this.target_element);
// trigger click function that will subscribe us to onSelect publish events
this.click()
},
// subscribe to events
click : function()
{
$.subscribe('activity.input.select', function ( event, data ){
// we have access to the value the user has clicked
console.log(data);
// trigger another function // do something else
});
}
}
Base Activity Boilerplate Code ( activity-factory.js )
var activity_factory = factory.extend({
init: function(e)
{
// init code
},
onSelect : function ( inputs ) {
inputs.on('click', function(){
// do some processing
// retrieve the value
var data = $(this).val();
// announce that the event has occured;
$.publish( 'activity.input.select', data );
});
}
}
});
Triggered when DOM is ready
$(function(){
// foreach DOM element with the class of activity_radio
$('.activity_radio').each(function(){
// trigger the init func in activity object
activity.init(this);
});
});
You can write your subscribe/publish as a plugins
$.each({
trigger : 'publish',
on : 'subscribe',
off : 'unsubscribe'
}, function ( key, val) {
jQuery.fn[val] = function() {
this[key].apply(this, Array.prototype.slice.call(arguments));
};
});
And you will be able to call it on $element
this.$element.subscribe('activity.input.select', function(event, data) {
and
onSelect: function ( inputs ) {
var self = this;
inputs.on('click', function(){
// do some processing
// retrieve the value
var data = $(this).val();
// announce that the event has occured;
self.$element.publish('activity.input.select', data);
});
}
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 want to add an event handle to an element that will be created later in DOM.
Basically, what I am trying to do is that, when I click p#one, new element p#two will be created, then I click p#two, tell me "p#two" clicked. However, it doesn't work, I didn't get the console.log result of 'p#two clicked' after I click p#two.
I use on() to add click event to p#two. What do I do wrong?
Thanks.
Below is my example code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>on() test</title>
<link type="text/css" href="http://localhost/jquery-ui-1.8.20.custom/css/smoothness/jquery-ui-1.8.20.custom.css" rel="stylesheet" />
<script type="text/javascript" src="http://localhost/jquery-ui-1.8.20.custom/js/jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="http://localhost/jquery-ui-1.8.20.custom/js/jquery-ui-1.8.20.custom.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('p#two').on('click', function() {
console.log('p#two clicked');
});
$('p#one').click(function() {
console.log('p#one clicked');
$('<p id="two">two</p>').insertAfter('p#one');
});
}); // end doc ready
</script>
</head>
<body>
<p id="one">one</p>
</body>
</html>
$('body').on('click','p#two', function() {
console.log('p#two clicked');
});
you can also use
$(document).on('click', 'p#two', function() {
});
Read more about .on()
you can also use .delegate()
$('body').delegate('#two', 'click', function() {
});
You can bind the $.on to a parent element that will always exist in dom like this.
$(document).on('click','p#two', function() {
console.log('p#two clicked');
});
Note that: you can replace document with any parent of the element that will always exist in dom, and the closer the parent the better.
Check doc of $.on
Live is depreciated. use $.on instead. Equivalent syntax of $.on for $.live and $.delegate
$(selector).live(events, data, handler); // jQuery 1.3+
$(document).delegate(selector, events, data, handler); // jQuery 1.4.3+
$(document).on(events, selector, data, handler); // jQuery 1.7+
I would suggest you to use $.on for all event handling purposes as all other methods routes through $.on method under the hood.
Check the definition of these functions from jQuery source v.1.7.2
bind: function( types, data, fn ) {
return this.on( types, null, data, fn );
},
unbind: function( types, fn ) {
return this.off( types, null, fn );
},
live: function( types, data, fn ) {
jQuery( this.context ).on( types, this.selector, data, fn );
return this;
},
die: function( types, fn ) {
jQuery( this.context ).off( types, this.selector || "**", fn );
return this;
},
delegate: function( selector, types, data, fn ) {
return this.on( types, selector, data, fn );
},
undelegate: function( selector, types, fn ) {
// ( namespace ) or ( selector, types [, fn] )
return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn );
}
You can see all methods are using $.on and $.off themselves. So using $.on you can at least save a function call though which isn't that significant most of the cases.
You want to use Jquery.on
$('body').on('click','p#two', function() {
console.log('p#two clicked');
});