laujonat
laujonat

Reputation: 342

Stenciljs: What is the right way to prepend/append Slot elements?

I am having some trouble with the lifecycle methods in web components.

We want to dynamically order child elements being passed in as slots.

To illustrate, this web component takes a prop, iconPos, and will determine whether the icon will be placed at the start or end of the slot.

<my-component iconPos="start"> 
   <img src="/path/icon.svg" /> 
   <div>{this.list}</div> 
</my-component>

I haven't had any luck getting it working with ref:

dc6b89e7.js:2926 TypeError: Cannot read properties of undefined (reading 'prepend')

Here's what I have so far:

 @State() slotElement!: HTMLDivElement;
 @Prop() iconPos: 'start' | 'end';
...

  private createSlots() {
    switch (this.iconPos) {
      case 'start':
        this.slotElement.prepend(<img />);
        break;
      case 'end':
        this.slotElement.append(<img />);
        break;
      default:
        throw new Error(
          `Invalid value \`${this.iconPos}\`, passed into \`iconPos\`. Expected valid values are \`start\`, \`end\``.
        );
    }
  }

  render() {
    return (
    // iconPos="start"
      <parent-component>
        <div ref={(el) => (this.slotElement= el as HTMLDivElement)}>
          <slot></slot>
        </div>
      </parent-component>
    )
  }

I would prefer to not use a CSS solution if possible. Any help would be much appreciated!

Upvotes: 0

Views: 1247

Answers (1)

Slotted content is NOT MOVED to <slot> elements; it is reflected!!

So all styling and element operations must be done in "lightDOM"

For (very) long read see: ::slotted CSS selector for nested children in shadowDOM slot

That means you have to append your elements in ligtDOM with:

this.append(this.firstElementChild)

You can't read the <my-component> innerHTML before it is parsed; so you need to wait till the innerHTML elements are created. Thus you will see the DOM change.
A better method might be to not use <slot> and declare your icon and content as attributes, and have the Web Component create the HTML.

<style>
  span::after { content: attr(id) }
  #FOO { background: lightgreen }
</style>

<my-component>
  <span id="FOO"></span>
  <span id="BAR"></span>
</my-component>
<my-component reversed>
  <span id="FOO"></span>
  <span id="BAR"></span>
</my-component>

<script>
  window.customElements.define('my-component', class extends HTMLElement {
    constructor() {
      super().attachShadow({mode:'open'})
             .innerHTML = `<style>::slotted(span){background:gold}</style>
                           ${this.nodeName}<slot></slot><br>`;
    }
    connectedCallback() {
      setTimeout(() => { // make sure innerHTML is parsed!
        if (this.hasAttribute("reversed")) {
          this.append(this.firstElementChild);
        }
      })
    }
  });
</script>

Upvotes: 1

Related Questions