Skip to content

每條游來游去的小魚,都是正在瀏覽這一頁的訪客,有一條是你自己 ( ´ ▽ ` )ノ 覺得干擾閱讀的話,可以把小魚關掉。

戈德堡表單 form ​

按鈕不會馬上解鎖,讓我們一步一步來。(´-∀-`)

靈感來自戈德堡機械(Rube Goldberg machine),動用一整套機關,大費周章完成一件按一下就能做完的事。

技術關鍵字 ​

名稱描述
Anime.js輕量級 JavaScript 動畫函式庫
SVG可在 DOM 中使用的圖形格式,適合複雜圖形和動畫,配合 Vue v-bind 可以實現基於資料的動態效果
JS 動畫基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等
Vue Provide/InjectVue 的依賴注入機制,允許父元件向其所有後代元件提供資料或方法
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;
}>()

v0.93.1