Search code examples
javascriptshadow-domcustom-componentcustom-elementcreateelement

How to embed a link in a custom component as a slot?


I'm trying to create a simple component which contains some animation. I have the name attrib set for the link's text. I have a slot set up for the destination of the link, but the template is not allowing the link to be set as a slot. I'm not sure what to try next.

When I hover on the link, it just says <slot name= instead of the actual URL.

const template = document.createElement('template');
template.innerHTML = `
  <style>
    .txt-link a {
      color: #005fec;
      font-weight: 700;
      font-size: 18px;
      text-decoration: none;
      position: relative;
    }
    .txt-link a:hover {
      text-decoration: none;
      cursor: pointer;
    }
    .txt-link a::before {
      content: "";
      position: absolute;
      bottom: -5px;
      left: 0;
      width: 0;
      height: 1px;
      background-color: #005fec;
      visibility: hidden;
      transition: all 0.3s ease-in-out 0s;
    }
    .txt-link a:hover::before {
      visibility: visible;
      width: 100%;
    }
    .txt-link.arrow-link img {
      height: 15px;
      position: relative;
      top: 2px;
    }
  </style>
  <div class="txt-link arrow-link">
    <a href="<slot name="destination" />"></a>
    <img alt="arrow right icon" class="learn-more-arrow" src="https://www.pikpng.com/pngl/m/73-734729_arrow-blue-right-transparent-blue-arrow-right-clipart.png" loading="lazy">
  </div>
`;

class CustomAnimLink extends HTMLElement {
  constructor() {
    super();

    this.showInfo = true;

    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this.shadowRoot.querySelector('a').innerText = this.getAttribute('name');
  }
}

window.customElements.define('custom-anim-link', CustomAnimLink);
<custom-anim-link name="Learn more">
  <div slot="destination">https://www.yahoo.com</div>
</custom-anim-link>


Solution

  • You can't do that with slots. Slots are used to place HTMLElements in the place of the slot and can't be used for anything other than that.

    Instead pass the URL as an attribute. That seems appropriate for the data type. Web Components has some nice life cycles to help you with this.

    I've added static get observedAttributes() to indicate that some attributes should be observed. Doing this will enable you to do something whenever the value in one of the attributes is changed, like updating the URL on the anchor in the Shadow DOM.

    Whenever an observed attribute value changes the attributeChangedCallback is called in which you can determine what should happen whenever a value changes.

    Both the observedAttributes and attributeChangedCallback are part of Web Component life cycle hooks.

    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        .txt-link a {
          color: #005fec;
          font-weight: 700;
          font-size: 18px;
          text-decoration: none;
          position: relative;
        }
        .txt-link a:hover {
          text-decoration: none;
          cursor: pointer;
        }
        .txt-link a::before {
          content: "";
          position: absolute;
          bottom: -5px;
          left: 0;
          width: 0;
          height: 1px;
          background-color: #005fec;
          visibility: hidden;
          transition: all 0.3s ease-in-out 0s;
        }
        .txt-link a:hover::before {
          visibility: visible;
          width: 100%;
        }
        .txt-link.arrow-link img {
          height: 15px;
          position: relative;
          top: 2px;
        }
      </style>
      <div class="txt-link arrow-link">
        <a href=""><slot></slot></a>
        <img alt="arrow right icon" class="learn-more-arrow" src="https://www.pikpng.com/pngl/m/73-734729_arrow-blue-right-transparent-blue-arrow-right-clipart.png" loading="lazy">
      </div>
    `;
    
    class CustomAnimLink extends HTMLElement {
    
      static get observedAttributes() {
        return ['name', 'destination'];
      }
    
      constructor() {
        super();
    
        this.showInfo = true;
        this.attachShadow({
          mode: 'open'
        });
        this.shadowRoot.appendChild(template.content.cloneNode(true));
      }
    
      get anchor() {
        return this.shadowRoot.querySelector('a');
      }
    
      get name() {
        return this.getAttribute('name');
      }
    
      set name(value) {
        if ('string' === typeof value) {
          this.setAttribute('name', value);
        }
      }
    
      get destination() {
        return this.getAttribute('destination');
      }
    
      set destination(value) {
        if ('string' === typeof value) {
          this.setAttribute('destination', value);
        }
      }
    
      attributeChangedCallback(attrName, oldValue, newValue) {
        switch (attrName) {
          case 'destination':
            this.anchor.href = newValue;
            break;
          case 'name':
            this.anchor.textContent = newValue;
            break;
        }
      }
    }
    
    window.customElements.define('custom-anim-link', CustomAnimLink);
    <custom-anim-link name="Learn more" destination="https://www.yahoo.com"></custom-anim-link>

    Alternatively, you could pass the <a> tag in the place for the slot. Then get all elements assigned to the destination slot and set the textContent properties based on the value of the name attribute. Same goes for the destination attribute we've added in the last example.

    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        .txt-link a {
          color: #005fec;
          font-weight: 700;
          font-size: 18px;
          text-decoration: none;
          position: relative;
        }
        .txt-link a:hover {
          text-decoration: none;
          cursor: pointer;
        }
        .txt-link a::before {
          content: "";
          position: absolute;
          bottom: -5px;
          left: 0;
          width: 0;
          height: 1px;
          background-color: #005fec;
          visibility: hidden;
          transition: all 0.3s ease-in-out 0s;
        }
        .txt-link a:hover::before {
          visibility: visible;
          width: 100%;
        }
        .txt-link.arrow-link img {
          height: 15px;
          position: relative;
          top: 2px;
        }
      </style>
      <div class="txt-link arrow-link">
        <slot name="destination"></slot>
        <img alt="arrow right icon" class="learn-more-arrow" src="https://www.pikpng.com/pngl/m/73-734729_arrow-blue-right-transparent-blue-arrow-right-clipart.png" loading="lazy">
      </div>
    `;
    
    class CustomAnimLink extends HTMLElement {
    
      static get observedAttributes() {
        return ['name', 'destination'];
      }
    
      constructor() {
        super();
    
        this.showInfo = true;
        this.attachShadow({
          mode: 'open'
        });
        this.shadowRoot.appendChild(template.content.cloneNode(true));
      }
    
      get destinations() {
        const slot = this.shadowRoot.querySelector('slot');
        const assignedElements = slot.assignedElements();
        return assignedElements;
      }
    
      get name() {
        return this.getAttribute('name');
      }
    
      set name(value) {
        if ('string' === typeof value) {
          this.setAttribute('name', value);
        }
      }
      
      get destination() {
        return this.getAttribute('destination');
      }
    
      set destination(value) {
        if ('string' === typeof value) {
          this.setAttribute('destination', value);
        }
      }
      
      attributeChangedCallback(attrName, oldValue, newValue) {
        switch(attrName) {
          case 'name':
            this.destinations.forEach(destination => {
              destination.textContent = newValue;
            });
            
            break;
          case 'destination':
            this.destinations.forEach(destination => {
              destination.href = newValue;
            });
            
            break;
        }
      }
    }
    
    window.customElements.define('custom-anim-link', CustomAnimLink);
    <custom-anim-link name="Learn more" destination="https://www.yahoo.com">
      <a slot="destination"></a>
    </custom-anim-link>

    Or as a last simple resort, just pass the <a> in it's entirety as child to your custom element.

    <custom-anim-link>
      <a href="https://www.yahoo.com">Learn more</a>
    </custom-anim-link>