I have two ponents where one is the parent ponent and the other is the child ponent. Now the parent has two children within it. which child click for add a style in it, i would require to remove the style in other children. so at a time only one child will keep the style. how to do this?
LiveDemo - click on the button. I am not able to remove the style back.
here is my code :
class Parent extends HTMLElement {
shadowRoot;
constructor(){
super();
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback(){
this.render();
}
render() {
this.shadowRoot.innerHTML = `<div>
<children-holder></children-holder>
<children-holder></children-holder>
<children-holder></children-holder>
</div>`
}
}
customElements.define('parent-holder', Parent);
class Children extends HTMLElement {
shadowRoot;
constructor(){
super()
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback(){
this.render();
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.shadowRoot.querySelector('button').style.border = "";
this.shadowRoot.querySelector('button').style.border = "3px solid red";
})
}
render() {
this.shadowRoot.innerHTML = `
<div><button class="button">Click me!</button></div>`
}
}
customElements.define('children-holder', Children);
I have two ponents where one is the parent ponent and the other is the child ponent. Now the parent has two children within it. which child click for add a style in it, i would require to remove the style in other children. so at a time only one child will keep the style. how to do this?
LiveDemo - click on the button. I am not able to remove the style back.
here is my code :
class Parent extends HTMLElement {
shadowRoot;
constructor(){
super();
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback(){
this.render();
}
render() {
this.shadowRoot.innerHTML = `<div>
<children-holder></children-holder>
<children-holder></children-holder>
<children-holder></children-holder>
</div>`
}
}
customElements.define('parent-holder', Parent);
class Children extends HTMLElement {
shadowRoot;
constructor(){
super()
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback(){
this.render();
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.shadowRoot.querySelector('button').style.border = "";
this.shadowRoot.querySelector('button').style.border = "3px solid red";
})
}
render() {
this.shadowRoot.innerHTML = `
<div><button class="button">Click me!</button></div>`
}
}
customElements.define('children-holder', Children);
Share
Improve this question
edited Dec 14, 2019 at 21:41
AndrewL64
16.3k8 gold badges50 silver badges85 bronze badges
asked Dec 14, 2019 at 16:02
3gwebtrain3gwebtrain
15.3k29 gold badges141 silver badges276 bronze badges
2
- Can you clarify your question? In live demo, when I clicked the button it gets styled. What button will make it's style remove? – Orgil Commented Dec 14, 2019 at 16:29
- when you click on a button it should be bordered with red color. other should be removed. so at a time only one button will have the border, which is you clicked. – 3gwebtrain Commented Dec 14, 2019 at 16:32
4 Answers
Reset to default 2a long answer for (eventually) 3 lines of code...
If you make Custom Element children access a parentNode, and loop its DOM elements..
You are creating a dependency between ponents
Event Driven solution:
- The click on a button bubbles up the DOM
- so the parent can capture that click event
- The
evt.target
will be the button clicked - The parent then emits a custom event
- The Children are listening for that Event, there is NO dependency on the parent
- Since the Event contains the button clicked, each listening element can do its select/unselect code
- And it is less and clearer code
class Parent extends HTMLElement {
constructor() {
super()
.attachShadow({mode: 'open'})
.shadowRoot.innerHTML = `<div>` +
`<children-holder></children-holder>`.repeat(3) +
`</div>`
}
connectedCallback() {
this.shadowRoot.addEventListener('click', evt => {
if (evt.target.nodeName === 'CHILDREN-HOLDER')
document.dispatchEvent(new CustomEvent('myStateEvent', {
detail: evt.target // THE BUTTON CLICKED
}));
});
}
}
customElements.define('parent-holder', Parent);
class Children extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'});
}
connectedCallback() {
this.shadowRoot.innerHTML = `<div><button class="button">Click me!</button></div>`;
document.addEventListener('myStateEvent', evt => {
let IwasClicked = evt.detail === this;
this.shadowRoot.querySelector('button').style.border = IwasClicked ? "3px solid red" : ""
});
}
}
customElements.define('children-holder', Children);
Notes
dispatch and listen are both on the
document
, you can attach them anywhereevents bubble UP, not down
the default events like
click
bubble out of shadow DOMCustom Events require
posed:true
read: https://developer.mozilla/en-US/docs/Web/API/Event/EventI did the dispatch in the Parent for clearity (A DEPENDENCY!)
It might be better to make the Child do the dispatchEvent, So it bees:
Yo! everyone listening! I was clicked, WE ALL do whatever WE need to do
And keep all logic in one ponent:
connectedCallback() {
let root = this.shadowRoot;
let eventName = "myStateEvent";
root.innerHTML = `<div><button class="button">Click me!</button></div>`;
document.addEventListener(eventName, evt => {
let button = root.querySelector("button");
button.style.border = evt.detail === button ? "3px solid red" : "";
});
root.addEventListener("click", evt =>
document.dispatchEvent(
new CustomEvent(eventName, {
detail: evt.target // THE BUTTON CLICKED
})
)
);
}
Now you understand Event driven solutions
And you might now ask: Why not use the click
event?
That is possible once you understand that event.target
is NOT what you might think it is.
When events originate from shadow DOM, the event.target
value is the last shadowDOM it pierced
So your button click sets different event.target
values:
Listener on <children-holder> event.target = button
Listener on <parent-holder> event.target = <children-holder>
Listener on document event.target = <parent-holder>
To solve your Button-Select-Color use-case with one click
event
the button click is the dispatcher, sending a click event UP the DOM,
through all shadowDOM boundaries
You have to check the event.posedPath()
function which retuns an Array of ALL DOM elements the Event passed.
(note: event.path
is Chrome only!!)
So all code required for your style question is:
connectedCallback() {
let root = this.shadowRoot;
root.innerHTML = `<div><button>Click me!</button></div>`;
root.host.getRootNode().addEventListener("click", evt => {
let button = root.querySelector("button");
button.style.border = evt.posedPath().includes(button) ? "3px solid red" : "";
});
}
Notes
root.host.getRootNode()
allows one selected button per parent Component- change to
document
and it is one button per page evt.posedPath().includes(root)
identifies the child-ponent
Working Fiddle: https://jsfiddle/WebComponents/bc9tw1qa/
You could achieve your desired behavior in a couple of ways, I'll describe 2 of them:
CSS-only:
When you click a button, it will receive the CSS focus
state. So using the css
button:focus {
border: 3px solid red;
}
Will give only the most recently clicked button a border. The focus
state will be removed when you click anywhere else on the screen.
JS solution The separate shadow-roots make it a bit hard to traverse all the buttons using JS in an elegant way, but this should do the trick:
const button = this.shadowRoot.querySelector('button');
button.addEventListener('click', () => {
const parentShadowRoot = this.shadowRoot.host.getRootNode();
const childrenHolders = parentShadowRoot.querySelectorAll('children-holder');
childrenHolders.forEach(holder => {
const button = holder.shadowRoot.querySelector('button');
button.style.border = "";
})
button.style.border = "3px solid red";
})
You could simplify your two classes, check the example snippet.
class Parent extends HTMLElement {
constructor() {
super();
this.sroot = this.attachShadow({
mode: 'open'
});
this.render();
}
render() {
this.sroot.innerHTML = `<div>
<children-holder></children-holder>
<children-holder></children-holder>
<children-holder></children-holder>
</div>`
}
}
customElements.define('parent-holder', Parent);
class Children extends HTMLElement {
constructor() {
super();
this.sroot = this.attachShadow({
mode: 'open'
});
let style = document.createElement("style");
style.append('button:focus {border: 3px solid red;}');
this.sroot.append(style);
this.render();
}
render() {
let button = document.createElement("button");
button.innerHTML = "Click me!";
button.classList.add("button");
let div = document.createElement("div");
div.append(button);
this.sroot.append(div);
}
}
customElements.define('children-holder', Children);
<parent-holder></parent-holder>
You can first retrieve all the button's siblings as well as the button itself then you can remove the border from all of them and finally, add the red border to the button that was clicked.
Retrieve the clicked button as well as it's siblings by using
parentNode.children
.You will get an
HTMLCollection
of the buttons on which you can now use Array.from to get a new, shallow-copied Array of your HTMLCollection which you can now iterate over.Finally, you can now just remove the border from all the buttons and then add the border to the clicked button.
this.shadowRoot.querySelector('button').addEventListener('click', () => {
let x = this.parentNode.children;
Array.from(x).forEach((e) => {
e.shadowRoot.querySelector('button').style.border = "";
});
this.shadowRoot.querySelector('button').style.border = "3px solid red";
});
Here is a live example of the above in JSFiddle: https://jsfiddle/AndrewL64/Lrbn7d8t/18/