I want to create a generic event handler that I can reuse on dom elements so I don't have to write boiler plate over and over again. I thought I had it figured out but I am getting errors.
The problem I am having is that I think the event handlers are bound at a different time than I need. Maybe at document.ready? Where I think I need to attach them with the .live() method? Though I may have no idea what I am talking about here.
Here is what I am trying to do:
Multi page application.
Multiple collections where data needs to be inserted.
Button code to show the insert form.
<button id="btnShowInsert" class="btn btn-success" rel="tooltip" title="add group">
<i id="btnIcon" class="icon-plus-sign icon-white"></i>
</button>
Template that shows the form based on the page (controller)
{{> groups_insert}}
Here is the form.
<template name="groups_insert">
{{#if acl_check}}
{{> alert}}
< p>
< form class="form-horizontal well hide" id="insert">
<fieldset>
< div class="control-group">
< label class="control-label" for="name">Name</label>
< div class="controls">
< input type="text" class="input-xlarge" id="name" name="name">
< /div>
< /div>
< div class="form-actions well">
< button id="btnReset" type="reset" class="btn btn-large">Reset</button>
< button id="btnSubmit" type="button" class="btn btn-primary btn-large">Submit</button>
< /div>
< /fieldset>
< /form>
< /p>
{{/if}}
< /template>
Here is the client code to implement the button that shows the form on the page.
Template.groups.events[ Meteor.eventhandler.btn_events('#btnShowInsert') ] = Meteor.eventhandler.make_btn_show_insert_form_click_handler();
Here is my generic event handler:
var EventHandler = Base.extend({
btn_events: function(selector) {
return 'click ' + selector; //, keydown '+selector+', focusout '+selector;
},
make_btn_show_insert_form_click_handler: function(){
//var click = options.click || function () {};
return function (event) {
if (event.type === "click") {
event.stopPropagation();
event.preventDefault;
try{
if ($('#btnIcon').hasClass('icon-plus-sign') ) {
$('#btnIcon').removeClass('icon-plus-sign');
$('#btnIcon').addClass('icon-minus-sign');
} else {
$('#btnIcon').removeClass('icon-minus-sign');
$('#btnIcon').addClass('icon-plus-sign');
}
$('#insert').slideToggle('slow', 'swing');
} catch(error) {
Alert.setAlert('Error', 'Critical Error: ' + error, 'alert-error');
}
}
}
},
});
Meteor.eventhandler = new EventHandler;
THE ERROR
Uncaught TypeError: Cannot call method 'btn_events' of undefined
BUT, if I define the event handler this way and call it this way it works.
Template.groups.events[ btn_events('#btnShowInsert') ] = make_btn_show_insert_form_click_handler();
var btn_events = function (selector) {
return 'click ' + selector; //, keydown '+selector+', focusout '+selector;
};
var make_btn_show_insert_form_click_handler =
function () {
//var click = options.click || function () {};
console.log( Meteor.request.controller );
return function (event) {
if (event.type === "click") {
event.stopPropagation();
event.preventDefault;
try{
if ($('#btnIcon').hasClass('icon-plus-sign') ) {
$('#btnIcon').removeClass('icon-plus-sign');
$('#btnIcon').addClass('icon-minus-sign');
} else {
$('#btnIcon').removeClass('icon-minus-sign');
$('#btnIcon').addClass('icon-plus-sign');
}
$('#insert').slideToggle('slow', 'swing');
} catch(error) {
Alert.setAlert('Error', 'Critical Error: ' + error, 'alert-error');
}
}
}
};
The Problem
I don't want to have to replicate code all over my site in order to implement a nice button that can slideToggle and form on any page. If I could get it abstracted then I should be able to have a Show Form type of button on all pages for any collection that I am rendering that allows data entry. As well, this leads into being able to create one form handler for all forms as well and then tying them to the controller through an action to the model.
Any ideas?
You can bind a high-level template to elements created with child templates. Then you only have to do the binding once. For example
HTML:
<template name="settings">
{{> login_settings }}
{{> account_settings }}
{{> data_settings }}
</template>
<template name="login_settings">
<btn class="slideToggle">Slide me for login!</btn>
</template>
<template name="account_settings">
<btn class="slideToggle">Slide me for account!</btn>
</template>
<template name="data_settings">
<btn class="slideToggle">Slide me for data!</btn>
</template>
JavaScript:
Template.settings.events {
'click .slideToggle': function() {
var clickedElement = event.target;
// add/remove CSS classes to clicked element
}
};
So if you end up creating 10 different template definitions under settings so you still only have to bind the handler to a single template.
I feel like you're overcomplicating things. Why not do this?
Template.someTemplate.events({
'click .button': buttonClicked
});
function buttonClicked(evt) {
// DRY code to handle a button being clicked
}
This has the right balance of separation: your event handler is defined once, but you can tell each template that you want its buttons to listen to some event. And if that's not good enough, you can further abstract it:
Template.someTemplate.events(genericEvents);
And possibly even merge genericEvents with specific events for that Template if you wanted.
Here is what I ended up doing. The example only shows the generic insert handler.
var EventHandler = Base.extend({
btnClickHandler: function(){
return function (event) {
event.preventDefault();
Meteor.eventhandler[event.currentTarget.id](event);
}
},
insert: function(event){
event.preventDefault();
var params = $('#insert-form').toJSON();
try{
window[Meteor.request.controller.capitalise()]['validateParams'](params);
var ts = new Date();
params.client_updated = ts;
var has_popup = params.has_popup;
delete params.has_popup;
window[Meteor.request.controller.capitalise()]['insert'](params, function(error, _id){
if(error){
Alert.setAlert('Error', error, 'alert-error', true, has_popup);
} else {
Alert.setAlert('Success', 'Record successfully created.', 'alert-success', true, has_popup);
$("#insert-form").reset();
Meteor.flush();
}
});
} catch(error) {
Alert.setAlert('Error', error, 'alert-error', true, params.has_popup);
}
}
});
Meteor.eventhandler = new EventHandler;
Now, I merely have to create handlebars templates without any significant javascript coding to handle generic events and wire them up as follows.
$(document).on("click", '#print', Meteor.eventhandler.btnClickHandler());
$(document).on("click", '#insert', Meteor.eventhandler.btnClickHandler());
$(document).on("click", '#remove', Meteor.eventhandler.btnClickHandler());
$(document).on("click", '#removeSubField', Meteor.eventhandler.btnClickHandler());
$(document).on("click", '#insertSubField', Meteor.eventhandler.btnClickHandler())
$(document).on("click", '#update', Meteor.eventhandler.btnClickHandler());
$(document).on("click", '#updateSubField', Meteor.eventhandler.btnClickHandler());
$(document).on("click", "#toggleActive", Meteor.eventhandler.btnClickHandler());
$(document).on("click", "#toggleChild", Meteor.eventhandler.btnClickHandler());
Now, I don't have to write any template event maps to handle basic CRUD. I can create any number of handlebars templates as long as the /route corresponds to the collection name. Although I do some tricky conversions from time to time. Basically, the generic event handler wires up the events, based on the route aka request.controller, to a collection and abstracts it through a client/server shared data model for validation and even access control alongside what is existing in Meteor.
It seems to work well and has reduced my code base significantly. I have dozens of collections where I haven't had to write any event maps handlers because basic CRUD is handled but abstracted enough that I can customize validation, security and other sanity checks on the client/server shared data model.
The approach I've taken to this problem in Meteor 1.0.2 is to use dynamic templates. See Dan Dascalescu's canonical answer and the docs.
Let's say you have a set of generic events attached to template "A" and you want to take advantage of them in templates "B", "C", and "D."
HTML:
<template name="A">
{{> Template.dynamic template=myTemplate}}
</template>
JS:
Template.A.events({
... your event code
})
You define a helper function for "A" that dynamically picks which of B, C, or D (...) you want to include:
Template.A.helpers({ // dynamically insert a template
myTemplate: function(){
if (...) return 'B'; // return a string with the name of the template to embed
if (...) return 'C';
if (...) return 'D';
}
})
The events defined in "A" will now be available in "B", "C", and "D."
Note that template "A" need not contain any HTML whatsoever.
Related
I have a generic template that I use multiple times.
{{#each item}}
{{> genericTemplate}}
{{/each}}
Inside of this template I have a button that when it is clicked fires a hidden file input in the generic template.
$(".upload").click();
Unfortunately for each template the ".upload" class gets fired. So if I had four items, it would give me 4 file inputs. I can't give the buttons a unique id="" because then I would have to explicitly define each event for each id, negating the entire reason for creating the generic template in the first place. What is the proper way to achieve something like this?
EDIT:
My template events look like this:
Template.generic.events({
'click .fileUpload' : function () {
$(".upload").click(); // sets off the 4 templates .upload class
},
'change .upload' : function (e) {
console.log('upload')
}
})
HTML:
<template name="generic">
<!--Hidden Inputs that get fired on click events -->
<div class="hiddenFile">
<input type="file" class="upload"/>
</div>
<button class="btn btn-success fileUpload">UPLOAD FILE </button>
</template>
Try this trick :
Template.generic.events({
'click .fileUpload' : function (event,template) {
// fires only the template instance .upload
template.$(".upload").click();
},
'change .upload' : function (e) {
console.log('upload')
}
});
You can use Template.instance to fire the event in only the appropriate instance:
'click .fileUpload' : function () {
var template = Template.instance();
template.$(".upload").click(); // should only set off this specific upload input.
}
That said, is there really no way you can achieve the same effect without manufacturing an event in the DOM? It's up to you, but can't you just run the code that's going to replace console.log('upload') directly?
Maybe smth like this?
Template.generic.events({
'click .fileUpload' : function (event, target) {
e.currentTarget.parent().find(".upload").click(); // sets off the 4 templates .upload class
},
'change .upload' : function (e) {
console.log('upload')
}
})
I have a Meteor template with 2 possible HTML blocks, depending on a conditional: already_facebook_authed. As the code below shows, already_facebook_authed is the result of a Session variable, and that Session variable is asynchronously set. It seems like when the template is rendered, the Session variable (and thus already_facebook_authed) is falsey so when trying to bind a click handler to #deauth_facebook_button, it does not exist yet because it is in the other block which is not yet rendered.
How can I bind a click handler to #deauth_facebook_button? Perhaps there is some callback for when a certain DOM element is rendered in which I can instantiate this click handler?
------------
-- auth.html
<template name="accounts_auth_with_facebook">
{{#if already_facebook_authed}}
<div class="col-md-4">
<button id="deauth_facebook_button" class="btn btn-primary"> Deauth Facebook </button>
</div>
{{else}}
<div class="col-md-4">
<div id="facebook_button"> Authenticate with FB </div>
</div>
{{/if}}
</template>
----------
-- auth.js
Template.accounts_auth_with_facebook.rendered = function () {
$('#facebook_button').unbind('click.auth').bind('click.auth', function() {
// some handler code
});
$('#deauth_facebook_button').unbind('click.deauth').bind('click.deauth', function() {
// some other handler code
});
};
Template.accounts_auth_with_facebook.already_facebook_authed = function() {
Meteor.call('get_composer_id', function (error, result) {
if (blah blah blah) {
Session.set('logged_in_with_facebook', true);
}
});
return Session.get('logged_in_with_facebook');
};
Do not use jQuery for setting up click handlers on Meteor templates, use the Meteor standard events mechanism :
Template.accounts_auth_with_facebook.events({
"click #facebook_button":function(event,template){
// some handler code
}
"click #deauth_facebook_button":function(event,template){
// some other handler code
}
});
Some checkbox buttons (using bootstrap) trigger an ajax get call on.click, and the activeness of the checked buttons serves a filtering mechanism.
Right now, why is the filtering only working correctly on.click of a second click event (if btn1 is clicked, the ajax runs, with /.one.two.three... and only when btn2 is subsequently clicked does it register as /.two.three.. and only when btn3 is subsequently clicked does it registers as /.three..? http://jsfiddle.net/ENJH9/2/
Alternatively, if the refreshData() call is preceded by removing the class on.click, the data is served correctly, but the view does not reflect the removeClass!? http://jsfiddle.net/ENJH9/3/
All the buttons start with an active class (http://getbootstrap.com/javascript/#buttons):
<div class="btn-group" data-toggle="buttons" id="theBtns">
<label class="btn btn1 active">
<input type="checkbox">one
</label>
<label class="btn btn2 active">
<input type="checkbox">two
</label>
<label class="btn btn3 active">
<input type="checkbox">three
</label>
<label class="btn btn4 active">
<input type="checkbox">four
</label>
</div>
Here's the .get Call that follows the .click event of each button:
function refreshData() {
console.log('started refreshData()')
URL = '';
var filtering = function() {
if($(".btn1").hasClass("active")) { URL += ".one"; }
if($(".btn2").hasClass("active")) { URL += ".two"; }
...
console.log('done filtering: ' + URL);
return URL;
};
$.when( filtering() ).done(function() {
console.log('now data refresh with %s filtering', URL)
$.ajax({
url:"http://localhost:4200/api/v1/data" + URL,
method:'get',
success: foundAllSuccess,
error: foundError
})
});
}
And here's the click event with the removeClass commented out (the buttons still remained visually active and the class still remained in the inspector, but the server accurately filters with this setup):
$( ".btn1" ).click(function() {
// if($(".btn1").hasClass("active")) {$(".btn1").removeClass("active"); console.log('hide btn1 data');}
// else {$(".btn1").addClass("active"); console.log('btn1 data active');}
refreshData();
}); // .btn1.click
Firstly, the main reason you are having a problem is that bootstrap and jQuery don't sit too comfortably together - at least not in this case. jQuery's click event is dispatched so early, it happens before bootstrap has toggled 'active'. Therefore refreshData() operates on the button state before the click, not after the click. This can be overcome with a timeout() to delay execution of refreshData() until after bootstrap has done its thing.
Secondly, you don't need to toggle active as bootstrap looks after that aspect.
Thirdly, the code can be written more efficiently by better leveraging jQuery.
function refreshData(e) {
var URL = $(e.target).closest(".btn-group").find("label").map(function (i) {
if ($(this).hasClass("active")) {
return ['.one', '.two', '.three', '.four'][i];
} else {
return null;
}
}).get().join(''); //.map() returns an array, .join() concatenates the strings in the array into a single string.
alert('now ajax call with filtering :' + URL);
}
$("#theBtns .btn").on('click', function (e) {
setTimeout(function () {
refreshData(e);
}, 0);
});
DEMO
Note that even a delay of zero is enough for our purpose, as code executed from a setTimeout will run in a separate event thread at the earliest opportunity after the current thread has run to completion.
There may be a more "bootstrapy" way to achieve the same end but I'm not about to wade through tons of bootstrap documentation to find out.
I have several jQuery click functions- each is attached to a different DOM element, and does slightly different things...
One, for example, opens and closes a dictionary, and changes the text...
$(".dictionaryFlip").click(function(){
var link = $(this);
$(".dictionaryHolder").slideToggle('fast', function() {
if ($(this).is(":visible")) {
link.text("dictionary ON");
}
else {
link.text("dictionary OFF");
}
});
});
HTML
<div class="dictionaryHolder">
<div id="dictionaryHeading">
<span class="dictionaryTitle">中 文 词 典</span>
<span class="dictionaryHeadings">Dialog</span>
<span class="dictionaryHeadings">Word Bank</span>
</div>
</div>
<p class="dictionaryFlip">toggle dictionary: off</p>
I have a separate click function for each thing I'd like to do...
Is there a way to define one click function and assign it to different DOM elements? Then maybe use if else logic to change up what's done inside the function?
Thanks!
Clarification:
I have a click function to 1) Turn on and off the dictionary, 2) Turn on and off the menu, 3) Turn on and off the minimap... etc... Just wanted to cut down on code by combining all of these into a single click function
You can of course define a single function and use it on multiple HTML elements. It's a common pattern and should be utilized if at all possible!
var onclick = function(event) {
var $elem = $(this);
alert("Clicked!");
};
$("a").click(onclick);
$(".b").click(onclick);
$("#c").click(onclick);
// jQuery can select multiple elements in one selector
$("a, .b, #c").click(onclick);
You can also store contextual information on the element using the data- custom attribute. jQuery has a nice .data function (it's simply a prefixed proxy for .attr) that allows you to easily set and retrieve keys and values on an element. Say we have a list of people, for example:
<section>
<div class="user" data-id="124124">
<h1>John Smith</h1>
<h3>Cupertino, San Franciso</h3>
</div>
</section>
Now we register a click handler on the .user class and get the id on the user:
var onclick = function(event) {
var $this = $(this), //Always good to cache your jQuery elements (if you use them more than once)
id = $this.data("id");
alert("User ID: " + id);
};
$(".user").click(onclick);
Here's a simple pattern
function a(elem){
var link = $(elem);
$(".dictionaryHolder").slideToggle('fast', function() {
if (link.is(":visible")) {
link.text("dictionary ON");
}
else {
link.text("dictionary OFF");
}
});
}
$(".dictionaryFlip").click(function(){a(this);});
$(".anotherElement").click(function(){a(this);});
Well, you could do something like:
var f = function() {
var $this = $(this);
if($this.hasClass('A')) { /* do something */ }
if($this.hasClass('B')) { /* do something else */ }
}
$('.selector').click(f);
and so inside the f function you check what was class of clicked element
and depending on that do what u wish
For better performance, you can assign only one event listener to your page. Then, use event.target to know which part was clicked and what to do.
I would put each action in a separate function, to keep code readable.
I would also recommend using a unique Id per clickable item you need.
$("body").click(function(event) {
switch(event.target.id) {
// call suitable action according to the id of clicked element
case 'dictionaryFlip':
flipDictionnary()
break;
case 'menuToggle':
toggleMenu()
break;
// other actions go here
}
});
function flipDictionnary() {
// code here
}
function toggleMenu() {
// code here
}
cf. Event Delegation with jQuery http://www.sitepoint.com/event-delegation-with-jquery/
I've defined the following HTML elements
<span class="toggle-arrow">▼</span>
<span class="toggle-arrow" style="display:none;">▶</span>
When I click on one of the elements the visibility of both should be toggled. I tried the following Prototype code:
$$('.toggle-arrow').each(function(element) {
element.observe('click', function() {
$(element).toggle();
});
});
but it doesn't work. I know everything would be much simpler if I used jQuery, but unfortunately this is not an option:
Instead of iterating through all arrows in the collection, you can use the invoke method, to bind the event handlers, as well as toggling them. Here's an example:
var arrows = $$('.toggle-arrow');
arrows.invoke("observe", "click", function () {
arrows.invoke("toggle");
});
DEMO: http://jsfiddle.net/ddMn4/
I realize this is not quite what you're asking for, but consider something like this:
<div class="toggle-arrow-container">
<span class="toggle-arrow" style="color: pink;">▶</span>
<span class="toggle-arrow" style="display:none; color: orange;">▶</span>
</div>
document.on('click', '.toggle-arrow-container .toggle-arrow', function(event, el) {
var buddies = el.up('.toggle-arrow-container').select('.toggle-arrow');
buddies.invoke('toggle');
});
This will allow you to have multiple "toggle sets" on the page. Check out the fiddle: http://jsfiddle.net/nDppd/
Hope this helps on your Prototype adventure.
Off the cuff:
function toggleArrows(e) {
e.stop();
// first discover clicked arow
var clickedArrow = e.findElement();
// second hide all arrows
$$('.toggle-arrow').invoke('hide');
// third find arrow that wasn't clicked
var arw = $$('.toggle-arrow').find(function(a) {
return a.identify() != clickedArrow.identify();
});
// fourth complete the toggle
if(arw)
arw.show();
}
Wire the toggle arrow function in document loaded event like this
document.on('click','.toggle-arrow', toggleArrows.bindAsEventListener());
That's it, however you would have more success if you took advantage of two css classes of: arrow and arrow-selected. Then you could easily write your selector using these class names to invoke your hide/show "toggle" with something like:
function toggleArrows(e) {
e.stop();
$$('.toggle-arrow').invoke('hide');
var arw = $$('.toggle-arrow').reject(function(r) {
r.hasClassName('arrow-selected'); });
$$('.arrow-selected').invoke('removeClassName', 'arrow-selected');
arw.show();
arw.addClassName('arrow-selected');
}