Event trigger on a class change - javascript

I'd like my event to be triggered when a div tag containing a trigger class is changed.
I have no idea how to make it listen to the class' adding event.
<div id="test">test</div>
<script type="text/javascript">
document.getElementById.setAttribute("class", "trigger");
function workOnClassAdd() {
alert("I'm triggered");
}
</script>

The future is here, and you can use the MutationObserver interface to watch for a specific class change.
let targetNode = document.getElementById('test')
function workOnClassAdd() {
alert("I'm triggered when the class is added")
}
function workOnClassRemoval() {
alert("I'm triggered when the class is removed")
}
// watch for a specific class change
let classWatcher = new ClassWatcher(targetNode, 'trigger', workOnClassAdd, workOnClassRemoval)
// tests:
targetNode.classList.add('trigger') // triggers workOnClassAdd callback
targetNode.classList.add('trigger') // won't trigger (class is already exist)
targetNode.classList.add('another-class') // won't trigger (class is not watched)
targetNode.classList.remove('trigger') // triggers workOnClassRemoval callback
targetNode.classList.remove('trigger') // won't trigger (class was already removed)
targetNode.setAttribute('disabled', true) // won't trigger (the class is unchanged)
I wrapped MutationObserver with a simple class:
class ClassWatcher {
constructor(targetNode, classToWatch, classAddedCallback, classRemovedCallback) {
this.targetNode = targetNode
this.classToWatch = classToWatch
this.classAddedCallback = classAddedCallback
this.classRemovedCallback = classRemovedCallback
this.observer = null
this.lastClassState = targetNode.classList.contains(this.classToWatch)
this.init()
}
init() {
this.observer = new MutationObserver(this.mutationCallback)
this.observe()
}
observe() {
this.observer.observe(this.targetNode, { attributes: true })
}
disconnect() {
this.observer.disconnect()
}
mutationCallback = mutationsList => {
for(let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
let currentClassState = mutation.target.classList.contains(this.classToWatch)
if(this.lastClassState !== currentClassState) {
this.lastClassState = currentClassState
if(currentClassState) {
this.classAddedCallback()
}
else {
this.classRemovedCallback()
}
}
}
}
}
}

Well there were mutation events, but they were deprecated and the future there will be Mutation Observers, but they will not be fully supported for a long time. So what can you do in the mean time?
You can use a timer to check the element.
function addClassNameListener(elemId, callback) {
var elem = document.getElementById(elemId);
var lastClassName = elem.className;
window.setInterval( function() {
var className = elem.className;
if (className !== lastClassName) {
callback();
lastClassName = className;
}
},10);
}
Running example: jsFiddle

Here's a simple, basic example on how to trigger a callback on Class attribute change
MutationObserver API
const attrObserver = new MutationObserver((mutations) => {
mutations.forEach(mu => {
if (mu.type !== "attributes" && mu.attributeName !== "class") return;
console.log("class was modified!");
});
});
const ELS_test = document.querySelectorAll(".test");
ELS_test.forEach(el => attrObserver.observe(el, {attributes: true}));
// Example of Buttons toggling several .test classNames
document.querySelectorAll(".btn").forEach(btn => {
btn.addEventListener("click", () => ELS_test.forEach(el => el.classList.toggle(btn.dataset.class)));
});
.blue {background: blue;}
.gold {color: gold;}
<div class="test">TEST DIV</div>
<button class="btn" data-class="blue">BACKGROUND</button>
<button class="btn" data-class="gold">COLOR</button>

Can use this onClassChange function to watch whenever classList of an element changes
function onClassChange(element, callback) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'class'
) {
callback(mutation.target);
}
});
});
observer.observe(element, { attributes: true });
return observer.disconnect;
}
var itemToWatch = document.querySelector('#item-to-watch');
onClassChange(itemToWatch, (node) => {
node.classList.contains('active')
? alert('class added')
: alert('class removed');
node.textContent = 'Item to watch. classList: ' + node.className;
});
function addClass() {
itemToWatch.classList.add('active');
}
function removeClass() {
itemToWatch.classList.remove('active');
}
<div id="item-to-watch">Item to watch</div>
<button onclick="addClass();">Add Class</button>
<button onclick="removeClass();">Remove Class</button>

I needed a class update listener for a project, so I whipped this up. I didn’t end up using it, so it’s not fully tested, but should be fine on browsers supporting Element.classList DOMTokenList.
Bonus: allows “chaining” of the 4 supported methods, for example el.classList.remove(“inactive”).remove(“disabled”).add(“active”)
function ClassListListener( el ) {
const ecl = el.classList;
['add','remove','toggle','replace'].forEach(prop=>{
el.classList['_'+prop] = ecl[prop]
el.classList[prop] = function() {
const args = Array.from(arguments)
this['_'+prop].apply(this, args)
el.dispatchEvent(new CustomEvent(
'classlistupdate',
{ detail: { method: prop, args } }
))
return this
}
})
return el
}
Useage:
const el = document.body
ClassListListener(el).addEventListener('classlistupdate', e => {
const args = e.detail.args.join(', ')
console.log('el.classList.'+e.detail.method+'('+args+')')
}, false)
el.classList
.add('test')
.replace('test', 'tested')

The idea is to substitute class manipulation functions, such as 'add', 'remove'... with wrappers, that send class change messages before or after class list changed. It's very simple to use:
choose element(s) or query that selects elements, and pass it to the function.
add 'class-change' and/or 'class-add', 'class-remove'... handlers to either elements or their container ('document', for example).
after that, any class list change by either add, remove, replace or toggle methods will fire corresponding events.
Event sequence is:
A) 'class-change' request event is fired, that can be rejected by handler by preventDefault() if needed. If rejected, then class change will be cancelled.
B) class change function will be executed
B) 'class-add' or 'class-remove'... information event is fired.
function addClassChangeEventDispatcher( el )
{
// select or use multiple elements
if(typeof el === 'string') el = [...document.querySelectorAll( el )];
// process multiple elements
if(Array.isArray( el )) return el.map( addClassChangeEventDispatcher );
// process single element
// methods that are called by user to manipulate
let clMethods = ['add','remove','toggle','replace'];
// substitute each method of target element with wrapper that fires event after class change
clMethods.forEach( method =>
{
let f = el.classList[method];
el.classList[method] = function()
{
// prepare message info
let detail = method == 'toggle' ? { method, className: arguments[0] } :
method == 'replace' ? { method, className: arguments[0], newClassName: arguments[1] } :
{ method, className: arguments[0], classNames: [...arguments] };
// fire class change request, and if rejected, cancel class operation
if(!el.dispatchEvent( new CustomEvent( 'class-change', {bubbles: true, cancelable: true, detail} ))) return;
// call original method and then fire changed event
f.call( this, ...arguments );
el.dispatchEvent( new CustomEvent( 'class-' + method, {bubbles: true, detail} ));
};
});
return el;
}

Related

attributeChangedCallback() always called twice ending up with multiple event listeners

Playing around with custom elements, I'm trying to get a click event fired depending on custom element attribute's value. But using the attributeChangedCallback() method (along with the connectedCallback() method) I'm ending up with multiple event listeners.
class HelloWorld extends HTMLElement {
static get observedAttributes() {
return ['attribute1', 'attribute2'];
}
get attribute1() {
return this.getAttribute('attribute1');
}
set attribute1(val) {
if (val) {
this.setAttribute('attribute1', val);
} else {
this.removeAttribute('attribute1');
}
}
get attribute2() {
return this.getAttribute('attribute2');
}
set attribute2(val) {
if (val) {
this.setAttribute('attribute2', val);
} else {
this.removeAttribute('attribute2');
}
}
constructor() {
super();
}
connectedCallback() {
this.textContent = 'Hello World';
update(this);
}
attributeChangedCallback(name, oldValue, newValue) {
update(this);
}
}
customElements.define('hello-world', HelloWorld);
function update(el) {
if (el.attribute1 === 'foo') {
el.addEventListener('click', e => {
el.textContent = (el.textContent.indexOf(' (') != -1 ? el.textContent.substring(0, el.textContent.indexOf(' (')) : el.textContent) + ' (clicked ' + (el.textContent.match(/\d+/) ? parseInt(el.textContent.match(/-?\d+/)[0]) + 1 : 1) + ' times)';
});
} else if (el.attribute1 === 'bar') {
el.addEventListener('click', e => {
el.textContent = (el.textContent.indexOf(' (') != -1 ? el.textContent.substring(0, el.textContent.indexOf(' (')) : el.textContent) + ' (clicked ' + (el.textContent.match(/\d+/) ? parseInt(el.textContent.match(/-?\d+/)[0]) - 1 : 1) + ' times)';
});
}
}
hello-world {
cursor: pointer;
}
<hello-world attribute1="foo" attribute2=""></hello-world>
Why is the attributeChangedCallback() method always called twice and therefore adds two event listeners? How to avoid this? What would be best practice?
It is called attributeChangedCallback, so fires on every single attribute change, including init when the element is added to the DOM
Attach listeners in the connectedCallback, but that can run again if you move the element in the DOM, so you have to remove them in the disconnectedCallback
Easier might be to use an inline EventHandler, there can only be one on an element.
customElements.define('hello-world', class extends HTMLElement {
static get observedAttributes() {
return ['attribute1', 'attribute2'];
}
get attribute1() {
return this.getAttribute('attribute1');
}
set attribute1(val) {
this.toggleAttribute('attribute1', val);
}
get attribute2() {
return this.getAttribute('attribute2');
}
set attribute2(val) {
this.toggleAttribute('attribute2', val);
}
connectedCallback() {
this.count = 0;
this.innerHTML = `Hello World clicked: <span>${this.count}</span> times`;
this.onclick = (evt) => {
this.count++;
this.querySelector("span").innerHTML = this.count;
}
}
attributeChangedCallback(name, oldValue, newValue) {
console.log("attributeChangedCallback", name, oldValue, newValue);
}
});
hello-world {
cursor: pointer;
}
<hello-world attribute1="foo" attribute2=""></hello-world>
constructor() {
super()
}
is not required, a non existing constructor will execute the constructor from its parent.. which is what super() does.
If you want to prevent multiple listeners you could try a method:
addListeners(){
.. add your listeners
this.addListeners = () => {}; // overload; Don't run its original code again
}
Also note Listeners you add on the Element (or its contents) are automaticall garbage collected/removed when the element is removed from the DOM.
Any Listeners you add on other DOM elements (eg. document) you have to remove yourself in the disconnectedCallback

mutationobserver firing when no change and method not found issue

I've placed an observer on an element using MutationObserver. In a one use case it works exactly as expected and fires after a change but in another user action where the element has not changed it appears to be firing - but in doing so it comes up with a method not found error, which doesn't appear in the first use case using the same observer.
The observer watches for an update within an element which gets updated with an image as a user selects an image.
In the working case a user selects an image from a list of images, it then updates and the observer fires - all great.
In the non-working case a user uploads an image - at this point though no update has happened to the target element (which is in view but below a colorbox.(not sure if that's relevant).
The firing itself would not normally be a problem but within the observer callback it calls a method which in the second case it says is not defined.
So in the first instance there are no errors but in the second instance:
I get an error _this.buttons is not a function at MutationObserver.callback
The code is being compiled with webpack
1. Why is the observer firing when the doesn't appear to the type of change being observed?
Why does this error occur in this scenario - when the method appears to exist and works as expected when there is a change?
Any help appreciated
here's the code - this class manages the actions for a page - I've removed some code to try and keep it brief (but still a bit lengthy - refactoring to be done):
First, here's the code of the observer:
const callback = (mutationsList, observer) =>{
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
module.buttons();
module.initialiseControls();
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
And here's the class in which the observer method is contained.
export let fileImageWidgetControls = class {
constructor({
previewBtn = '.preview-image',
addBtn = '#add-image',
replaceBtn = '#replace-image',
removeBtn = '#remove-image'
} = {}) {
this.options = {
previewBtn: previewBtn,
addBtn: addBtn,
replaceBtn: replaceBtn,
removeBtn: removeBtn
}
this.filemanager = new filemanagerHandler; //file selector class
this.imageWidget = new updateWidget; //handles updating the image
this.initialiseControls(); //sets up the page controls
this.observer(); //sets up the observer
}
openFileManager = () =>{
//open colbox (which opens filemanager page
//When filemanager loaded then initialise filemanager
$(document).bind('cbox_complete', ()=>{
console.log('Colbox complete');
this.filemanager.init();
});
//handle colbox closing and update image in widget (if needed)
$(document).bind('cbox_closed', ()=>{
let selectedAsset = this.filemanager.getSelectedAsset();
if(selectedAsset) {
this.imageWidget.update(selectedAsset.filename);
}
});
colBox.init({
href: this.options.serverURL
});
colBox.colorbox()
}
remove = ()=> {
//clear file and update visible buttons
this.buttons();
}
/**
* preview the image in a colorbox
* #param filename
*/
preview = function () {
//open image in preview
}
/**
* select image via filemanager
*/
select = () =>{
console.log('select');
this.openFileManager();
}
replace = () => {
// image already exists in widget but needs replacing
console.log('replace');
this.openFileManager();
}
initialiseControls = () => {
console.log('init controls');
//preview button
$(this.options.previewBtn).on('click', (e) => {
e.preventDefault();
this.preview();
}).attr('disabled', false);
$('#img-preview-link').on('click', (e)=> {
e.preventDefault();
this.preview();
});
// add button
$(this.options.addBtn).on('click', (e) => {
e.preventDefault();
this.select();
}).attr('disabled', false);
//replace button
$(this.options.replaceBtn).on('click', (e) => {
e.preventDefault();
this.replace();
}).attr('disabled', false);
//remove button
$(this.options.removeBtn).on('click', (e) => {
e.preventDefault();
this.remove();
}).attr('disabled', false);
this.buttons();
}
//set an observer to watch preview image for changes
observer= ()=> {
const module = this;
const targetNode = document.getElementById('image-preview-panel');
const config = { attributes: true, childList: true, subtree: true };
const callback = (mutationsList, observer) =>{
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
module.buttons();
module.initialiseControls();
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
}
buttons = function() {
let imagePreview = $('#image-preview');
if(imagePreview.data('updated')=== true && imagePreview.data('updated') !== "false") {
console.log('image present');
$(this.options.addBtn).fadeOut().attr('disabled', true);
$(this.options.removeBtn).fadeIn().attr('disabled', false);
$(this.options.replaceBtn).fadeIn().attr('disabled', false);
$(this.options.previewBtn).fadeIn().attr('disabled', false);
} else {
console.log('image not present', imagePreview.data());
console.log('image element:', imagePreview);
$(this.options.addBtn).fadeIn().attr('disabled', false);
$(this.options.removeBtn).fadeOut().attr('disabled', true);
$(this.options.replaceBtn).fadeOut().attr('disabled', true);
$(this.options.previewBtn).fadeOut().attr('disabled', true);
}
}
}
I copied code from a tutorial hence some of the comments until I refactor
Added const module = this; within the method and referenced that within the nested function and now pointing to the correctthis`

Apply multi-event do same function on same element [duplicate]

So my dilemma is that I don't want to write the same code twice. Once for the click event and another for the touchstart event.
Here is the original code:
document.getElementById('first').addEventListener('touchstart', function(event) {
do_something();
});
document.getElementById('first').addEventListener('click', function(event) {
do_something();
});
How can I compact this? There HAS to be a simpler way!
I thought some might find this approach useful; it could be applied to any similarly repetitive code:
ES6
['click','ontouchstart'].forEach( evt =>
element.addEventListener(evt, dosomething, false)
);
ES5
['click','ontouchstart'].forEach( function(evt) {
element.addEventListener(evt, dosomething, false);
});
You can just define a function and pass it. Anonymous functions are not special in any way, all functions can be passed around as values.
var elem = document.getElementById('first');
elem.addEventListener('touchstart', handler, false);
elem.addEventListener('click', handler, false);
function handler(event) {
do_something();
}
Maybe you can use a helper function like this:
// events and args should be of type Array
function addMultipleListeners(element,events,handler,useCapture,args){
if (!(events instanceof Array)){
throw 'addMultipleListeners: '+
'please supply an array of eventstrings '+
'(like ["click","mouseover"])';
}
//create a wrapper to be able to use additional arguments
var handlerFn = function(e){
handler.apply(this, args && args instanceof Array ? args : []);
}
for (var i=0;i<events.length;i+=1){
element.addEventListener(events[i],handlerFn,useCapture);
}
}
function handler(e) {
// do things
};
// usage
addMultipleListeners(
document.getElementById('first'),
['touchstart','click'],
handler,
false);
[Edit nov. 2020] This answer is pretty old. The way I solve this nowadays is by using an actions object where handlers are specified per event type, a data-attribute for an element to indicate which action should be executed on it and one generic document wide handler method (so event delegation).
const firstElemHandler = (elem, evt) =>
elem.textContent = `You ${evt.type === "click" ? "clicked" : "touched"}!`;
const actions = {
click: {
firstElemHandler,
},
touchstart: {
firstElemHandler,
},
mouseover: {
firstElemHandler: elem => elem.textContent = "Now ... click me!",
outerHandling: elem => {
console.clear();
console.log(`Hi from outerHandling, handle time ${
new Date().toLocaleTimeString()}`);
},
}
};
Object.keys(actions).forEach(key => document.addEventListener(key, handle));
function handle(evt) {
const origin = evt.target.closest("[data-action]");
return origin &&
actions[evt.type] &&
actions[evt.type][origin.dataset.action] &&
actions[evt.type][origin.dataset.action](origin, evt) ||
true;
}
[data-action]:hover {
cursor: pointer;
}
<div data-action="outerHandling">
<div id="first" data-action="firstElemHandler">
<b>Hover, click or tap</b>
</div>
this is handled too (on mouse over)
</div>
For large numbers of events this might help:
var element = document.getElementById("myId");
var myEvents = "click touchstart touchend".split(" ");
var handler = function (e) {
do something
};
for (var i=0, len = myEvents.length; i < len; i++) {
element.addEventListener(myEvents[i], handler, false);
}
Update 06/2017:
Now that new language features are more widely available you could simplify adding a limited list of events that share one listener.
const element = document.querySelector("#myId");
function handleEvent(e) {
// do something
}
// I prefer string.split because it makes editing the event list slightly easier
"click touchstart touchend touchmove".split(" ")
.map(name => element.addEventListener(name, handleEvent, false));
If you want to handle lots of events and have different requirements per listener you can also pass an object which most people tend to forget.
const el = document.querySelector("#myId");
const eventHandler = {
// called for each event on this element
handleEvent(evt) {
switch (evt.type) {
case "click":
case "touchstart":
// click and touchstart share click handler
this.handleClick(e);
break;
case "touchend":
this.handleTouchend(e);
break;
default:
this.handleDefault(e);
}
},
handleClick(e) {
// do something
},
handleTouchend(e) {
// do something different
},
handleDefault(e) {
console.log("unhandled event: %s", e.type);
}
}
el.addEventListener(eventHandler);
Update 05/2019:
const el = document.querySelector("#myId");
const eventHandler = {
handlers: {
click(e) {
// do something
},
touchend(e) {
// do something different
},
default(e) {
console.log("unhandled event: %s", e.type);
}
},
// called for each event on this element
handleEvent(evt) {
switch (evt.type) {
case "click":
case "touchstart":
// click and touchstart share click handler
this.handlers.click(e);
break;
case "touchend":
this.handlers.touchend(e);
break;
default:
this.handlers.default(e);
}
}
}
Object.keys(eventHandler.handlers)
.map(eventName => el.addEventListener(eventName, eventHandler))
Unless your do_something function actually does something with any given arguments, you can just pass it as the event handler.
var first = document.getElementById('first');
first.addEventListener('touchstart', do_something, false);
first.addEventListener('click', do_something, false);
Simplest solution for me was passing the code into a separate function and then calling that function in an event listener, works like a charm.
function somefunction() { ..code goes here ..}
variable.addEventListener('keyup', function() {
somefunction(); // calling function on keyup event
})
variable.addEventListener('keydown', function() {
somefunction(); //calling function on keydown event
})
I have a small solution that attaches to the prototype
EventTarget.prototype.addEventListeners = function(type, listener, options,extra) {
let arr = type;
if(typeof type == 'string'){
let sp = type.split(/[\s,;]+/);
arr = sp;
}
for(let a of arr){
this.addEventListener(a,listener,options,extra);
}
};
Allows you to give it a string or Array. The string can be separated with a space(' '), a comma(',') OR a Semicolon(';')
I just made this function (intentionally minified):
((i,e,f)=>e.forEach(o=>i.addEventListener(o,f)))(element, events, handler)
Usage:
((i,e,f)=>e.forEach(o=>i.addEventListener(o,f)))(element, ['click', 'touchstart'], (event) => {
// function body
});
The difference compared to other approaches is that the handling function is defined only once and then passed to every addEventListener.
EDIT:
Adding a non-minified version to make it more comprehensible. The minified version was meant just to be copy-pasted and used.
((element, event_names, handler) => {
event_names.forEach( (event_name) => {
element.addEventListener(event_name, handler)
})
})(element, ['click', 'touchstart'], (event) => {
// function body
});
I'm new at JavaScript coding, so forgive me if I'm wrong.
I think you can create an object and the event handlers like this:
const myEvents = {
click: clickOnce,
dblclick: clickTwice,
};
function clickOnce() {
console.log("Once");
}
function clickTwice() {
console.log("Twice");
}
Object.keys(myEvents).forEach((key) => {
const myButton = document.querySelector(".myButton")
myButton.addEventListener(key, myEvents[key]);
});
<h1 class="myButton">Button</h1>
And then click on the element.
document.getElementById('first').addEventListener('touchstart',myFunction);
document.getElementById('first').addEventListener('click',myFunction);
function myFunction(e){
e.preventDefault();e.stopPropagation()
do_something();
}
You should be using e.stopPropagation() because if not, your function will fired twice on mobile
This is my solution in which I deal with multiple events in my workflow.
let h2 = document.querySelector("h2");
function addMultipleEvents(eventsArray, targetElem, handler) {
eventsArray.map(function(event) {
targetElem.addEventListener(event, handler, false);
}
);
}
let counter = 0;
function countP() {
counter++;
h2.innerHTML = counter;
}
// magic starts over here...
addMultipleEvents(['click', 'mouseleave', 'mouseenter'], h2, countP);
<h1>MULTI EVENTS DEMO - If you click, move away or enter the mouse on the number, it counts...</h1>
<h2 style="text-align:center; font: bold 3em comic; cursor: pointer">0</h2>
What about something like this:
['focusout','keydown'].forEach( function(evt) {
self.slave.addEventListener(evt, function(event) {
// Here `this` is for the slave, i.e. `self.slave`
if ((event.type === 'keydown' && event.which === 27) || event.type === 'focusout') {
this.style.display = 'none';
this.parentNode.querySelector('.master').style.display = '';
this.parentNode.querySelector('.master').value = this.value;
console.log('out');
}
}, false);
});
// The above is replacement of:
/* self.slave.addEventListener("focusout", function(event) { })
self.slave.addEventListener("keydown", function(event) {
if (event.which === 27) { // Esc
}
})
*/
You can simply do it iterating an Object. This can work with a single or multiple elements. This is an example:
const ELEMENTS = {'click': element1, ...};
for (const [key, value] of Object.entries(ELEMENTS)) {
value.addEventListener(key, () => {
do_something();
});
}
When key is the type of event and value is the element when you are adding the event, so you can edit ELEMENTS adding your elements and the type of event.
Semi-related, but this is for initializing one unique event listener specific per element.
You can use the slider to show the values in realtime, or check the console.
On the <input> element I have a attr tag called data-whatever, so you can customize that data if you want to.
sliders = document.querySelectorAll("input");
sliders.forEach(item=> {
item.addEventListener('input', (e) => {
console.log(`${item.getAttribute("data-whatever")} is this value: ${e.target.value}`);
item.nextElementSibling.textContent = e.target.value;
});
})
.wrapper {
display: flex;
}
span {
padding-right: 30px;
margin-left: 5px;
}
* {
font-size: 12px
}
<div class="wrapper">
<input type="range" min="1" data-whatever="size" max="800" value="50" id="sliderSize">
<em>50</em>
<span>Size</span>
<br>
<input type="range" min="1" data-whatever="OriginY" max="800" value="50" id="sliderOriginY">
<em>50</em>
<span>OriginY</span>
<br>
<input type="range" min="1" data-whatever="OriginX" max="800" value="50" id="sliderOriginX">
<em>50</em>
<span>OriginX</span>
</div>
//catch volume update
var volEvents = "change,input";
var volEventsArr = volEvents.split(",");
for(var i = 0;i<volknob.length;i++) {
for(var k=0;k<volEventsArr.length;k++) {
volknob[i].addEventListener(volEventsArr[k], function() {
var cfa = document.getElementsByClassName('watch_televised');
for (var j = 0; j<cfa.length; j++) {
cfa[j].volume = this.value / 100;
}
});
}
}
'onclick' in the html works for both touch and click event. Here's the example.
This mini javascript libary (1.3 KB) can do all these things
https://github.com/Norair1997/norjs/
nor.event(["#first"], ["touchstart", "click"], [doSomething, doSomething]);

Mixpanel track_links does not work with dynamically added elements

I'm having trouble using mixpanel.track_links with links added dynamically (after page load).
For a general example, given this page:
<div id="link-div"></div>
<input type="button" id="add-link" />
<script type="text/javascript">
mixpanel.track_links(".mixpanel-event", "event name", function(ele) { return { "type": $(ele).attr("type")}});
</script>
At some user action, links are added to the page using jquery. For example:
$('#add-link).click(function() {
$('#link-div').html('<a class="mixpanel-event" type="event-type" href="#>Link to track</a>');
})
The problem is that track_links isn't triggered on click of the newly created link. I'm hoping someone can share their experience in enabling the track_link function to work for dynamically added links.
I was curious so I checked out their code and went ahead and did as they suggested. I tested it, and it worked fine. This requires jQuery though.
Example usage: mixpanel.delegate_links(document.body, 'a', 'clicked link');
// with jQuery and mixpanel
mixpanel.delegate_links = function (parent, selector, event_name, properties) {
properties = properties || {};
parent = parent || document.body;
parent = $(parent);
parent.on('click', selector, function (event) {
var new_tab = event.which === 2 || event.metaKey || event.target.target === '_blank';
properties.url = event.target.href;
function callback() {
if (new_tab) {
return;
}
window.location = properties.url;
}
if (!new_tab) {
event.preventDefault();
setTimeout(callback, 300);
}
mixpanel.track(event_name, properties, callback);
});
};
I had a somewhat hard time trying to get tracking links working as expected on react. The main caveat I noticed was that duplicated events may be sent to mixpanel in bursts.
I used a slightly modified version of #Kyle to solve my problem. Additionally, this accounts for properties being possibly a function as supported by the mixpanel API.
// mixpanelSetup.js
import md5 from "md5";
const setup = () => {
mixpanel.init(TOKEN);
// Sets ensure unique items
mixpanel.delegated_links = new Set();
mixpanel.delegate_links = (parent, selector, eventName, eventProperties, {ignoreUrl=false}) => {
// Hash by whatever thing(s) the use case considers 'unique' (e.g md5(`${selector}__${eventName}`))
const linkHash = md5(selector);
parent = parent || document.body;
parent = $(parent);
// Do not add additional trackers for an already tracked event.
if (mixpanel.delegated_links.has(linkHash)) {
return;
}
mixpanel.delegated_links.add(linkHash);
parent.on("click", selector, (event) => {
const newTab = event.which === 2 || event.metaKey || event.target.target === "_blank";
if (typeof eventProperties === "function") {
eventProperties = eventProperties(event.target) || {};
}
eventProperties.url = event.target.href;
// In case we don't want the url on the properties.
if (ignoreUrl) {
delete eventProperties.url;
}
const callback = () => {
if (newTab) {
return;
}
window.location = event.target.href;
};
if (!newTab) {
event.preventDefault();
setTimeout(callback, 300);
}
console.debug("Tracking link click!");
mixpanel.track(eventName, eventProperties, callback);
});
};
}
And can be used as:
// MyComponent.jsx
import React, { useEffect } from "react";
import { Link, useLocation } from "#reach/router";
const MyComponent = ({ moduleName, key, ...props }) => {
const id = `#${id}__${moduleName}`;
useEffect(() => {
mixpanel.delegate_links(document.parent, id, event => {
return {
module: event.id.split("__").pop(),
...props.otherPropsToTrack
};
})
}, [])
return <>
<Link {...props} to="/some/path" id={id}>My Page</Link>
</>
}

adding multiple event listeners to one element

So my dilemma is that I don't want to write the same code twice. Once for the click event and another for the touchstart event.
Here is the original code:
document.getElementById('first').addEventListener('touchstart', function(event) {
do_something();
});
document.getElementById('first').addEventListener('click', function(event) {
do_something();
});
How can I compact this? There HAS to be a simpler way!
I thought some might find this approach useful; it could be applied to any similarly repetitive code:
ES6
['click','ontouchstart'].forEach( evt =>
element.addEventListener(evt, dosomething, false)
);
ES5
['click','ontouchstart'].forEach( function(evt) {
element.addEventListener(evt, dosomething, false);
});
You can just define a function and pass it. Anonymous functions are not special in any way, all functions can be passed around as values.
var elem = document.getElementById('first');
elem.addEventListener('touchstart', handler, false);
elem.addEventListener('click', handler, false);
function handler(event) {
do_something();
}
Maybe you can use a helper function like this:
// events and args should be of type Array
function addMultipleListeners(element,events,handler,useCapture,args){
if (!(events instanceof Array)){
throw 'addMultipleListeners: '+
'please supply an array of eventstrings '+
'(like ["click","mouseover"])';
}
//create a wrapper to be able to use additional arguments
var handlerFn = function(e){
handler.apply(this, args && args instanceof Array ? args : []);
}
for (var i=0;i<events.length;i+=1){
element.addEventListener(events[i],handlerFn,useCapture);
}
}
function handler(e) {
// do things
};
// usage
addMultipleListeners(
document.getElementById('first'),
['touchstart','click'],
handler,
false);
[Edit nov. 2020] This answer is pretty old. The way I solve this nowadays is by using an actions object where handlers are specified per event type, a data-attribute for an element to indicate which action should be executed on it and one generic document wide handler method (so event delegation).
const firstElemHandler = (elem, evt) =>
elem.textContent = `You ${evt.type === "click" ? "clicked" : "touched"}!`;
const actions = {
click: {
firstElemHandler,
},
touchstart: {
firstElemHandler,
},
mouseover: {
firstElemHandler: elem => elem.textContent = "Now ... click me!",
outerHandling: elem => {
console.clear();
console.log(`Hi from outerHandling, handle time ${
new Date().toLocaleTimeString()}`);
},
}
};
Object.keys(actions).forEach(key => document.addEventListener(key, handle));
function handle(evt) {
const origin = evt.target.closest("[data-action]");
return origin &&
actions[evt.type] &&
actions[evt.type][origin.dataset.action] &&
actions[evt.type][origin.dataset.action](origin, evt) ||
true;
}
[data-action]:hover {
cursor: pointer;
}
<div data-action="outerHandling">
<div id="first" data-action="firstElemHandler">
<b>Hover, click or tap</b>
</div>
this is handled too (on mouse over)
</div>
For large numbers of events this might help:
var element = document.getElementById("myId");
var myEvents = "click touchstart touchend".split(" ");
var handler = function (e) {
do something
};
for (var i=0, len = myEvents.length; i < len; i++) {
element.addEventListener(myEvents[i], handler, false);
}
Update 06/2017:
Now that new language features are more widely available you could simplify adding a limited list of events that share one listener.
const element = document.querySelector("#myId");
function handleEvent(e) {
// do something
}
// I prefer string.split because it makes editing the event list slightly easier
"click touchstart touchend touchmove".split(" ")
.map(name => element.addEventListener(name, handleEvent, false));
If you want to handle lots of events and have different requirements per listener you can also pass an object which most people tend to forget.
const el = document.querySelector("#myId");
const eventHandler = {
// called for each event on this element
handleEvent(evt) {
switch (evt.type) {
case "click":
case "touchstart":
// click and touchstart share click handler
this.handleClick(e);
break;
case "touchend":
this.handleTouchend(e);
break;
default:
this.handleDefault(e);
}
},
handleClick(e) {
// do something
},
handleTouchend(e) {
// do something different
},
handleDefault(e) {
console.log("unhandled event: %s", e.type);
}
}
el.addEventListener(eventHandler);
Update 05/2019:
const el = document.querySelector("#myId");
const eventHandler = {
handlers: {
click(e) {
// do something
},
touchend(e) {
// do something different
},
default(e) {
console.log("unhandled event: %s", e.type);
}
},
// called for each event on this element
handleEvent(evt) {
switch (evt.type) {
case "click":
case "touchstart":
// click and touchstart share click handler
this.handlers.click(e);
break;
case "touchend":
this.handlers.touchend(e);
break;
default:
this.handlers.default(e);
}
}
}
Object.keys(eventHandler.handlers)
.map(eventName => el.addEventListener(eventName, eventHandler))
Unless your do_something function actually does something with any given arguments, you can just pass it as the event handler.
var first = document.getElementById('first');
first.addEventListener('touchstart', do_something, false);
first.addEventListener('click', do_something, false);
Simplest solution for me was passing the code into a separate function and then calling that function in an event listener, works like a charm.
function somefunction() { ..code goes here ..}
variable.addEventListener('keyup', function() {
somefunction(); // calling function on keyup event
})
variable.addEventListener('keydown', function() {
somefunction(); //calling function on keydown event
})
I have a small solution that attaches to the prototype
EventTarget.prototype.addEventListeners = function(type, listener, options,extra) {
let arr = type;
if(typeof type == 'string'){
let sp = type.split(/[\s,;]+/);
arr = sp;
}
for(let a of arr){
this.addEventListener(a,listener,options,extra);
}
};
Allows you to give it a string or Array. The string can be separated with a space(' '), a comma(',') OR a Semicolon(';')
I just made this function (intentionally minified):
((i,e,f)=>e.forEach(o=>i.addEventListener(o,f)))(element, events, handler)
Usage:
((i,e,f)=>e.forEach(o=>i.addEventListener(o,f)))(element, ['click', 'touchstart'], (event) => {
// function body
});
The difference compared to other approaches is that the handling function is defined only once and then passed to every addEventListener.
EDIT:
Adding a non-minified version to make it more comprehensible. The minified version was meant just to be copy-pasted and used.
((element, event_names, handler) => {
event_names.forEach( (event_name) => {
element.addEventListener(event_name, handler)
})
})(element, ['click', 'touchstart'], (event) => {
// function body
});
I'm new at JavaScript coding, so forgive me if I'm wrong.
I think you can create an object and the event handlers like this:
const myEvents = {
click: clickOnce,
dblclick: clickTwice,
};
function clickOnce() {
console.log("Once");
}
function clickTwice() {
console.log("Twice");
}
Object.keys(myEvents).forEach((key) => {
const myButton = document.querySelector(".myButton")
myButton.addEventListener(key, myEvents[key]);
});
<h1 class="myButton">Button</h1>
And then click on the element.
document.getElementById('first').addEventListener('touchstart',myFunction);
document.getElementById('first').addEventListener('click',myFunction);
function myFunction(e){
e.preventDefault();e.stopPropagation()
do_something();
}
You should be using e.stopPropagation() because if not, your function will fired twice on mobile
This is my solution in which I deal with multiple events in my workflow.
let h2 = document.querySelector("h2");
function addMultipleEvents(eventsArray, targetElem, handler) {
eventsArray.map(function(event) {
targetElem.addEventListener(event, handler, false);
}
);
}
let counter = 0;
function countP() {
counter++;
h2.innerHTML = counter;
}
// magic starts over here...
addMultipleEvents(['click', 'mouseleave', 'mouseenter'], h2, countP);
<h1>MULTI EVENTS DEMO - If you click, move away or enter the mouse on the number, it counts...</h1>
<h2 style="text-align:center; font: bold 3em comic; cursor: pointer">0</h2>
What about something like this:
['focusout','keydown'].forEach( function(evt) {
self.slave.addEventListener(evt, function(event) {
// Here `this` is for the slave, i.e. `self.slave`
if ((event.type === 'keydown' && event.which === 27) || event.type === 'focusout') {
this.style.display = 'none';
this.parentNode.querySelector('.master').style.display = '';
this.parentNode.querySelector('.master').value = this.value;
console.log('out');
}
}, false);
});
// The above is replacement of:
/* self.slave.addEventListener("focusout", function(event) { })
self.slave.addEventListener("keydown", function(event) {
if (event.which === 27) { // Esc
}
})
*/
You can simply do it iterating an Object. This can work with a single or multiple elements. This is an example:
const ELEMENTS = {'click': element1, ...};
for (const [key, value] of Object.entries(ELEMENTS)) {
value.addEventListener(key, () => {
do_something();
});
}
When key is the type of event and value is the element when you are adding the event, so you can edit ELEMENTS adding your elements and the type of event.
Semi-related, but this is for initializing one unique event listener specific per element.
You can use the slider to show the values in realtime, or check the console.
On the <input> element I have a attr tag called data-whatever, so you can customize that data if you want to.
sliders = document.querySelectorAll("input");
sliders.forEach(item=> {
item.addEventListener('input', (e) => {
console.log(`${item.getAttribute("data-whatever")} is this value: ${e.target.value}`);
item.nextElementSibling.textContent = e.target.value;
});
})
.wrapper {
display: flex;
}
span {
padding-right: 30px;
margin-left: 5px;
}
* {
font-size: 12px
}
<div class="wrapper">
<input type="range" min="1" data-whatever="size" max="800" value="50" id="sliderSize">
<em>50</em>
<span>Size</span>
<br>
<input type="range" min="1" data-whatever="OriginY" max="800" value="50" id="sliderOriginY">
<em>50</em>
<span>OriginY</span>
<br>
<input type="range" min="1" data-whatever="OriginX" max="800" value="50" id="sliderOriginX">
<em>50</em>
<span>OriginX</span>
</div>
//catch volume update
var volEvents = "change,input";
var volEventsArr = volEvents.split(",");
for(var i = 0;i<volknob.length;i++) {
for(var k=0;k<volEventsArr.length;k++) {
volknob[i].addEventListener(volEventsArr[k], function() {
var cfa = document.getElementsByClassName('watch_televised');
for (var j = 0; j<cfa.length; j++) {
cfa[j].volume = this.value / 100;
}
});
}
}
'onclick' in the html works for both touch and click event. Here's the example.
This mini javascript libary (1.3 KB) can do all these things
https://github.com/Norair1997/norjs/
nor.event(["#first"], ["touchstart", "click"], [doSomething, doSomething]);

Categories

Resources