最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to add style or class name and remove other element from web component? - Stack Overflow

programmeradmin5浏览0评论

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
Add a ment  | 

4 Answers 4

Reset to default 2

a 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 anywhere

  • events bubble UP, not down

  • the default events like click bubble out of shadow DOM

  • Custom Events require posed:true
    read: https://developer.mozilla/en-US/docs/Web/API/Event/Event

  • I 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.

  1. Retrieve the clicked button as well as it's siblings by using parentNode.children.

  2. 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.

  3. 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/

发布评论

评论列表(0)

  1. 暂无评论