第九章:前端互動實踐

如果 manifest.json 是 Skin 的身份證,template.html 是它的骨架,那麼 main.js 就是賦予其生命、智慧和反應能力的「大腦和神經系統」。

一個優秀的 main.js 腳本是一個小型的、由資料驅動的應用程式。它的職責是:渲染 UI、管理狀態、處理使用者互動、並與後端引擎進行通訊。本章將介紹實現這些功能的最佳實踐。

1. 存取表單資料 (與引擎的數據接口)

在您的 main.js 執行之前,MoriForms 後端引擎已經將該頁面上所有表單所需的資料,打包成一個全域的 JavaScript 物件,名為 moriformsData。這是您所有工作的起點。

moriformsData 的結構如下:

JavaScript

window.moriformsData = {
  "nonce": "a1b2c3d4e5", // 用於 API 提交的安全權杖
  "forms": {
    "123": { // 表單 ID
      "jsonData": { /* 完整的 Form Schema 2.0 JSON 物件 */ },
      "apiEndpoint": "https://yoursite.com/wp-json/MoriForms/v1/forms/123/submit",
      "skinOptions": { // 使用者在後台為此 Skin 設定的選項
        "primary_color": "#ff0000",
        "layout_style": "stacked"
      }
    },
    "456": { // 頁面上的第二個表單
      // ...
    }
  }
};

初始化腳本的 boilerplate (樣板碼):

一個健壯的 main.js 應該能處理同頁面上存在多個表單的情況。

JavaScript

document.addEventListener('DOMContentLoaded', function () {
    // 檢查 moriformsData 是否存在
    if (typeof moriformsData === 'undefined' || !moriformsData.forms) {
        console.warn('MoriForms data not found on this page.');
        return;
    }

    // 遍歷所有需要初始化的表單
    for (const formId in moriformsData.forms) {
        const formConfig = moriformsData.forms[formId];
        const container = document.getElementById('MoriForms-container-' + formId);
    
        if (container) {
            // 為每個表單建立一個實例,進行初始化
            new MoriFormsSkin(container, formConfig);
        }
    }
});

// 將我們的邏輯封裝在一個類別中,方便管理
class MoriFormsSkin {
    constructor(container, config) {
        this.container = container;
        this.config = config;
        this.state = {}; // 用於儲存所有欄位的當前值

        this.render();
        this.bindEvents();
    }

    render() {
        // ... 渲染表單的邏輯 ...
        console.log('Rendering form:', this.config.jsonData.formName);
    }

    bindEvents() {
        // ... 綁定事件監聽的邏輯 ...
    }
}

2. 渲染表單 (動態建立 UI)

不要將所有 HTML 寫死。您應該根據 jsonData.layout 陣列,動態地建立每一個元素。推薦使用「工廠模式」來處理不同類型的元素。

JavaScript

// 在 MoriFormsSkin 類別中
render() {
    const schema = this.config.jsonData;
    const fieldsContainer = document.createElement('form'); // 使用 <form> 標籤包裹
  
    schema.layout.forEach(elementData => {
        const elementNode = this.createElement(elementData);
        if (elementNode) {
            fieldsContainer.appendChild(elementNode);
        }
    });

    // ... 加上送出按鈕 ...
  
    this.container.appendChild(fieldsContainer);
}

createElement(data) {
    // 根據元素類型,呼叫不同的建立函數
    switch (data.type) {
        case 'text':
        case 'email':
            return this.createTextField(data);
        case 'select':
            return this.createSelectField(data);
        case 'group':
            return this.createGroupContainer(data);
        // ... 其他類型
        default:
            return null;
    }
}

createTextField(data) { /* ... 建立 label 和 input ... */ }

// 對於容器,需要遞迴呼叫 createElement
createGroupContainer(data) {
    const group = document.createElement('fieldset');
    // ... 建立 legend ...
    data.children.forEach(childData => {
        const childElement = this.createElement(childData);
        if (childElement) {
            group.appendChild(childElement);
        }
    });
    return group;
}

3. 狀態管理與條件邏輯

表單的互動性來自於對「狀態」的即時反應。您需要:

  1. 監聽輸入:bindEvents 方法中,為所有輸入欄位綁定 inputchange 事件。每當有欄位的值改變時,更新 this.state 物件。
  2. 評估條件: 當一個被依賴的欄位(即出現在 conditions 規則中的 fieldId)的值發生變化時,觸發一個 evaluateConditions 的方法。
  3. 執行動作: evaluateConditions 方法會遍歷 jsonData.conditions 陣列。對於每一條規則,它會根據 this.state 中儲存的當前表單值來判斷 if 條件是否成立,然後執行 thenelse 區塊中定義的動作(例如,為目標元素增刪一個 .is-hidden 的 CSS class,或設定其 disabled 屬性)。

4. 處理表單提交

這是前端生命週期的最後一步,也是最關鍵的一步。

監聽提交事件:

在 bindEvents 中,監聽 <form> 元素的 submit 事件。

JavaScript

// 在 bindEvents 方法中
const formElement = this.container.querySelector('form');
formElement.addEventListener('submit', (event) => {
    event.preventDefault(); // 【重要】阻止瀏覽器預設的提交行為
    this.handleSubmit();
});

handleSubmit 方法的實作:

JavaScript

// 在 MoriFormsSkin 類別中
async handleSubmit() {
    // 1. 顯示載入中的狀態 (例如,禁用按鈕)
    const submitButton = this.container.querySelector('button[type="submit"]');
    submitButton.disabled = true;
    submitButton.textContent = '傳送中...';

    // 2. 收集表單資料 (this.state)
    const submissionPayload = {
        submissionData: this.state
    };

    // 3. 使用 Fetch API 發送請求
    try {
        const response = await fetch(this.config.apiEndpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': moriformsData.nonce // 附上 Nonce 安全權杖
            },
            body: JSON.stringify(submissionPayload)
        });

        const responseData = await response.json();

        if (!response.ok) { // 處理錯誤 (例如 400, 500)
            this.handleErrorResponse(responseData);
        } else { // 處理成功 (200 OK)
            this.handleSuccessResponse(responseData);
        }

    } catch (error) { // 處理網路等其他錯誤
        console.error('Submission failed:', error);
        // 在 UI 上顯示一個通用的網路錯誤訊息
    } finally {
        // 4. 恢復按鈕狀態
        submitButton.disabled = false;
        submitButton.textContent = this.config.jsonData.settings.submitButtonText || 'Submit';
    }
}

處理 API 回應:

  • handleSuccessResponse(data):
    • 檢查 data.postSubmitAction.type
    • 如果是 "success_message",則清空表單,並在容器中顯示 data.postSubmitAction.content 的內容。
    • 如果是 "redirect",則執行 window.location.href = data.postSubmitAction.url
  • handleErrorResponse(data):
    • 這是處理驗證失敗的地方。
    • 遍歷 data.data.errors 物件。
    • 對於每一個 fieldId,找到對應的 HTML 欄位,並在其旁邊或下方顯示錯誤訊息 data.data.errors[fieldId]
    • 清除上一次的舊錯誤訊息。

下一步

您現在已經掌握了編寫一個專業、健壯的前端 Skin 腳本所需的核心知識。從接收資料、渲染 UI,到管理狀態和與後端 API 進行完整的通訊,整個流程都已清晰。

在最後的開發章節中,我們將回到伺服器端,探討如何利用 functions.php 和 WordPress 的 Hooks,為您的 Skin 增加自訂的後端邏輯,實現更深度的功能整合。

本頁目錄