การสร้าง SPA Hybrid ด้วย LitElement + Google Apps Script

 

โค้ดตัวอย่างนี้เป็นเว็บเพจแบบ Single Page Application (SPA) ที่ผสมผสาน LitElement สำหรับสร้าง component แบบ reusable และ Google Apps Script สำหรับดึงข้อมูลจาก Google Sheets พร้อมระบบ routing และ dynamic template บนหน้าเว็บ


1. โครงสร้าง HTML หลัก

<!DOCTYPE html> <html lang="th"> <head> ... </head> <body> <header id="header"></header> <main class="container my-4"> <div class="spinner" id="spinner">...</div> <div id="app"></div> </main> <footer id="footer" class="text-center text-muted py-3"></footer> <script type="module"> ... </script> </body> </html>

  • <header> และ <footer> เป็นพื้นที่สำหรับ component แบบ reusable
  • <main> เป็น container สำหรับเนื้อหาหลักของแต่ละหน้า SPA
  • <div id="spinner"> ใช้แสดง loading indicator ระหว่างดึงข้อมูล
  • <div id="app"> เป็นพื้นที่หลักที่เราจะ render content ของแต่ละ page


2. การโหลด Bootstrap และ CSS

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <style> body { background: #f8f9fa; } nav a.active { font-weight: bold; color: #0d6efd !important; } .spinner { display: none; justify-content: center; align-items: center; height: 60vh; } main { height: 100%; min-height: 75vh; } </style>

  • ใช้ Bootstrap 5.3 เพื่อทำให้หน้าตาสวยงามแบบ responsive
  • nav a.active กำหนดสีและ font-weight ของเมนูที่ถูกเลือก
  • .spinner ซ่อนโดย default และจัดให้อยู่ตรงกลาง
  • main กำหนดความสูงขั้นต่ำ เพื่อให้ layout สวยงามแม้ข้อมูลน้อย


3. Import Lit Bundle แบบครบทุกอย่าง

import { html, render, LitElement, css, unsafeHTML } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js';

  • ใช้ Lit’s all-in-one bundle (lit-all.min.js) รวม directives และฟังก์ชันทั้งหมด
  • html ใช้สร้าง template literals
  • render ใช้ render template ลงใน DOM
  • LitElement สำหรับสร้าง custom element
  • css สำหรับ styling ของ component
  • unsafeHTML สำหรับ render HTML string ที่มาจาก server (ต้องระวัง XSS)


4. Header Component

const headerTemplate = html` <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom"> <div class="container-fluid"> <a class="navbar-brand fw-bold" href="#">MyApp</a> <div class="navbar-nav"> <a class="nav-link" href="#home">Home</a> <a class="nav-link" href="#about">About</a> <a class="nav-link" href="#data">Data</a> </div> </div> </nav>`; render(headerTemplate, document.getElementById('header'));

  • สร้าง navbar แบบ Bootstrap
  • ใช้ hash URL เช่น #home, #about, #data สำหรับ routing SPA
  • ใช้ render เพื่อ render header ลง <header id="header">


5. Footer Component

const footerTemplate = html`<small>&copy; 2025 My Google Apps Script SPA</small>`; render(footerTemplate, document.getElementById('footer'));

  • Footer แบบง่าย ๆ
  • render ลง <footer id="footer">


6. LitElement Component สำหรับตารางข้อมูล

class SheetTable extends LitElement { static properties = { rows: { type: Array } }; static styles = css` table { width: 100%; border-collapse: collapse; } th, td { padding: 8px; border: 1px solid #dee2e6; text-align: left; } th { background: #e9ecef; } `; constructor() { super(); this.rows = []; } connectedCallback() { super.connectedCallback(); this.loadData(); } loadData() { google.script.run .withSuccessHandler(data => { if (Array.isArray(data) && data.length) { const headers = Object.keys(data[0]); const rows = data.map(obj => headers.map(h => obj[h])); this.rows = [headers, ...rows]; this.requestUpdate(); } }) .withFailureHandler(err => { console.error('Error:', err); }) .getSheetData(); } render() { if (!this.rows || this.rows.length === 0) { return html`<p class="text-muted">กำลังโหลดข้อมูล...</p>`; } return html` <table class="table table-striped"> <thead><tr>${this.rows[0].map(col => html`<th>${col}</th>`)}</tr></thead> <tbody>${this.rows.slice(1).map(row => html`<tr>${row.map(cell => html`<td>${cell}</td>`)}</tr>`)}</tbody> </table>`; } } customElements.define('sheet-table', SheetTable);

การทำงานของ component:

  • properties: กำหนด rows เป็น array
  • styles: กำหนด CSS ของตาราง
  • connectedCallback(): เรียก loadData() หลัง component ถูก mount
  • loadData(): เรียก google.script.run.getSheetData() เพื่อดึงข้อมูลจาก Google Sheet
  • render(): แสดงตาราง หรือข้อความ “กำลังโหลดข้อมูล...”


7. ระบบ Router

const app = document.getElementById('app'); const spinner = document.getElementById('spinner'); function showSpinner(show) { spinner.style.display = show ? 'flex' : 'none'; } function loadPage(page) { showSpinner(true); render(html`${unsafeHTML('')}`, app); google.script.run .withSuccessHandler(htmlText => { showSpinner(false); render(html`${unsafeHTML(htmlText)}`, app); document.querySelectorAll('nav a').forEach(a => { a.classList.toggle('active', a.getAttribute('href') === '#' + page); }); }) .withFailureHandler(err => { showSpinner(false); render(html`<div class="alert alert-danger">Error loading page: ${page}</div>`, app); }) .getTemplate(page); } function route() { const hash = location.hash.replace('#', '') || 'home'; loadPage(hash); } window.addEventListener('hashchange', route); route();

การทำงาน:

1. route(): ตรวจสอบ hash ใน URL และโหลดหน้าให้ตรงกับ hash
2. loadPage(page):

  • แสดง spinner
  • ล้างเนื้อหาเดิม
  • เรียก google.script.run.getTemplate(page) เพื่อดึง HTML ของ page จาก Google Apps Script
  • render HTML ลง <div id="app">
  • อัปเดต class active ของเมนู navbar

3. window.addEventListener('hashchange', route): ฟังการเปลี่ยน hash เพื่อทำ SPA navigation

8. การเชื่อมต่อ Google Apps Script

  • ฟังก์ชันสำคัญจากฝั่ง GAS:
    • getSheetData() → คืนค่า array ของ objects จาก Google Sheet
    • getTemplate(page) → คืนค่า HTML string ของแต่ละ page
  • ทำให้หน้าเว็บ ไม่ต้องโหลดใหม่ทั้งหมด และ อัปเดตเนื้อหาแบบ dynamic

9. จุดเด่นของระบบนี้

  • SPA Hybrid: ใช้ LitElement สำหรับ component + Google Apps Script เป็น backend
  • Reusable Components: header, footer, table สามารถ reuse ได้
  • Dynamic Table: ตารางดึงจาก Google Sheet โดยตรง และ render ด้วย LitElement
  • Routing แบบ Hash: ไม่ต้อง reload page ทั้งหมด
  • Bootstrap Styling: หน้าเว็บสวยและ responsive พร้อม spinner
  • Loading & Error Handling: มี spinner และ alert หากโหลด page ไม่สำเร็จ


10. Code.gs

function doGet(e) {
  const page = (e && e.parameter && e.parameter.page) ? e.parameter.page : 'home';
  const t = HtmlService.createTemplateFromFile('index');
  t.initialPage = page;
  return t.evaluate()
    .setTitle("Single Page App")
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// โหลด template (fragment) จากไฟล์ HTML
function getTemplate(name) {
  const safe = name.replace(/[^a-z0-9_\-]/gi, '');
  try {
    return HtmlService.createHtmlOutputFromFile(`${safe}`).getContent();
  } catch (err) {
    return `<div class="alert alert-warning">❌ Page not found: ${safe}</div>`;
  }
}

// ดึงข้อมูลจาก Google Sheet
function getSheetData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Data');
  const values = sheet.getDataRange().getDisplayValues();
  const headers = values.shift();
  return values.map(row => {
    const obj = {};
    headers.forEach((h, i) => obj[h] = row[i]);
    return obj;
  });
}


    สรุป

    โค้ดนี้เป็นตัวอย่าง Google Apps Script SPA แบบครบวงจร

    • ผสม LitElement และ Bootstrap
    • ใช้ google.script.run ดึงข้อมูลและ template
    • ระบบ hash router ทำให้หน้าเว็บเปลี่ยน content แบบ dynamic
    • รองรับ componentization (header/footer/table)
    • มี loading indicator และ error handling

    เหมาะกับเว็บแอปที่ต้อง เชื่อม Google Sheets เป็น backend และต้องการ SPA ที่เบาและโมดูลาร์


    Download File!   Demo


    แสดงความคิดเห็น (0)
    ใหม่กว่า เก่ากว่า