Code Logs

22.08.18

<slot> 태그를 이용해 커스텀 엘리먼트에 자식 엘리먼트를 주입할 수 있는 방법을 소개 합니다.
<slot> 태그를 이용해 조금더 유연한 형태의 dialog 커스텀 엘리먼트를 만들기
고양이 빵 Cat bread - 세마리의 고양이와 홈베이킹 이야기

Web component - slot

<slot> 태그를 이용해 커스텀 엘리먼트에 자식 엘리먼트를 주입할 수 있는 방법을 소개 합니다. <slot> 태그를 이용해 조금더 유연한 형태의 dialog 커스텀 엘리먼트를 만들기

Table of contents

  1. Slot
    1. Slot 사용하기
    2. Styling
  2. Dialog 만들기

Slot

<slot> 태그는 웹 컴포넌트 내 특정 영역의 마크업을 외부로 부터 주입 받기 위해 사용되는 태그다.

예를들면 dialog 컴포넌트 처럼 팝업이 나타나고 닫기 버튼을 통해 다시 팝업을 닫는 것과 같은 기능은 컴포넌트 내부에서 구현되지만 팝업의 내용을 채우기 위해서는 외부로 부터 DOM을 주입 받아야 하는데 이럴때 사용할 수 있는 태그가 <slot>이다.

Slot 사용하기

<slot> 태그를 통해 커스텀 엘리먼트의 내부로 DOM을 넣는 예제는 아래와 같다.

customElements.define(
  'custom-slot',
  class extends HTMLElement {
    constructor() {
      super()
      this.attachShadow({ mode: 'open' })
      this.shadowRoot.innerHTML = `
        <style>
          #container {
            padding: 20px 10px;
            background-color: skyblue;
          }
        </style>

        <div id="container">
          <slot></slot>
        </div>
      `
    }
  }
)
...
<custom-slot>
  <p>Slotted</p>
</custom-slot>
...

<slot>name attribute를 통해 식별할 수 있다. 만약 <slot> 태그에 name attribute가 설정되어 있다면 외부에서 대상 <slot>을 식별 할 수 있도록 slot attribute를 통해 반드시 대상 <slot>name을 입력해야 한다.

customElements.define(
  'custom-slot',
  class extends HTMLElement {
    constructor() {
      super()
      this.attachShadow({ mode: 'open' })
      this.shadowRoot.innerHTML = `
        <style>
          #container {
            padding: 20px 10px;
            background-color: skyblue;
          }
        </style>

        <div id="container">
          <slot name="first-slot"></slot>
          <slot name="second-slot"></slot>
        </div>
      `
    }
  }
)
...
<custom-slot>
  <p>Slotted</p>
  <p slot="second-slot">Second Slotted</p>
  <p slot="first-slot">First Slotted</p>
</custom-slot>
...

위 예제와 같이 <slot>을 주입하고 name, slot attribute를 설정하면 아래와 같은 형태로 화면에 출력된다.

  • <p>Slotted</p>slot attribute가 없어 대상 <slot>을 찾을 수 없기 때문에 화면에 출력되지 않음
  • <p slot="second-slot">Second Slotted</p><p slot="first-slot">First Slotted</p>slot attribute를 통해 대상 <slot>을 찾아 화면에 출력됨
  • 화면에 출력된 두 <p> 태그는 주입시 순서와 달리 <custom-slot> 컴포넌트가 지정한 <slot>의 위치에 따라 순서가 바뀐 상태로 출력됨

<slot> 태그에 name이 설정되어 있지 않다면 <slot> 태그에 주입되는 자식 요소중 slot attribute가 설정되지 않은 모든 엘리먼트<slot>에 삽입한다.

<slot> 태그에 name attribute가 설정되어 있다면 slot attribute에 name과 동일한 값을 가지고 있는 모든 엘리먼트<slot>에 삽입 된다.

Styling

<slot> 태그를 통해 외부에서 주입 받은 엘리먼트에 대한 스타일을 정의하기 위해 ::slotted pseudo-elements를 사용한다.

::slotted를 이용한 CSS는 반드시 shadow DOM 내부에 정의해야 한다. 이렇게 정의된 selectortext node를 제외한 <slot> 내부의 모든 엘리먼트를 대상으로 한다.

customElements.define(
  'custom-slot',
  class extends HTMLElement {
    constructor() {
      super()
      this.attachShadow({ mode: 'open' })
      this.shadowRoot.innerHTML = `
        <style>
          #container {
            padding: 20px 10px;
            background-color: skyblue;
          }
          ::slotted(*) {
            font-size: 20px;
          }
          ::slotted([slot=first-slot]) {
            color: tomato;
          }
          ::slotted([slot=second-slot]) {
            color: green;
          }
        </style>

        <div id="container">
          <slot name="first-slot"></slot>
          <slot name="second-slot"></slot>
        </div>
      `
    }
  }
)

예제와 같은 style 정의를 통해 외부에서 전달 받은 엘리먼트에 대한 스타일을 적용할 수 있다.

[slot=second-slot] 에서 확인 할 수 있듯 특정 attribute를 통해 대상 엘리먼트를 select 할 경우 외부에서 정의한 엘리먼트의 attribute를 참조한다.

Dialog 만들기

<slot> 태그와 ::slotted pseudo element를 이용해 외부에서 컨텐츠 영역의 DOM을 주입 할 수 있는 custom dialog 컴포넌트를 만든다.

customElements.define(
  'custom-dialog',
  class extends HTMLElement {
    constructor() {
      super()
      this.attachShadow({ mode: 'open' })
      this.shadowRoot.innerHTML = `
        <style>
          :host {
            display: none;
          }
          :host([open]) {
            display: initial;
          }
          #modal {
            position: absolute;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            background-color: black;
            opacity: 0.5;
            z-index: 1;
          }
          #dialog {
            position: absolute;
            background-color: white;
            border-radius: 6px;
            z-index: 2;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
          }
          #dialog > header {
            padding: 5px;
            display: flex;
            gap: 10px;
          }
          #close-button {
            margin: auto 0;
            border: none;
            background-color: transparent;
          }
          #content {
            padding: 10px;
          }
          ::slotted([slot=title]) {
            flex: 1;
            font-size: 1rem;
            font-weight: bold;
            padding: 10px 0;
            margin: 0;
          }
        </style>

        <div id="modal"></div>
        <section id="dialog">
          <header>
            <slot name="title"></slot>
            <button id="close-button">X</button>
          </header>
          <section id="content">
            <slot name="content"></slot>
          </section>
        </section>
      `
    }

    connectedCallback() {
      const closeButton = this.shadowRoot.querySelector('#close-button')
      closeButton.onclick = this.close.bind(this)
    }

    open() {
      this.setAttribute('open', '')
    }

    close() {
      this.removeAttribute('open')
    }
  }
)

<custom-dialog> 컴포넌트는 titlecontent 두개의 slot 영역을 가지고 있다. 각 영역에 기본적으로 적용되야 할 스타일은 ::slotted pseudo-elements를 통해 정의되었고 openclose 두개의 메서드를 제공한다.

연관 포스팅

카테고리 더보기

    참고

    댓글