Access to the actual/visual DOM using custom elements and template

41 Views Asked by At

Say I have this thing called a "panel". It contains a hidden section and a button to show it. Easy enough.

However I got a little fancy and decided to use a custom element <custom-panel> to markup the boundaries for each panel, a <template> for the contents and <slots> for the configuration (title, details, etc).

Now I am a little confused as to how to hook up the buttons. The template has CSS that will show/hide the details of the panel if the correct class is set. But how to I set it? I have only figured out how to get to the contents of the template (without the resolved slots) or the custom-panel's contents (with no template information).

Complete Example:

customElements.define(
  'custom-panel',
  class extends HTMLElement {
    constructor() {
      super();

      const template = document.getElementById('custom-panel-template');
      const templateContent = template.content;

      this.attachShadow({
        mode: 'open'
      }).appendChild(
        templateContent.cloneNode(true)
      );
    }
  }
);
  <custom-panel>
    <span slot="openButtonText">Open Panel 1</span>
    <span slot="closeButtonText">Close Panel 1</span>
    <span slot="panelName">Panel 1</span>
    <div slot="">Panel 1 Details</div>
  </custom-panel>
  <custom-panel>
    <span slot="openButtonText">Open Panel 2</span>
    <span slot="closeButtonText">Close Panel 2</span>
    <span slot="panelName">Panel 2</span>
    <div slot="panelContent">Panel 2 Details</div>
  </custom-panel>

  <template id="custom-panel-template">
        <style type="text/css">
            #panel {
                display: none;
            }
            
            #panel.open {
                display: revert;
            }
        </style>
        
        <!-- how do these get hooked in? -->
        <button type="button"><slot name="openButtonText">Default Open Button Text</slot></button>
        <button type="button"><slot name="closeButtonText">Default Close Button Text</slot></button>
        
        <fieldset id="panel">
            <legend><slot name="panelName">Default Panel Name</slot></legend>
            <slot name="panelContent">Default Panel Content</slot>
        </fieldset>
    </template>

2

There are 2 best solutions below

0
voidnull On

customElements.define(
  'custom-panel',
  class extends HTMLElement {
    constructor() {
      super();

      const template = document.getElementById('custom-panel-template');
      const templateContent = template.content;

      this.attachShadow({
        mode: 'open'
      }).appendChild(
        templateContent.cloneNode(true)
      );

      // get a reference to the panel
      const panel = this.shadowRoot.querySelector("#panel");

      // hookup the open
      this.shadowRoot.querySelector("button#open").addEventListener("click", () => {
        panel.className = "open";
      });


      // hookup the close
      this.shadowRoot.querySelector("button#close").addEventListener("click", () => {
        panel.className = "";
      });
    }
  }
);

0
Danny '365CSI' Engelman On

Its your own component, no one is ever going to set multiple Event Listeners to one button.
So use inline Event Handlers instead of addEventListener

You can then use cleaner DOM creation code with a createElement( tag, props ) function

No need for a fancy <template> You only need to create a handful DOM elements.
If you did append a template use:
this.shadowRoot.querySelector("button#open").onclick = (evt) => { }

You also do not want to set a [open] state buried deep inside a class in shadowDOM.

State should be set on the Web Component itself as: <custom-panel open>

:host(:not([open)) fieldset { display:none } then toggles visibility

Also read my Dev.To post on <details-accordion>

<script>
customElements.define('custom-panel', class extends HTMLElement {
    constructor() {
      const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
      super() // sets AND returns 'this'
        .attachShadow({mode:'open'}) // sets AND returns this.shadowRoot
        .append(
           createElement( 'style' , {
             innerHTML: `:host(:not([open])) fieldset { display:none }`   
           }),
           createElement( 'button', {
             innerHTML: `<slot name="openButtonText"></slot>`,
             onclick  : (evt) => this.open = true
           }),
           createElement( 'button', {
             innerHTML: `<slot name="closeButtonText"></slot>`,
             onclick  : (evt) => this.open = false
           }),
           createElement( 'fieldset', {
             // id : "panel" // not required
             innerHTML: `<legend><slot name="panelName">Default Panel Name</slot></legend>` +
                        `<slot name="panelContent">Default Panel Content</slot>`
           })
        );
    }
    get open(){
      return this.hasAttribute("open");
    }
    set open(state=true){
      this.toggleAttribute("open", state)
    }
    toggle(){
      this.open = !this.open;
    }
  });
</script>
<custom-panel>
  <span slot="openButtonText">Open Panel 1</span>
  <span slot="closeButtonText">Close Panel 1</span>
  <span slot="panelName">Panel 1</span>
  <div slot="">Panel 1 Details</div>
</custom-panel>
<custom-panel open>
  <span slot="openButtonText">Open Panel 2</span>
  <span slot="closeButtonText">Close Panel 2</span>
  <span slot="panelName">Panel 2</span>
  <div slot="panelContent">Panel 2 Details</div>
</custom-panel>