Delaying default events in Javascript - javascript

I would like to be able to delay the default action of an event until some other action has been taken.
What it's for: I'm trying to build a reusable, unobtrusive way to confirm actions with a modal-type dialogue. The key wishlist item is that any Javascript handlers are attached by a script, and not written directly inline.
To make this truly reusable, I want to use it on different types of items: html links, checkboxes, and even other Javascript-driven actions. And for purely HTML elements like links or checkboxes, I want them to degrade gracefully so they're usable without Javascript turned on.
Here's how I would envision the implementation:
Some Link
_________
<script>
attachEvent('a.confirm','click', confirmAction.fire)
var confirmAction = (function(){
var public = {
fire: function(e){
e.default.suspend();
this.modal();
},
modal: function(){
showmodal();
yesbutton.onclick = this.confirmed;
nobutton.onclick = this.canceled;
},
confirmed: function(){
hidemodal();
e.default.resume();
},
canceled: function(){
hidemodal();
e.default.prevent();
}
}
return public;
})()
</script>
I know about the e.preventDefault function, but that will kill the default action without giving me the ability to resume it. Obviously, the default object with the suspend, resume and prevent methods is made up to illustrate my desired end.
By the way, I'm building this using the Ext.Core library, if that helps. The library provides a good deal of normalization for handling events. But I'm really very interested in learning the general principles of this in Javascript.

To resume, you could try saving the event and re-fire it, setting a flag that can be used to skip the handlers that call suspend() ('confirmAction.fire', in your example).
Some Link
_________
<script>
function bindMethod(self, method) {
return function() {
method.apply(self, arguments);
}
}
var confirmAction = (function(){
var public = {
delayEvent: function(e) {
if (e.suspend()) {
this.rememberEvent(e);
return true;
} else {
return false;
}
},
fire: function(e){
if (this.delayEvent(e)) {
this.modal();
}
},
modal: function(){
showmodal();
yesbutton.onclick = bindMethod(this, this.confirmed);
nobutton.onclick = bindMethod(this, this.canceled);
},
confirmed: function(){
hidemodal();
this.rememberEvent().resume();
},
canceled: function(){
hidemodal();
this.forgetEvent();
},
rememberEvent: function (e) {
if (e) {
this.currEvent=e;
} else {
return this.currEvent;
}
},
forgetEvent: function () {
delete this.currEvent;
}
}
return public;
})()
// delayableEvent should be mixed in to the event class.
var delayableEvent = (function (){
return {
suspend: function() {
if (this.suspended) {
return false;
} else {
this.suspended = true;
this.preventDefault();
return true;
}
},
resume: function () {
/* rest of 'resume' is highly dependent on event implementation,
but might look like */
this.target.fireEvent(this);
}
};
})();
/* 'this' in event handlers will generally be the element the listener is registered
* on, so you need to make sure 'this' has the confirmAction methods.
*/
mixin('a.confirm', confirmAction);
attachEvent('a.confirm','click', confirmAction.fire);
</script>
This still has potential bugs, such as how it interacts with other event listeners.

Related

addEventListener firing multiple times for the same form

Here is my Javascript module:
const Calculator = (function() {
return {
listen: function (formId) {
this.formId = formId;
this.calculatorForm = document.querySelector(`#form_${this.formId}`);
if (this.calculatorForm) {
this.addEventListeners();
}
},
addEventListeners: function() {
const self = this;
this.calculatorForm.addEventListener('submit', function(event) {
console.log('calculatorForm submit', self);
self.calculatorSubmission(event);
}, false);
},
calculatorSubmission: function(event) {
event.preventDefault();
console.log('Form submitted', this.calculatorForm);
}
};
})();
export default Calculator;
I build all the Javascript using Webpack so I can load modules like this:
import Calculator from './modules/calculator';
The page in question where the Javascript is loaded has tabbed content. Each tab contains a different form, all using the Calculator module so when I switch between tabs, I call:
Calculator.listen('form-id');
The issue I have is when I switch between tabs a few times. Say I view tab 3, 5 times and then fill out and submit form in tab 3. The form is submitted 5 times because of the addEventListener called each time I view tab 3. Make sense?
I'm struggling to fix it - probably because I've been looking at it for hours now and my head is now mash.
Is the problem my module setup?
What best approach to overcoming my issue?
Thanks
I've updated my Calculator to be a class and this seems to work as expected now.
Any improvements welcome!
class Calculator {
constructor() {
if (!Calculator.instance) {
Calculator.instance = this;
}
this.calculatorSubmissionHandler = function(event) {
Calculator.instance.calculatorSubmission(event);
};
return Calculator.instance;
}
listen(formId) {
this.formId = formId;
this.calculatorForm = document.querySelector(`#${this.formId}`);
if (this.calculatorForm) {
this.addEventListeners();
}
}
addEventListeners() {
this.calculatorForm.addEventListener('submit', this.calculatorSubmissionHandler, false);
}
calculatorSubmission(event) {
event.preventDefault();
console.log('Form submitted', Calculator.instance.formId);
}
}

Managing and Maintaining too many on click jquery handlers

I recently have been upgrading the Phonegap to the latest version and now it forces me to follow the Chrome's Content Security Policy which in a way is good. But now I am forced to remove the all the onclick handlers in the HTML code and add them in the jquery handler some$(document).ready(function(evt){
$('#addRecordBtn').on('click', function(){
alert("Adding Record");
AddValueToDB();
});
$('#refreshBtn').on('click', function(){
alert("Refresh Records");
ListDBValues();
});
});
But as per what my app is scaled upto I feel that there will be too many of these handlers. Is there an example which shows maintenance of such handlers and a proper way or proper place of defining such handlers.
Here's an idea. You could make an object that stores all of the functions that also knows how to give up the function
var handlers = {
getHandler: function (str) {
return this[str];
},
'#addRecordBtn': function () {
alert("Adding Record");
AddValueToDB();
},
'#refreshBtn': function () {
alert("Refresh Records");
ListDBValues();
}
};
Then apply all of your handlers using this form.
$('#addRecordBtn').on('click', handlers.getHandler('#addRecordBtn'));
$('#refreshBtn').on('click', handlers.getHandler('#refreshBtn'));
Optimization Time if you want to get really fancy and you assign a unique ID to every button as convention
var handlers = {
defer: function () {
return function (){
handlers[$(this).attr('id')](arguments);
};
},
registerHandlers: function () {
for (var key in this) {
if (this.hasOwnProperty(key) && typeof(key) === "string") {
$('#' + key).on('click', this.defer());
}
}
},
'addRecordBtn': function () {
alert("Adding Record");
AddValueToDB();
},
'refreshBtn': function () {
alert("Refresh Records");
ListDBValues();
}
};
call it with
$('#addRecordBtn').on('click', handlers.defer());
$('#refreshBtn').on('click', handlers.defer());
or register everything automatically
handlers.registerHandlers();
Here is a fiddle of my solution
Do you look for something like this?
$('[data-clickhandler]').on('click', function(e) {
var $btn = $(e.currentTarget);
var handler = $btn.data('clickhandler');
alert('Refresh ' + handler);
window[handler] && window[handler](e);
e.preventDefault();
});
Now your elements can specify their clickhandler like so:
<a data-clickhandler="AddValueToDB" href="">...</a>
Or so:
<span data-clickhandler="ListDBValues">...</span>

Functions in Namespace Not Attaching Event Listeners to HTML Elements

I have a local page to help in HTML and JavaScript that helps me with some basic tasks at work. I've been going back over my code and rewriting it to use best practices, since it helps me learn, and recently I've been trying to study namespacing and put it to use by rewriting the common page functions and event listeners.
window.onload = (function() {
var automationPageWrapper = (function() {
var self = {}
self.evntListeners = {
btnTextChange: function() {
// Code that changes button text when clicked
},
btnColorChange: function(formID) {
// Code that iterates through buttons with a certain name
// and makes them all the same default color
}
}
self.listeners = {
btnListeners: function() {
// Add all event listeners having to do with buttons here
}
}
return self;
});
automationPageWrapper.listeners.btnListeners();
});
Why isn't this attaching the event listeners?
Is there a better way to be formatting/calling this?
Is this a professional method for setting up JavaScript code?
I tested the event listeners by taking the functions and posting them into the Chrome console, so I think they work.
The full text, since some people like reading through all of it:
// Global namespace for the Page Functions
window.addEventListener("onload", function() {
var automationPageWrapper = (function() {
var self = {};
// Namespace for event listeners
self.evtListeners = {
// Function to change the color of a selected button
btnColorChange: function(formName) {
var elementsByName = document.getElementsByName(formName);
for (var i = 0; i < elementsByName.length; i++) {
if (elementsByName[i].className == "active") {
elementsByName[i].className = "inactive";
break;
}
}
},
// Add the event listeners
listeners: {
btnListeners: (function () {
document.getElementById('sidebar').addEventListener("click", function(e){
self.evtListeners.btnColorChange('sidebuttons');
e.target.className = "active";
});
})()
}
}
return self;
})();
automationPageWrapper.listeners.btnColorChange();
});

Elegant way to prevent stop event firing immediately after start

I'm writing a jQuery plugin where the events which start/stop the plugin are customisable, so potentially the same event could both start and stop the plugin (e.g. click to start and click to stop).
What's an elegant way, ideally not involving timeouts or unbinding and rebinding of listeners (and not too many "isPlaying" "isBeingStarted" flags etc..) to make sure the correct callback is called
(Note: When I posted this answer, the question had a typo in it which made it seem like binding/unbinding would be okay as long as timeouts weren't involved.)
I don't see any need for timeouts, just bind/unbind as appropriate:
this.bind(startEvent, start);
function start() {
$(this).unbind(startEvent).bind(stopEvent, stop);
}
function stop() {
$(this).unbind(stopEvent).bind(startEvent, start);
}
In the above, I assume that startEvent is the configured start event name (and I'd probably add a namespace to it, e.g. the user passes in "click" but you add ".niftyplugin" to it resulting in startEvent containing "click.niftyplugin" so you can bind/unbind at will), and stopEvent is the configured stop event name (with namespace).
Here's a full example, with namespaces and using data to remember the options (you could use a closure if you prefer) - live copy:
// Plugin stuff
(function($) {
$.fn.niftyPlugin = niftyPlugin;
function niftyPlugin(options) {
var data;
data = {
startEvent: (options && options.startEvent || "click") + ".niftyplugin",
stopEvent: (options && options.stopEvent || "click") + ".niftyplugin"
};
this.data("niftyPlugin", data).bind(data.startEvent, start);
return this;
}
function start() {
var $this = $(this),
data = $this.data("niftyPlugin");
$this.unbind(data.startEvent).bind(data.stopEvent, stop);
display("Start");
}
function stop() {
var $this = $(this),
data = $this.data("niftyPlugin");
$this.unbind(data.stopEvent).bind(data.startEvent, start);
display("Stop");
}
function display(msg) {
$("<p>").html(msg).appendTo(document.body);
}
})(jQuery);
// Use
jQuery(function($) {
$("#theButton").click(function() {
$("<p>Non-plugin hook fired</p>").appendTo(document.body);
}).niftyPlugin({
startEvent: "click"
});
});
The only other alternative I see is stopImmediatePropagation - live example:
// Plugin stuff
(function($) {
$.fn.niftyPlugin = niftyPlugin;
function niftyPlugin(options) {
var startEvent, stopEvent, running = false;
startEvent = (options && options.startEvent || "click") + ".niftyplugin";
stopEvent = (options && options.stopEvent || "click") + ".niftyplugin";
this.bind(startEvent, start).bind(stopEvent, stop);
return this;
function start(event) {
if (running) {
return;
}
running = true;
display("Start");
event.stopImmediatePropagation();
}
function stop(event) {
if (!running) {
return;
}
running = false;
display("Stop");
event.stopImmediatePropagation();
}
}
function display(msg) {
$("<p>").html(msg).appendTo(document.body);
}
})(jQuery);
// Use
jQuery(function($) {
$("#theButton").click(function() {
$("<p>Non-plugin hook fired</p>").appendTo(document.body);
}).niftyPlugin({
startEvent: "click"
});
});
I don't like it, though, because it interferes with other handlers for the event. For instance, in the above, if I change the use to this:
// Use
jQuery(function($) {
$("#theButton").niftyPlugin({
startEvent: "click"
}).click(function() {
$("<p>Non-plugin hook fired</p>").appendTo(document.body);
});
});
...so the plug-in grabs the events before the non-plug-in code, boom, the non-plug-in code never sees the event (example).
So despite the overhead, I suspect bind/unbind are your friends here.
It may be overkill, but an elegant way to not have to maintain a bunch of flags (e.g. "isPlaying") is to use a Finite State Machine.
Here's a jQuery implementation: https://github.com/DukeLeNoir/jquery-machine
The eventual solution I've gone for is to do a quick uniqueness test for events used for stopping and starting and if there are any events used for both stopping and starting then a different listener (which does an isPlaying check) is attached to these. There's a small performance hit on loading the plugin, but after that the event handling code is about as efficient as can be.
function processEvents() {
var tempStart = opts.startEvent.split(" ").sort(),
tempStop = opts.stopEvent.split(" ").sort();
startEventLoop: for(var i=0, il = tempStart.length;i<il;i++) {
for(var j=0, jl = tempStop.length;j<jl;j++) {
if(tempStart[i] == tempStop[j]) {
stopStartEvents.push(tempStart[i])
tempStop.splice(j,1);
continue startEventLoop;
}
}
startEvents.push(tempStart[i])
}
startEvents = startEvents.join(" ");
stopEvents = tempStop.join(" ");
stopStartEvents = stopStartEvents.join(" ");
}
$this.on(stopEvents, function() {
$this.trigger("stop.flickBook");
}).on(startEvents, function() {
$this.trigger("start.flickBook");
}).on(stopStartEvents, function() {
playing ? $this.trigger("stop.flickBook") : $this.trigger("start.flickBook");
});

Adding a jQuery style event handler of iPhone OS events

I'm looking for a super simple jQuery extension. Basically I need to use some events that jQuery does not explicitly support. These events are the iPhone touch events like ontouchstart, ontouchend, and ontouchmove.
I have it working via this:
// Sucks
$('.clickable').each(function() {
this.ontouchstart = function(event) {
//do stuff...
};
}
Which kind of sucks and is unjqueryish. Here is what I would like:
// Better
$('.clickable').touchstart(function() {
//do stuff...
}
Or even better with 1.4
// Awesome
$('.clickable').live('touchstart', function() {
//.. do stuff
}
These events need no special handling and should work just like any other events, but I can't seem to figure out how to extend jquery to make them work just like all the other events do.
I wrote the plugin, if the user does have touch available, use, otherwise, call click
jQuery.event.special.tabOrClick = {
setup: function (data, namespaces) {
var elem = this, $elem = jQuery(elem);
if (window.Touch) {
$elem.bind('touchstart', jQuery.event.special.tabOrClick.onTouchStart);
$elem.bind('touchmove', jQuery.event.special.tabOrClick.onTouchMove);
$elem.bind('touchend', jQuery.event.special.tabOrClick.onTouchEnd);
} else {
$elem.bind('click', jQuery.event.special.tabOrClick.click);
}
},
click: function (event) {
event.type = "tabOrClick";
jQuery.event.handle.apply(this, arguments);
},
teardown: function (namespaces) {
var elem = this, $elem = jQuery(elem);
if (window.Touch) {
$elem.unbind('touchstart', jQuery.event.special.tabOrClick.onTouchStart);
$elem.unbind('touchmove', jQuery.event.special.tabOrClick.onTouchMove);
$elem.unbind('touchend', jQuery.event.special.tabOrClick.onTouchEnd);
} else {
$elem.unbind('click', jQuery.event.special.tabOrClick.click);
}
},
onTouchStart: function (e) {
this.moved = false;
},
onTouchMove: function (e) {
this.moved = true;
},
onTouchEnd: function (event) {
if (!this.moved) {
event.type = "tabOrClick";
jQuery.event.handle.apply(this, arguments)
}
}
};
$("#xpto").bind("tabOrClick", function () {
alert("aaaa");
});
I've made a small update to Alexandre's plugin to include Android support. Android's browser does not currently support the window.Touch method of detecting touch support.
I love how Alexandre's script waits to ensure movement didn't occur to prevent triggering the event when the user swipes to scroll across the screen. However a downfall of that approach is that it causes its own delay by waiting for the user to lift their finger off of the screen before triggering. I've updated his plugin to include a "touchactive" class that gets applied to items that a user is currently touching. If you take advantage of that class you can provide immediate visual feedback to users without causing an actual event to get triggered until after movement check has completed.
jQuery.event.special.touchclick = {
setup: function (data, namespaces) {
var elem = this, $elem = jQuery(elem);
var ua = navigator.userAgent.toLowerCase();
var isAndroid = ua.indexOf("android") > -1;
if (window.Touch || isAndroid) {
$elem.bind('touchstart', jQuery.event.special.touchclick.onTouchStart);
$elem.bind('touchmove', jQuery.event.special.touchclick.onTouchMove);
$elem.bind('touchend', jQuery.event.special.touchclick.onTouchEnd);
} else {
$elem.bind('click', jQuery.event.special.touchclick.click);
}
},
click: function (event) {
event.type = "touchclick";
jQuery.event.handle.apply(this, arguments);
},
teardown: function (namespaces) {
var elem = this, $elem = jQuery(elem);
if (window.Touch) {
$elem.unbind('touchstart', jQuery.event.special.touchclick.onTouchStart);
$elem.unbind('touchmove', jQuery.event.special.touchclick.onTouchMove);
$elem.unbind('touchend', jQuery.event.special.touchclick.onTouchEnd);
} else {
$elem.unbind('click', jQuery.event.special.touchclick.click);
}
},
onTouchStart: function (e) {
this.moved = false;
$(this).addClass('touchactive');
},
onTouchMove: function (e) {
this.moved = true;
$(this).removeClass('touchactive');
},
onTouchEnd: function (event) {
if (!this.moved) {
event.type = "touchclick";
jQuery.event.handle.apply(this, arguments)
}
$(this).removeClass('touchactive');
}
};
I've also posted this to github in case there are further caveats that are discovered https://github.com/tuxracer/jquery-touchclick
This now works, just like it's stubbed out above, on the latest jQuery release. Go jQuery!
Here's a start:
$.fn.touchstart = function(fn) { return this[fn ? "bind" : "trigger"]("touchstart", fn); };
$.event.special.touchstart = {
setup: function() {
$.event.add(this, "mouseenter", extendedClickHandler, {});
},
teardown: function() {
$.event.remove(this, "mouseenter", extendedClickHandler);
}
};
Where extendedClickHandler is the function that does what it's suppose to do.
More info here: http://brandonaaron.net/blog/2009/03/26/special-events
jQuery.com is a great source of information like this.
If you build your own plugin you'll be able to use whatever naming you like on your method calls.

Categories