사용자가 정의할 수 있고, 재사용가능하고, 캡슐화 가능한 HTML 태그를 생성할 수 있는 웹 플랫폼 API
일단 React 처럼 Component 를 만들어야 사용할 수 있으므로 Component 를 정의하고 window 에 등록한다.
등록하고 나면 일반적인 html tag 처럼 자연스럽게 사용할 수 있다.
class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);
// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', AppDrawer);
class AppDrawer extends HTMLElement {
// A getter/setter for an open property.
get open() {
return this.hasAttribute('open');
}
set open(val) {
// Reflect the value of the open property as an HTML attribute.
if (val) {
this.setAttribute('open', '');
} else {
this.removeAttribute('open');
}
this.toggleDrawer();
}
// A getter/setter for a disabled property.
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
// Reflect the value of the disabled property as an HTML attribute.
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
}
// Can define constructor arguments if you wish.
constructor() {
// If you define a constructor, always call super() first!
// This is specific to CE and required by the spec.
super();
// Setup a click listener on <app-drawer> itself.
this.addEventListener('click', e => {
// Don't toggle the drawer if it's disabled.
if (this.disabled) {
return;
}
this.toggleDrawer();
});
}
toggleDrawer() {
...
}
}
customElements.define('app-drawer', AppDrawer);
<app-drawer/>
과 같이 사용할 수 없다.<app-drawer></app-drawer>
****식으로 써야한다.생명주기에 넣은 함수들은 동기적으로 작동한다.
예를들어 el.setAttribute()
요소를 호출하면 브라우저는 즉시 attributeChangedCallback()
을 호출된다.
disconnectedCallback()
도 마찬가지로, el.remove()
을 사용해 요소가 DOM에서 제거된 직후 호출된다.
class AppDrawer extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
...
}
connectedCallback() {
...
}
disconnectedCallback() {
...
}
attributeChangedCallback(attrName, oldVal, newVal) {
...
}
}
customElement 는attributeChangedCallback
을 사용해 속성이 변경되었을 때 반응을 정의할 수 있다.
브라우저는 observedAttributes
에서 반환한 배열 에 나열된 속성이 변경될 때마다 attributeChangedCallback
를 호출합니다 .
class AppDrawer extends HTMLElement {
...
static get observedAttributes() {
return ['disabled', 'open'];
}
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
}
// Only called for the disabled and open attributes due to observedAttributes
attributeChangedCallback(name, oldValue, newValue) {
// When the drawer is disabled, update keyboard/screen reader behavior.
if (this.disabled) {
this.setAttribute('tabindex', '-1');
this.setAttribute('aria-disabled', 'true');
} else {
this.setAttribute('tabindex', '0');
this.setAttribute('aria-disabled', 'false');
}
// TODO: also react to the open attribute changing.
}
}
shadow dom 은 전체 dom tree 안에서 특정 부분을 분리시킬 수 있다.
customElement 에서 shadowDom 을 적용하려면 constructor 에서 설정하면 된다.
let tmpl = document.createElement('template');
tmpl.innerHTML = `
<style>:host { ... }</style> <!-- look ma, scoped styles -->
<b>I'm in shadow dom!</b>
<slot></slot>
`;
customElements.define('x-foo-shadowdom', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// Attach a shadow root to the element.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(tmpl.content.cloneNode(true));
}
...
});
html 에 이런식으로 나온다.
<x-foo-shadowdom>
<p><b>User's</b> custom text</p>
</x-foo-shadowdom>
<!-- renders as -->
<x-foo-shadowdom>
#shadow-root
<b>I'm in shadow dom!</b>
<slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>
<template>
template 요소를 사용하면 parse 되었지만 페이지 로드 시 비활성화되며 런타임시 활성화될 수 있는 DOM 조각을 선언할 수 있다.
vue 처럼 미리 html 구조를 선언해 놓고 JS 에서 window 에 등록해 사용
html tag 와 JS API 를 분리해서 관리할 수 있는 장점이 있다.
<template id="x-foo-from-template">
<style>
p { color: green; }
</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.</p>
</template>
<script>
let tmpl = document.querySelector('#x-foo-from-template');
// If your code is inside of an HTML Import you'll need to change the above line to:
// let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');
customElements.define('x-foo-from-template', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(tmpl.content.cloneNode(true));
}
...
});
</script>
<!-- user-defined styling -->
<style>
app-drawer {
display: flex;
}
panel-item {
transition: opacity 400ms ease-in-out;
opacity: 0.3;
flex: 1;
text-align: center;
border-radius: 50%;
}
panel-item:hover {
opacity: 1.0;
background: rgb(255, 0, 255);
color: white;
}
app-panel > panel-item {
padding: 5px;
list-style: none;
margin: 0 7px;
}
</style>
<app-drawer>
<panel-item>Do</panel-item>
<panel-item>Re</panel-item>
<panel-item>Mi</panel-item>
</app-drawer>
template 로 선언하고 아직 정의되지 않은 customElement 의 스타일을 지정할 수 있다.
사용자가 키보드등으로 정의되지 않은 customElement 에 focus 하는 것을 방지하는데 사용할 수 있다.
app-drawer:not(:defined) {
/* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
display: inline-block;
height: 100vh;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
참고
How we use Web Components at GitHub | The GitHub Blog
https://github.com/github/github-elements
The Complete Web Component Guide: Custom Elements | by Marius Bongarts | JavaScript in Plain English
Web Component: Why You Should Stick to React | by Nathan Sebhastian | Bits and Pieces (bitsrc.io)
https://www.webcomponents.org/introduction
Web Component 에 대하여 간략하게 알아봅시다. :: GoodBye World (tistory.com)
Shadow DOM v1: Self-Contained Web Components | Web Fundamentals | Google Developers