skip to Main Content

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

Answers


  1. Chosen as BEST ANSWER

    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 = "";
          });
        }
      }
    );


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

    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>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search