# สัปดาห์ที่ 10.3 - SPA (Single Page Application)

## บทนำ

**SPA (Single Page Application)** คือเว็บแอปพลิเคชันที่ใช้ไฟล์ HTML เพียงหนึ่งไฟล์ และแปลงเนื้อหาแบบไดนามิกผ่าน JavaScript โดยไม่ต้องโหลดหน้าใหม่ตลอดเวลา

ตัวอย่างที่รู้จักกันดี: Gmail, Facebook, Google Maps, Netflix

---

## 1. SPA vs Traditional Web

### Traditional Web Application

```
User clicks "About"
    ↓
Browser sends: GET /about
    ↓
Server responds: HTML page
    ↓
Browser: Load entire page + CSS + JS + Images (SLOW)
```

### SPA (Single Page Application)

```
User clicks "About"
    ↓
JavaScript fetches: GET /api/about (JSON only)
    ↓
Browser: Update HTML dynamically (FAST ✨)
```

---

## 2. โครงสร้างพื้นฐาน SPA

### โฟลเดอร์ Structure

```
my-spa/
├── index.html           (ไฟล์ HTML เดียว)
├── css/
│   └── style.css
├── js/
│   ├── main.js          (ไฟล์หลัก)
│   ├── router.js        (จัดการ routing)
│   ├── api.js           (จัดการ API calls)
│   └── views/           (ไฟล์ views/pages)
│       ├── home.js
│       ├── about.js
│       └── contact.js
└── data/
    └── sample-data.json
```

---

## 3. ตัวอย่าง: Simple SPA

### index.html

```html
<!DOCTYPE html>
<html lang="th">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My SPA</title>
    <link rel="stylesheet" href="css/style.css" />
  </head>
  <body>
    <!-- Header -->
    <header>
      <nav>
        <a href="#/">Home</a>
        <a href="#/about">About</a>
        <a href="#/contact">Contact</a>
      </nav>
    </header>

    <!-- Main Content Area (จะเปลี่ยนแปลง dynamically)
  <main id="app"></main>

  <!-- Footer -->
    <footer>
      <p>&copy; 2024 My SPA</p>
    </footer>

    <!-- Scripts -->
    <script src="js/api.js"></script>
    <script src="js/router.js"></script>
    <script src="js/main.js"></script>
  </body>
</html>
```

### css/style.css

```css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: Arial, sans-serif;
  background-color: #f5f5f5;
}

header {
  background-color: #333;
  color: white;
  padding: 20px;
}

nav a {
  color: white;
  text-decoration: none;
  margin-right: 20px;
  font-weight: bold;
}

nav a:hover {
  text-decoration: underline;
}

main {
  max-width: 1000px;
  margin: 20px auto;
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

footer {
  text-align: center;
  padding: 20px;
  color: #666;
  background-color: #f0f0f0;
}
```

### js/api.js (ดึงข้อมูล)

```javascript
// API utility functions
const API = {
  // Mock data แทนการเรียก API จริง
  users: [
    { id: 1, name: "Alice", email: "alice@example.com" },
    { id: 2, name: "Bob", email: "bob@example.com" },
    { id: 3, name: "Charlie", email: "charlie@example.com" },
  ],

  // ดึงรายชื่อผู้ใช้
  getUsers: async function () {
    return new Promise((resolve) => {
      setTimeout(() => resolve(this.users), 500); // จำลองการดึงข้อมูลจาก API
    });
  },

  // ดึงผู้ใช้เดียว
  getUserById: async function (id) {
    return new Promise((resolve) => {
      setTimeout(() => {
        const user = this.users.find((u) => u.id === parseInt(id));
        resolve(user);
      }, 300);
    });
  },

  // ดึงข้อมูล real API (ตัวอย่าง)
  getPosts: async function () {
    try {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/posts?_limit=5",
      );
      return await response.json();
    } catch (error) {
      console.error("Error fetching posts:", error);
      return [];
    }
  },
};
```

### js/router.js (จัดการ routing)

```javascript
// Router - จัดการการเปลี่ยนหน้า
const Router = {
  // เก็บ views
  views: {
    home: `
      <h1>🏠 Home</h1>
      <p>ยินดีต้อนรับสู่ SPA ของฉัน</p>
      <button onclick="loadUsersList()">ดูรายชื่อผู้ใช้</button>
      <div id="users-list"></div>
    `,

    about: `
      <h1>ℹ️ About</h1>
      <p>นี่คือเพจ About ของแอปพลิเคชัน SPA</p>
      <p>SPA ช่วยให้เว็บแอปทำงานได้เร็วขึ้นโดยไม่ต้องโหลดหน้าใหม่</p>
    `,

    contact: `
      <h1>📧 Contact</h1>
      <form onsubmit="handleContactSubmit(event)">
        <label>ชื่อ:
          <input type="text" name="name" required>
        </label>
        <br><br>
        <label>อีเมล:
          <input type="email" name="email" required>
        </label>
        <br><br>
        <label>ข้อความ:
          <textarea name="message" required></textarea>
        </label>
        <br><br>
        <button type="submit">ส่งข้อความ</button>
      </form>
    `,

    notFound: `
      <h1>❌ 404 - ไม่พบหน้าที่ค้นหา</h1>
      <p><a href="#/">กลับไปหน้า Home</a></p>
    `,
  },

  // เรนเดอร์ view
  render: function (page) {
    const appDiv = document.getElementById("app");
    appDiv.innerHTML = this.views[page] || this.views.notFound;
  },

  // นำทาง
  navigate: function (page) {
    window.location.hash = `#/${page}`;
  },
};

// ฟังเหตุการณ์ hash change
window.addEventListener("hashchange", function () {
  const hash = window.location.hash.slice(2); // ตัด #/
  const page = hash || "home";
  Router.render(page);
});

// เมื่อหน้าโหลด
window.addEventListener("load", function () {
  const hash = window.location.hash.slice(2);
  const page = hash || "home";
  Router.render(page);
});
```

### js/main.js (ตรรกะเพิ่มเติม)

```javascript
// ฟังก์ชันดึงข้อมูลผู้ใช้
async function loadUsersList() {
  const usersList = document.getElementById("users-list");
  usersList.innerHTML = "<p>กำลังโหลด...</p>";

  const users = await API.getUsers();

  let html = "<h3>รายชื่อผู้ใช้:</h3>";
  html += "<ul>";
  users.forEach((user) => {
    html += `<li>${user.name} (${user.email})</li>`;
  });
  html += "</ul>";

  usersList.innerHTML = html;
}

// ฟังก์ชันจัดการการส่งฟอร์ม
function handleContactSubmit(event) {
  event.preventDefault();

  const formData = new FormData(event.target);
  const data = {
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message"),
  };

  console.log("ส่งข้อความ:", data);
  alert(`ขอบคุณ ${data.name}! เราจะติดต่อกลับไปยังอีเมล ${data.email}`);

  event.target.reset();
}
```

---

## 4. SPA กับ State Management

### ตัวอย่าง: จัดการ State

```javascript
// State - สถานะของแอปพลิเคชัน
const AppState = {
  currentUser: null,
  users: [],
  isLoading: false,
  error: null,

  // ตั้งค่า user
  setCurrentUser: function (user) {
    this.currentUser = user;
    this.updateUI();
  },

  // ตั้งค่า loading state
  setLoading: function (loading) {
    this.isLoading = loading;
    this.updateUI();
  },

  // ตั้งค่า error
  setError: function (error) {
    this.error = error;
    this.updateUI();
  },

  // อัปเดต UI ตามสถานะ
  updateUI: function () {
    console.log("State updated:", this);
    // ที่นี่สามารถอัปเดต DOM ได้
  },
};

// ใช้งาน
AppState.setCurrentUser({ id: 1, name: "Alice" });
AppState.setLoading(true);
AppState.setError(null);
```

---

## 5. ตัวอย่าง SPA ที่ซับซ้อนขึ้น

### โครงสร้างที่ดีกว่า

```javascript
// main.js - ประกาศ class สำหรับจัดการ SPA
class SPA {
  constructor() {
    this.currentView = "home";
    this.data = {};
    this.init();
  }

  // เริ่มต้นแอป
  async init() {
    this.setupRouting();
    this.loadInitialData();
  }

  // ตั้งค่า routing
  setupRouting() {
    window.addEventListener("hashchange", () => this.handleRoute());
    window.addEventListener("load", () => this.handleRoute());
  }

  // จัดการ route เมื่อ hash เปลี่ยน
  handleRoute() {
    const hash = window.location.hash.slice(2).split("/");
    const page = hash[0] || "home";
    const id = hash[1];

    this.currentView = page;
    this.render(page, id);
  }

  // โหลดข้อมูลเริ่มต้น
  async loadInitialData() {
    try {
      this.data.users = await API.getUsers();
      this.data.posts = await API.getPosts();
    } catch (error) {
      console.error("Error loading initial data:", error);
    }
  }

  // เรนเดอร์ view
  render(page, id) {
    const appDiv = document.getElementById("app");

    switch (page) {
      case "home":
        appDiv.innerHTML = this.renderHome();
        break;
      case "user":
        appDiv.innerHTML = this.renderUserDetail(id);
        break;
      case "about":
        appDiv.innerHTML = this.renderAbout();
        break;
      default:
        appDiv.innerHTML = "<h1>404 Not Found</h1>";
    }
  }

  // Home view
  renderHome() {
    return `
      <h1>🏠 Home</h1>
      <p>ยินดีต้อนรับ!</p>
      <h2>ผู้ใช้:</h2>
      <ul>
        ${this.data.users
          .map(
            (u) => `
          <li>
            <a href="#/user/${u.id}">${u.name}</a>
          </li>
        `,
          )
          .join("")}
      </ul>
    `;
  }

  // User Detail view
  renderUserDetail(id) {
    const user = this.data.users.find((u) => u.id === parseInt(id));
    if (!user) return "<h1>ไม่พบผู้ใช้</h1>";

    return `
      <h1>👤 ${user.name}</h1>
      <p>Email: ${user.email}</p>
      <a href="#/">← กลับไป</a>
    `;
  }

  // About view
  renderAbout() {
    return `
      <h1>ℹ️ About</h1>
      <p>นี่คือ SPA ที่สร้างด้วย Vanilla JavaScript</p>
    `;
  }
}

// เริ่มต้นแอป
const app = new SPA();
```

---

## 6. ข้อดี & ข้อเสีย SPA

### ✅ ข้อดี

| ข้อดี                  | ตัวอย่าง                        |
| ---------------------- | ------------------------------- |
| **เร็ว**               | ไม่ต้องโหลดหน้าใหม่             |
| **Responsive**         | ปรสิตกรรมที่ดีเหมือน Native App |
| **ใช้ Bandwidth น้อย** | ดึงเฉพาะ JSON บางส่วน           |
| **Simple Navigation**  | สามารถใช้ hash routing ได้      |

### ❌ ข้อเสีย

| ข้อเสีย                            | วิธีแก้                           |
| ---------------------------------- | --------------------------------- |
| **JavaScript ต้องโหลด**            | ขนาดไฟล์ JS อาจใหญ่               |
| **SEO ยากขึ้น**                    | ใช้ Server-Side Rendering (SSR)   |
| **History/Back button ต้องจัดการ** | ใช้ hash routing หรือ History API |
| **Initial Load ช้า**               | Code splitting, Lazy loading      |

---

## 7. Performance Optimization

```javascript
// Lazy Loading - โหลดข้อมูลเมื่อต้องการ
class SPA {
  async renderUserDetail(id) {
    // โหลดเฉพาะเมื่อผู้ใช้เข้าหน้านี้
    const user = await API.getUserById(id);
    return `<h1>${user.name}</h1>`;
  }
}

// Caching - เก็บข้อมูลไว้เพื่อไม่ต้องดึงใหม่
class API {
  constructor() {
    this.cache = {};
  }

  async getUsers() {
    if (this.cache.users) {
      return this.cache.users; // ส่งข้อมูลที่เก็บไว้
    }

    const data = await fetch("/api/users").then((r) => r.json());
    this.cache.users = data;
    return data;
  }
}
```

---

## 8. SPA ตัวอย่างจริง

### ตัวอย่าง: Todo SPA

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Todo SPA</title>
    <style>
      body {
        font-family: Arial;
        max-width: 500px;
        margin: 50px auto;
      }
      .todo-item {
        padding: 10px;
        margin: 5px 0;
        background: #f0f0f0;
        border-radius: 4px;
      }
      .done {
        text-decoration: line-through;
        opacity: 0.6;
      }
      input {
        width: 70%;
        padding: 10px;
      }
      button {
        padding: 10px 15px;
      }
    </style>
  </head>
  <body>
    <h1>todos</h1>
    <input id="input" type="text" placeholder="Add a todo..." />
    <button onclick="addTodo()">Add</button>
    <div id="todos"></div>

    <script>
      let todos = JSON.parse(localStorage.getItem("todos")) || [];

      function render() {
        const html = todos
          .map(
            (todo, i) => `
        <div class="todo-item ${todo.done ? "done" : ""}">
          <input type="checkbox" 
            ${todo.done ? "checked" : ""} 
            onchange="toggleTodo(${i})">
          ${todo.text}
          <button onclick="deleteTodo(${i})">Delete</button>
        </div>
      `,
          )
          .join("");

        document.getElementById("todos").innerHTML = html;
      }

      function addTodo() {
        const input = document.getElementById("input");
        if (input.value.trim()) {
          todos.push({ text: input.value, done: false });
          input.value = "";
          saveTodos();
          render();
        }
      }

      function toggleTodo(i) {
        todos[i].done = !todos[i].done;
        saveTodos();
        render();
      }

      function deleteTodo(i) {
        todos.splice(i, 1);
        saveTodos();
        render();
      }

      function saveTodos() {
        localStorage.setItem("todos", JSON.stringify(todos));
      }

      render();
    </script>
  </body>
</html>
```

---

## สรุป SPA

- **SPA** ใช้ไฟล์ HTML เดียว และเปลี่ยนเนื้อหาด้วย JavaScript
- **ข้อดี**: เร็ว, responsive, ใช้ bandwidth น้อย
- **ข้อเสีย**: SEO ยากขึ้น, Initial load ช้า, ต้องจัดการ routing
- **Routing**: ใช้ hash (#/) หรือ History API
- **State**: ต้องจัดการสถานะอย่างดี
- **Frameworks**: React, Vue, Angular ชอบสำหรับ SPA ที่ซับซ้อน
