新手指南:使用 Alpine.js 實現前端標籤輸入功能

更新日期: 2025 年 1 月 27 日

在網頁表單中,標籤輸入是一種常見的功能,可以幫助用戶添加多個關鍵字或分類,以描述內容或過濾搜索結果。

本篇文章將以簡單易懂的方式,帶你逐步了解如何用 Alpine.js 實現這樣的功能。


功能概述

這段代碼實現了一個動態標籤輸入功能,主要特點如下:

  1. 新增標籤: 用戶可以輸入標籤,按下 Enter 後將其添加到列表中。
  2. 刪除標籤: 每個標籤都附有一個刪除按鈕,用於移除該標籤。
  3. 隱藏輸出: 用戶輸入的標籤最終以逗號分隔的形式存入隱藏輸入欄位,便於後端處理。

接下來,我們將分段說明這段代碼的結構與邏輯。

<div x-data="{ tags: [], newTag: '' }" class="flex flex-col space-y-2 mt-4">
    <div class="flex items-center">
        <label for="tags" class="text-gray-700 font-medium">
            <strong>
                標籤:
            </strong>
        </label>
        <span class="text-red-500 text-sm ml-1">*必填 (輸入完按下 Enter) </span>
    </div>

    <!-- 輸入標籤的欄位 -->
    <input type="text" x-model="newTag" @keydown.enter.prevent="
            if (newTag.trim() !== '' && !tags.includes(newTag.trim())) {
                tags.push(newTag.trim());
                newTag = '';
            }
        " placeholder="<範例> 影片剪輯 | 字幕設計" class="bg-white text-gray-800 border border-gray-300 rounded-md p-2 w-full" />

    <!-- 已輸入的標籤顯示 -->

    <div class="flex flex-wrap gap-2 mt-2">
        <template x-for="(tag, index) in tags" :key="index">
            <div class="flex items-center bg-indigo-500 text-white text-sm px-3 py-1 rounded-full">
                <span x-text="tag"></span>
                <button type="button" @click="tags.splice(index, 1)"
                    class="ml-2 py-1 text-white hover:text-red-300 focus:outline-none">
                    X
                </button>
            </div>
        </template>
    </div>

    <!-- 隱藏的標籤輸出,用於送出到 Django -->
    <input type="hidden" name="tags" :value="tags.join(',')" />
</div>

HTML 結構與數據綁定

這段代碼的核心是 Alpine.js 的數據綁定 和 HTML 的結構設計。

整個功能區域用 <div> 包裹並綁定了 Alpine.js 的狀態:

<div x-data="{ tags: [], newTag: '' }" class="flex flex-col space-y-2 mt-4">

Alpine.js 的狀態管理

  • x-data:定義了一個物件,來存放所有與標籤功能相關的數據。
    • tags:儲存當前所有已添加的標籤,是一個陣列。
    • newTag:用來綁定用戶在輸入框中輸入的標籤內容。

顯示與樣式結構

這裡採用了 Tailwind CSS 來進行樣式設計,主要的排版與樣式:

  • flexspace-y-2:建立垂直方向的彈性布局,讓每個子元素之間有固定的間距。
  • mt-4:設置距離頂部的外邊距。

輸入框功能詳解

在標籤功能中,輸入框負責接收用戶輸入並處理新增邏輯。

以下是代碼的詳細說明:

輸入框代碼

<input
    type="text"
    x-model="newTag"
    @keydown.enter.prevent="
        if (newTag.trim() !== '' && !tags.includes(newTag.trim())) {
            tags.push(newTag.trim());
            newTag = '';
        }
    "
    placeholder="<範例> 影片剪輯 | 字幕設計"
    class="bg-white text-gray-800 border border-gray-300 rounded-md p-2 w-full"
/>

x-model 雙向綁定

x-model 是 Alpine.js 提供的一個核心功能,用於實現 雙向數據綁定

在這段代碼中:

<input
    type="text"
    x-model="newTag"
    ...
/>
  • 綁定 newTag 變數:
    • 當用戶在輸入框中鍵入內容時,輸入的值會自動更新到 newTag
    • 反之,如果 JavaScript 程式中修改了 newTag 的值(如清空輸入框的操作),輸入框中的內容也會即時同步。

作用:
x-model 的雙向綁定消除了手動操作 DOM 的需求,用戶在輸入框的輸入行為可以自動影響到程式內部的狀態。

範例:

  • 當用戶輸入 “字幕設計” 時,newTag 的值就會變為 "字幕設計"
  • 當用戶完成操作後程式執行 newTag = '',輸入框的內容也會自動清空。

按下 Enter 新增標籤

在這段代碼中,輸入框還監聽用戶的 Enter 鍵事件,用來新增標籤:

@keydown.enter.prevent="
    if (newTag.trim() !== '' && !tags.includes(newTag.trim())) {
        tags.push(newTag.trim());
        newTag = '';
    }
"

功能解析:

  1. 監聽 Enter 鍵:
    • @keydown.enter.prevent 是 Alpine.js 中的事件綁定語法:
      • keydown.enter:只在用戶按下 Enter 鍵時觸發。
      • .prevent:阻止表單的默認提交行為,避免頁面刷新。
  2. 新增標籤邏輯: 當用戶按下 Enter 後,程式執行以下邏輯檢查和操作:
    • newTag.trim() !== ''
      確保用戶輸入的內容不是空值或只有空白字元。如果條件不成立,不執行後續邏輯。
      範例:
      • " " → 經過 trim() 後變成 "",不會新增標籤。
      • "字幕設計 " → 經過 trim() 後變成 "字幕設計",通過條件檢查。
    • !tags.includes(newTag.trim())
      避免用戶輸入重複的標籤。該條件檢查 tags 陣列中是否已經存在用戶輸入的標籤。如果條件不成立(標籤已存在),則不執行新增。
      範例:
      • tags = ["影片剪輯"] 時,輸入 "影片剪輯" 不會新增。
      • tags = ["字幕設計"] 時,輸入 "影片剪輯" 會通過檢查並新增。
    • tags.push(newTag.trim())
      通過條件檢查後,將標籤加入到 tags 陣列。trim() 確保標籤內容不包含首尾多餘的空白字元。
    • newTag = ''
      新增標籤後,清空 newTag 的值,從而清空輸入框,讓用戶可以繼續輸入新的標籤。

提示與樣式設計

輸入框還設置了一些視覺樣式和提示,讓用戶更容易使用該功能:

  1. placeholder 提示:placeholder="<範例> 影片剪輯 | 字幕設計"
    • 當輸入框為空時,會顯示占位提示文字,例如「影片剪輯 | 字幕設計」,幫助用戶理解應輸入的內容格式。
    • 當用戶開始輸入時,這段提示文字會自動消失。
  2. CSS 樣式設計: 使用 Tailwind CSS 提供的實用工具類,快速設計出美觀且實用的輸入框樣式:
    • 白色背景(bg-white): 提升輸入框的可見性和對比度。
    • 灰色邊框(border border-gray-300): 增強輸入框的邊界感,幫助用戶辨識其可交互性。
    • 圓角設計(rounded-md): 提供柔和的視覺效果,避免尖銳的邊緣。
    • 內部填充(p-2): 增加輸入框內文字與邊框的距離,提升可讀性和輸入體驗。
    • 全寬設置(w-full): 讓輸入框自適應父容器寬度,適應不同的螢幕尺寸。

完整的輸入框功能運作流程

  1. 用戶輸入標籤內容,例如「字幕設計」。
  2. 按下 Enter 鍵觸發事件,程式檢查:
    • 是否為空值。
    • 是否與現有標籤重複。
  3. 如果通過檢查:
    • 新標籤會被新增到 tags 陣列。
    • 輸入框清空,等待用戶輸入下一個標籤。
  4. 標籤內容即時同步更新到網頁視圖,並可以通過刪除按鈕進行操作。

這段代碼的實現不僅簡潔高效,還考慮到了用戶體驗,特別是輸入框的即時反饋和清晰的提示,為整個功能的實現提供了良好的基礎。


標籤顯示與刪除功能

x-for 循環渲染

Alpine.js 提供的 x-for 指令 用於迭代數據陣列並在視圖中動態生成元素。在這段代碼中:

<template x-for="(tag, index) in tags" :key="index">
    <div class="flex items-center bg-indigo-500 text-white text-sm px-3 py-1 rounded-full">
        <span x-text="tag"></span>
        <button
            type="button"
            @click="tags.splice(index, 1)"
            class="ml-2 py-1 text-white hover:text-red-300 focus:outline-none">
            X
        </button>
    </div>
</template>
  • x-for="(tag, index) in tags"
    • tags 陣列中的每一個元素(即標籤)進行迭代。
    • tag:表示當前迭代的標籤值。
    • index:表示當前標籤在陣列中的索引值。
    • 每次迭代會生成一個新的 <div> 元素,對應一個標籤。
  • :key="index"
    • 設置唯一鍵,告訴瀏覽器每個標籤的唯一性,方便高效地更新 DOM。
    • 避免渲染錯誤: 如果沒有設置唯一鍵,Alpine.js 在更新標籤時可能會錯誤地重複渲染或刪除標籤。

範例: 假設 tags 陣列的值為 ["影片剪輯", "字幕設計"]x-for 會渲染以下 HTML 結構:

<div>
    <div class="flex items-center bg-indigo-500 text-white text-sm px-3 py-1 rounded-full">
        <span>影片剪輯</span>
        <button> X </button>
    </div>
    <div class="flex items-center bg-indigo-500 text-white text-sm px-3 py-1 rounded-full">
        <span>字幕設計</span>
        <button> X </button>
    </div>
</div>

補充::key 值的作用是什麼?

在大多數前端框架(包括 Alpine.js、Vue.js 等)中,當數據改變時,框架會幫助你「重新更新畫面」。

為了讓這個更新過程高效,框架需要知道哪些元素應該保持不變、哪些需要新增或刪除。

如果不設置 :key,框架如何處理?
  1. 當標籤只是顯示字串(如 “影片剪輯” 或 “字幕設計”),這些字串對框架來說只是 表面數據,它不知道字串背後的身份。
  2. 沒有 :key,框架會用標籤在數組中的「順序位置」來猜測它的身份。例如:
    • 索引 0 對應的是第一個標籤,索引 1 對應的是第二個標籤。
  3. 如果數據變動(比如刪除),框架可能會誤判:
    • 假如刪掉了第一個標籤,框架可能直接「重新排列」所有標籤,將第二個標籤移動到第一個位置,並嘗試覆蓋它的內容。

這就可能導致畫面閃爍或顯示錯誤,因為框架不知道哪些標籤是真正被刪除的,哪些只是位置變動。

設置 :key 如何解決問題?

:key="index" 或其他唯一值,相當於為每個標籤分配了一個獨特的「身份證」。

當數據更新時,框架可以直接根據 key 來判斷每個標籤的真實身份,而不是單純依賴字串或位置。

:key 的更新邏輯:

假設 tags 陣列為:["影片剪輯", "字幕設計"],並設置了 :key

  1. 初始狀態:
    • 標籤 1(key: 0):”影片剪輯”。
    • 標籤 2(key: 1):”字幕設計”。
  2. 刪除 “影片剪輯”(key: 0):
    • 框架根據 key: 0 確認它需要刪除的是第一個標籤。
    • 同時,key: 1 對應的標籤(”字幕設計”)保持不變,無需重新渲染。
為什麼不能直接用字串當 key

你可能會問:「為什麼不用字串本身當作 key 呢,比如 :key="tag"?」

這其實是可行的,但只有在確保標籤的字串內容是絕對 唯一 並且 不會改變 的情況下才安全。例如:

  • 當標籤是 "影片剪輯""字幕設計",它們是唯一的,用 :key="tag" 是沒問題的。

但是如果:

  1. 標籤的內容可能重複(例如多次輸入同樣的字串);
  2. 標籤的內容可能被編輯或動態改變;

那麼用字串作為 key 就會導致問題,因為框架會誤判這些重複或變動的標籤是同一個元素,從而造成渲染混亂。

標籤的外觀設計

每個標籤的顯示樣式設計精簡而實用,以下是主要的樣式亮點:

  • 背景與文字顏色:
    • 背景為紫色(bg-indigo-500): 強調標籤,讓其從頁面其他元素中脫穎而出。
    • 文字為白色(text-white): 與深色背景形成對比,提升可讀性。
  • 圓形邊框(rounded-full):
    • 圓角設計讓標籤看起來更加柔和,模仿「膠囊式」設計風格。
    • 提升用戶視覺體驗。
  • 字體與間距:
    • 字體大小(text-sm): 小字體適合顯示標籤,避免佔用過多空間。
    • 內邊距(px-3 py-1): 保持文字與邊框之間的距離,讓標籤看起來緊湊且易於辨識。
  • 彈性布局(flex items-center):
    • 使用 Flexbox 將文字與刪除按鈕對齊在同一行。
    • 提升排版整齊度,避免文字與按鈕出現錯位。

範例: 單個標籤的設計如下:

<div class="flex items-center bg-indigo-500 text-white text-sm px-3 py-1 rounded-full">
    <span>字幕設計</span>
    <button> X </button>
</div>

刪除標籤功能

每個標籤旁都設有刪除按鈕,用於從 tags 陣列中移除該標籤。

刪除按鈕代碼:

<button
    type="button"
    @click="tags.splice(index, 1)"
    class="ml-2 py-1 text-white hover:text-red-300 focus:outline-none">
    X
</button>

功能解析:

  • 點擊事件綁定:
    • @click="tags.splice(index, 1)"
      • tags.splice(index, 1) 是 JavaScript 的原生方法,用於從 tags 陣列中移除索引為 index 的標籤。
      • 1 表示只刪除一個元素(即當前標籤)。

範例: 假設 tags["影片剪輯", "字幕設計", "動畫設計"],當用戶點擊第二個標籤(”字幕設計”)的刪除按鈕時,執行以下操作:

tags.splice(1, 1);

刪除後的 tags 變為:["影片剪輯", "動畫設計"]

刪除按鈕樣式設計

刪除按鈕的設計兼顧視覺效果與交互體驗:

  • 按鈕顏色:
    • 默認狀態下文字為白色(text-white),與標籤背景顏色保持一致。
    • 滑鼠懸停時變為紅色(hover:text-red-300),提示刪除操作。
  • 間距與對齊:
    • ml-2 設置刪除按鈕與標籤文字之間的左邊距,避免緊貼標籤內容。
    • py-1 增加上下內邊距,讓按鈕點擊範圍更大。
  • 去除默認樣式:
    • focus:outline-none 去除按鈕獲得焦點時的默認邊框樣式,保持整潔。

範例: 單個按鈕的設計如下:

<button
    type="button"
    @click="tags.splice(index, 1)"
    class="ml-2 py-1 text-white hover:text-red-300 focus:outline-none">
    X
</button>

隱藏輸出以整合後端

隱藏的輸入欄位

隱藏輸入欄位的目的是將標籤數據封裝起來,不直接顯示在前端頁面中,但仍可以被提交到後端進行處理。

代碼:

<input type="hidden" name="tags" :value="tags.join(',')" />

功能解析:

  1. type="hidden"
    • 這個屬性讓輸入框對用戶不可見,但它仍然是表單的一部分,可以在表單提交時攜帶數據到後端。
    • 例如,網頁中即使看不到這個欄位,當用戶提交表單時,它的值會自動被包含在請求中。
  2. :value="tags.join(',')"
    • tags 是用戶輸入的標籤數據,存儲在 Alpine.js 的陣列變數中(例如:["影片剪輯", "字幕設計"])。
    • .join(',') 方法會將這個陣列轉換為一個逗號分隔的字符串:
      • 範例:["影片剪輯", "字幕設計"]"影片剪輯,字幕設計"
    • 將這樣的字符串設置為隱藏輸入欄位的值,方便後端處理。

範例: 假設用戶輸入了以下標籤:

tags = ["影片剪輯", "字幕設計", "動畫設計"];

則隱藏輸入欄位的值會是:

<input type="hidden" name="tags" value="影片剪輯,字幕設計,動畫設計" />

方便後端接收數據

隱藏的輸入欄位通過表單提交時,數據會被以鍵值對的形式傳遞到後端進行處理。

  1. name="tags"
    • 在表單提交時,這個隱藏欄位的值會使用 name="tags" 的名稱傳遞到後端。
    • 後端接收到的數據類似於以下格式: tags=影片剪輯,字幕設計,動畫設計
  2. 後端處理數據的便利性:
    • 由於標籤數據是用逗號分隔的字符串形式,後端可以直接將其轉換為陣列或進行分割處理。
  3. 用於後端的應用場景:
    • 存儲到資料庫: 可以直接將標籤存入資料庫中的一個字段(比如一個 tags 欄位),以逗號分隔的形式存儲。
    • 進一步處理: 後端也可以將這些標籤用於分類搜索、推薦內容或其他業務邏輯。

結合前端與後端的完整流程

以下是前後端協同工作的完整數據流:

  1. 用戶在前端頁面中輸入標籤(如:「影片剪輯」、「字幕設計」)。
  2. 每次輸入完畢後,標籤會動態添加到 tags 陣列中。
  3. tags 陣列被自動轉換為逗號分隔的字符串,設置為隱藏輸入欄位的值。
  4. 當用戶提交表單時,隱藏輸入欄位的值會和其他表單數據一起提交到後端。
  5. 後端接收 tags 欄位的數據,進行解析或存儲。

結語

透過以上步驟,我們完成了一個功能完整的標籤輸入區域。

本範例結合 Alpine.jsTailwind CSS,讓整個流程簡單高效。如果你是新手,這是學習如何將 JavaScript 框架與現代 CSS 工具結合的一個很好起點。

Similar Posts