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
}
});
Related
I've got the following controller on my HTML page:
...
<div data-controller="parent">
<div data-target="parent.myDiv">
<div data-controller="child">
<span data-target="child.mySpan"></span>
</div>
</div>
</div>
...
This child controller is mapped to the following child_controller.js class:
export default class {
static targets = ["mySpan"];
connect() {
document.addEventListener("myEvent", (event) => this.handleMyEvent(event));
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // Manipulate the span. No problem.
}
}
As you can see, there is an event listener on the connect() of the Stimulus controller, and when it detects that the event was fired up, it logs the event and manipulates the span target.
The problem arises when I replace the contents of the target myDiv from my parent_controller.js:
...
let childControllerHTML = "<div data-controller="child">...</div>"
myDivTarget.innerHTML= childControllerHTML;
...
Now that the myEvent gets fired up, the event listener picks it not once, but twice (because the same event got logged twice). With every subsequent replacement of the child HTML, the event gets logged one more time than it did before.
I know that one can make use of document.removeEventListener to prevent the old controller from still listening to the events:
export default class {
static targets = ["mySpan"];
connect() {
this.myEventListener = document.addEventListener("myEvent", (event) => this.handleMyEvent(event));
}
disconnect() {
document.removeEventListener("myEvent", this.myEventListener);
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // FAILS. Can't find span.
}
}
But doing it like this makes the handleMyEvent method lose the context as it no longer finds the mySpanTarget under this.
How can I remove the listener from the child controller to which I already got no access as it is no longer in the DOM, while retaining the context?
I found the answer on StimulusJS's Discourse page.
One has to make use of the bind method when initializing the controller:
export default class {
static targets = ["mySpan"];
initialize() {
this.boundHandleMyEvent = this.handleMyEvent.bind(this);
}
connect() {
document.addEventListener("myEvent", this.boundHandleMyEvent);
}
disconnect() {
document.removeEventListener("myEvent", this.boundHandleMyEvent);
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // Manipulate the span. No problem.
}
...
}
Now, the event is only listened once, and the context is not lost inside the handleMyEvent method.
I'm trying to rewrite my functional code to module pattern js, and I have this issue - When I try to delete input field which is dynamically created, I use jQuery $(this) to access dom element and delete its parent 'div'. But this refers to the Modal object, not the component I clicked. How to solve it, without making some field counter and creating fields with unique ID's, then catching ids on click and deleting that input field?
My modal:
var s,
Modal = {
settings: {
addInputBtn: $("#add-input"),
inputContainer: $("#modal-input-form"),
checkBoxesList: $(".check-box"),
inputFieldsList: $(".form-control"),
inputFieldsOptionalList: $(".optional-modal"),
inputHtml: `
<div class="input-group mb-3 optional-modal">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="checkbox" class="check-box">
</div>
</div>
<input type="text" class="form-control">
<button type="button" class="close">
<span>×</span>
</button>
</div>`
},
init: function () {
s = this.settings;
this.bindUIActions();
},
bindUIActions: function () {
s.addInputBtn.on("click", () => Modal.addInput());
s.inputContainer.on("click", ".close", () => Modal.deleteInput());
},
addInput: function () {
s.inputContainer.append(s.inputHtml);
},
deleteInput: function () {);
$(this).parent('div').remove();
}
}
Modal.init();
deleteInput: function (e) {);
$(e.target).parent('div').remove();
}
Event handlers are passed an event argument. Among its useful properties is target, which is the html element that the event originated on. Note that it could be different from the this of the event handler, which is the element that the event handler is attached to, as events bubble up through the DOM. So if you have this html:
<div id="parent">
<div id="child"></div>
</div>
then for this JS function:
$("#parent").on('click', function(e){
//this will equal <div id="parent"> unless you bind the handler to something else
//e.target will equal the clicked element:
//if the user clicked the child, it will equal the child;
//if the user clicked parent directly, it will equal parent.
$(e.target).closest('#parent').doSomething();
});
Did you try with:
deleteInput: function () {;
$(this.settings).parent('div').remove();
}
Also you have a typo at deleteInput: function () { ); ... the rest of the code, delete that extra ) after function. I suggest you trying $(this.settings).. because, this gets the Modal, and then you need to access the other object inside that object, which is settings. So your modal object, consists of other object, and I'm thinking this way you should be able to get it :)
I'm trying to detect with jQuery's on only events that are not namespaced.
I tried checking the namespace within the callback of on as follows, but it's always undefined:
$(".selector").on(
"click",
function (e) {
console.log(e.namespace); // Undefined, not "mynamespace"
}
);
$(".selector").trigger("click.mynamespace");
I've also read that you can append ".$" to the event name to rule out any namespaces, but that only seems to apply within trigger, not within on. The following runs even if the event is namespaced:
$(".selector").on(
"click.$",
function (e) {
// runs even when triggered with a namespace
}
);
I'm not sure where to go from here since when I log the event object, the namespace is nowhere to be found.
Thanks in advance!
Try to access it through handleObj.
HTML:
<div class="selector">
BOX
</div>
<button onclick="clickWithNamespace()">with ns</button>
<button onclick="clickWithoutNamespace()">without ns</button>
JS:
$(".selector").on(
"click.mynamespace",
manageClick
);
$(".selector").on(
"click",
manageClick
);
function manageClick(event) {
console.log(event.handleObj.namespace);
$(".selector").text(event.handleObj.namespace);
}
function clickWithNamespace() {
$(".selector").trigger("click.mynamespace");
}
function clickWithoutNamespace() {
$(".selector").trigger("click");
}
https://jsfiddle.net/j52rmxrs/15/
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 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.