I am trying to get my code onto a page where I am not allowed a lot of changes; the person hosting the page allows me a <script> tag & a <div>, that's it.
--- page ---
<head>
<script type='text/javascript' src='https://example.com/myJSfile.js'>
</head>
<body>
<div id='mydiv'></div>
</body>
------------
When the page loads, how do I turn mydiv into a button, when I can only customize myJSfile.js?
I cannot promise any typical libraries such as jQuery will be loaded for me,
the host's site does load CSS, but I don't know the structure of their styles. Maybe I will have to learn some of it at some point.
If my code needs to load jQuery, I first have to check that it isn't already loaded. How would you do that specifically that check?
If I need to load my own css then I will have to do so dynamically using myJSfile.js
myJSfile.js js file can be anything I need it to be. I have full control over it.
How would you go about this?
Please remember that, besides myJSfile.js, I am pretty much locked out of anything on the page except the script & div tags.
Use insertbefore() function to add the new element, then the remove() function, to remove the existing <div> element:
// Create a <li> node:
var newItemDocument.createElement("LI");
// Create a text node
var textnode = document.createTextNode("Water");
// Append the text to <li>:
newItemDocument.appendChild(textnode);
// Get the <ul> element to insert a new node:
var list = document.getElementById("myList");
// Insert <li> before the first child of <ul>:
list.insertBefore(newItemDocument, list.childNodes[0]);
While you've already accepted an answer, I thought I'd take a moment to try and offer an answer that might anticipate your future requirements of adding event-listeners to the element(s) you want to insert, and perhaps replacing multiple elements with a common element:
// using a named, rather than an anonymous, function in
// order that the same function can be reused multiple
// times:
function replaceElementWith(opts) {
// these are the default settings:
let settings = {
'replaceWhat': '#mydiv',
'replaceWith': 'button',
'text': 'this is a button',
'eventHandlers': null
};
// here we iterate over all the keys in the
// (user-supplied) opts Object to override
// the defaults; we do this by first retrieving
// the keys of the opts Object, which returns
// an Array of those keys, and iterating over
// that Array using Array.prototype.forEach():
Object.keys(opts).forEach(
// here we use an arrow function syntax, since
// don't need to work with an updated 'this'
// within the function.
// key : the current key of the Array of keys,
// here we update the current key of the
// settings Object (or add a new key to that
// Object) to the same value as held in the
// opts Object:
key => settings[key] = opts[key]
);
// in order to allow you to perform the same replacement
// on multiple elements, we use document.querySelectorAll()
// to retrieve all elements matching the supplied CSS
// selector, and then pass that Array-like NodeList to
// Array.from(), which converts an Array-like structure to
// an Array:
let targets = Array.from(
document.querySelectorAll(
// this is the CSS selector passed via the
// opts.replaceWhat value (here '#mydiv'):
settings.replaceWhat
)
),
// here we create a new element according to the value
// passed via the settings.replaceWith value:
newElem = document.createElement(
settings.replaceWith
),
// an empty, uninitialised variable to hold the cloned
// newElem element within the loop (later):
clone;
// we set the textContent of the created Element to be
// equal to the passed-in text, via the opts.text property:
newElem.textContent = settings.text;
// here we iterate over the Array of found elements that
// are to be replaced:
targets.forEach(
// again, using an Arrow function expression:
target => {
// here we clone the created-element, along with
// any child nodes:
clone = newElem.cloneNode(true);
// unfortunately Node.cloneNode() doesn't clone
// event-listeners, so we have to perform this
// step on every iteration, we first test to
// see if settings.eventHandlers is a truthy
// value (so not the default null):
if (settings.eventHandlers) {
// if there are eventHandlers, then we retrieve
// the keys of the settings.eventHandlers
// Object as above:
Object.keys(settings.eventHandlers).forEach(
// using Arrow function again;
// the keys of this object are the event-types
// we're listening for and the values are the
// functions to handle that event, so here
// we add the 'eventType' as the event,
// and the 'settings.eventHandlers[eventType]
// (which retrieves the function) as the handler:
eventType => clone.addEventListener(eventType, settings.eventHandlers[eventType])
)
}
// here we find the parentNode of the element to be
// replaced, and use Node.replaceChild() to add the
// new element ('clone') in place of the target element:
target.parentNode.replaceChild(clone, target)
})
}
// calling the function, passing in values:
replaceElementWith({
// CSS selector to identify the element(s) to be removed:
'replaceWhat': '#mydiv',
// the eventHandlers Object to define the
// created element's event-handling:
'eventHandlers': {
// in the form of:
// 'eventName' : eventHandler
'click': function(e) {
e.preventDefault();
document.location.hash = 'buttonClicked';
this.style.opacity = this.style.opacity == 0.5 ? 1 : 0.5;
},
'mouseenter': function(e) {
this.style.borderColor = '#f90';
},
'mouseleave': function(e) {
this.style.borderColor = 'limegreen';
}
}
});
function replaceElementWith(opts) {
let settings = {
'replaceWhat': '#mydiv',
'replaceWith': 'button',
'text': 'this is a button',
'eventHandlers': null
};
Object.keys(opts).forEach(
key => settings[key] = opts[key]
);
let targets = Array.from(document.querySelectorAll(settings.replaceWhat)),
newElem = document.createElement(settings.replaceWith),
clone;
newElem.textContent = settings.text;
targets.forEach(
target => {
clone = newElem.cloneNode(true, true);
if (settings.eventHandlers) {
Object.keys(settings.eventHandlers).forEach(
eventType => clone.addEventListener(eventType, settings.eventHandlers[eventType]);
)
}
target.parentNode.replaceChild(clone, target)
})
}
replaceElementWith({
'replaceWhat': '#mydiv',
'eventHandlers': {
'click': function(e) {
e.preventDefault();
document.location.hash = 'buttonClicked';
this.style.opacity = this.style.opacity == 0.5 ? 1 : 0.5;
},
'mouseenter': function(e) {
this.style.borderColor = '#f90';
},
'mouseleave': function(e) {
this.style.borderColor = 'limegreen';
}
}
});
div {
border: 2px solid #f90;
}
button {
border: 2px solid limegreen;
}
<div id='mydiv'></div>
JS Fiddle demo.
The below demo does exactly the same as above, but does so working with multiple elements to be replaced, and the only change made is to the function call:
replaceElementWith({
// changed this selector to select by class,
// rather than id (and added multiple elements
// to the HTML):
'replaceWhat': '.mydiv',
'eventHandlers': {
'click': function(e) {
e.preventDefault();
document.location.hash = 'buttonClicked';
this.style.opacity = this.style.opacity == 0.5 ? 1 : 0.5;
},
'mouseenter': function(e) {
this.style.borderColor = '#f90';
},
'mouseleave': function(e) {
this.style.borderColor = 'limegreen';
}
}
});
function replaceElementWith(opts) {
let settings = {
'replaceWhat': '#mydiv',
'replaceWith': 'button',
'text': 'this is a button',
'eventHandlers': null
};
Object.keys(opts).forEach(
key => settings[key] = opts[key]
);
let targets = Array.from(document.querySelectorAll(settings.replaceWhat)),
newElem = document.createElement(settings.replaceWith),
clone;
newElem.textContent = settings.text;
targets.forEach(
target => {
clone = newElem.cloneNode(true, true);
if (settings.eventHandlers) {
Object.keys(settings.eventHandlers).forEach(
eventType => clone.addEventListener(eventType, settings.eventHandlers[eventType]);
)
}
target.parentNode.replaceChild(clone, target)
})
}
replaceElementWith({
'replaceWhat': '.mydiv',
'eventHandlers': {
'click': function(e) {
e.preventDefault();
document.location.hash = 'buttonClicked';
this.style.opacity = this.style.opacity == 0.5 ? 1 : 0.5;
},
'mouseenter': function(e) {
this.style.borderColor = '#f90';
},
'mouseleave': function(e) {
this.style.borderColor = 'limegreen';
}
}
});
div {
border: 2px solid #f90;
}
button {
border: 2px solid limegreen;
}
<div id='mydiv'></div>
JS Fiddle demo.
References:
Array.from().
Array.prototype.forEach().
Arrow Functions.
document.querySelectorAll().
EventTarget.addEventListener().
Node.cloneNode().
Node.replaceChild().
Object.keys().
Related
What is the best way ( fastest / proper ) fashion to do event delegation in vanilla js?
For example if I had this in jQuery:
$('#main').on('click', '.focused', function(){
settingsPanel();
});
How can I translate that to vanilla js? Perhaps with .addEventListener()
The way I can think of doing this is:
document.getElementById('main').addEventListener('click', dothis);
function dothis(){
// now in jQuery
$(this).children().each(function(){
if($(this).is('.focused') settingsPanel();
});
}
But that seems inefficient especially if #main has many children.
Is this the proper way to do it then?
document.getElementById('main').addEventListener('click', doThis);
function doThis(event){
if($(event.target).is('.focused') || $(event.target).parents().is('.focused') settingsPanel();
}
Rather than mutating the built-in prototypes (which leads to fragile code and can often break things), just check if the clicked element has a .closest element which matches the selector you want. If it does, call the function you want to invoke. For example, to translate
$('#main').on('click', '.focused', function(){
settingsPanel();
});
out of jQuery, use:
document.querySelector('#main').addEventListener('click', (e) => {
if (e.target.closest('#main .focused')) {
settingsPanel();
}
});
Unless the inner selector may also exist as a parent element (which is probably pretty unusual), it's sufficient to pass the inner selector alone to .closest (eg, .closest('.focused')).
When using this sort of pattern, to keep things compact, I often put the main part of the code below an early return, eg:
document.querySelector('#main').addEventListener('click', (e) => {
if (!e.target.matches('.focused')) {
return;
}
// code of settingsPanel here, if it isn't too long
});
Live demo:
document.querySelector('#outer').addEventListener('click', (e) => {
if (!e.target.closest('#inner')) {
return;
}
console.log('vanilla');
});
$('#outer').on('click', '#inner', () => {
console.log('jQuery');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="outer">
<div id="inner">
inner
<div id="nested">
nested
</div>
</div>
</div>
I've come up with a simple solution which seems to work rather well (legacy IE support notwithstanding). Here we extend the EventTarget's prototype to provide a delegateEventListener method which works using the following syntax:
EventTarget.delegateEventListener(string event, string toFind, function fn)
I've created a fairly complex fiddle to demonstrate it in action, where we delegate all events for the green elements. Stopping propagation continues to work and you can access what should be the event.currentTarget through this (as with jQuery).
Here is the solution in full:
(function(document, EventTarget) {
var elementProto = window.Element.prototype,
matchesFn = elementProto.matches;
/* Check various vendor-prefixed versions of Element.matches */
if(!matchesFn) {
['webkit', 'ms', 'moz'].some(function(prefix) {
var prefixedFn = prefix + 'MatchesSelector';
if(elementProto.hasOwnProperty(prefixedFn)) {
matchesFn = elementProto[prefixedFn];
return true;
}
});
}
/* Traverse DOM from event target up to parent, searching for selector */
function passedThrough(event, selector, stopAt) {
var currentNode = event.target;
while(true) {
if(matchesFn.call(currentNode, selector)) {
return currentNode;
}
else if(currentNode != stopAt && currentNode != document.body) {
currentNode = currentNode.parentNode;
}
else {
return false;
}
}
}
/* Extend the EventTarget prototype to add a delegateEventListener() event */
EventTarget.prototype.delegateEventListener = function(eName, toFind, fn) {
this.addEventListener(eName, function(event) {
var found = passedThrough(event, toFind, event.currentTarget);
if(found) {
// Execute the callback with the context set to the found element
// jQuery goes way further, it even has it's own event object
fn.call(found, event);
}
});
};
}(window.document, window.EventTarget || window.Element));
I have a similar solution to achieve event delegation.
It makes use of the Array-functions slice, reverse, filter and forEach.
slice converts the NodeList from the query into an array, which must be done before it is allowed to reverse the list.
reverse inverts the array (making the final traversion start as close to the event-target as possible.
filter checks which elements contain event.target.
forEach calls the provided handler for each element from the filtered result as long as the handler does not return false.
The function returns the created delegate function, which makes it possible to remove the listener later.
Note that the native event.stopPropagation() does not stop the traversing through validElements, because the bubbling phase has already traversed up to the delegating element.
function delegateEventListener(element, eventType, childSelector, handler) {
function delegate(event){
var bubble;
var validElements=[].slice.call(this.querySelectorAll(childSelector)).reverse().filter(function(matchedElement){
return matchedElement.contains(event.target);
});
validElements.forEach(function(validElement){
if(bubble===undefined||bubble!==false)bubble=handler.call(validElement,event);
});
}
element.addEventListener(eventType,delegate);
return delegate;
}
Although it is not recommended to extend native prototypes, this function can be added to the prototype for EventTarget (or Node in IE). When doing so, replace element with this within the function and remove the corresponding parameter ( EventTarget.prototype.delegateEventListener = function(eventType, childSelector, handler){...} ).
Delegated events
Event delegation is used when in need to execute a function when existent or dynamic elements (added to the DOM in the future) receive an Event.
The strategy is to assign to event listener to a known static parent and follow this rules:
use evt.target.closest(".dynamic") to get the desired dynamic child
use evt.currentTarget to get the #staticParent parent delegator
use evt.target to get the exact clicked Element (WARNING! This might also be a descendant element, not necessarily the .dynamic one)
Snippet sample:
document.querySelector("#staticParent").addEventListener("click", (evt) => {
const elChild = evt.target.closest(".dynamic");
if ( !elChild ) return; // do nothing.
console.log("Do something with elChild Element here");
});
Full example with dynamic elements:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Delegated events
el("#staticParent").addEventListener("click", (evt) => {
const elDelegator = evt.currentTarget;
const elChild = evt.target.closest(".dynamicChild");
const elTarget = evt.target;
console.clear();
console.log(`Clicked:
currentTarget: ${elDelegator.tagName}
target.closest: ${elChild?.tagName}
target: ${elTarget.tagName}`)
if (!elChild) return; // Do nothing.
// Else, .dynamicChild is clicked! Do something:
console.log("Yey! .dynamicChild is clicked!")
});
// Insert child element dynamically
setTimeout(() => {
el("#staticParent").append(elNew("article", {
className: "dynamicChild",
innerHTML: `Click here!!! I'm added dynamically! <span>Some child icon</span>`
}))
}, 1500);
#staticParent {
border: 1px solid #aaa;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dynamicChild {
background: #eee;
padding: 1rem;
}
.dynamicChild span {
background: gold;
padding: 0.5rem;
}
<section id="staticParent">Click here or...</section>
Direct events
Alternatively, you could attach a click handler directly on the child - upon creation:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Create new comment with Direct events:
const newComment = (text) => elNew("article", {
className: "dynamicChild",
title: "Click me!",
textContent: text,
onclick() {
console.log(`Clicked: ${this.textContent}`);
},
});
//
el("#add").addEventListener("click", () => {
el("#staticParent").append(newComment(Date.now()))
});
#staticParent {
border: 1px solid #aaa;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dynamicChild {
background: #eee;
padding: 0.5rem;
}
<section id="staticParent"></section>
<button type="button" id="add">Add new</button>
Resources:
Event.target
Element.closest()
What is the best way ( fastest / proper ) fashion to do event delegation in vanilla js?
For example if I had this in jQuery:
$('#main').on('click', '.focused', function(){
settingsPanel();
});
How can I translate that to vanilla js? Perhaps with .addEventListener()
The way I can think of doing this is:
document.getElementById('main').addEventListener('click', dothis);
function dothis(){
// now in jQuery
$(this).children().each(function(){
if($(this).is('.focused') settingsPanel();
});
}
But that seems inefficient especially if #main has many children.
Is this the proper way to do it then?
document.getElementById('main').addEventListener('click', doThis);
function doThis(event){
if($(event.target).is('.focused') || $(event.target).parents().is('.focused') settingsPanel();
}
Rather than mutating the built-in prototypes (which leads to fragile code and can often break things), just check if the clicked element has a .closest element which matches the selector you want. If it does, call the function you want to invoke. For example, to translate
$('#main').on('click', '.focused', function(){
settingsPanel();
});
out of jQuery, use:
document.querySelector('#main').addEventListener('click', (e) => {
if (e.target.closest('#main .focused')) {
settingsPanel();
}
});
Unless the inner selector may also exist as a parent element (which is probably pretty unusual), it's sufficient to pass the inner selector alone to .closest (eg, .closest('.focused')).
When using this sort of pattern, to keep things compact, I often put the main part of the code below an early return, eg:
document.querySelector('#main').addEventListener('click', (e) => {
if (!e.target.matches('.focused')) {
return;
}
// code of settingsPanel here, if it isn't too long
});
Live demo:
document.querySelector('#outer').addEventListener('click', (e) => {
if (!e.target.closest('#inner')) {
return;
}
console.log('vanilla');
});
$('#outer').on('click', '#inner', () => {
console.log('jQuery');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="outer">
<div id="inner">
inner
<div id="nested">
nested
</div>
</div>
</div>
I've come up with a simple solution which seems to work rather well (legacy IE support notwithstanding). Here we extend the EventTarget's prototype to provide a delegateEventListener method which works using the following syntax:
EventTarget.delegateEventListener(string event, string toFind, function fn)
I've created a fairly complex fiddle to demonstrate it in action, where we delegate all events for the green elements. Stopping propagation continues to work and you can access what should be the event.currentTarget through this (as with jQuery).
Here is the solution in full:
(function(document, EventTarget) {
var elementProto = window.Element.prototype,
matchesFn = elementProto.matches;
/* Check various vendor-prefixed versions of Element.matches */
if(!matchesFn) {
['webkit', 'ms', 'moz'].some(function(prefix) {
var prefixedFn = prefix + 'MatchesSelector';
if(elementProto.hasOwnProperty(prefixedFn)) {
matchesFn = elementProto[prefixedFn];
return true;
}
});
}
/* Traverse DOM from event target up to parent, searching for selector */
function passedThrough(event, selector, stopAt) {
var currentNode = event.target;
while(true) {
if(matchesFn.call(currentNode, selector)) {
return currentNode;
}
else if(currentNode != stopAt && currentNode != document.body) {
currentNode = currentNode.parentNode;
}
else {
return false;
}
}
}
/* Extend the EventTarget prototype to add a delegateEventListener() event */
EventTarget.prototype.delegateEventListener = function(eName, toFind, fn) {
this.addEventListener(eName, function(event) {
var found = passedThrough(event, toFind, event.currentTarget);
if(found) {
// Execute the callback with the context set to the found element
// jQuery goes way further, it even has it's own event object
fn.call(found, event);
}
});
};
}(window.document, window.EventTarget || window.Element));
I have a similar solution to achieve event delegation.
It makes use of the Array-functions slice, reverse, filter and forEach.
slice converts the NodeList from the query into an array, which must be done before it is allowed to reverse the list.
reverse inverts the array (making the final traversion start as close to the event-target as possible.
filter checks which elements contain event.target.
forEach calls the provided handler for each element from the filtered result as long as the handler does not return false.
The function returns the created delegate function, which makes it possible to remove the listener later.
Note that the native event.stopPropagation() does not stop the traversing through validElements, because the bubbling phase has already traversed up to the delegating element.
function delegateEventListener(element, eventType, childSelector, handler) {
function delegate(event){
var bubble;
var validElements=[].slice.call(this.querySelectorAll(childSelector)).reverse().filter(function(matchedElement){
return matchedElement.contains(event.target);
});
validElements.forEach(function(validElement){
if(bubble===undefined||bubble!==false)bubble=handler.call(validElement,event);
});
}
element.addEventListener(eventType,delegate);
return delegate;
}
Although it is not recommended to extend native prototypes, this function can be added to the prototype for EventTarget (or Node in IE). When doing so, replace element with this within the function and remove the corresponding parameter ( EventTarget.prototype.delegateEventListener = function(eventType, childSelector, handler){...} ).
Delegated events
Event delegation is used when in need to execute a function when existent or dynamic elements (added to the DOM in the future) receive an Event.
The strategy is to assign to event listener to a known static parent and follow this rules:
use evt.target.closest(".dynamic") to get the desired dynamic child
use evt.currentTarget to get the #staticParent parent delegator
use evt.target to get the exact clicked Element (WARNING! This might also be a descendant element, not necessarily the .dynamic one)
Snippet sample:
document.querySelector("#staticParent").addEventListener("click", (evt) => {
const elChild = evt.target.closest(".dynamic");
if ( !elChild ) return; // do nothing.
console.log("Do something with elChild Element here");
});
Full example with dynamic elements:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Delegated events
el("#staticParent").addEventListener("click", (evt) => {
const elDelegator = evt.currentTarget;
const elChild = evt.target.closest(".dynamicChild");
const elTarget = evt.target;
console.clear();
console.log(`Clicked:
currentTarget: ${elDelegator.tagName}
target.closest: ${elChild?.tagName}
target: ${elTarget.tagName}`)
if (!elChild) return; // Do nothing.
// Else, .dynamicChild is clicked! Do something:
console.log("Yey! .dynamicChild is clicked!")
});
// Insert child element dynamically
setTimeout(() => {
el("#staticParent").append(elNew("article", {
className: "dynamicChild",
innerHTML: `Click here!!! I'm added dynamically! <span>Some child icon</span>`
}))
}, 1500);
#staticParent {
border: 1px solid #aaa;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dynamicChild {
background: #eee;
padding: 1rem;
}
.dynamicChild span {
background: gold;
padding: 0.5rem;
}
<section id="staticParent">Click here or...</section>
Direct events
Alternatively, you could attach a click handler directly on the child - upon creation:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Create new comment with Direct events:
const newComment = (text) => elNew("article", {
className: "dynamicChild",
title: "Click me!",
textContent: text,
onclick() {
console.log(`Clicked: ${this.textContent}`);
},
});
//
el("#add").addEventListener("click", () => {
el("#staticParent").append(newComment(Date.now()))
});
#staticParent {
border: 1px solid #aaa;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dynamicChild {
background: #eee;
padding: 0.5rem;
}
<section id="staticParent"></section>
<button type="button" id="add">Add new</button>
Resources:
Event.target
Element.closest()
What is the best way ( fastest / proper ) fashion to do event delegation in vanilla js?
For example if I had this in jQuery:
$('#main').on('click', '.focused', function(){
settingsPanel();
});
How can I translate that to vanilla js? Perhaps with .addEventListener()
The way I can think of doing this is:
document.getElementById('main').addEventListener('click', dothis);
function dothis(){
// now in jQuery
$(this).children().each(function(){
if($(this).is('.focused') settingsPanel();
});
}
But that seems inefficient especially if #main has many children.
Is this the proper way to do it then?
document.getElementById('main').addEventListener('click', doThis);
function doThis(event){
if($(event.target).is('.focused') || $(event.target).parents().is('.focused') settingsPanel();
}
Rather than mutating the built-in prototypes (which leads to fragile code and can often break things), just check if the clicked element has a .closest element which matches the selector you want. If it does, call the function you want to invoke. For example, to translate
$('#main').on('click', '.focused', function(){
settingsPanel();
});
out of jQuery, use:
document.querySelector('#main').addEventListener('click', (e) => {
if (e.target.closest('#main .focused')) {
settingsPanel();
}
});
Unless the inner selector may also exist as a parent element (which is probably pretty unusual), it's sufficient to pass the inner selector alone to .closest (eg, .closest('.focused')).
When using this sort of pattern, to keep things compact, I often put the main part of the code below an early return, eg:
document.querySelector('#main').addEventListener('click', (e) => {
if (!e.target.matches('.focused')) {
return;
}
// code of settingsPanel here, if it isn't too long
});
Live demo:
document.querySelector('#outer').addEventListener('click', (e) => {
if (!e.target.closest('#inner')) {
return;
}
console.log('vanilla');
});
$('#outer').on('click', '#inner', () => {
console.log('jQuery');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="outer">
<div id="inner">
inner
<div id="nested">
nested
</div>
</div>
</div>
I've come up with a simple solution which seems to work rather well (legacy IE support notwithstanding). Here we extend the EventTarget's prototype to provide a delegateEventListener method which works using the following syntax:
EventTarget.delegateEventListener(string event, string toFind, function fn)
I've created a fairly complex fiddle to demonstrate it in action, where we delegate all events for the green elements. Stopping propagation continues to work and you can access what should be the event.currentTarget through this (as with jQuery).
Here is the solution in full:
(function(document, EventTarget) {
var elementProto = window.Element.prototype,
matchesFn = elementProto.matches;
/* Check various vendor-prefixed versions of Element.matches */
if(!matchesFn) {
['webkit', 'ms', 'moz'].some(function(prefix) {
var prefixedFn = prefix + 'MatchesSelector';
if(elementProto.hasOwnProperty(prefixedFn)) {
matchesFn = elementProto[prefixedFn];
return true;
}
});
}
/* Traverse DOM from event target up to parent, searching for selector */
function passedThrough(event, selector, stopAt) {
var currentNode = event.target;
while(true) {
if(matchesFn.call(currentNode, selector)) {
return currentNode;
}
else if(currentNode != stopAt && currentNode != document.body) {
currentNode = currentNode.parentNode;
}
else {
return false;
}
}
}
/* Extend the EventTarget prototype to add a delegateEventListener() event */
EventTarget.prototype.delegateEventListener = function(eName, toFind, fn) {
this.addEventListener(eName, function(event) {
var found = passedThrough(event, toFind, event.currentTarget);
if(found) {
// Execute the callback with the context set to the found element
// jQuery goes way further, it even has it's own event object
fn.call(found, event);
}
});
};
}(window.document, window.EventTarget || window.Element));
I have a similar solution to achieve event delegation.
It makes use of the Array-functions slice, reverse, filter and forEach.
slice converts the NodeList from the query into an array, which must be done before it is allowed to reverse the list.
reverse inverts the array (making the final traversion start as close to the event-target as possible.
filter checks which elements contain event.target.
forEach calls the provided handler for each element from the filtered result as long as the handler does not return false.
The function returns the created delegate function, which makes it possible to remove the listener later.
Note that the native event.stopPropagation() does not stop the traversing through validElements, because the bubbling phase has already traversed up to the delegating element.
function delegateEventListener(element, eventType, childSelector, handler) {
function delegate(event){
var bubble;
var validElements=[].slice.call(this.querySelectorAll(childSelector)).reverse().filter(function(matchedElement){
return matchedElement.contains(event.target);
});
validElements.forEach(function(validElement){
if(bubble===undefined||bubble!==false)bubble=handler.call(validElement,event);
});
}
element.addEventListener(eventType,delegate);
return delegate;
}
Although it is not recommended to extend native prototypes, this function can be added to the prototype for EventTarget (or Node in IE). When doing so, replace element with this within the function and remove the corresponding parameter ( EventTarget.prototype.delegateEventListener = function(eventType, childSelector, handler){...} ).
Delegated events
Event delegation is used when in need to execute a function when existent or dynamic elements (added to the DOM in the future) receive an Event.
The strategy is to assign to event listener to a known static parent and follow this rules:
use evt.target.closest(".dynamic") to get the desired dynamic child
use evt.currentTarget to get the #staticParent parent delegator
use evt.target to get the exact clicked Element (WARNING! This might also be a descendant element, not necessarily the .dynamic one)
Snippet sample:
document.querySelector("#staticParent").addEventListener("click", (evt) => {
const elChild = evt.target.closest(".dynamic");
if ( !elChild ) return; // do nothing.
console.log("Do something with elChild Element here");
});
Full example with dynamic elements:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Delegated events
el("#staticParent").addEventListener("click", (evt) => {
const elDelegator = evt.currentTarget;
const elChild = evt.target.closest(".dynamicChild");
const elTarget = evt.target;
console.clear();
console.log(`Clicked:
currentTarget: ${elDelegator.tagName}
target.closest: ${elChild?.tagName}
target: ${elTarget.tagName}`)
if (!elChild) return; // Do nothing.
// Else, .dynamicChild is clicked! Do something:
console.log("Yey! .dynamicChild is clicked!")
});
// Insert child element dynamically
setTimeout(() => {
el("#staticParent").append(elNew("article", {
className: "dynamicChild",
innerHTML: `Click here!!! I'm added dynamically! <span>Some child icon</span>`
}))
}, 1500);
#staticParent {
border: 1px solid #aaa;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dynamicChild {
background: #eee;
padding: 1rem;
}
.dynamicChild span {
background: gold;
padding: 0.5rem;
}
<section id="staticParent">Click here or...</section>
Direct events
Alternatively, you could attach a click handler directly on the child - upon creation:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Create new comment with Direct events:
const newComment = (text) => elNew("article", {
className: "dynamicChild",
title: "Click me!",
textContent: text,
onclick() {
console.log(`Clicked: ${this.textContent}`);
},
});
//
el("#add").addEventListener("click", () => {
el("#staticParent").append(newComment(Date.now()))
});
#staticParent {
border: 1px solid #aaa;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dynamicChild {
background: #eee;
padding: 0.5rem;
}
<section id="staticParent"></section>
<button type="button" id="add">Add new</button>
Resources:
Event.target
Element.closest()
I am writing my first jQuery plugin which is a tree browser. It shall first show the top level elements and on click go deeper and show (depending on level) the children in a different way.
I got this up and running already. But now I want to implement a "back" functionality and for this I need to store an array of clicked elements for each instance of the tree browser (if multiple are on the page).
I know that I can put instance private variables with "this." in the plugin.
But if I assign an event handler of the onClick on a topic, how do I get this instance private variable? $(this) is referencing the clicked element at this moment.
Could please anyone give me an advise or a link to a tutorial how to get this done?
I only found tutorial for instance specific variables without event handlers involved.
Any help is appreciated.
Thanks in advance.
UPDATE: I cleaned out the huge code generation and kept the logical structure. This is my code:
(function ($) {
$.fn.myTreeBrowser = function (options) {
clickedElements = [];
var defaults = {
textColor: "#000",
backgroundColor: "#fff",
fontSize: "1em",
titleAttribute: "Title",
idAttribute: "Id",
parentIdAttribute: "ParentId",
levelAttribute: "Level",
treeData: {}
};
var opts = $.extend({}, $.fn.myTreeBrowser.defaults, options);
function getTreeData(id) {
if (opts.data) {
$.ajax(opts.data, { async: false, data: { Id: id } }).success(function (resultdata) {
opts.treeData = resultdata;
});
}
}
function onClick() {
var id = $(this).attr('data-id');
var parentContainer = getParentContainer($(this));
handleOnClick(parentContainer, id);
}
function handleOnClick(parentContainer, id) {
if (opts.onTopicClicked) {
opts.onTopicClicked(id);
}
clickedElements.push(id);
if (id) {
var clickedElement = $.grep(opts.treeData, function (n, i) { return n[opts.idAttribute] === id })[0];
switch (clickedElement[opts.levelAttribute]) {
case 1:
renderLevel2(parentContainer, clickedElement);
break;
case 3:
renderLevel3(parentContainer, clickedElement);
break;
default:
debug('invalid level element clicked');
}
} else {
renderTopLevel(parentContainer);
}
}
function getParentContainer(elem) {
return $(elem).parents('div.myBrowserContainer').parents()[0];
}
function onBackButtonClick() {
clickedElements.pop(); // remove actual element to get the one before
var lastClickedId = clickedElements.pop();
var parentContainer = getParentContainer($(this));
handleOnClick(parentContainer, lastClickedId);
}
function renderLevel2(parentContainer, selectedElement) {
$(parentContainer).html('');
var browsercontainer = $('<div>').addClass('myBrowserContainer').appendTo(parentContainer);
//... rendering the div ...
// for example like this with a onClick handler
var div = $('<div>').attr('data-id', element[opts.idAttribute]).addClass('fct-bs-col-md-4 pexSubtopic').on('click', onClick).appendTo(subtopicList);
// ... rendering the tree
var backButton = $('<button>').addClass('btn btn-default').text('Back').appendTo(browsercontainer);
backButton.on('click', onBackButtonClick);
}
function renderLevel3(parentContainer, selectedElement) {
$(parentContainer).html('');
var browsercontainer = $('<div>').addClass('myBrowserContainer').appendTo(parentContainer);
//... rendering the div ...
// for example like this with a onClick handler
var div = $('<div>').attr('data-id', element[opts.idAttribute]).addClass('fct-bs-col-md-4 pexSubtopic').on('click', onClick).appendTo(subtopicList);
// ... rendering the tree
var backButton = $('<button>').addClass('btn btn-default').text('Back').appendTo(browsercontainer);
backButton.on('click', onBackButtonClick);
}
function renderTopLevel(parentContainer) {
parentContainer.html('');
var browsercontainer = $('<div>').addClass('fct-page-pa fct-bs-container-fluid pexPAs myBrowserContainer').appendTo(parentContainer);
// rendering the top level display
}
getTreeData();
//top level rendering! Lower levels are rendered in event handlers.
$(this).each(function () {
renderTopLevel($(this));
});
return this;
};
// Private function for debugging.
function debug(debugText) {
if (window.console && window.console.log) {
window.console.log(debugText);
}
};
}(jQuery));
Just use one more class variable and pass this to it. Usually I call it self. So var self = this; in constructor of your plugin Class and you are good to go.
Object oriented way:
function YourPlugin(){
var self = this;
}
YourPlugin.prototype = {
constructor: YourPlugin,
clickHandler: function(){
// here the self works
}
}
Check this Fiddle
Or simple way of passing data to eventHandler:
$( "#foo" ).bind( "click", {
self: this
}, function( event ) {
alert( event.data.self);
});
You could use the jQuery proxy function:
$(yourElement).bind("click", $.proxy(this.yourFunction, this));
You can then use this in yourFunction as the this in your plugin.
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;
}