# web component

[web](master) ⟩ component

{% hint style="success" %}
_<mark style="color:purple;">**Web component**</mark>_ 讓你<mark style="color:yellow;">自定 HTML 元素</mark> ([custom element](component/custom-element))，封裝 [HTML](html) 結構、[CSS](css) 樣式和 [JavaScript](js) 行為。
{% endhint %}

{% tabs %}
{% tab title="🔴" %}
* [implement](component/implement "mention")：[custom-element](component/custom-element "mention")╱[shadow-dom](component/shadow-dom "mention")╱[template](component/template "mention")
* [use](component/use "mention")
* [examples](component/examples "mention")
{% endtab %}

{% tab title="💾" %}
📁 <mark style="color:yellow;">自訂元素</mark> ([custom element](component/custom-element))

```javascript
// ⭐️ 1️⃣ 宣告自訂元素
class MyComponent extends HTMLElement {
  
  // constructor
  constructor() {

    super();
    
    // ⭐️ 2️⃣ 建立 shadow DOM
    this.attachShadow({ mode: 'open' });  // returns a reference to this.shadowRoot
    
    // ⭐️ 3️⃣ 建立樣板 4️⃣ 複製樣板內容到 shadow root
    this.shadowRoot.innerHTML = `
        <style> ... </style>
        <div class="container"> ... </div>
    `;
    
  }// end of constructor()
  
}// end of MyComponent

// ⭐️ 5️⃣ 註冊標籤             ╭──tag name──╮  ╭─ class ─╮
window.customElements.define('my-component', MyComponent);
```

📁 <mark style="color:yellow;">自訂內建元素</mark> (<mark style="color:orange;">customized built-in element</mark>)

```javascript
// ⭐️ customized built-in element
class FancyButton extends HTMLButtonElement { 
    constructor(){...} 
}

// ⭐️ register new tag
//                    ╭─── name ───╮  ╭─ class ─╮  ╭───────⭐️────────╮
customElements.define('fancy-button', FancyButton, {extends: 'button'});
```
{% endtab %}

{% tab title="🛠" %}
* [Lit](https://lit.dev) (Google)
{% endtab %}

{% tab title="📗" %}
* [ ] 📗 JS.info ⟩ [Web component](https://javascript.info/web-components)
* [ ] :green\_book: Building Native Web Components (2021)
* [ ] :green\_book: JavaScript: The Definitive Guide (15.6 Web Components)
* [ ] frameworks：Angular, [React](react), Vue.js
* [ ] 📘 Google ⟩ Web Components ⟩&#x20;
  * [Custom Elements](https://developers.google.com/web/fundamentals/web-components/customelements)
  * [Shadow DOM](https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=en)
  * [Tips & Tricks](https://developers.google.com/web/fundamentals/web-components/shadowdom#tricks)
  * [Best Practices](https://developers.google.com/web/fundamentals/web-components/best-practices)
{% endtab %}

{% tab title="📘" %}
* MDN ⟩ &#x20;
  * [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) ⟩ [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) ⟩ [attachShadow()](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow)
  * [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) ⟩
    * [Using shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) ⭐️
      * [Using custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) ⭐️
      * [Using templates and slots](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots)
      * [Using External Styles](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM#internal_versus_external_styles)
* Google ⟩ [Web components](https://developers.google.com/web/fundamentals/web-components)&#x20;
  * 📘 [Shadow DOM v1: Self-Contained Web Components](https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=en)
{% endtab %}

{% tab title="💈" %}
* [A Typewriter, but using a New HTML Tag](https://dev.to/auroratide/a-typewriter-but-using-a-new-html-tag-60i)
* [Calculator](../js/proj/calculator#v-3-componet)
* [element](appendix/custom/element "mention")
{% endtab %}

{% tab title="👥" %}
* [client-side-javascript-timeline](browser/event/client-side-javascript-timeline "mention") - web components are not defined until fully loaded.
{% endtab %}

{% tab title="❓ " %}
{% hint style="warning" %}
問：「[<mark style="color:blue;">`attachShadow()`</mark>](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow) 應該放在 [<mark style="color:blue;">`constructor()`</mark>](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#implementing_a_custom_element) 還是 [<mark style="color:blue;">`connectedCallback()`</mark>](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks)❓」
{% endhint %}

{% hint style="success" %}
答： :point\_right: [Gemini](https://g.co/gemini/share/785a07b0af65) , [ChatGPT](https://chatgpt.com/share/676cf842-9d34-800e-8d06-76bed175e03a),&#x20;
{% endhint %}
{% endtab %}
{% endtabs %}

## 宣告方式 <a href="#how-to-define" id="how-to-define"></a>

{% tabs %}
{% tab title="1️⃣ custom element" %}
### 1️⃣ **定義** custom element <a href="#define" id="define"></a>

先定義一個繼承 <mark style="color:blue;">HTMLElement</mark> 的 [custom element](component/custom-element) ([class](js/val/class))：

```javascript
// ⭐️ 1️⃣ custom element
class MyComponent extends HTMLElement {
  constructor() {
  
    // ⭐️ 呼叫父類別的 constructor
    super(); 

    // 在這裡初始化你的 component
  }
}
```
{% endtab %}

{% tab title="2️⃣ shadow DOM" %}
### 2️⃣ **建立 Shadow DOM** <a href="#shadow-dom" id="shadow-dom"></a>

[Shadow DOM](component/shadow-dom) 為 _<mark style="color:purple;">**web component**</mark>_ 建立一個<mark style="color:yellow;">封裝的 DOM 樹</mark> (<mark style="color:orange;">encapsulated DOM tree</mark>)，讓內部的樣式 ([CSS](css) styles) 和腳本 ([JS](js) scripts) 不會影響到外面 ([light DOM](component/light-dom)) 的其他部分，反之亦然。

```javascript
constructor() {

  super();

  // ⭐️ 2️⃣ 建立 shadow DOM
  //   mode: 
  //   - 'open'  : 允許 JS 從外部訪問 shadow DOM
  //   - 'closed': 不允許
  this.attachShadow({ mode: 'open' }); 
}
```
{% endtab %}

{% tab title="3️⃣ template" %}
3️⃣ 建立樣板：以下提供<mark style="color:yellow;">三個方式</mark>建立 [template](component/template "mention")

{% tabs %}
{% tab title="1: HTML" %}
📁 HTML：<mark style="color:yellow;">定義</mark> [<mark style="color:blue;">`<template>`</mark>](component/template) 的 [HTML](html) <mark style="color:yellow;">結構</mark>：

```html
<!-- ⭐️ 3️⃣ 建立樣板 -->
<template id="my-component-template">

  <!---------- ⭐️ 內部的樣式 ---------->
  <style>
    .container {
      border: 1px solid black;
      padding: 10px;
    }
  </style>
  
  <!---------- ⭐️ HTML 結構 ---------->
  <div class="container">
    <h1>Hello from My Component!</h1>
    <!-- ⭐️ "unnamed slot"：預留給使用者插入自訂內容 -->
    <slot></slot>
  </div>
  
</template>
```
{% endtab %}

{% tab title="2: JS" %}
📁 JS：或將 3️⃣ 4️⃣ 兩步驟濃縮成這樣：

```javascript
// 1️⃣ 宣告 custom element
class MyComponent extends HTMLElement {
  
  // constructor
  constructor() {

    super();
    
    // 2️⃣ 建立 shadow DOM
    this.attachShadow({ mode: 'open' });  // returns a reference to this.shadowRoot
    
    // ⭐️ 3️⃣ 建立樣板 4️⃣ 複製樣板內容到 shadow root
    this.shadowRoot.innerHTML = `
        <style> ... </style>
        <div class="container"> ... </div>
    `;
    
  }// end of constructor()
  
}// end of MyComponent
```
{% endtab %}

{% tab title="3: static property" %}
📁 JS：或是將 [\<template>](component/template) 放在[靜態屬性](js/val/class/member/static)中：

```javascript
class MyComponent extends HTMLElement {
	
	// ⭐️ MyComponent.template
	static get template() {
		
		// 如果還沒初始化，執行初始化，然後將結果存到
		// 私有靜態屬性(MyComponent._template)中
		if (!this._template) {		// this == MyComponent
			// create <template> element
			this._template = document.createElement("template"); 
			// define template HTML structure
			this._template.innerHTML = `...`;
		}
		
		return this._template;
	}
	
	// constructor
	constructor() {
		
		// ---- super init (HTMLElement) ----
		super();

		// ---- self init (MyComponent) ----
		
		// ⭐️ attach shadow root
		let root = this.attachShadow({ mode: "open" });	// root == this.shadowRoot
		// ⭐️ clone template
		let copy = MyComponent.template.content.cloneNode(true); 
		// ⭐️ add template copy to shadow root
		root.appendChild(copy); 

		// other init process ...
	}
}
```
{% endtab %}
{% endtabs %}
{% endtab %}

{% tab title="4️⃣ shadow root" %}
### 4️⃣ 將樣板加入 Shadow DOM <a href="#append-template-content" id="append-template-content"></a>

<mark style="color:yellow;">複製樣板內容</mark>到 <mark style="color:orange;">shadow root</mark>：

```javascript
constructor() {
  super();
  this.attachShadow({ mode: 'open' });    // 2️⃣ 建立 shadow DOM
  
  // 3️⃣ 建立 <template>

  // ⭐️ 4️⃣ 複製樣板內容到 shadow root
  const template = document.getElementById('my-component-template');
  this.shadowRoot.appendChild(template.content.cloneNode(true));
}
```

或將 3️⃣ 4️⃣ 兩個步驟濃縮成這樣：

```javascript
// 1️⃣ 宣告 custom element
class MyComponent extends HTMLElement {
  
  // constructor
  constructor() {

    super();
    
    // 2️⃣ 建立 shadow DOM
    this.attachShadow({ mode: 'open' });  
    
    // ⭐️ 3️⃣ 建立樣板 4️⃣ 複製樣板內容到 shadow root
    this.shadowRoot.innerHTML = `
        <style> ... </style>
        <div class="container"> ... </div>
    `;
    
  }// end of constructor()
  
}// end of MyComponent
```
{% endtab %}

{% tab title="5️⃣ register" %}
### 5️⃣ 註冊 Component <a href="#register" id="register"></a>

用 <mark style="color:blue;">`customElements.define()`</mark> 註冊 _<mark style="color:purple;">**web component**</mark>_：

{% hint style="warning" %}
:star: 注意：_<mark style="color:purple;">**web component**</mark>_ 的<mark style="color:yellow;">標籤名稱</mark> (<mark style="color:orange;">tag name</mark>) <mark style="color:red;">必須</mark><mark style="color:yellow;">包含連字號</mark> "<mark style="color:blue;">`-`</mark>":exclamation:
{% endhint %}

```javascript
// ⭐️ 5️⃣ 註冊 Component       ╭──tag name──╮  ╭─ class ─╮
window.customElements.define('my-component', MyComponent);
```
{% endtab %}
{% endtabs %}

## 使用方式 <a href="#how-to-use" id="how-to-use"></a>

{% tabs %}
{% tab title="自訂元素" %}
📁 HTML：<mark style="color:yellow;">使用</mark>自訂元素 ([custom element](component/custom-element))

```markup
<!-- ⭐️ custom element -->
<my-component>
    <!-- slotted nodes -->
</my-component>
```

📁 JS：<mark style="color:yellow;">定義並宣告</mark>

```javascript
// ⭐️ autonomous custom element ---------------
// autonomous：獨立存在的（非內建的，如：HTMLButtonElement 等）
class MyComponent extends HTMLElement {

    // constructor
    constructor() {

        // tell HTMLElement to initialize itself.
        super();

        // get `width`, `height` attributes
        const height = this.getAttribute('height') || '100px';
        const width = this.getAttribute('width') || '100px';
        // ...
    }
    
    // connected to document
    connectedCallback() {

        // attach shadow root
        let root = this.attachShadow({mode: 'open'});

        // ⭐️ clone nodes from <template>
        let clone = tmpl.content.cloneNode(true);
        root.append( clone );

        // ...
    }

    // template HTML
    static template(height, width) { 
        return ` 
            <style> 
                .placeholder { 
                    background-color: pink; 
                    width: ${height}; 
                    height: ${width}; 
                }
            </style>

            <div class='placeholder'>Placeholder</div>
        `;
    }
} 

// ⭐️  register <my-component>
// ⭐️⭐️ custom element names must contain a "hyphen".
//                    ╭──tag name──╮  ╭─ class ─╮
customElements.define('my-component', MyComponent);
```
{% endtab %}

{% tab title="自訂內建元素" %}
📁 JS：<mark style="color:yellow;">定義並宣告</mark>

```javascript
// ⭐️ customized built-in element
class FancyButton extends HTMLButtonElement { 
    constructor(){...} 
}

// ⭐️ register new tag
//                    ╭─── name ───╮  ╭─ class ─╮  ╭───────⭐️────────╮
customElements.define('fancy-button', FancyButton, {extends: 'button'});
```

📁 HTML：<mark style="color:yellow;">使用</mark>自訂內建元素 (<mark style="color:orange;">customized built-in element</mark>)

```markup
<!-- ⭐️ customized built-in element -->
<!--       ╭─── name ───╮           -->
<button is="fancy-button">some text</button>
```

📁 JS：<mark style="color:yellow;">使用</mark>自訂內建元素

```javascript
// ⭐️ 方法ㄧ： `new MyComponent()`
let button = new FancyButton();

// ⭐️ 方法二： `document.createElement('my-component')` 
//                                  ╭─tag──╮  ╭────────⭐️────────╮
let button = document.createElement('button', {is: 'fancy-button'});

// customize
button.textContent = 'Fancy button!';
button.disabled = true;

// add to DOM tree
document.body.appendChild(button);
```
{% endtab %}

{% tab title="💈範例" %}
* [Calculator](../js/proj/calculator#v-3-componet) ❤️
{% endtab %}
{% endtabs %}
