Anchor elements (<a>) are created when the user interacts with a web component. The problem is, that I cannot get the anchor element returned from the "outside" of the web component when an anchor is clicked.
I add an event listener to document listening for click events. When an element somewhere in the DOM is clicked I expect the e.target to be the clicked element.
In the case of a click somewhere inside the web component the custom element (<fancy-list></fancy-list>) will be returned - not the clicked element.
When the mode of the shadow DOM is set to open the DOM should be accessible.
class Fancylist extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('div');
wrapper.innerHTML = `<ul></ul><button>Add item</button>`;
shadow.appendChild(wrapper);
this.on_root_click = this.on_root_click.bind(this);
}
connectedCallback() {
this.ul_elm = this.shadowRoot.querySelector('ul');
this.shadowRoot.addEventListener('click', this.on_root_click, false);
}
on_root_click(e){
switch(e.target.nodeName){
case 'BUTTON':
this.ul_elm.innerHTML += '<li>List item</li>';
break;
case 'A':
e.preventDefault();
console.log('You clicked a link!');
break;
}
}
}
customElements.define('fancy-list', Fancylist);
<!DOCTYPE html>
<html>
<head>
<title>List</title>
<meta charset="utf-8" />
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', e => {
document.body.addEventListener('click', e => {
//console.log(e.composedPath());
console.log(e.target); // why is this not returning an anchor element when an anchor is clickend inside the <fancy-list>?
}, false);
}, false);
</script>
</head>
<body>
<h1>List</h1>
<fancy-list></fancy-list>
</body>
</html>
The purpose of the Shadow DOM is precisely to mask the HTML content the Shadow DOM from the containter point of view.
That's also why inner events are retargeted in order to expose the Shadow DOM host.
However, you can still get the real target by getting the first item of the Event.path Array property.
event.path[0]
NB: of course it will work only with open Shadow DOM.
class Fancylist extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('div');
wrapper.innerHTML = `<ul></ul><button>Add item</button>`;
shadow.appendChild(wrapper);
this.on_root_click = this.on_root_click.bind(this);
}
connectedCallback() {
this.ul_elm = this.shadowRoot.querySelector('ul');
this.shadowRoot.addEventListener('click', this.on_root_click, false);
}
on_root_click(e){
switch(e.target.nodeName){
case 'BUTTON':
this.ul_elm.innerHTML += '<li>List item</li>';
break;
case 'A':
e.preventDefault();
break;
}
}
}
customElements.define('fancy-list', Fancylist);
<!DOCTYPE html>
<html>
<head>
<title>List</title>
<meta charset="utf-8" />
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', e => {
document.body.addEventListener('click', e => {
console.log(e.path[0]);
}, false);
}, false);
</script>
</head>
<body>
<h1>List</h1>
<fancy-list></fancy-list>
</body>
</html>
Update 2021
As commented now you should use event.composedPath().
Related
I am trying to attach a DOM element with a callback in JavaScript. Basically, the following is what I want:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="http://static.robotwebtools.org/roslibjs/current/roslib.min.js"></script>
<script src="http://static.robotwebtools.org/EventEmitter2/current/eventemitter2.min.js"></script>
<script>
var ros = new ROSLIB.Ros({ url: "ws://localhost:9090" });
var listener = new ROSLIB.Topic({
ros: ros,
name: "/listener",
messageType: "std_msgs/String",
element: document.getElementById("hi") // this is added to show my need
});
listener.subscribe(function (event) {
// do crazy things here. let say replace the text
event.element.innerHTML = event.message.data; // this is added to show my need
});
</script>
</head>
<body>
<div id="hi"> hello world </div>
</body>
</html>
I want to use the DOM element inside the callback, which was declared while initializing the listener. I found a similar question here but could not make it work in this case.
function Subscriber(element, topic) {
var listener = new ROSLIB.Topic({
ros: ros,
name: topic,
messageType: "std_msgs/String"
});
listener.addEventListener("subscribe", this, false);
this.listener = listener;
this.element = element;
}
Subscriber.prototype.handleEvent = function (event) {
switch (event.type) {
case "subscribe":
this.element.dispatchEvent(new CustomEvent("subscribe", { details: { msg: message, e: element } }));
}
};
var element = document.getElementById("hi");
var topic = "/listener";
var subscriber = new Subscriber(element, topic);
subscriber.handleEvent(function (event) {
// do crazy things here. let say replace the text
event.details.e.innerHTML = event.details.msg.data;
});
As listener is a custom object, addEventListener is unavailable. Therefore, the following error is reported at the console:
listener.addEventListener("subscribe", this, false);
Uncaught TypeError: listener.addEventListener is not a function
How to modify the subscribe callback to bind a DOM element with it in JavaScript?
I want to implement event system on pure js object that similar to the DOM events, which can be dispatched and bubbling from child to parent object.
Can I use existing interfaces like EventTarget or Event to do this? What is the correct way to achieve this?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview Pro</title>
</head>
<body>
<div id="app"></div>
<script>
class body extends EventTarget {
constructor(name, parent) {
super();
this.name = name;
this.type = 'body';
this.parentNode = parent;
}
};
class hand extends body {
constructor(name, parent) {
super(name, parent);
this.type = 'hand';
}
}
class finger extends body {
constructor(name, parent) {
super(name, parent);
this.type = 'finger';
}
}
var human = new body('stanley');
var left_hand = new hand('left_hand', human);
var thumb = new finger('thumb', left_hand);
left_hand.addEventListener('touch', function (e) {
// bubbling
console.log(e)
});
thumb.addEventListener('touch', function (e) {
// target
console.log(e)
});
thumb.dispatchEvent(new Event('touch', {bubbles: true}));
</script>
</body>
</html>
After a little bit of searching, I found this answer. It still use DOM tree to make sure Event bubble throuth child to parent. Can this be done without using DOM tree?
I'm working on Cole Steele's Web Developer Bootcamp on Udemy #264. Event Bubbling. I'm trying to build a function which will allow one or more objects to be passed in and to execute the same action (toggle the classList 'hide' so that the 'Click Here to Hide' button goes away and the 'Click Here to Show' button appears) on each of them.
I am able to get this working by calling the function separately, such as
const container = document.querySelector('#container')
const show = document.querySelector('#show')
function hideOneElement(ele){
ele.classList.toggle('hide');
}
show.addEventListener('click', function () {
hideOneElement(container);
hideOneElement(show);
})
However, when I try to call the function with both container (the div that says 'Click here to hide') and show at the same time, I can't get it to work. I tried writing the hide function as a for...of, such as
function hideElements(elements){
for (const ele of elements) {
ele.classList.toggle('hide')
}
}
let stuffToHide = [container,show]
hideElements(stuffToHide)
However this does not seem to work. I also tried passing in as two separate arguments but that also doesn't seem to work:
function hideElements(ele1, ele2) {
ele1.classList.toggle('hide');
ele2.classList.toggle('hide')
}
hideElements(container, show);
At this point, I'm not sure where to go, and my Google-jitsu is not finding anything useful. This isn't really part of the course exercise, but seems like I'm fundamentally missing something about calling functions.
Full code and HTML below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.hide {
display: none;
}
</style>
</head>
<body>
<section onclick="alert('sectopm clicked')">
<p onclick="alert('paragraph clicked')">I am a paragraph
<button onclick="alert('button clicked')">Click</button>
</p>
</section>
<div id="container">
Click to Hide
<button id="colorbtn">Change Color</button>
</div>
<div id="show">
Click here to Show
</div>
<script src="app.js"></script>
</body>
</html>
const makeRandColor = () => {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return (`rgb(${r},${g},${b})`);
}
const button = document.querySelector('#colorbtn')
const container = document.querySelector('#container')
const show = document.querySelector('#show')
function hideElements(ele1, ele2) {
// for (const ele of elements) {
// ele.classList.toggle('hide')
// }
// elements.classList.toggle('hide')
ele1.classList.toggle('hide');
ele2.classList.toggle('hide')
}
function hideOneElement(ele){
ele.classList.toggle('hide');
}
//hideElements(show); //run function once to toggle on the 'hide' class so that this is not shown by default.
hideElements(show)
button.addEventListener('click', function (e) {
container.style.backgroundColor = makeRandColor();
e.stopPropagation();
})
container.addEventListener('click', function () {
hideElements(container, show);
//hideElements(container, show); //hide the 'click here to hide' stuff
})
show.addEventListener('click', function () {
hideElements(container);
hideElements(show);
})
show.addEventListener('click', function () {
hideOneElement(container);
hideOneElement(show);
})
"I'm trying to build a function which will allow one or more objects to be passed in" so you need to be able to call hideElements as either hideElements(ele1) or hideElements(ele1, ele2)?
If that's the case you'll need to pass an array vs individual variables. You were on the right track with the commented out code here:
// for (const ele of elements) {
// ele.classList.toggle('hide')
// }
// elements.classList.toggle('hide')
But you weren't trying to pass them as an array and you can't loop through an individual element (there's only one). If you wrap your variables in an array using [] that should fix your issues.
Example:
function hideElements(elements) {
for (const ele of elements) {
ele.classList.toggle('hide')
}
}
hideElements([show]); //run function once to toggle on the 'hide' class so that this is not shown by default.
container.addEventListener('click', function() {
hideElements([container, show]);
})
show.addEventListener('click', function() {
hideElements([container, show]);
})
Working Example:
const makeRandColor = () => {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return (`rgb(${r},${g},${b})`);
}
const button = document.querySelector('#colorbtn')
const container = document.querySelector('#container')
const show = document.querySelector('#show')
function hideElements(elements) {
for (const ele of elements) {
ele.classList.toggle('hide')
}
}
function hideOneElement(ele) {
ele.classList.toggle('hide');
}
hideElements([show]); //run function once to toggle on the 'hide' class so that this is not shown by default.
button.addEventListener('click', function(e) {
container.style.backgroundColor = makeRandColor();
e.stopPropagation();
})
container.addEventListener('click', function() {
hideElements([container, show]);
})
show.addEventListener('click', function() {
hideElements([container, show]);
})
.hide {
display: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<section onclick="alert('sectopm clicked')">
<p onclick="alert('paragraph clicked')">I am a paragraph
<button onclick="alert('button clicked')">Click</button>
</p>
</section>
<div id="container">
Click to Hide
<button id="colorbtn">Change Color</button>
</div>
<div id="show">
Click here to Show
</div>
<script src="app.js"></script>
</body>
</html>
Some blocks will not work for sure - as you call hideElements with a single argument - the second arg will be undefined and there's no classList on undefined of course (causes error).
And also it's very confusing because you add event listener on the show element twice..
Copied from your post and added comments:
//hideElements(show); //run function once to toggle on the 'hide' class so that this is not shown by default.
hideElements(show) // - this will error like I said above as elem2 will be undefined..
button.addEventListener('click', function (e) {
container.style.backgroundColor = makeRandColor();
e.stopPropagation();
})
container.addEventListener('click', function () {
hideElements(container, show);
//hideElements(container, show); //hide the 'click here to hide' stuff
})
//below you make same event listener twice which is very confusing :
show.addEventListener('click', function () {
hideElements(container);
hideElements(show);
})
show.addEventListener('click', function () {
hideOneElement(container);
hideOneElement(show);
})
Say, I need to call a function in a custom element, such that when a slider moves in that element the text gets updated(only for that element). The main.js file,
class OninputClassDemo extends HTMLElement {
constructor(){
super();
const shadow = this.attachShadow({mode:'open'});
this.parent = document.createElement('div');
this.slider = document.createElement('input');
this.slider.setAttribute('type','range');
this.slider.setAttribute('min','0');
this.slider.setAttribute('max','99');
this.slider.setAttribute('value','0')
this.text = document.createElement('input');
this.text.setAttribute('type','text');
this.text.setAttribute('value','');
this.parent.appendChild(this.slider);
this.parent.appendChild(this.text);
shadow.appendChild(this.parent);
this.slider.setAttribute('oninput','OninputClassDemo.changeValue()');
}
changeValue = function(){
this.text.setAttribute('value',this.slider.getAttribute('value'));
}
}
window.customElements.define('demo-element',OninputClassDemo);
On the template side,
<!DOCTYPE html>
<html lang="en">
<head>
<script src="main.js"></script>
</head>
<body>
<demo-element></demo-element>
</body>
</html>
The error I am getting is,
OninputClassDemo.changeValue is not a function
at HTMLInputElement.oninput
I do not know how to reference the method for that particular object in this.slider.setAttribute('oninput','WhatShouldIPutHere'),so that the text box for only that object gets changed.
You should be binding event listeners with addEventListener. You should be binding to the method with this, not the class name. Set the value property, do not set an attribute.
class OninputClassDemo extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({
mode: 'open'
});
this.parent = document.createElement('div');
this.slider = document.createElement('input');
this.slider.setAttribute('type', 'range');
this.slider.setAttribute('min', '0');
this.slider.setAttribute('max', '99');
this.slider.setAttribute('value', '0')
this.text = document.createElement('input');
this.text.setAttribute('type', 'text');
this.text.setAttribute('value', '');
this.parent.appendChild(this.slider);
this.parent.appendChild(this.text);
shadow.appendChild(this.parent);
this.slider.addEventListener('input', (event) => this.changeValue());
}
changeValue() {
this.text.value = this.slider.value;
}
}
window.customElements.define('demo-element', OninputClassDemo);
<demo-element></demo-element>
You should write
changeValue() {
this.text.setAttribute('value',this.slider.getAttribute('value'));
}
instead of
changeValue = function(){
this.text.setAttribute('value',this.slider.getAttribute('value'));
}
I made JS script:
var zzz;
zzz = {
fff: function (Id) {
alert("You did it! Id="+Id);
},
main: function (Id) {
var button, elements;
button = document.createElement("input");
button.type = "submit";
button.onclick = function () {
zzz.fff(Id);
};
elements = document.getElementById(Id);
elements.appendChild(button);
}
};
and HTML, where I tested it:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8">
<title>My Web Page!</title>
<script type="text/javascript" src="test.js"></script>
</head>
<body>
<div id="div001"></div>
<div id="div002"></div>
<script type="text/javascript">
object1 = zzz;
object1.main("div001");
object2 = zzz;
object2.main("div002");
</script>
</body>
</html>
Why it works only if I write button.onclick = function () { zzz.fff(Id); }; and with this.fff(Id) it doesn't work?
When you bind an event handler (such as onclick), inside the handler this becomes the element that triggered the event (except if you used an inline onclick="" attribute, which should be avoided).
Instead of using zzz, you could also copy this to another variable that would be available inside the handler via closure:
var that = this;
button.onclick = function () {
that.fff(Id);
};
Or you could use Function.prototype.bind:
var clickHandler = button.onclick = function () {
this.fff(Id);
};
button.onclick = clickHandler.bind(this);