เทมเพลต สล็อต และเงา

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

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

ในส่วนนี้ เราจะสร้างองค์ประกอบ <star-rating> ซึ่งเป็นคอมโพเนนต์เว็บที่ช่วยให้ผู้ใช้ให้คะแนนประสบการณ์การใช้งานได้ตั้งแต่ 1 ถึง 5 ดาว เมื่อตั้งชื่อองค์ประกอบที่กำหนดเอง แนะนำให้ใช้ตัวอักษรพิมพ์เล็กทั้งหมด นอกจากนี้ ให้ใส่เครื่องหมายขีดกลาง เพราะจะช่วยแยกแยะระหว่างองค์ประกอบปกติกับองค์ประกอบที่กำหนดเอง

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

องค์ประกอบ <template>

องค์ประกอบ <template> ใช้เพื่อประกาศส่วนย่อยของ HTML ที่จะถูกโคลนและแทรกลงใน DOM ด้วย JavaScript เนื้อหาขององค์ประกอบจะไม่แสดงผลโดยค่าเริ่มต้น แต่จะมีการสร้างอินสแตนซ์โดยใช้ JavaScript

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

เนื่องจากไม่ได้เขียนเนื้อหาขององค์ประกอบ <template> ลงในหน้าจอ ระบบจึงไม่แสดงผล <form> และเนื้อหาภายในนั้น ใช่ Codepen นี้ว่างเปล่า แต่หากคุณตรวจสอบแท็บ HTML คุณจะเห็นมาร์กอัป <template>

ในตัวอย่างนี้ <form> ไม่ใช่องค์ประกอบย่อยของ <template> ใน DOM แต่เนื้อหาขององค์ประกอบ <template> เป็นองค์ประกอบย่อยของ DocumentFragment ที่แสดงผลโดยพร็อพเพอร์ตี้ HTMLTemplateElement.content หากต้องการให้แสดงตัว ต้องใช้ JavaScript เพื่อเก็บเนื้อหาและผนวกเนื้อหาเหล่านั้นลงใน DOM

JavaScript แบบย่อนี้ไม่ได้สร้างองค์ประกอบที่กำหนดเอง ตัวอย่างนี้ได้เพิ่มเนื้อหาของ <template> ต่อท้าย <body> แล้ว เนื้อหาได้กลายเป็นส่วนหนึ่งของ DOM ที่มองเห็นและจัดรูปแบบได้

ภาพหน้าจอของตัวแปลงรหัสก่อนหน้าที่แสดงใน DOM

การกำหนดให้ JavaScript ใช้เทมเพลตสำหรับการให้ดาวเพียง 1 ดาวไม่มีประโยชน์มากนัก แต่การสร้างคอมโพเนนต์เว็บสำหรับวิดเจ็ตการให้คะแนนอันดับดาวที่ปรับแต่งได้และนำมาใช้งานซ้ำนั้นมีประโยชน์

องค์ประกอบ <slot>

เรามีช่องโฆษณาที่จะรวมคำอธิบายรายการที่กำหนดเองแต่ละรายการ HTML จะมีองค์ประกอบ <slot> เป็นตัวยึดตำแหน่งภายใน <template> ซึ่งหากระบุชื่อก็จะสร้าง "ช่องโฆษณาที่มีชื่อ" ช่องโฆษณาที่มีชื่อสามารถใช้ เพื่อปรับแต่งเนื้อหาภายในคอมโพเนนต์เว็บ องค์ประกอบ <slot> เป็นวิธีควบคุมตำแหน่งที่ควรแทรกองค์ประกอบย่อยขององค์ประกอบที่กำหนดเองภายในโครงสร้างเงาขององค์ประกอบ

ในเทมเพลตของเรา เราเปลี่ยน <legend> เป็น <slot>:

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

แอตทริบิวต์ name ใช้สำหรับกำหนดสล็อตให้กับองค์ประกอบอื่นๆ หากองค์ประกอบมีแอตทริบิวต์ slot ที่มีค่าตรงกับชื่อของช่องโฆษณาที่มีชื่อ หากองค์ประกอบที่กำหนดเองไม่มีรายการที่ตรงกันกับช่องโฆษณา ระบบจะแสดงเนื้อหาของ <slot> เราจึงรวม <legend> ที่มีเนื้อหาทั่วไปซึ่งสามารถแสดงผลได้หากใครใส่ <star-rating></star-rating> โดยไม่มีเนื้อหาใดๆ ไว้ใน HTML

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

แอตทริบิวต์ slot เป็นแอตทริบิวต์ร่วมที่ใช้เพื่อแทนที่เนื้อหาของ <slot> ภายใน <template> ในองค์ประกอบที่กำหนดเอง องค์ประกอบที่มีแอตทริบิวต์ช่องโฆษณา คือ <legend> ซึ่งไม่จำเป็นต้องเป็นแบบนั้นเลย ในเทมเพลตของเรา <slot name="star-rating-legend"> จะแทนที่ด้วย <anyElement slot="star-rating-legend"> โดยที่ <anyElement> อาจเป็นองค์ประกอบใดก็ได้ หรือจะเป็นองค์ประกอบที่กำหนดเองอีกรายการก็ได้

องค์ประกอบที่ไม่ได้ระบุ

ใน<template> เราใช้องค์ประกอบ <rating> ซึ่งไม่ใช่องค์ประกอบที่กำหนดเอง แต่เป็นองค์ประกอบที่ไม่รู้จัก เบราว์เซอร์อาจจะยังไม่รู้จักองค์ประกอบหนึ่งๆ เสมอไป เบราว์เซอร์จะดำเนินการกับองค์ประกอบ HTML ที่ไม่รู้จักเป็นองค์ประกอบในบรรทัดที่ไม่ระบุตัวตนซึ่งสามารถจัดรูปแบบด้วย CSS ได้ องค์ประกอบ <rating> และ <star-rating> คล้ายกับ <span> ตรงที่ไม่มีรูปแบบหรือความหมายที่ใช้ User Agent

โปรดทราบว่า <template> และเนื้อหาจะไม่แสดงผล <template> เป็นองค์ประกอบที่รู้จักซึ่งมีเนื้อหาที่จะไม่แสดงผล ยังไม่ได้กำหนดองค์ประกอบ <star-rating> เบราว์เซอร์จะแสดงเหมือนกับองค์ประกอบที่ไม่รู้จักทั้งหมดจนกว่าเราจะระบุองค์ประกอบได้ ในตอนนี้ระบบจะถือว่า <star-rating> ที่ไม่รู้จักเป็นองค์ประกอบในบรรทัดที่ไม่ระบุชื่อ ดังนั้นเนื้อหารวมถึงคำอธิบายและ <p> ใน <star-rating> ที่ 3 จะแสดงอย่างที่ควรจะเป็นหากอยู่ใน <span> แทน

มากำหนดองค์ประกอบของเราเพื่อแปลงองค์ประกอบที่ไม่รู้จักเป็นองค์ประกอบที่กำหนดเองกัน

องค์ประกอบที่กำหนดเอง

ต้องใช้ JavaScript เพื่อกำหนดองค์ประกอบที่กำหนดเอง เมื่อกำหนดแล้ว เนื้อหาขององค์ประกอบ <star-rating> จะถูกแทนที่ด้วยรูทเงาที่มีเนื้อหาทั้งหมดของเทมเพลตที่เราเชื่อมโยงด้วย ระบบจะแทนที่องค์ประกอบ <slot> จากเทมเพลตด้วยเนื้อหาขององค์ประกอบภายใน <star-rating> ซึ่งมีค่าแอตทริบิวต์ slot ตรงกับค่าของชื่อของ <slot> หากมี มิฉะนั้น เนื้อหาของช่องของเทมเพลตจะปรากฏ

เนื้อหาภายในองค์ประกอบที่กำหนดเองซึ่งไม่ได้เชื่อมโยงกับช่องโฆษณา <p>Is this text visible?</p> ใน <star-rating> ที่ 3 ไม่ได้รวมอยู่ใน Shadowรูท จึงไม่แสดง

เรากำหนดองค์ประกอบที่กำหนดเองชื่อ star-rating โดยการขยาย HTMLElement:

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

เมื่อมีการกำหนดองค์ประกอบแล้ว ทุกครั้งที่เบราว์เซอร์พบองค์ประกอบ <star-rating> ก็จะแสดงผลตามที่กำหนดโดยองค์ประกอบที่มี #star-rating-template ซึ่งก็คือเทมเพลตของเรา เบราว์เซอร์จะแนบ Shadow DOM Tree กับโหนด โดยโคลนเนื้อหาเทมเพลตต่อท้าย Shadow DOM นั้น โปรดทราบว่าองค์ประกอบที่คุณสามารถattachShadow()ถูกจำกัด

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

หากดูเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ คุณจะเห็นว่า <form> จาก <template> เป็นส่วนหนึ่งของรากเงาขององค์ประกอบที่กำหนดเองแต่ละรายการ การโคลนของเนื้อหา <template> จะปรากฏให้เห็นในองค์ประกอบที่กำหนดเองแต่ละรายการในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์และมองเห็นได้ในเบราว์เซอร์ แต่เนื้อหาขององค์ประกอบที่กำหนดเองเองจะไม่แสดงบนหน้าจอ

ภาพหน้าจอของเครื่องมือสำหรับนักพัฒนาเว็บแสดงเนื้อหาเทมเพลตที่โคลนในองค์ประกอบที่กำหนดเองแต่ละรายการ

ในตัวอย่าง <template> เราใส่เนื้อหาเทมเพลตต่อท้ายเนื้อหาเอกสาร โดยเพิ่มเนื้อหาไปยัง DOM ปกติ ในคำจำกัดความของ customElements เราใช้ appendChild() เดียวกัน แต่ระบบเพิ่มเนื้อหาของเทมเพลตที่โคลนเข้ากับ Shadow DOM ที่ปกปิดไว้

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

Shadow DOM

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

เนื่องจากเราได้เพิ่มเนื้อหาต่อท้าย Shadow DOM แล้ว เราจึงรวมองค์ประกอบ <style> ที่ให้ CSS ที่ปกปิดอยู่ในองค์ประกอบที่กำหนดเองได้

เนื่องจากมีขอบเขตอยู่ที่องค์ประกอบที่กำหนดเอง เราจึงไม่ต้องกังวลว่ารูปแบบจะเล็ดลอดออกมาในส่วนอื่นๆ ของเอกสาร เราสามารถลดความเจาะจงของเครื่องมือเลือกได้เป็นอย่างมาก เช่น เนื่องจากอินพุตเดียวที่ใช้ในองค์ประกอบที่กำหนดเองคือปุ่มตัวเลือก เราจึงใช้ input แทน input[type="radio"] เป็นตัวเลือกได้

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

แม้ว่าคอมโพเนนต์เว็บจะถูกห่อหุ้มด้วยมาร์กอัปใน <template> และสไตล์ CSS จะกำหนดขอบเขตเป็น Shadow DOM และซ่อนจากทุกอย่างที่อยู่ภายนอกคอมโพเนนต์ แต่เนื้อหาของช่องที่แสดงผล ส่วน <anyElement slot="star-rating-legend"> ของ <star-rating> ไม่ได้ถูกห่อหุ้มไว้

การจัดรูปแบบอยู่นอกขอบเขตปัจจุบัน

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

แผนผังเงาคือแผนผัง DOM ภายใน Shadow DOM ส่วนรากเงาคือโหนดรากของต้นไม้เงา

คลาส Pseudo :host จะเลือก <star-rating> ซึ่งเป็นองค์ประกอบโฮสต์เงา โฮสต์ Shadow คือโหนด DOM ที่ Shadow DOM แนบอยู่ หากต้องการกำหนดเป้าหมายเฉพาะโฮสต์เวอร์ชันที่เจาะจง ให้ใช้ :host() ซึ่งจะเลือกเฉพาะองค์ประกอบของโฮสต์เงาที่ตรงกับพารามิเตอร์ที่ส่งผ่าน เช่น ตัวเลือกคลาสหรือแอตทริบิวต์ หากต้องการเลือกองค์ประกอบที่กำหนดเองทั้งหมด ให้ใช้ star-rating { /* styles */ } ใน CSS ส่วนกลาง หรือ :host(:not(#nonExistantId)) ในรูปแบบเทมเพลต ในด้านของความเฉพาะเจาะจง CSS ทั่วโลกชนะ

องค์ประกอบจำลอง ::slotted() ข้ามขอบเขต Shadow DOM จากภายใน Shadow DOM ซึ่งจะเลือกองค์ประกอบแบบสล็อตหากตรงกับตัวเลือก ในตัวอย่างของเรา ::slotted(legend) ตรงกับคำอธิบายทั้ง 3 ของเรา

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

หากมาร์กอัปเทมเพลตของเราเริ่มต้นเป็นเช่นนี้

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

เราสามารถกำหนดเป้าหมาย <form> และ <fieldset> ด้วย

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

ชื่อส่วนทำหน้าที่คล้ายกับคลาส โดยองค์ประกอบหนึ่งอาจมีชื่อส่วนที่คั่นด้วยการเว้นวรรคได้หลายชื่อ และองค์ประกอบหลายรายการจะมีชื่อส่วนเดียวกันได้

Google มีรายการตรวจสอบที่ยอดเยี่ยมสำหรับการสร้างองค์ประกอบที่กำหนดเอง นอกจากนี้ คุณอาจต้องดูข้อมูลเกี่ยวกับ DPA DMB Shadow DOM

ตรวจสอบความเข้าใจของคุณ

ทดสอบความรู้ของคุณเกี่ยวกับเทมเพลต สล็อต และเงา

โดยค่าเริ่มต้น รูปแบบจากภายนอก Shadow DOM จะจัดรูปแบบองค์ประกอบภายใน

จริง
โปรดลองอีกครั้ง
เท็จ
ถูกต้องแล้ว!

คำตอบใดเป็นคำอธิบายที่ถูกต้องขององค์ประกอบ <template>

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