戈德堡表單 form
按鈕不會馬上解鎖,讓我們一步一步來。(´-∀-`)
靈感來自戈德堡機械(Rube Goldberg machine),動用一整套機關,大費周章完成一件按一下就能做完的事。
技術關鍵字
| 名稱 | 描述 |
|---|---|
| Anime.js | 輕量級 JavaScript 動畫函式庫 |
| SVG | 可在 DOM 中使用的圖形格式,適合複雜圖形和動畫,配合 Vue v-bind 可以實現基於資料的動態效果 |
| JS 動畫 | 基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等 |
| Vue Provide/Inject | Vue 的依賴注入機制,允許父元件向其所有後代元件提供資料或方法 |
| Element getBoundingClientRect() | 取得元素相對視窗的座標與尺寸 |
使用範例
基本用法
輸入框、單選、下拉選單、勾選框、按鈕本身就是機關,放進 form-rube-goldberg 就會自動串起來。
球從漏斗出發,沿著每個元件頂端的導軌左右來回,一路滾到按鈕。
查看範例原始碼
vue
<template>
<div class="relative w-full flex justify-center">
<form-rube-goldberg
ref="machineRef"
class="max-w-[26rem] w-full flex flex-col gap-2"
@submit.prevent="showSubmitted.start()"
>
<form-rube-goldberg-input
v-model="form.nickname"
:label="t('暱稱')"
:placeholder="t('至少 3 個字')"
:min-length="3"
/>
<form-rube-goldberg-radio
v-model="form.frequency"
:label="t('寄送頻率')"
:options="frequencyOptionList"
/>
<form-rube-goldberg-select
v-model="form.topic"
:label="t('感興趣的主題')"
:placeholder="t('選一個主題')"
:options="topicOptionList"
/>
<form-rube-goldberg-checkbox
v-model="form.agreed"
:label="t('我同意收到電子報')"
/>
<form-rube-goldberg-button>
{{ t('訂閱') }}
</form-rube-goldberg-button>
</form-rube-goldberg>
<transition name="opacity">
<div
v-if="isSubmitted"
class="absolute inset-0 z-[40] flex flex-col cursor-pointer items-center justify-center gap-6 rounded-xl bg-slate-600 bg-opacity-90 text-white"
@click="reset"
>
<span class="text-xl tracking-wide">
{{ t('訂閱成功!(*´∀`)~♥') }}
</span>
<span class="text-xs">
{{ t('點一下再來一次') }}
</span>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { useTimeoutFn } from '@vueuse/core'
import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import FormRubeGoldbergButton from '../form-rube-goldberg-button.vue'
import FormRubeGoldbergCheckbox from '../form-rube-goldberg-checkbox.vue'
import FormRubeGoldbergInput from '../form-rube-goldberg-input.vue'
import FormRubeGoldbergRadio from '../form-rube-goldberg-radio.vue'
import FormRubeGoldbergSelect from '../form-rube-goldberg-select.vue'
import FormRubeGoldberg from '../form-rube-goldberg.vue'
const { t } = useI18n()
const machineRef = useTemplateRef('machineRef')
const frequencyOptionList = computed(() => [
{ label: t('每天'), value: 'daily' },
{ label: t('每週'), value: 'weekly' },
{ label: t('每月'), value: 'monthly' },
])
const topicOptionList = computed(() => [
{ label: t('程式'), value: 'code' },
{ label: t('設計'), value: 'design' },
{ label: t('遊戲'), value: 'game' },
{ label: t('音樂'), value: 'music' },
{ label: t('電影'), value: 'movie' },
{ label: t('攝影'), value: 'photo' },
{ label: t('美食'), value: 'food' },
])
const form = ref({
nickname: '',
frequency: undefined as string | undefined,
topic: undefined as string | undefined,
agreed: false,
})
const isSubmitted = ref(false)
// 送出時機器會跑一段慶祝,等它跑完再蓋上遮罩
const showSubmitted = useTimeoutFn(() => {
isSubmitted.value = true
}, 900, { immediate: false })
function reset() {
isSubmitted.value = false
form.value = {
nickname: '',
frequency: undefined,
topic: undefined,
agreed: false,
}
machineRef.value?.reset()
}
</script>
<style lang="sass" scoped>
.opacity-enter-active, .opacity-leave-active
transition-duration: 0.4s
.opacity-enter-from, .opacity-leave-to
opacity: 0 !important
</style>原理
外框依 DOM 順序替元件排出方向,第一個由左往右、第二個由右往左,元件之間用管子接起來。球只有一顆,由外框負責畫,滾進元件後交給元件處理。元件決定球怎麼滾、在哪裡等、播什麼動畫,最後再從出口交回外框。
球帶著速度滾動,下坡受重力加速,平路的摩擦把它拉回基本速度,速度在各段之間延續,經過已經打開的機關也不會減速。勾勾射出的球則依拋體運動飛行。
原始碼
API
外框搭配五個欄位元件使用:
- form-rube-goldberg:外框,負責接管子、滾球。本身就是
<form>,@submit、action、novalidate等屬性直接寫在它身上,不要再包一層 form。 - form-rube-goldberg-input:輸入框。輸入框由兩條彈簧吊在導軌下,另有一條吊繩連到擋板的曲柄,曲柄前端被卡榫勾住。右側的直立進度條隨輸入由下往上填滿,彈簧跟著拉長一點。有效時重量過了臨界點,輸入框擠開卡榫往下掉,扯動曲柄讓擋板倒下放行。
- form-rube-goldberg-radio:單選。
- form-rube-goldberg-select:下拉選單。右側有一座星形輪,第 0 格是擋片,其餘最多 7 個凹槽,選項依序輪流分到各個凹槽,數量不限。選了就把那個凹槽轉到頂端,球落進去後轉盤轉半圈,把球送進出口管;選回提示時擋片轉回頂端,球已通過就彈飛重跑。
- form-rube-goldberg-checkbox:勾選框。
- form-rube-goldberg-button:按鈕,固定排在最後。柵欄完全拉開前會自動加上
disabled,在輸入框按 Enter 也送不出表單。
name、required、autocomplete 等屬性會交給元件裡的原生控制項,class 與 style 留在元件外層,可以直接用在原生表單送出。
主色用 color 調整,球、正在等待的欄位、漣漪與按鈕會一起換色。要分亮暗色,傳 light-dark(亮色, 暗色) 即可。
vue
<form-rube-goldberg color="light-dark(#0ea5e9, #38bdf8)">
...
</form-rube-goldberg>🧩 FormRubeGoldberg
Props
interface Props {
/** 時間倍率,2 表示快一倍,最小 0.1
*
* @default 1
*/
speed?: number;
/** 表單左右留給連接管的寬度,單位 px,建議至少 28
*
* @default 28
*/
gutter?: number;
/** 主色,用在球、正在等待的欄位、漣漪與按鈕。任何 CSS 顏色都行,
* 要分亮暗色可傳 `light-dark(亮色, 暗色)`
*
* @default 'light-dark(#f43f5e, #fb7185)'
*/
color?: string;
}Emits
const emit = defineEmits<{
/** 球抵達按鈕,柵欄拉起 */
unlock: [];
/** 重新上鎖:已解鎖後有欄位變回無效,或呼叫 reset */
lock: [];
/** 球抵達第 index 個元件 */
progress: [index: number];
}>()Slots
defineSlots<{
default?: (params: {
/** 是否已解鎖 */
unlocked: boolean;
}) => unknown;
}>()Methods
interface Expose {
/** 機關全部復原,球回到漏斗重新開始。只重置機關,不會清空欄位的值 */
reset: () => void;
}🧩 FormRubeGoldbergInput
使用 v-model 綁定字串。
Props
interface Props {
label?: string;
type?: 'text' | 'email' | 'password' | 'tel' | 'url' | 'search';
placeholder?: string;
/** 至少要幾個字才算有效
*
* @default 1
*/
minLength?: number;
/** 要符合的正規表達式 */
pattern?: string;
/** 欄位是否有效。有傳就以它為準,min-length 與 pattern 不再作用 */
ready?: boolean;
}🧩 FormRubeGoldbergRadio
使用 v-model 綁定選中選項的 value。
Props
interface Props {
/** 群組名稱,顯示在選項左邊 */
label?: string;
/** 選項清單,元件建立後請勿增減數量 */
options: Option[];
/** 欄位是否有效。有傳就以它為準,預設是選了任何一個選項就有效 */
ready?: boolean;
}🧩 FormRubeGoldbergSelect
使用 v-model 綁定選中選項的 value,選回提示時為 undefined。
Props
interface Props {
label?: string;
/** 選項清單,數量不限。轉盤最多 7 個凹槽,選項依序輪流分到各個凹槽 */
options: Option[];
/** 還沒選時顯示的提示,也是一個可以選回去的無效選項 */
placeholder?: string;
/** 欄位是否有效。有傳就以它為準,預設是選了任何一個選項就有效 */
ready?: boolean;
}🧩 FormRubeGoldbergCheckbox
使用 v-model 綁定布林值。
Props
interface Props {
label?: string;
/** 欄位是否有效。有傳就以它為準,預設是打勾才有效 */
ready?: boolean;
}Slots
defineSlots<{
/** 標籤內容,預設顯示 label */
default?: () => unknown;
}>()🧩 FormRubeGoldbergButton
Props
interface Props {
/** @default 'submit' */
type?: 'submit' | 'button';
}Slots
defineSlots<{
/** 按鈕文字 */
default?: () => unknown;
}>()