ข้ามไปยังเนื้อหา

Template Element

เวลาต้อง stamp DOM structure เดิมซ้ำ ๆ หลายครั้ง (เช่น การ์ดในลิสต์) การสร้าง element ด้วย innerHTML string ทุกครั้งทั้งช้าและเสี่ยง XSS — <template> แก้ปัญหานี้โดยให้คุณนิยาม markup ไว้ล่วงหน้าแบบ parse ครั้งเดียวแล้ว clone ได้ไม่จำกัด

<template> element เก็บ markup ของ HTML ที่จะไม่ถูก render เมื่อหน้าเว็บโหลด ต่างจาก <div> ที่ซ่อนไว้ เนื้อหาของ template ไม่มี side-effect เลย script ภายในไม่ทำงาน รูปไม่โหลด และ style ไม่ถูกนำมาใช้ property content ของตัวเองเปิดเผย markup ออกมาในรูปของ DocumentFragment ที่เป็น container ที่เบา บรรจุ node ที่ถูก parse แล้วไว้ล่วงหน้า และอยู่นอก document tree จริงจนกว่าคุณจะแทรกเข้าไปอย่างชัดเจน

ใช้ document.querySelector('template').content (หรือ getElementById) เพื่อดึง DocumentFragment ออกมา เรียก content.cloneNode(true) เพื่อทำสำเนาแบบลึก (deep copy) ที่รวม node ลูกหลานทั้งหมด พร้อมแทรกได้ทุกที่ใน document

<template id="card-tpl">
<div class="card"><p class="msg"></p></div>
</template>
const tpl = document.getElementById('card-tpl');
const clone = tpl.content.cloneNode(true);
clone.querySelector('.msg').textContent = 'Hello from template!';
document.body.appendChild(clone);

ทุกครั้งที่เรียก cloneNode(true) จะได้สำเนาที่เป็นอิสระต่อกัน การเปลี่ยนแปลง clone ตัวหนึ่งจะไม่กระทบ DocumentFragment ต้นฉบับหรือ clone ตัวอื่น

เนื่องจากเนื้อหาของ template อยู่ใน DocumentFragment ที่เป็น inert browser จึงไม่ทำ layout ไม่ทำ styling และไม่รัน script ให้เลยจนกว่าคุณจะ clone แล้วแทรกเข้าไป ต้นทุนการ parse จ่ายเพียงครั้งเดียวตอนหน้าเว็บโหลด และทุก ๆ cloneNode(true) หลังจากนั้นเป็นการคัดลอกโครงสร้างที่รวดเร็ว ทำให้ <template> เหมาะอย่างยิ่งสำหรับการ stamp instance ของ markup เดียวกันจำนวนมาก เช่น list item, card หรือ custom element ที่ใช้ซ้ำ

custom element my-card ด้านล่างใช้ <template> ที่นิยามไว้ใน light DOM ภายใน connectedCallback จะ attach shadow root และ append clone ของเนื้อหา template เข้าไป ทั้งสอง instance ใช้ template ที่ถูก parse แล้วร่วมกัน เป็นการสาธิตแพทเทิร์น “parse once, stamp many”

<template> อยู่ใน light DOM และถูกเข้าถึงผ่าน ID แต่ละ instance ของ my-card จะได้ shadow root ของตัวเอง ที่ถูกเติมด้วย clone อิสระของเนื้อหา template

ตัวเลือกBenefitCost
<template> + cloneNode(true)parse markup เพียงครั้งเดียว แล้ว clone ได้เร็วและปลอดภัยจาก XSS เพราะไม่ผ่าน string parsing ซ้ำต้องเขียนโค้ดเพิ่มเพื่อดึงและ clone content ไม่ได้สั้นเท่าการต่อ string ตรง ๆ
สร้าง markup ด้วย innerHTML string ซ้ำ ๆเขียนง่าย เห็นผลเร็วสำหรับ prototypeช้ากว่าเพราะต้อง parse HTML ใหม่ทุกครั้ง และเสี่ยง XSS ถ้ามีข้อมูลจากผู้ใช้งานปนอยู่ใน string
  • ลืมเรียก .cloneNode(true) ก่อนใช้งาน content ของ templatetemplate.content เป็น DocumentFragment ที่ใช้ร่วมกันเพียงชุดเดียว การแทรกตรง ๆ โดยไม่ clone จะทำให้ fragment ต้นฉบับถูกย้าย (moved) ออกไป ทำให้ instance ถัดไปได้ fragment ว่างเปล่า
  • เรียก cloneNode() โดยไม่ใส่ true — จะได้ shallow clone ที่ไม่มี node ลูกหลานเลย ต้องระบุ true เสมอเพื่อทำ deep copy
  • คาดหวังว่า script หรือ style ภายใน <template> จะทำงานทันทีตอนหน้าเว็บโหลด — เนื้อหาของ template เป็น inert จนกว่าจะถูก clone และแทรกเข้า document จริง script จะไม่รันและ style จะไม่ถูกนำไปใช้จนกว่าจะถึงตอนนั้น

💡 ตัวอย่างจากของจริง

Lit ใช้แนวคิดคล้าย <template> ภายใน internal เพื่อ cache parsed markup แล้ว clone ซ้ำเมื่อ re-render คอมโพเนนต์ ลดต้นทุนการ parse ซ้ำ

Shoelace และ web component library อื่น ๆ มักใช้ <template> ร่วมกับ cloneNode(true) ในคอมโพเนนต์ที่ต้อง stamp โครงสร้างเดิมซ้ำหลายครั้ง เช่น list item หรือ menu option

`template.content` คืนค่าอะไร?
ทำไมเนื้อหาของ `<template>` จึงไม่ถูก render โดย default?
เมธอดใดที่สร้างสำเนาของเนื้อหา template ที่พร้อมแทรก?