Shadow DOM 201

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

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

เกริ่นนำ

ว่ากันตามจริง มาร์กอัปที่ไม่มีสไตล์ไม่มีความเซ็กซี่ โชคดีสำหรับเรา บุคลากรชั้นยอดที่อยู่เบื้องหลังคอมโพเนนต์เว็บ มองเห็นสิ่งนี้และทำให้พวกเรารอไม่ไหว โมดูลขอบเขตของ 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 รายการเดียวที่ตรงกับตัวเลือก 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>

Gocha หนึ่งคือกฎในหน้าหลักมีความจำเพาะสูงกว่ากฎ :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 เพื่อจัดสไตล์องค์ประกอบภายในต้นไม้เงา

องค์ประกอบ Pseudo-shadow ::shadow

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

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

<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 และ /deep/

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

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

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 ของตน ปุ่มหนึ่งใช้สำหรับจัดรูปแบบแบบอักษรของปุ่มภายในและอีกปุ่มหนึ่งสำหรับสีปุ่ม

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 แก่เราในการควบคุมสิ่งที่เกิดขึ้นที่ขอบเขตเงา ซึ่งเปรียบได้กับการเริ่มต้นใหม่เมื่อสร้างคอมโพเนนต์ใหม่

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 อย่างสมเหตุสมผล แต่ยังคงเป็นองค์ประกอบย่อยขององค์ประกอบโฮสต์ จุดแทรกเป็นเพียงตัวแสดงผลเท่านั้น

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

::content องค์ประกอบเทียม

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