티스토리 뷰

Custom Element

클래스에서 설명하는 사용자 지정 HTML 요소를 자체 방법과 속성, 이벤트 등으로 만들 수 있습니다. 사용자 지정 요소가 정의되면 내장된 HTML 요소와 동등하게 사용할 수 있습니다.

HTML Dictionary은 풍부하지만 무한하지는 않기 때문에 이 선택이 좋습니다.

HTML Dictionary에는 easy-tabs, sliding-carousel, beautiful-upload등이 없습니다… 우리가 필요로 할 다른 tag들에 대해 생각해봅시다.

그를 특별 클래스로 정의한 다음 항상 HTML의 일부인 것처럼 사용할 수 있습니다.

두 가지 종류의 사용자 지정 요소가 있습니다:

  1. Autonomous custom elements자율 사용자 지정 요소 - 추상적인 HTMLElement 클래스를 확장하는 "완전히 새로운" 요소
  2. Customized built-in elements맞춤형 내장 요소 - HTMLButtonElement 등을 기반으로 하는 맞춤형 버튼 같은 내장 요소를 기반으로 확장합니다.

먼저 자율 요소를 다룬 다음 맞춤형 내장 요소로 이동합니다.

사용자 지정 요소를 만들려면 브라우저에 표시 방법, 요소를 페이지에 추가하거나 삭제할 때 수행할 작업 등 몇 가지 세부 정보를 알려줘야 합니다.

이는 특별한 방법으로 수업을 구성하여 이루어집니다. 방법이 거의 없고 모두 선택 사항이기 때문에 쉽습니다.

전체 목록이 포함된 스케치는 다음과 같습니다:

class MyElement extends HTMLElement {
  constructor() {
    super();
    // element 생성
  }

  connectedCallback() {
    // 브라우저는 요소가 문서에 추가될 때 이 메서드를 호출합니다
    // (요소를 반복적으로 추가/removed하는 경우 여러 번 호출할 수 있습니다)
  }

  disconnectedCallback() {
    // 브라우저는 문서에서 요소가 제거되면 이 메서드를 호출합니다
    // (요소를 반복적으로 추가/removed하는 경우 여러 번 호출할 수 있습니다)
  }

  static get observedAttributes() {
    return [/* 변경 사항을 모니터링할 속성 이름 배열 */];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // 위에 나열된 속성 중 하나가 수정되면 호출됩니다
  }

  adoptedCallback() {
    // 요소를 새 문서로 이동할 때 호출됩니다
    // (document.adaptNode에서 발생하며, 거의 사용되지 않습니다)
  }

  // 다른 요소 방법 및 속성이 있을 수 있습니다
}

그런 다음 요소를 등록해야 합니다:


//브라우저에 가서 <my-element>가 우리의 새로운 클래스로서 serve되는 것을 알립니다.
customElements.define("my-element", MyElement);

이제 태그 my-element가 있는 모든 HTML 요소에 대해 MyElement의 인스턴스가 생성되고 앞서 언급한 방법이 호출됩니다. 또한 document.createElement('my-element') 도 사용할 수 있습니다.


  • 사용자 지정 요소 이름에는 하이픈이 포함되어야 합니다. -
    사용자 지정 요소 이름에는 하이픈이 있어야 합니다. -,ex my-element, super-button은 유효한 이름이지만 myelement는 아닙니다.

이는 내장된 HTML 요소와 사용자 지정 HTML 요소 간에 이름 충돌이 발생하지 않도록 하기 위한 것입니다.


Example: “time-formatted”

<time>element가 이미 날짜/시간에 대한 HTML의 요소입니다. 하지만 자체적으로 포맷을 수행하지는 않습니다.

<time-formatted> element를 만들어봅시다. 언어 인식 형식으로 시간을 표시하는 요소입니다:

<script>
class TimeFormatted extends HTMLElement { // (1)

  connectedCallback() {
    let date = new Date(this.getAttribute('datetime') || Date.now());

    this.innerHTML = new Intl.DateTimeFormat("default", {
      year: this.getAttribute('year') || undefined,
      month: this.getAttribute('month') || undefined,
      day: this.getAttribute('day') || undefined,
      hour: this.getAttribute('hour') || undefined,
      minute: this.getAttribute('minute') || undefined,
      second: this.getAttribute('second') || undefined,
      timeZoneName: this.getAttribute('time-zone-name') || undefined,
    }).format(date);
  }

}

customElements.define("time-formatted", TimeFormatted); // (2)
</script>

<!-- (3) -->
<time-formatted datetime="2019-12-01"
  year="numeric" month="long" day="numeric"
  hour="numeric" minute="numeric" second="numeric"
  time-zone-name="short"
></time-formatted>
  1. 클래스에는 한 가지 방법만 있습니다. connectedCallback() – 브라우저는 다음과 같이 부릅니다. <time-formatted> 요소가 페이지에 추가되고(또는 HTML 파서가 감지할 때) 내장된 Intl을 사용합니다.브라우저 전반에서 잘 지원되는 DateTimeFormat 데이터 형식을 사용하면 형식이 좋은 시간을 표시할 수 있습니다.
  2. 다음까지 새 요소를 등록해야 합니다. customElements.define(tag, class)`.
  3. 그런 다음 어디에서나 사용할 수 있습니다.

Custom elements 업그레이드

만일 브라우저가 어떤 <time-formatted>customElements.define이전에 직면했다면,이는 오류가 아닙니다. 그러나 이 element는 표준이 아닌 태그와 마찬가지로 아직 알려지지 않았습니다.

이러한 "정의되지 않은" 요소는 CSS 선택기로 스타일링할 수 있습니다. :not(:defined).

customElement.define이 호출되었을 때, 그들은 "upgraded"됩니다."upgraded": TimeFormatted의 새로운 인스턴스는 각각 만들어지고, connectedCallback 를 호출합니다. 그리고 마침내 다음의 상태가 됩니다. :defined.

사용자 지정 요소에 대한 정보를 얻으려면 다음과 같은 방법이 있습니다:

  • customElements.get(name)name이 지정된 사용자 지정 요소의 클래스를 반환합니다.
  • customElements.whenDefined(name)name이 주어진 커스텀 엘리멘트가 define된 후에 resolve된 프라미스를 (값 없이) 반환한다.

constructor가 아니라 connectedCallback에서 렌더링 된다. ,

위의 예에서 요소 콘텐츠는 connectedCallback에서 렌더링(생성)됩니다.

constructor이 아닌가?

이유는 간단합니다: constructor이 호출되었을 때, 아직 이르다고 판단합니다. element가 생성되었지만 브라우저는 아직 이 단계에서 속성을 처리/할당하지 않았습니다: getAttribute를 호출하면 null이 반환될 것 입니다. 따라서 실제로 렌더링할 수 없습니다.

또한, 생각해보면 정말 필요할 때까지 작업을 지연시키는 것이 성과 측면에서 더 좋습니다.

connectedCallback은 요소가 문서에 추가될 때 트리거됩니다. 자식으로써 다른 요소에 추가되었을 뿐만 아니라 실제로 페이지의 일부가 됩니다. 따라서 분리된 DOM을 구축하고 요소를 생성하여 나중에 사용할 수 있도록 준비할 수 있습니다. 페이지에 들어갈 때만 실제로 렌더링됩니다.

Observing attributes 옵저버 속성

<time-formatted>가 현재 실행되는 도중, 요소가 렌더링된 후에는 추가 속성 변경이 아무런 영향을 미치지 않습니다. HTML 요소치고는 이상한 일입니다. 일반적으로 a.href,와 같은 속성을 변경할 때는 변경 사항이 즉시 가시화될 것으로 예상합니다. 그럼 이 문제를 해결해 보겠습니다.

observedAttributes() static getter 안에서 그 속성들의 목록을 제공함으로써 우리는 속성을 관찰할 수 있습니다.

이러한 속성의 경우, attributeChangedCallback 를 수정할 때 호출합니다. 이것은 성능상의 이유로 속성에 대해 트리거되지 않습니다.

여기 속성이 변경될 때 자동으로 updates되는 새로운 <time-formatted>가 있습니다.

<script>
class TimeFormatted extends HTMLElement {

  render() { // (1)
    let date = new Date(this.getAttribute('datetime') || Date.now());

    this.innerHTML = new Intl.DateTimeFormat("default", {
      year: this.getAttribute('year') || undefined,
      month: this.getAttribute('month') || undefined,
      day: this.getAttribute('day') || undefined,
      hour: this.getAttribute('hour') || undefined,
      minute: this.getAttribute('minute') || undefined,
      second: this.getAttribute('second') || undefined,
      timeZoneName: this.getAttribute('time-zone-name') || undefined,
    }).format(date);
  }

  connectedCallback() { // (2)
    if (!this.rendered) {
      this.render();
      this.rendered = true;
    }
  }

  static get observedAttributes() { // (3)
    return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
  }

  attributeChangedCallback(name, oldValue, newValue) { // (4)
    this.render();
  }

}

customElements.define("time-formatted", TimeFormatted);
</script>

<time-formatted id="elem" hour="numeric" minute="numeric" second="numeric"></time-formatted>

<script>
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
</script>
오전 2:15:14
  1. 렌더링 로직이 render() 도우미 메서드로 이동한다.
  2. 요소를 페이지에 삽입하면 한 번 호출합니다.
  3. 속성 변경의 경우 다음에 나열되어 있습니다. - observedAttributes(), attributeChangedCallback 트리거.
  4. ...요소를 다시 renders합니다.
  5. 마지막에는 라이브 타이머를 쉽게 만들 수 있습니다.

Rendering order

HTML parser가 DOM을 구축하면 부모가 자녀보다 먼저 요소가 차례로 처리됩니다.

예: <outer><inner></inner></outer>,그리고나서 <outer> 요소가 생성되어 먼저 DOM에 연결된 다음 <inner>.

이는 맞춤형 요소에 중요한 결과로 이어집니다.

예를 들어, 사용자 지정 요소가 액세스를 시도하는 경우 connectedCallback안의 innerHTML이 아무것도 얻지 못하는 결과를 확인해봅시다.

아무것도 얻지 못하는 결과:

<script>
customElements.define('user-info', class extends HTMLElement {

  connectedCallback() {
    alert(this.innerHTML); // empty (*)
  }

});
</script>

<user-info>John</user-info>

실행하면 alert는 비어 있습니다.

바로 그 stage에 자식이 없고 돔이 미완성이기 때문입니다. HTML 파서가 사용자 지정 요소에 연결되었습니다. <user-info>, 그리고 자식들에게 진행할 예정이지만 아직 진행되지 않았습니다.

사용자 지정 요소에 정보를 전달하려면 속성을 사용할 수 있습니다. 즉시 사용할 수 있습니다.

또는 자녀가 정말 필요한 경우 자녀에 대한 액세스를 지연 없이 setTimeout로 연기할 수 있습니다.

작동합니다:

<script>
customElements.define('user-info', class extends HTMLElement {

  connectedCallback() {
    setTimeout(() => alert(this.innerHTML)); // John (*)
  }

});
</script>

<user-info>John</user-info>

이제 alert 줄을 서서 (*) 는 HTML 구문 분석이 완료된 후 비동기식으로 실행할 때 "John"을 보여줍니다. 필요한 경우 자식을 처리하고 초기화를 완료할 수 있습니다.

반면에 이 솔루션도 완벽하지 않습니다. 중첩된 사용자 지정 요소도 setTimeout을 사용합니다. 스스로 초기화한 다음 줄을 서 있습니다: 외부 setTimeout을 먼저 트리거하고 내부 트리거를 트리거합니다.

따라서 외부 요소는 내부 요소보다 먼저 초기화를 완료합니다.

이를 예로 들어 설명하겠습니다:

<script>
customElements.define('user-info', class extends HTMLElement {
  connectedCallback() {
    alert(`${this.id} connected.`);
    setTimeout(() => alert(`${this.id} initialized.`));
  }
});
</script>

<user-info id="outer">
  <user-info id="inner"></user-info>
</user-info>

출력 순서:

  1. 외부 연결됨.
  2. 내부 연결.
  3. 외부 초기화됨.
  4. 내부 초기화됨.

외부 요소가 초기화를 완료하는 것을 명확하게 볼 수 있습니다. (3) 내면의 것 앞에 (4).

중첩된 요소가 준비된 후 트리거되는 내장 콜백은 없습니다. 필요한 경우 자체적으로 구현할 수 있습니다. 예를 들어, 내부 요소는 initialized과 같은 이벤트를 디스패치할 수 있습니다. 그리고 바깥쪽 사람들은 귀를 기울이고 반응할 수 있습니다.


Customized built-in elements 맞춤형 내장 요소

<time-formatted>과 같은 새로운 요소를 생성합니다. 이것에는 연관된 의미론이 없습니다. 검색 엔진에는 알려지지 않았으며 접근성 장치로는 이를 처리할 수 없습니다.

하지만 그런 것들이 중요할 수 있습니다. 예를 들어, 검색 엔진은 실제로 시간을 표시한다는 사실에 관심이 있을 것입니다. 특별한 종류의 버튼을 만들고 있다면 기존 버튼을 재사용하는 것은 어떨까요? <button> 기능처럼요.

클래스에서 상속하여 내장된 HTML 요소를 확장하고 사용자 지정할 수 있습니다.

예를 들어, 버튼은 다음과 같은 인스턴스입니다. HTMLButtonElement, 그것을 기반으로 합니다.

  1. HTMLButtonElement를 클래스와 함께 확장합니다. 
class HelloButton extends HTMLButtonElement { /* custom element methods */ }
  1. 세 번째 인수를 제공합니다. customElements.define, 태그를 지정합니다:
customElements.define('hello-button', HelloButton, {extends: 'button'});

동일한 DOM 클래스를 공유하는 태그가 다를 수 있으므로 필요한 extends를 지정합니다.

  1. 마지막에 사용자 지정 요소를 사용하려면 일반 요소 <button> 를 삽입합니다. 추가로 태그 안에 is="hello-button"을 설정합니다.

결과:

<button is="hello-button">...</button>

전체 예는 다음과 같습니다:

<script>
// The button that says "hello" on click
class HelloButton extends HTMLButtonElement {
  constructor() {
    super();
    this.addEventListener('click', () => alert("Hello!"));
  }
}


customElements.define('hello-button', HelloButton, {extends: 'button'});
</script>

<button is="hello-button">Click me</button>

<button is="hello-button" disabled>Disabled</button>

새로운 버튼은 내장 버튼을 확장합니다. 따라서 disabled 과 같은 스타일과 표준 기능을 유지합니다.

 

 


References
HTML Living Standard: https://html.spec.whatwg.org/#custom-elements.
Compatiblity: https://caniuse.com/#feat=custom-elements.


https://ko.javascript.info/custom-elements 의 글을 번역한 내용입니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함