Shadow DOM 201

CSS และการจัดรูปแบบ

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

บทนำ

ว่ากันตามจริง มาร์กอัปที่ไม่มีการจัดรูปแบบนั้นไม่น่าดึงดูดใจ เราโชคดีที่มีทีมที่ยอดเยี่ยมที่อยู่เบื้องหลัง Web Components คาดการณ์เรื่องนี้ไว้และไม่ได้ปล่อยให้เรารอ โมดูลการกำหนดขอบเขต CSS จะกำหนดตัวเลือกมากมายสำหรับการจัดสไตล์เนื้อหาในทรีเงา

การห่อหุ้มสไตล์

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

<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
  <style>
    h3 {
      color: red;
    }
  </style>
  <h3>Shadow DOM</h3>
`;
</script>

ข้อสังเกตที่น่าสนใจ 2 ข้อเกี่ยวกับการสาธิตนี้ ได้แก่

  • มี h3 อื่นๆ ในหน้านี้ แต่มีเพียงรายการเดียวที่ตรงกับตัวเลือก h3 และได้รับการจัดสไตล์เป็นสีแดง ซึ่งก็คือรายการที่อยู่ใน ShadowRoot อีกครั้ง รูปแบบที่มีขอบเขตโดยค่าเริ่มต้น
  • กฎสไตล์อื่นๆ ที่กําหนดไว้ในหน้านี้ซึ่งกำหนดเป้าหมายเป็น h3 จะไม่ส่งผลต่อเนื้อหาของฉัน เนื่องจากตัวเลือกจะไม่ข้ามขอบเขตเงา

สรุปเรื่องราว เรามีการห่อหุ้มสไตล์จากโลกภายนอก ขอบคุณ Shadow DOM

จัดแต่งองค์ประกอบโฮสต์

:host ช่วยให้คุณเลือกและจัดรูปแบบองค์ประกอบที่โฮสต์ทรีเงาได้ ดังนี้

<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
  <style>
    :host {
      text-transform: uppercase;
    }
  </style>
  <content></content>
`;
</script>

ข้อควรระวังอย่างหนึ่งคือกฎในหน้าหลักมีความเฉพาะเจาะจงมากกว่ากฎ :host ที่กําหนดไว้ในองค์ประกอบ แต่มีความเฉพาะเจาะจงน้อยกว่าแอตทริบิวต์ style ที่กําหนดไว้ในองค์ประกอบโฮสต์ ซึ่งช่วยให้ผู้ใช้ลบล้างการจัดรูปแบบของคุณจากภายนอกได้ นอกจากนี้ :host ยังใช้ได้ในบริบทของ ShadowRoot เท่านั้น คุณจึงใช้นอก Shadow DOM ไม่ได้

รูปแบบฟังก์ชันของ :host(<selector>) ช่วยให้คุณกําหนดเป้าหมายองค์ประกอบโฮสต์ได้หากตรงกับ <selector>

ตัวอย่าง - จับคู่เฉพาะในกรณีที่องค์ประกอบเองมีคลาส .different (เช่น <x-foo class="different"></x-foo>)

:host(.different) {
    ...
}

การตอบสนองต่อสถานะของผู้ใช้

กรณีการใช้งานที่พบบ่อยของ :host คือเมื่อคุณสร้างองค์ประกอบที่กําหนดเองและต้องการตอบสนองต่อสถานะต่างๆ ของผู้ใช้ (:hover, :focus, :active ฯลฯ)

<style>
  :host {
    opacity: 0.4;
    transition: opacity 420ms ease-in-out;
  }
  :host(:hover) {
    opacity: 1;
  }
  :host(:active) {
    position: relative;
    top: 3px;
    left: 3px;
  }
</style>

การกำหนดธีมองค์ประกอบ

คลาสจำลอง :host-context(<selector>) จะจับคู่กับองค์ประกอบโฮสต์หากองค์ประกอบนั้นหรือบรรพบุรุษขององค์ประกอบตรงกับ <selector>

การใช้งาน :host-context() ที่พบบ่อยคือการกำหนดธีมให้กับองค์ประกอบโดยอิงตามองค์ประกอบรอบๆ ตัวอย่างเช่น ผู้ใช้จํานวนมากใช้ธีมโดยการใช้คลาสกับ <html> หรือ <body>

<body class="different">
  <x-foo></x-foo>
</body>

คุณ:host-context(.different)เพื่อจัดสไตล์ <x-foo> ได้เมื่อเป็นองค์ประกอบที่สืบทอดมาจากองค์ประกอบที่มีคลาส .different ดังนี้

:host-context(.different) {
  color: red;
}

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

รองรับโฮสต์หลายประเภทจากภายในรูทเงาเดียว

อีกการใช้งานหนึ่งของ :host คือในกรณีที่คุณสร้างไลบรารีธีมและต้องการรองรับการจัดสไตล์องค์ประกอบโฮสต์หลายประเภทจากภายใน Shadow DOM เดียวกัน

:host(x-foo) {
    /* Applies if the host is a <x-foo> element.*/
}

:host(x-foo:host) {
    /* Same as above. Applies if the host is a <x-foo> element. */
}

:host(div) {
    /* Applies if the host element is a <div>. */
}

จัดแต่งสไตล์ภายในของ Shadow DOM จากภายนอก

องค์ประกอบจำลอง ::shadow และตัวรวม /deep/ เปรียบเสมือนการมีดาบ Vorpal ที่ทรงอำนาจของ CSS ซึ่งช่วยให้เจาะผ่านขอบเขตของ Shadow DOM เพื่อจัดรูปแบบองค์ประกอบภายในต้นไม้เงาได้

องค์ประกอบจำลอง ::shadow

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

เช่น หากองค์ประกอบโฮสต์รูทเงา คุณสามารถเขียน #host::shadow span {} เพื่อจัดสไตล์สแปนทั้งหมดภายในต้นไม้เงาได้

<style>
  #host::shadow span {
    color: red;
  }
</style>

<div id="host">
  <span>Light DOM</span>
</div>

<script>
  var host = document.querySelector('div');
  var root = host.createShadowRoot();
  root.innerHTML = `
    <span>Shadow DOM</span>
    <content></content>
  `;
</script>

ตัวอย่าง (องค์ประกอบที่กําหนดเอง) - <x-tabs> มีองค์ประกอบย่อย <x-panel> ใน Shadow DOM แต่ละแผงจะโฮสต์ต้นไม้เงาของตัวเองซึ่งมีส่วนหัว h2 หากต้องการจัดรูปแบบส่วนหัวเหล่านั้นจากหน้าหลัก ให้เขียนดังนี้

x-tabs::shadow x-panel::shadow h2 {
    ...
}

ตัวคอมบิเนเตอร์ /deep/

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

ตัวรวม /deep/ มีประโยชน์อย่างยิ่งในโลกขององค์ประกอบที่กําหนดเองซึ่งมี Shadow DOM หลายระดับอยู่ทั่วไป ตัวอย่างที่เห็นได้ชัดคือการวางซ้อนองค์ประกอบที่กำหนดเองหลายรายการ (แต่ละรายการโฮสต์ทรีเงาของตัวเอง) หรือการสร้างองค์ประกอบที่รับค่ามาจากองค์ประกอบอื่นโดยใช้ <shadow>

ตัวอย่าง (องค์ประกอบที่กําหนดเอง) - เลือกองค์ประกอบ <x-panel> ทั้งหมดที่เป็นรายการสืบทอดของ <x-tabs> ในทุกที่ในต้นไม้

x-tabs /deep/ x-panel {
    ...
}

ตัวอย่าง - จัดรูปแบบองค์ประกอบทั้งหมดที่มีคลาส .library-theme ได้ทุกที่ในทรีเงา

body /deep/ .library-theme {
    ...
}

การใช้งาน querySelector()

คอมบิเนเตอร์จะเปิดทรีเงาสําหรับการเรียกดูตัวเลือกเช่นเดียวกับที่ .shadowRoot เปิดทรีเงาสําหรับการเรียกดู DOM แทนที่จะเขียนเงื่อนไขแบบซ้อนกัน คุณเขียนคำสั่งเดียวได้ดังนี้

// No fun.
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');

การจัดรูปแบบองค์ประกอบเนทีฟ

การควบคุม HTML ดั้งเดิมจัดรูปแบบได้ยาก หลายคนจึงเลิกใช้และสร้างแอปขึ้นมาเอง แต่ ::shadow และ /deep/ จะช่วยให้คุณจัดสไตล์องค์ประกอบใดก็ได้ในแพลตฟอร์มเว็บที่ใช้ Shadow DOM ตัวอย่างที่ดี ได้แก่ <input> และ <video>

video /deep/ input[type="range"] {
  background: hotpink;
}

การสร้างฮุกสไตล์

การปรับแต่งทำได้ดี ในบางกรณี คุณอาจต้องการเจาะรูในเกราะการจัดสไตล์ของ Shadow และสร้างฮุกเพื่อให้ผู้อื่นจัดสไตล์

การใช้ ::shadow และ /deep/

/deep/ มีความสามารถมากมาย ซึ่งช่วยให้ผู้เขียนคอมโพเนนต์มีวิธีกำหนดองค์ประกอบแต่ละรายการให้ปรับแต่งสไตล์ได้ หรือกำหนดองค์ประกอบจำนวนมากให้ปรับธีมได้

ตัวอย่าง - จัดรูปแบบองค์ประกอบทั้งหมดที่มีคลาส .library-theme โดยละเว้น Shadow Tree ทั้งหมด

body /deep/ .library-theme {
    ...
}

การใช้องค์ประกอบจำลองที่กําหนดเอง

ทั้ง WebKit และ Firefox กำหนดองค์ประกอบจำลองสำหรับการจัดสไตล์ชิ้นส่วนภายในขององค์ประกอบเบราว์เซอร์เนทีฟ ตัวอย่างที่ดีคือ input[type=range] คุณจัดรูปแบบแถบเลื่อน <span style="color:blue">blue</span> ได้โดยกำหนดเป้าหมาย ::-webkit-slider-thumb ดังนี้

input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

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

คุณสามารถกำหนดองค์ประกอบเป็นองค์ประกอบจำลองที่กำหนดเองได้โดยใช้แอตทริบิวต์ pseudo ค่าหรือชื่อขององค์ประกอบต้องขึ้นต้นด้วย "x-" ซึ่งจะสร้างการเชื่อมโยงกับองค์ประกอบนั้นในทรีเงาและกำหนดช่องทางให้บุคคลภายนอกข้ามขอบเขตเงา

ต่อไปนี้เป็นตัวอย่างการสร้างวิดเจ็ตแถบเลื่อนที่กําหนดเองและอนุญาตให้ผู้ใช้จัดรูปแบบแถบเลื่อนเป็นสีฟ้า

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <div>
      <div pseudo="x-slider-thumb"></div>' +
    </div>
  `;
</script>

การใช้ตัวแปร CSS

วิธีสร้างฮุกสำหรับธีมที่มีประสิทธิภาพคือการใช้ตัวแปร CSS หลักๆ แล้วก็คือการสร้าง "ตัวยึดตำแหน่งสไตล์" ให้ผู้ใช้รายอื่นกรอก

ลองจินตนาการถึงผู้เขียนองค์ประกอบที่กําหนดเองซึ่งทำเครื่องหมายตัวยึดตําแหน่งตัวแปรใน Shadow DOM 1 รายการสำหรับกำหนดสไตล์แบบอักษรของปุ่มภายใน และอีก 1 รายการสำหรับกำหนดสี

button {
  color: var(--button-text-color, pink); /* default color will be pink */
  font-family: var(--button-font);
}

จากนั้นผู้ฝังองค์ประกอบจะกําหนดค่าเหล่านั้นตามต้องการ อาจเป็นเพราะธีม Comic Sans ที่เจ๋งสุดๆ ของหน้าเว็บ

#host {
  --button-text-color: green;
  --button-font: "Comic Sans MS", "Comic Sans", cursive;
}

เนื่องจากตัวแปร CSS มีการรับค่ามา ทุกอย่างจึงเรียบร้อยดีและทำงานได้อย่างยอดเยี่ยม ภาพรวมทั้งหมดจะมีลักษณะดังนี้

<style>
  #host {
    --button-text-color: green;
    --button-font: "Comic Sans MS", "Comic Sans", cursive;
  }
</style>
<div id="host">Host node</div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <style>
      button {
        color: var(--button-text-color, pink);
        font-family: var(--button-font);
      }
    </style>
    <content></content>
  `;
</script>

รีเซ็ตรูปแบบ

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

resetStyleInheritance

ด้านล่างนี้คือตัวอย่างที่แสดงให้เห็นว่าการเปลี่ยนแปลง resetStyleInheritance ส่งผลต่อต้นไม้เงาอย่างไร

<div>
  <h3>Light DOM</h3>
</div>

<script>
  var root = document.querySelector('div').createShadowRoot();
  root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
  root.innerHTML = `
    <style>
      h3 {
        color: red;
      }
    </style>
    <h3>Shadow DOM</h3>
    <content select="h3"></content>
  `;
</script>

<div class="demoarea" style="width:225px;">
  <div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
  <button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>

<script>
  var container = document.querySelector('#style-ex-inheritance');
  var root = container.createShadowRoot();
  //root.resetStyleInheritance = false;
  root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';

  document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
    root.resetStyleInheritance = !root.resetStyleInheritance;
    e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
    document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
  });
</script>
พร็อพเพอร์ตี้ที่รับค่ามาจากเครื่องมือสำหรับนักพัฒนาเว็บ

การทําความเข้าใจ .resetStyleInheritance นั้นทําได้ยากกว่าเล็กน้อย เนื่องจากมีผลกระทบต่อพร็อพเพอร์ตี้ CSS ที่รับค่าได้เท่านั้น ข้อความระบุว่า: เมื่อคุณกําลังมองหาพร็อพเพอร์ตี้ที่จะรับค่ามา ที่ขอบเขตระหว่างหน้าเว็บกับ ShadowRoot อย่ารับค่าจากโฮสต์ แต่ให้ใช้ค่า initial แทน (ตามข้อกําหนดของ CSS)

หากไม่แน่ใจว่าพร็อพเพอร์ตี้ใดรับช่วงใน CSS โปรดดูรายการที่มีประโยชน์นี้ หรือสลับช่องทำเครื่องหมาย "แสดงรายการที่รับช่วง" ในแผงองค์ประกอบ

จัดสไตล์โหนดที่กระจาย

โหนดที่กระจายคือองค์ประกอบที่แสดงผลที่จุดแทรก (องค์ประกอบ <content>) องค์ประกอบ <content> ช่วยให้คุณเลือกโหนดจาก Light DOM และแสดงผลโหนดเหล่านั้นในตำแหน่งที่กําหนดไว้ล่วงหน้าใน Shadow DOM ได้ องค์ประกอบเหล่านี้ไม่ได้อยู่ใน Shadow DOM ในทางตรรกะ แต่ยังคงเป็นองค์ประกอบย่อยขององค์ประกอบโฮสต์ จุดแทรกเป็นเพียงเรื่องของการแสดงผล

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

องค์ประกอบจำลอง ::content

โหนดที่กระจายอยู่เป็นองค์ประกอบย่อยขององค์ประกอบโฮสต์ เราจะกำหนดเป้าหมายโหนดเหล่านั้นจากภายใน Shadow DOM ได้อย่างไร คำตอบคือองค์ประกอบสมมติ ::content ของ CSS นี่เป็นวิธีกำหนดเป้าหมายโหนด Light DOM ที่ผ่านจุดแทรก เช่น

::content > h3 จัดรูปแบบแท็ก h3 ทั้งหมดที่ผ่านจุดแทรก

มาดูตัวอย่างกัน

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>

<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
  <style>
    h3 { color: red; }
      content[select="h3"]::content > h3 {
      color: green;
    }
    ::content section p {
      text-decoration: underline;
    }
  </style>
  <h3>Shadow DOM</h3>
  <content select="h3"></content>
  <content select="section"></content>
`;
</script>

รีเซ็ตรูปแบบที่จุดแทรก

เมื่อสร้าง ShadowRoot คุณจะมีตัวเลือกในการรีเซ็ตสไตล์ที่รับค่ามา จุดแทรก <content> และ <shadow> ก็มีตัวเลือกนี้ด้วย เมื่อใช้องค์ประกอบเหล่านี้ ให้ตั้งค่า .resetStyleInheritance ใน JS หรือใช้แอตทริบิวต์บูลีน reset-style-inheritance ในองค์ประกอบนั้นๆ

  • สําหรับจุดแทรก ShadowRoot หรือ <shadow>: reset-style-inheritance หมายความว่าระบบตั้งค่าพร็อพเพอร์ตี้ CSS ที่รับค่าได้เป็น initial ที่โฮสต์ ก่อนที่จะนำไปใช้กับเนื้อหาเงา ตำแหน่งนี้เรียกว่า "ขอบเขตบน"

  • สำหรับจุดแทรก <content>: reset-style-inheritance หมายถึงรับค่าได้ ระบบจะตั้งค่าพร็อพเพอร์ตี้ CSS เป็น initial ก่อนกระจายรายการย่อยของโฮสต์ ณ จุดแทรก ตำแหน่งนี้เรียกว่าขอบเขตล่าง

บทสรุป

ในฐานะผู้เขียนองค์ประกอบที่กำหนดเอง เราจึงมีตัวเลือกมากมายในการควบคุมรูปลักษณ์ของเนื้อหา Shadow DOM เป็นพื้นฐานของยุคใหม่นี้

Shadow DOM ช่วยให้เราห่อหุ้มสไตล์แบบมีขอบเขตและมีวิธีรับข้อมูลภายนอกได้มาก (หรือน้อย) เท่าที่ต้องการ การกําหนดองค์ประกอบจำลองที่กําหนดเองหรือใส่ตัวยึดตําแหน่งตัวแปร CSS ช่วยให้ผู้เขียนระบุฮุกการจัดสไตล์ที่สะดวกสําหรับบุคคลที่สามเพื่อปรับแต่งเนื้อหาเพิ่มเติมได้ โดยสรุปแล้ว ผู้เขียนเว็บจะควบคุมการแสดงเนื้อหาของตนเองได้อย่างเต็มที่