Web component with optional styled slot container?

91 views Asked by At

I believe one of the principles of web component slots is they are intended to mimic standard html expectations. They allow us to project elements into components in the same way as developers are already used to with other elements such as select's option elements and ul's li elements.

However one key difference I'm seeing is slots appear to be mandatory by default - the slot element always appears in consuming client browsers. This causes problems when it or a parent is styled, but shouldn't be when there's no slot present as the styling always persists.

Take the scenario where we have a card web component which is intended to have an optional header section implemented as a slot. Since this header section requires a different background color, the slot's parent has relevant styles applied against it.

I have managed to make the header optional where a check is made to count the slot's assigned nodes, however to meet parity with "standard" html elements, I would expect it to be reactive over when it's hidden by CSS also.

I've made an example jsfiddle demonstrating what I mean:https://jsfiddle.net/ajbrun/obLxdc9t/

You can see the first section where I have successfully made the header section show or hide based on the presence of the header slot. However beneath that I have a similar demonstration which is not successful to show a header appearing based on its visibility set by a parent class. In each case, there's a standard HTML representation on the left with similar code.

Is what I'm trying to achieve possible, or is the "expectation" from consumers of web components that they should completely remove the slot element to make it optional?

.cards {
  display: flex;
  flex-direction: row;
  gap: 10px;
}

.html-card,
.wc-card {
  width: 50%;
}

.card {
  border: 1px solid lightgray;
  padding: 10px;
}

.card .header {
  background: grey;
  padding: 10px;
}
<script type="module">
import {LitElement, html, css, styleMap} from 'https://cdn.jsdelivr.net/gh/lit/[email protected]/all/lit-all.min.js';

class CardComponent extends LitElement {
  static get styles() {
    return [css`
                 :host {
                    display:block;
                }
        
        
.card {
  border: 1px solid lightgray;
  padding: 10px;
}

.card .header {
  background: grey;
  padding: 10px;
}
            `];
  }
  
  static properties = {
    showHeader: {type: Boolean},
  };

  render() {
  const headerSlotStyles = { display: this.showHeader ? '' : 'none' };
    return html`
            <div class="card">
        <div class="header" style=${styleMap(headerSlotStyles)}>
            <slot name="header" @slotchange="${this.handleSlotChange}"></slot>
        </div>
        <slot></slot>
      </div>
        `;
  }

  handleSlotChange() {
  console.log('slot change');
        const headerSlot = this.shadowRoot.querySelector('slot[name=header]');
    this.showHeader = headerSlot.assignedNodes().length > 0;
  }
}

window.customElements.define('demo-card', CardComponent);

</script>

<script type="text/javascript">
  let htmlHeaderElemCopy, wcHeaderElemCopy;

  function toggleHeaderElement() {
    const htmlHeaderElem = document.querySelector('.slot-exists .header');
    const wcHeaderElem = document.querySelector('.slot-exists [slot=header]');
    if (!htmlHeaderElem) {
      document.querySelector('.slot-exists .card').prepend(htmlHeaderElemCopy);
      document.querySelector('.slot-exists .wc-card demo-card').prepend(wcHeaderElemCopy);
    } else {
      htmlHeaderElemCopy = htmlHeaderElem.cloneNode(true);
      wcHeaderElemCopy = wcHeaderElem.cloneNode(true);
      htmlHeaderElem.remove();
      wcHeaderElem.remove();
    }
  }

  function toggleParentClass() {
    const hasClass = document.querySelector('.parent-class.no-header');
    if (!hasClass) {
      document.querySelector('.parent-class').classList.add('no-header');
    } else {
      hasClass.classList.remove('no-header');
    }
  }

</script>

<style>
  .parent-class.no-header .header,
  .parent-class.no-header [slot=header] {
    display: none;
  }

</style>

<h1>
  Slot exists
  <button onclick="toggleHeaderElement()">
    Toggle header
  </button>
</h1>
<div class="cards slot-exists">
  <div class="html-card">
    <h2>
      Html card
    </h2>
    <div class="card">
      <div class="header">
        Card header
      </div>
      Content...
    </div>
  </div>
  <div class="wc-card">
    <h2>
      Web component card &#10004;
    </h2>
    <demo-card>
      <div slot="header">
        Card header
      </div>
      Content...
    </demo-card>
  </div>
</div>

<h1>
  Parent class toggled
  <button onclick="toggleParentClass()">
    Toggle header
  </button>
</h1>
<div class="cards parent-class">
  <div class="html-card">
    <h2>
      Html card
    </h2>
    <div class="card">
      <div class="header">
        Card header
      </div>
      Content...
    </div>
  </div>
  <div class="wc-card">
    <h2>
      Web component card &#10007;
    </h2>
    <demo-card>
      <div slot="header">
        Card header
      </div>
      Content...
    </demo-card>
  </div>
</div>

1

There are 1 answers

0
Danny '365CSI' Engelman On

Like I said in the comments, if you strip your code to a minimal example, you end up with:

<div style="padding:10px"><slot></slot></div>

You will always see that padding:

For the current discusion on new SLOT features follow:
https://github.com/w3c/csswg-drafts/issues/6867

<style id="STYLE">
  div {
    padding: 5px;
    background: hotpink;
    font-size: 10px;
  }
</style>

<div id="ONE">ONE</div><hr>
<div id="TWO"></div><hr>
<wc-number id="THREE"><div>THREE</div></wc-number><hr>
<wc-number id="FOUR">FOUR</wc-number><hr>
<wc-number id="FIVE"></wc-number>

<script>
  customElements.define("wc-number", class extends HTMLElement {
    constructor() {
      super()
        .attachShadow({mode: "open"})
        .innerHTML = STYLE.outerHTML + `<div><slot></slot></div>`;
    }
  })
</script>