Shadow DOM ในภาษา JavaScript

Shadow DOM ในภาษา JavaScript

บทความนี้จะอธิบายเรื่อง Shadow DOM ในภาษา JavaScript

YouTube Video

javascript-html-shadow-dom.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    h1, h2 {
 32        font-size: 1.2rem;
 33        color: #007bff;
 34        margin-top: 0.5em;
 35        margin-bottom: 0.5em;
 36        border-left: 5px solid #007bff;
 37        padding-left: 0.6em;
 38        background-color: #e9f2ff;
 39    }
 40
 41    button {
 42        display: block;
 43        margin: 1em auto;
 44        padding: 0.75em 1.5em;
 45        font-size: 1rem;
 46        background-color: #007bff;
 47        color: white;
 48        border: none;
 49        border-radius: 6px;
 50        cursor: pointer;
 51        transition: background-color 0.3s ease;
 52    }
 53
 54    button:hover {
 55        background-color: #0056b3;
 56    }
 57
 58    #output {
 59        margin-top: 1em;
 60        background-color: #1e1e1e;
 61        color: #0f0;
 62        padding: 1em;
 63        border-radius: 8px;
 64        min-height: 200px;
 65        font-family: Consolas, monospace;
 66        font-size: 0.95rem;
 67        overflow-y: auto;
 68        white-space: pre-wrap;
 69    }
 70
 71    .highlight {
 72        outline: 3px solid #ffc107; /* yellow border */
 73        background-color: #fff8e1;  /* soft yellow background */
 74        transition: background-color 0.3s ease, outline 0.3s ease;
 75    }
 76
 77    .active {
 78        background-color: #28a745; /* green background */
 79        color: #fff;
 80        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 81        transition: background-color 0.3s ease, box-shadow 0.3s ease;
 82    }
 83  </style>
 84</head>
 85<body>
 86    <div class="container">
 87        <h2>HTML Sample</h2>
 88        <div id="content"></div>
 89        <div id="shadow-host">Shadow Root Element</div>
 90        <my-card></my-card>
 91    </div>
 92
 93    <div class="container">
 94        <h1>JavaScript Console</h1>
 95        <button id="executeBtn">Execute</button>
 96        <div id="output"></div>
 97    </div>
 98
 99    <div>
100        <h2>Slot Sample</h2>
101        <my-element>
102            <h3 slot="header">Header Content</h1>
103            <p slot="content">Main Content</p>
104        </my-element>
105    </div>
106
107    <script>
108        // Override console.log to display messages in the #output element
109        (function () {
110            const originalLog = console.log;
111            console.log = function (...args) {
112                originalLog.apply(console, args);
113                const output = document.getElementById('output');
114                output.textContent += args.map(String).join(' ') + '\n';
115            };
116        })();
117
118        document.getElementById('executeBtn').addEventListener('click', () => {
119            // Prevent multiple loads
120            if (document.getElementById('externalScript')) return;
121
122            const script = document.createElement('script');
123            script.src = 'javascript-html-shadow-dom.js';
124            script.id = 'externalScript';
125            //script.onload = () => console.log('javascript-html-shadow-dom.js loaded and executed.');
126            //script.onerror = () => console.log('Failed to load javascript-html-shadow-dom.js.');
127            document.body.appendChild(script);
128        });
129    </script>
130</body>
131</html>

การทำความเข้าใจกับ Shadow DOM

Shadow DOM เป็นฟีเจอร์สำคัญของมาตรฐาน Web Components ที่ช่วยให้สามารถแคปซูลสไตล์และโครงสร้าง DOM ภายในคอมโพเนนต์ได้ ฟีเจอร์นี้ช่วยป้องกันการรบกวนระหว่างสไตล์และสคริปต์ของคอมโพเนนต์และเอกสารหลัก

Shadow DOM ในภาษา JavaScript

Shadow DOM ช่วยสร้างโดมที่มีขอบเขตเฉพาะ ซึ่งเชื่อมโยงกับองค์ประกอบ DOM ปกติ Shadow tree นี้ถูกแยกออกจากเอกสารโดยรวม ซึ่งสไตล์และสคริปต์ภายนอกจะไม่ส่งผลต่อมัน และสไตล์และสคริปต์ภายในก็จะไม่รั่วไหลออกไปเช่นกัน

ตัวอย่างเช่น หากคุณสร้างคอมโพเนนต์ปุ่มที่กำหนดเองโดยใช้ Shadow DOM สไตล์ของมันจะไม่รบกวนกับองค์ประกอบอื่นบนหน้าเว็บ ในทำนองเดียวกัน องค์ประกอบที่มีชื่อคลาสเดียวกันก็จะไม่เกิดความขัดแย้งกัน

เนื้อหา HTML ปกติที่อยู่นอก Shadow DOM จะเรียกว่า Light DOM

ข้อดีของ Shadow DOM

  1. การห่อหุ้มข้อมูล (Encapsulation)

    • Shadow DOM แยกสไตล์และฟังก์ชันออกจากกัน ช่วยป้องกันปัญหาการชนกันกับ style และ script ส่วนกลาง
  2. การนำกลับมาใช้ซ้ำ (Reusability)

    • คอมโพเนนต์ที่สร้างด้วย Shadow DOM สามารถนำกลับมาใช้ซ้ำในโปรเจกต์อื่น ๆ ได้โดยไม่ต้องกังวลเรื่อง style ที่จะชนกัน
  3. ความง่ายต่อการดูแลรักษา (Maintainability)

    • การห่อหุ้มข้อมูลทำให้ logic และ style ของคอมโพเนนต์แยกออกจากกันอย่างชัดเจน จึงง่ายต่อการดีบักและดูแลรักษา

การสร้าง Shadow DOM

ในการใช้ Shadow DOM คุณจำเป็นต้องแนบ shadow root เข้ากับองค์ประกอบ HTML ตัวอย่างง่าย ๆ มีดังนี้:

1// Select the host element
2const hostElement = document.querySelector('#shadow-host');
3
4// Attach a shadow root
5const shadowRoot = hostElement.attachShadow({ mode: 'open' });

คำอธิบาย

โค้ดนี้ประกอบด้วยองค์ประกอบต่อไปนี้:

  1. Host Element

    • องค์ประกอบ DOM ปกติที่นำ shadow root ไปแนบ (ในตัวอย่างนี้คือ #shadow-host)
  2. Shadow Root

    • รากของ shadow tree ที่สร้างโดยใช้คำสั่ง attachShadow
  3. โหมด (Mode)

    • ในโหมด open JavaScript ภายนอกสามารถเข้าถึง shadow root ได้ผ่าน element.shadowRoot ในทางกลับกัน โหมด closed ไม่อนุญาตให้มีการเข้าถึง

การจัดสไตล์ภายใน Shadow DOM

Shadow DOM มีขอบเขตสไตล์ของตัวเอง สไตล์ที่กำหนดใน shadow tree มีผลเฉพาะกับองค์ประกอบภายใน tree นั้น นี่คือตัวอย่าง:

1// Add content to the shadow root
2shadowRoot.innerHTML = `
3    <style>
4        p {
5            color: green;
6        }
7    </style>
8    <p>Scoped style inside Shadow DOM.</p>
9`;

ถึงแม้จะมี style ชนกันในเอกสารหลัก ก็จะไม่ส่งผลกับย่อหน้าที่อยู่ใน shadow tree

1const content = document.getElementById('content');
2content.innerHTML = `
3    <style>
4        p {
5            color: red;
6        }
7    </style>
8    <p>This is in the main DOM.</p>
9`;
  • ย่อหน้าภายใน Shadow DOM จะยังคงเป็นสีเขียว ในขณะที่ย่อหน้าภายนอกจะเป็นสีแดง

เหตุการณ์ภายใน Shadow DOM

เหตุการณ์ภายใน Shadow DOM คล้ายกับเหตุการณ์ใน DOM ทั่วไป แต่อาจทำงานแตกต่างกันในแง่ของการแพร่กระจายเนื่องจากการห่อหุ้ม

นี่คือตัวอย่าง:

 1// Add an event listener inside Shadow DOM
 2shadowRoot.innerHTML = `
 3    <div id="button-container">
 4        <button id="shadow-button">Click Me</button>
 5    </div>
 6`;
 7
 8shadowRoot.querySelector('#shadow-button').addEventListener('click', (event) => {
 9    console.log('Button : Button clicked inside Shadow DOM');
10    console.log(event.target);
11});
12
13shadowRoot.querySelector('#button-container').addEventListener('click', (event) => {
14    console.log('Container : Button clicked inside Shadow DOM');
15    console.log(event.target);
16});
17
18hostElement.addEventListener('click', (event) => {
19    console.log('Event bubbled to the host element');
20    console.log(event.target);
21});
22
23document.addEventListener('click', (event) => {
24    console.log('Document listener');
25    console.log(event.target);
26});
  • เมื่อคลิกปุ่ม ตัวดักจับเหตุการณ์ทั้งสองตัวจะถูกเรียกใช้งาน แสดงการทำงานของการแพร่กระจายเหตุการณ์
  • เมื่อเหตุการณ์ที่เกิดขึ้นภายใน Shadow DOM ถูกส่งต่อไปยัง Light DOM ค่า target ของเหตุการณ์นั้นจะถูกเปลี่ยนเป็นโฮสต์เอลิเมนต์แทนแหล่งที่มาดั้งเดิม
    • ในตัวอย่างนี้ event.target ภายใน Shadow DOM คือองค์ประกอบ button จริงๆ แต่ถ้าอยู่นอก Shadow DOM จะถูกแทนที่ด้วยองค์ประกอบโฮสต์ div

Shadow DOM และอิลิเมนต์แบบกำหนดเอง

Shadow DOM และอิลิเมนต์แบบกำหนดเองเป็นส่วนประกอบสำคัญของ Web Components เทคโนโลยีเหล่านี้ใช้ในการสร้างองค์ประกอบ UI ที่สามารถนำกลับมาใช้ใหม่และมีการห่อหุ้มอย่างสมบูรณ์

 1class MyCard extends HTMLElement {
 2    constructor() {
 3        super();
 4        const shadow = this.attachShadow({ mode: 'open' });
 5        shadow.innerHTML = `
 6        <style>
 7            p {
 8                color: blue;
 9            }
10        </style>
11        <p>I'm inside shadow DOM</p>
12        `;
13    }
14}
15
16customElements.define('my-card', MyCard);

เอกสารหลักมี HTML ดังนี้:

1<my-card></my-card>
  • โดยการใช้ Shadow DOM ภายในอิลิเมนต์แบบกำหนดเอง คุณสามารถสร้างองค์ประกอบที่นำกลับมาใช้ใหม่ได้และป้องกันปัญหาสไตล์ชนกัน ในโค้ดนี้จะมีการสร้างแท็กชื่อ my-card และเชื่อมโยงกับคลาส MyCard องค์ประกอบ <p> ภายใน my-card จะไม่ได้รับผลกระทบจากสไตล์ภายนอกและจะแสดงเป็นสีน้ำเงินเสมอ

Slots: กระจายเนื้อหาของ Light DOM

Slots ช่วยให้คุณสามารถนำเนื้อหาจาก Light DOM มาแสดงใน Shadow DOM ได้ นี่คือตัวอย่าง:

 1class MyElement extends HTMLElement {
 2    constructor() {
 3        super();
 4        const shadow = this.attachShadow({ mode: 'open' });
 5
 6        shadow.innerHTML = `
 7        <style>
 8            .container {
 9                border: 2px solid #ccc;
10                padding: 16px;
11                border-radius: 8px;
12                font-family: sans-serif;
13            }
14            .header {
15                font-size: 1.2em;
16                color: darkblue;
17                margin-bottom: 8px;
18            }
19            .content {
20                font-size: 1em;
21                color: #333;
22            }
23        </style>
24        <div class="container">
25            <div class="header">
26                <slot name="header"></slot>
27            </div>
28            <div class="content">
29                <slot name="content"></slot>
30            </div>
31        </div>
32        `;
33    }
34}
35
36customElements.define('my-element', MyElement);

เอกสารหลักมี HTML ดังนี้:

1<my-element>
2    <h3 slot="header">Header Content</h1>
3    <p slot="content">Main Content</p>
4</my-element>
  • องค์ประกอบ slot ที่อยู่ใน Shadow DOM จะใช้แสดงเนื้อหาจาก Light DOM ที่มี attribute slot ตรงกัน

สรุป

Shadow DOM เป็นเครื่องมือสำคัญสำหรับการสร้างส่วนประกอบเว็บที่แข็งแกร่ง ใช้งานซ้ำได้ และดูแลง่าย โดยการห่อหุ้มสไตล์และฟังก์ชันการทำงาน มันช่วยลดความเป็นไปได้ของการขัดแย้งและทำให้การจัดการโค้ดง่ายขึ้น

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video