Shadow DOM 201

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

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

บทนำ

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

การรวมสไตล์

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

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

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

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

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

<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 ขององค์ประกอบที่จัดสไตล์องค์ประกอบนั้นโดยเฉพาะตามบริบทได้

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

การใช้งาน :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 Tree ได้

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

หากองค์ประกอบมี Shadow Tree อย่างน้อย 1 รายการ องค์ประกอบจำลอง ::shadow จะตรงกับ Shadow Root เอง ซึ่งช่วยให้คุณเขียนตัวเลือกที่จัดรูปแบบโหนดภายใน 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 แต่ละแผงจะมี Shadow Tree ของตัวเองซึ่งมีส่วนหัว h2 รายการ หากต้องการจัดรูปแบบส่วนหัวเหล่านั้นจากหน้าหลัก คุณสามารถเขียนโค้ดต่อไปนี้

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

ตัวรวม /deep/

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

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

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

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

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

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

การทำงานกับ querySelector()

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

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

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

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

resetStyleInheritance

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

องค์ประกอบเทียม ::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 ที่โฮสต์ก่อนที่จะเข้าถึงเนื้อหา Shadow ตำแหน่งนี้เรียกว่าขอบเขตบน

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

บทสรุป

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

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