I'm building a webpage that is composed of several controls, and trying to come up with an effective somewhat generic client side sibling control communication model. One of the controls is the menu control. Whenever an item is clicked in here I wanted to expose a custom client side event that other controls can subscribe to, so that I can achieve a loosely coupled sibling control communication model.
To that end I've created a simple Javascript event collection class (code below) that acts as like a hub for control event registration and event subscription. This code certainly gets the job done, but my question is is there a better more elegant way to do this in terms of best practices or tools, or is this just a fools errand?
/// Event collection object - acts as the hub for control communication.
function ClientEventCollection()
{
this.ClientEvents = {};
this.RegisterEvent = _RegisterEvent;
this.AttachToEvent = _AttachToEvent;
this.FireEvent = _FireEvent;
function _RegisterEvent(eventKey)
{
if (!this.ClientEvents[eventKey])
this.ClientEvents[eventKey] = [];
}
function _AttachToEvent(eventKey, handlerFunc)
{
if (this.ClientEvents[eventKey])
this.ClientEvents[eventKey][this.ClientEvents[eventKey].length] = handlerFunc;
}
function _FireEvent(eventKey, triggerId, contextData )
{
if (this.ClientEvents[eventKey])
{
for (var i = 0; i < this.ClientEvents[eventKey].length; i++)
{
var fn = this.ClientEvents[eventKey][i];
if (fn)
fn(triggerId, contextData);
}
}
}
}
// load new collection instance.
var myClientEvents = new bsdClientEventCollection();
// register events specific to the control that owns it, this will be emitted by each respective control.
myClientEvents.RegisterEvent("menu-item-clicked");
Here is the part where this code above is consumed by source and subscriber controls.
// menu control
$(document).ready(function()
{
$(".menu > a").click( function(event)
{
//event.preventDefault();
myClientEvents.FireEvent("menu-item-clicked", $(this).attr("id"), null);
});
});
<div style="float: left;" class="menu">
<a id="1" href="#">Menu Item1</a><br />
<a id="2" href="#">Menu Item2</a><br />
<a id="3" href="#">Menu Item3</a><br />
<a id="4" href="#">Menu Item4</a><br />
</div>
// event subscriber control
$(document).ready(function()
{
myClientEvents.AttachToEvent("menu-item-clicked", menuItemChanged);
myClientEvents.AttachToEvent("menu-item-clicked", menuItemChanged2);
myClientEvents.AttachToEvent("menu-item-clicked", menuItemChanged3);
});
function menuItemChanged(id, contextData)
{
alert('menuItemChanged ' + id);
}
function menuItemChanged2(id, contextData)
{
alert('menuItemChanged2 ' + id);
}
function menuItemChanged3(id, contextData)
{
alert('menuItemChanged3 ' + id);
}
jQuery's event system can pass additional handler parameters when you trigger events. We also separate the control namespace from jQuery selectors by creating a registry that maps control names to selectors. To deal with handlers binding to a control before the control is registered, we implement a binding delay mechanism.
var controls = {};
(function ControlRegistry(controls) {
controls.items = {};
function bindNow(selector, event, eventData, handler) {
$(selector).bind(event, eventData, handler);
}
function delayBinding(queue, event, eventData, handler) {
queue.push([event, eventData, handler]);
}
function bindAll(queue, selector) {
for (var i=0; i<queue.length; ++i) {
var args = queue[i];
args.unshift(selector);
bindNow.apply(controls, args);
}
}
controls.register = function (name, selector) {
if (typeof this.items[name] == 'object') {
bindAll(this.items[name], selector);
}
this.items[name] = selector;
};
controls.bind = function (control, event, eventData, handler) {
jQuery.isFunction( eventData ) {
handler = eventData;
eventData = null;
}
switch (typeof this.items[control]) {
case 'undefined':
this.items[control] = [];
// FALLTHRU
case 'object':
delayBinding(this.items[control], event, eventData, handler);
break;
case 'string':
bindNow(this.items[control], event, eventData, handler);
break;
}
}
})(controls);
$(document).ready(function()
{
controls.register('menuItem', '.menu > a');
$(".menu > a").click( function(event)
{
$(this).trigger("menu-item-clicked", [$(this).attr("id"), 'cow', 'moo']);
});
});
Elsewhere:
function menuItemChanged(evt, id, animal, speech)
{
alert('menuItemChanged ' + id
+ '\nThe ' + animal + ' says "' + speech + '."');
}
function menuItemChanged2(evt, id, animal, speech))
{
alert('menuItemChanged2 ' + id
+ '\nThe ' + animal + ' says "' + speech + '."');
}
function menuItemChanged3(evt, id, animal, speech))
{
alert('menuItemChanged3 ' + id
+ '\nThe ' + animal + ' says "' + speech + '."');
}
$(document).ready(function()
{
controls.bind('menuItem', "menu-item-clicked", menuItemChanged);
controls.bind('menuItem', "menu-item-clicked", menuItemChanged2);
controls.bind('menuItem', "menu-item-clicked", menuItemChanged3);
});
Update
if you include the restriction that a control be registered before handlers are bound to its events, the control registry can be vastly simplified:
var controls = {
register: function (name, selector) {
if (typeof this[name] != 'function') {
this[name] = selector;
}
};
};
...
controls.register('menuItem', '.menu > a');
$(document).ready(function()
{
$(".menu > a").click( function(event)
{
$(this).trigger("menu-item-clicked", [$(this).attr("id"), 'cow', 'moo']);
});
});
...
$(document).ready(function()
{
$(controls.menuItem).bind("menu-item-clicked", menuItemChanged);
$(controls.menuItem).bind("menu-item-clicked", menuItemChanged2);
$(controls.menuItem).bind("menu-item-clicked", menuItemChanged3);
});
This is a reasonable restriction, as you can register early (within the script for the control) and bind late (in $(document).ready).
My original solution ended up being the right one for me because it achieves the loose coupling I was after in a straight-forward and simple way.
Related
currently i'm starting with Ember, and i'm loving it! I'm with some difficulties, especially when it comes to components.
For you to understand, I'm going through old code to Ember, and I would like to turn this code into a Component, but I do not know actually how to start, since I do not know how to catch the button being clicked, and I also realized that Ember has several helpers, maybe I do not need any of this giant code to do what I want.
This is the old code result: http://codepen.io/anon/pen/WQjobV?editors=110
var eventObj = {};
var eventInstances = {};
var actual;
var others;
var clicked;
var createEventInstance = function (obj) {
for (var key in obj) {
eventInstances[key] = new Event(obj[key]);
}
};
var returnStyle = function (inCommon) {
var $inCommon = inCommon;
$inCommon.css({
width: '342.4px',
minWidth: '342.4px'
});
$inCommon.find('.cta').removeClass('hidden');
$inCommon.find('.event-close').removeClass('inline');
$inCommon.find('.event-info_list').removeClass('inline');
$inCommon.removeClass('hidden');
$inCommon.find('.expanded').slideUp();
$inCommon.find('.expanded').slideUp();
$inCommon.find('.event-arrow').remove();
$inCommon.find('h2').find('ul').remove('ul');
};
var Event = function (id) {
this.id = id;
};
Event.prototype.expandForm = function () {
actual.css('width', '100%');
actual.find('.event-info_list').addClass('inline');
actual.find('.expanded').slideDown().css('display', 'block');
actual.find('.event-close').addClass('inline');
};
Event.prototype.close = function () {
returnStyle(actual);
returnStyle(others);
};
Event.prototype.hideElements = function () {
clicked.addClass('hidden');
others.addClass('hidden');
};
Event.prototype.maskPhone = function () {
$('[name$=phone]').mask('(99) 99999-9999', {
placeholder: '(00) 0000-0000'
});
};
$('.submit-form').on('click', function (e) {
e.preventDefault();
var id = '.' + $(this).data('id');
var name = $(id).children('#person-name').val();
var email = $(id).children('#person-email').val();
var guests = $(id).children('#person-obs.guests').val();
var phone = $(id).children('#person-phone').val();
var participants = $(id).children('#booking-participants').val();
if (name === '' || email === '' || phone === '' || participants === '' || guests === '') {
alert('Preencha os campos obrigatórios.');
} else {
$(id).submit();
}
});
Event.prototype.createDropDown = function () {
actual.find('h2').addClass('event-change')
.append('<span class="event-arrow" aria-hidden="true">â–¼</span>')
.append(function () {
var self = $(this);
var list = '<ul class="dropdown hidden">';
$('.event').each(function (index) {
if ($(this).find('h2')[0] != self[0]) {
list += '<li data-index="' + index + '">' + $(this).find('h2').text() + '</li>';
}
});
return list;
}).click(function () {
if ($(this).attr('data-expanded') == true) {
$(this).find('ul').toggleClass('hidden');
$(this).attr('data-expanded', false);
} else {
$(this).find('ul').toggleClass('hidden');
$(this).attr('data-expanded', true);
}
}).find('li').click(function (e) {
e.stopPropagation();
actual.find('.event-info_list').removeClass('inline');
actual.find('h2').attr('data-expanded', false);
actual.find('h2').removeClass('event-change');
actual.find('.expanded').slideUp().css('display', 'inline-block');
others.removeClass('hidden');
actual.find('.cta').removeClass('hidden');
actual.find('h2').find('.event-arrow').remove();
actual.find('h2').off('click');
actual.find('h2').find('ul').remove('ul');
$($('.event')[$(this).attr('data-index')]).find('.cta').trigger('click');
});
};
Event.prototype.open = function () {
actual = $('[data-id="' + this.id + '"]');
others = $('.event').not(actual);
clicked = actual.find('.cta');
this.hideElements();
this.expandForm();
this.createDropDown();
this.maskPhone();
};
$('.event').each(function (i, event) {
var prop = 'id' + $(event).data('id');
var value = $(event).data('id');
eventObj[prop] = value;
});
createEventInstance(eventObj);
Basically i have this boxes, which box represent one booking in some event (will be populate by the server). When the user clicks in one box, this boxes expands and the other disappear. But than a dropbox will be created with the other boxes, so the user can navigate in the events by this dropdown.
I didn't do much with Ember, i transform the "events" div into a component with the name "BookingBoxComponent" and two actions:
SiteApp.BookingBoxComponent = Ember.Component.extend({
actions:
open: function() {
// HOW COULD I ACCESS THE CLICKED BUTTON HERE?
},
close: function() {
}
});
As you can see, i put two actions, one for opening the box and other for closing, should i just put the logic in both, or i can improve this like a Ember way?
I don't know if i am asking to much here, so if i am, at least i would like to know how to access the button clicked in the open method, i was trying passing as a parameter, like:
<button {{action 'open' this}}></button>
But didn't work.
I could offer 50 of my points to someone who help transform the old cold in a Ember way code.
Thanks.
The event object will be passed with every action as the last parameter, so when you specified this you were actually passing whatever object has context in that block. In your open function, do not pass this and do
open: function(event) {
// event.currentTarget would be the button
}
And now you can do something like event.currentTarget or event.target
is there any way, how can I globally (in service) disable and enable all ng-click and ng-submit events?
For example when user is offline I want to disable all actions till he gets connection back..
I tried to bind all elements with an onClick event which will call stopImmediatePropagation but it didn't work..
$('*[ng-click]').click(function( event ) {
event.stopImmediatePropagation();
});
Also this question is a little bit different from this one:
Disable ng-click on certain conditions of application for all types of element
I'd like to disable/enable all events in APP globally from service, I'm not able to modify all ng-* calls on all elements in the APP..
Try including a return false too:
$('*[ng-click]').click(function( event ) {
event.stopImmediatePropagation();
return false;
});
Snippet
The below snippet demonstrates that multiple event handlers attached to a single <a> works too.
$(function () {
$("a").click(function () {
alert("Hello!");
return false;
});
$("a").click(function () {
alert("Bye!");
return false;
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
Click Me
So finally I end up with temporarily disabling all events on the page using jquery..
I got inspired from this plugin http://ignitersworld.com/lab/eventPause.html which for some reason did not work (without any error)
So I took main parts and put it to this class which is working now using jquery v2.1.1:
var EventManager = function() {
var self = this;
var nullFun=function(){};
var getIndex = function(array,value){
for(var i=0; i< array.length; i++){
if(array[i]==value){
return i;
}
}
return -1;
};
this.pauseEvent = function(elm,eventAry){
var events = $._data(elm, "events");
if (events) {
$.each(events, function(type, definition) {
if((getIndex(eventAry,type)!=-1)||(eventAry=='')){
$.each(definition, function(index, event) {
if (event.handler.toString() != nullFun.toString()){
if(!$._iwEventPause) $._iwEventPause = {};
$._iwEventPause["iw-event" + event.guid] = event.handler;
event.handler = nullFun;
}
})
}
})
}
};
this.activeEvent = function(elm,eventAry){
var events = $._data(elm, "events");
if (events) {
$.each(events, function(type, definition) {
if((getIndex(eventAry,type)!=-1)||(eventAry=='')){
$.each(definition, function(index, event) {
if (event.handler.toString() == nullFun.toString()){
event.handler = $._iwEventPause["iw-event" + event.guid];
}
})
}
})
}
};
this.disableAll = function(el) {
el = el || $('*');
el.each(function() {
self.pauseEvent($(this)[0], '');
});
self.pauseEvent($(window)[0], '');
};
this.enableAll = function(el) {
el = el || $('*');
el.each(function() {
self.activeEvent($(this)[0], '');
});
self.activeEvent($(window)[0], '');
};
return this;
};
var eManager = new EventManager();
eManager.disableAll();
eManager.enableAll();
This will go through window object and all elements on the page, move their event handlers away to _iwEventPause object and replace handlers with dummy function.. When enabling, it will move handlers back so they get normally called..
This solution does not handle event handlers added after disabling..
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>
I am calling some jQuery plugins that attaches themselves to element on DOM ready. These plugins manipulate the DOM when certain events has occurred (click, change etc,)
$("body").find("input[type='checkbox']").checkbox();
Above works fine on DOM ready. However, if I'm loading some HTML from an AJAX call I have to use .on() to guarantee events gets bound consistently.
The question is which event I should bind the plugin to? I have tried below and it doesn't seem to respond.
$("body").on("load", "input[type='checkbox']", function(){
$(this).checkbox();
});
Here's the checkbox() plugin I'm referring to above. If that's any help. :)
'use strict';
define(['jquery'], function($){
return function(){
$.fn.checkbox = function (options) {
options = options || {};
var defaults = {
'className': 'jquery-checkbox',
'checkedClass': 'jquery-checkbox-on'
};
var settings = jQuery.extend(defaults, options);
return this.each(function () {
var self = jQuery(this);
var replacement = jQuery(
'<span class="' + settings.className + '-wrapper">' +
'<a class="' + settings.className + '" href="#" name="' + self.attr('id') + '"></a>' +
'</span>');
var element = jQuery('a', replacement);
if (self.prop('checked')) {
element.addClass(settings.checkedClass);
}
element.on('click', function (event) {
event.preventDefault();
event.stopPropagation();
var input = jQuery('input#' + jQuery(this).attr('name'), replacement.parent());
if (input.prop('checked')) {
input.removeAttr('checked');
} else {
input.prop('checked', true);
}
input.trigger('change');
return false;
});
element.on('focusin', function (event) {
$(this).addClass('checkbox-focus');
});
element.on('focusout', function (event) {
$(this).removeClass('checkbox-focus');
});
element.on("keypress", function(e){
if ( e.which === 32 ){ self.prop('checked', !self.prop('checked')).change(); }
});
self.on('change', function (event) {
var input = jQuery(this);
if (input.prop('checked')) {
jQuery('a[name=' + input.attr('id') + ']', replacement.parent()).addClass(settings.checkedClass);
} else {
jQuery('a[name=' + input.attr('id') + ']', replacement.parent()).removeClass(settings.checkedClass);
}
return true;
});
self.css({
'position': 'absolute',
'top': '-200px',
'left': '-10000px'
}).before(replacement);
});
}
};
});
You appear to want to apply add-ins to elements that have been loaded dynamically. That is not what 'on' is for.
Delegated events listen for specific events (like "click") at a parent/ancestor element then filter the possible recipients, then executes the supplied function against any matching elements that caused the event.
You actually need to apply the add-in code after your Ajax load completes.
Example:
In the success part of your ajax load, apply the addin:
$("input[type='checkbox']").checkbox();
If you loaded a specific part of the screen (likely), then target the selector at that element:
e.g.
$("#myloadcontainer input[type='checkbox']").checkbox();
I want to add more functionality to an already rendered button code.
The only way to do it is Javascript since it's rendered before from the server and I only have access to the JSP.
Here is what I have now, but it's not working, it just displays the same message twice. And if I click the button, the alert "hello" is not shown.
<script type="text/javascript">
function addEventBefore(element, type, fn) {
var old = element['on' + type] || function() {};
element['on' + type] = function () { fn(); old(); };
}
function sayHello(){
alert('hello');
}
var arr = document.getElementsByTagName("button");
for (i = 0; i < arr.length; i++) {
if (arr[i].getAttribute("name") == "Complete"){
var theclick = arr[i].getAttribute("onClick");
alert(theclick);
addEventBefore(arr[i], 'Click', sayHello);
var theclick2 = arr[i].getAttribute("onClick");
alert(theclick2);
}
}
</script>
This is the rendered source code of the page:
<td valign='top' align='right' >
<button name="Complete" title="The complete Button"
onClick="document.forms.TaskInfoPage.action='http://prodserver:8080/Approval.jsp?windowId=dd4c0&eventTarget=stepApproval&eventName=Complete'
document.forms.InfoPage.submit();return false;">Complete</button>
</td>
So, I want to place the sayHello() before it does the action on the onClick.
It's onclick rather than onClick, so you need this:
addEventBefore(arr[i], 'click', sayHello);
// ^ lower case here
The attribute itself still isn't going to display anything different, but the event themselves will work, you can test it here. Also, to maintain this for your event handlers, I recommend you at least .call() the functions, like this:
function addEventBefore(element, type, fn) {
var old = element['on' + type] || function() {};
element['on' + type] = function () { fn.call(this); old.call(this); };
}