mcpField
I
inertia-rails-forms

inertia-rails-forms

github.com/cole-robertson开发3888 次安装32 周前更新
代码仓库

关于 inertia-rails-forms

面向 Inertia Rails 开发者的表单构建指南,涵盖 React、Vue 和 Svelte。介绍表单状态、验证、文件上传与 Rails 控制器处理模式。

主要功能

  1. 01管理响应式表单状态
  2. 02处理验证错误与提交状态
  3. 03上传文件并查看进度
  4. 04支持嵌套数据和多表单
  5. 05提供实时验证与乐观更新

适用场景

  1. 01构建用户注册或编辑表单
  2. 02显示服务器返回的验证错误
  3. 03提交包含文件的表单
  4. 04在同页隔离多个表单的错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cole-robertson/inertia-rails-skills --skill inertia-rails-forms

安装后在 Claude Code 中输入/inertia-rails-forms确认已加载。

详细介绍

Inertia Rails 表单

使用 React、Vue 或 Svelte 在 Inertia Rails 应用中构建表单的全面指南。

useForm 辅助函数

useForm 辅助函数提供响应式表单状态管理,并内置验证、文件上传和提交处理功能。

React
import { useForm } from '@inertiajs/react'

export default function CreateUser() {
  const { data, setData, post, processing, errors, reset } = useForm({
    name: '',
    email: '',
    password: '',
    avatar: null,
  })

  function submit(e) {
    e.preventDefault()
    post('/users', {
      onSuccess: () => reset('password'),
      preserveScroll: true,
    })
  }

  return (
    <form onSubmit={submit}>
      <div>
        <label>Name</label>
        <input
          type="text"
          value={data.name}
          onChange={(e) => setData('name', e.target.value)}
        />
        {errors.name && <span className="error">{errors.name}</span>}
      </div>

      <div>
        <label>Email</label>
        <input
          type="email"
          value={data.email}
          onChange={(e) => setData('email', e.target.value)}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <label>Password</label>
        <input
          type="password"
          value={data.password}
          onChange={(e) => setData('password', e.target.value)}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>

      <div>
        <label>Avatar</label>
        <input
          type="file"
          onChange={(e) => setData('avatar', e.target.files[0])}
        />
      </div>

      <button type="submit" disabled={processing}>
        {processing ? 'Creating...' : 'Create User'}
      </button>
    </form>
  )
}
Vue 3
<script setup>
import { useForm } from '@inertiajs/vue3'

const form = useForm({
  name: '',
  email: '',
  password: '',
  avatar: null,
})

function submit() {
  form.post('/users', {
    onSuccess: () => form.reset('password'),
    preserveScroll: true,
  })
}
</script>

<template>
  <form @submit.prevent="submit">
    <div>
      <label>Name</label>
      <input v-model="form.name" type="text" />
      <span v-if="form.errors.name" class="error">{{ form.errors.name }}</span>
    </div>

    <div>
      <label>Email</label>
      <input v-model="form.email" type="email" />
      <span v-if="form.errors.email" class="error">{{ form.errors.email }}</span>
    </div>

    <div>
      <label>Password</label>
      <input v-model="form.password" type="password" />
      <span v-if="form.errors.password" class="error">{{ form.errors.password }}</span>
    </div>

    <div>
      <label>Avatar</label>
      <input type="file" @change="form.avatar = $event.target.files[0]" />
      <progress v-if="form.progress" :value="form.progress.percentage" max="100" />
    </div>

    <button type="submit" :disabled="form.processing">
      {{ form.processing ? 'Creating...' : 'Create User' }}
    </button>
  </form>
</template>
Svelte
<script>
import { useForm } from '@inertiajs/svelte'

const form = useForm({
  name: '',
  email: '',
  password: '',
  avatar: null,
})

function submit() {
  form.post('/users', {
    onSuccess: () => form.reset('password'),
    preserveScroll: true,
  })
}
</script>

<form onsubmit={(e) => { e.preventDefault(); submit() }}>
  <div>
    <label>Name</label>
    <input type="text" bind:value={form.name} />
    {#if form.errors.name}
      <span class="error">{form.errors.name}</span>
    {/if}
  </div>

  <div>
    <label>Email</label>
    <input type="email" bind:value={form.email} />
    {#if form.errors.email}
      <span class="error">{form.errors.email}</span>
    {/if}
  </div>

  <div>
    <label>Password</label>
    <input type="password" bind:value={form.password} />
  </div>

  <div>
    <label>Avatar</label>
    <input type="file" onchange={(e) => (form.avatar = e.target.files[0])} />
  </div>

  <button type="submit" disabled={form.processing}>
    {form.processing ? 'Creating...' : 'Create User'}
  </button>
</form>

Rails 控制器模式

处理表单提交的标准模式:

class UsersController < ApplicationController
  def new
    render inertia: {}
  end

  def create
    user = User.new(user_params)

    if user.save
      redirect_to users_url, notice: 'User created successfully!'
    else
      redirect_to new_user_url, inertia: { errors: user.errors }
    end
  end

  def edit
    user = User.find(params[:id])
    render inertia: { user: user.as_json(only: [:id, :name, :email]) }
  end

  def update
    user = User.find(params[:id])

    if user.update(user_params)
      redirect_to user_url(user), notice: 'User updated successfully!'
    else
      redirect_to edit_user_url(user), inertia: { errors: user.errors }
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation, :avatar)
  end
end

useForm 属性和方法

属性
属性类型描述
dataObject当前表单数据
errorsObject来自服务端的验证错误
hasErrorsBoolean是否存在错误
processingBoolean表单是否正在提交
progressObject文件上传进度
wasSuccessfulBoolean成功提交后为 True
recentlySuccessfulBoolean成功后的 2 秒内为 True
isDirtyBoolean表单数据是否已更改
方法
方法描述
setData(key, value)设置单个字段的值
setData(values)设置多个字段的值
reset()将所有字段重置为初始值
reset(...fields)重置指定字段
clearErrors()清除所有验证错误
clearErrors(...fields)清除指定字段的错误
setError(field, message)设置自定义错误
setError(errors)设置多个错误
transform(callback)提交前转换数据
defaults()更新重置时使用的默认值
get(url, options)提交 GET 请求
post(url, options)提交 POST 请求
put(url, options)提交 PUT 请求
patch(url, options)提交 PATCH 请求
delete(url, options)提交 DELETE 请求
cancel()取消正在进行的表单提交
提交选项
form.post('/users', {
  // Preserve component state on validation errors
  preserveState: true,       // or 'errors' to preserve only on errors

  // Preserve scroll position
  preserveScroll: true,      // or 'errors' to preserve only on errors

  // Custom headers
  headers: { 'X-Custom': 'value' },

  // Force FormData even without files
  forceFormData: true,

  // Error bag for multiple forms on same page
  errorBag: 'createUser',

  // Event callbacks
  onBefore: (visit) => confirm('Submit form?'),
  onStart: (visit) => {},
  onProgress: (progress) => {},
  onSuccess: (page) => form.reset(),
  onError: (errors) => console.log(errors),
  onCancel: () => {},
  onFinish: () => {},
})

文件上传

Inertia 会自动将包含文件的表单转换为 FormData:

const form = useForm({
  name: '',
  avatar: null,
  documents: [],  // Multiple files
})

// Single file
<input type="file" onChange={(e) => setData('avatar', e.target.files[0])} />

// Multiple files
<input
  type="file"
  multiple
  onChange={(e) => setData('documents', Array.from(e.target.files))}
/>
上传进度
<template>
  <div v-if="form.progress">
    <progress :value="form.progress.percentage" max="100" />
    <span>{{ form.progress.percentage }}%</span>
  </div>
</template>
使用 PUT/PATCH 上传文件

有些服务端不支持 multipart PUT/PATCH。请使用方法伪装:

// Instead of form.put()
form.post(`/users/${user.id}`, {
  _method: 'put',  // Rails recognizes this
})

嵌套数据

使用方括号表示法设置嵌套属性:

const form = useForm({
  user: {
    name: '',
    profile: {
      bio: '',
    },
  },
})

// Access errors
form.errors['user.name']
form.errors['user.profile.bio']

或者使用 Rails 风格的参数:

<input name="user[name]" v-model="form.user.name" />
<input name="user[profile][bio]" v-model="form.user.profile.bio" />

同一页面上的多个表单

使用错误包隔离验证错误:

// Login form
const loginForm = useForm({ email: '', password: '' })
loginForm.post('/login', { errorBag: 'login' })

// Register form
const registerForm = useForm({ name: '', email: '', password: '' })
registerForm.post('/register', { errorBag: 'register' })

服务端:

def create
  # ...
  redirect_to root_url, inertia: {
    errors: { login: { email: 'Invalid credentials' } }
  }
end

访问错误:page.props.errors.login.email

表单数据转换

提交前转换数据:

const form = useForm({
  first_name: 'John',
  last_name: 'Doe',
})

form
  .transform((data) => ({
    ...data,
    full_name: `${data.first_name} ${data.last_name}`,
  }))
  .post('/users')

Form 组件(声明式)

对于更简单的表单,可以使用 Form 组件:

Vue
<script setup>
import { Form } from '@inertiajs/vue3'
</script>

<template>
  <Form action="/users" method="post" v-slot="{ errors, processing }">
    <input type="text" name="name" />
    <span v-if="errors.name">{{ errors.name }}</span>

    <input type="email" name="email" />
    <span v-if="errors.email">{{ errors.email }}</span>

    <button type="submit" :disabled="processing">
      Submit
    </button>
  </Form>
</template>
React
import { Form } from '@inertiajs/react'

export default function CreateUser() {
  return (
    <Form action="/users" method="post">
      {({ errors, processing }) => (
        <>
          <input type="text" name="name" />
          {errors.name && <span>{errors.name}</span>}

          <input type="email" name="email" />
          {errors.email && <span>{errors.email}</span>}

          <button type="submit" disabled={processing}>
            Submit
          </button>
        </>
      )}
    </Form>
  )
}
Form 组件高级功能(v3)

v3 中的 Form 组件支持使用点号表示法处理嵌套数据和数组:

import { Form } from '@inertiajs/react'

export default function CreateReport() {
  return (
    <Form action="/reports" method="post">
      {({ errors, processing, isDirty, wasSuccessful }) => (
        <>
          {/* Dotted notation for nested data */}
          <input type="text" name="report.title" />
          {errors['report.title'] && <span>{errors['report.title']}</span>}

          <input type="text" name="report.metadata.author" />

          {/* Array support */}
          <input type="text" name="tags[]" />
          <input type="text" name="tags[]" />

          {/* Disable inputs while processing */}
          <button type="submit" disabled={processing}>
            Submit
          </button>
        </>
      )}
    </Form>
  )
}
useFormContext Hook

无需逐层传递属性,即可从深层嵌套的组件访问表单状态:

import { useFormContext } from '@inertiajs/react'

function SubmitButton() {
  const { processing, isDirty } = useFormContext()

  return (
    <button type="submit" disabled={processing || !isDirty}>
      {processing ? 'Saving...' : 'Save'}
    </button>
  )
}
Precognition(实时验证)

在服务端实时验证表单,无需在客户端重复验证规则:

import { Form } from '@inertiajs/react'

export default function CreateUser() {
  return (
    <Form action="/users" method="post" precognition>
      {({ errors, valid, invalid, validate }) => (
        <>
          <input
            type="email"
            name="email"
            onBlur={() => validate('email')}
          />
          {valid('email') && <span className="text-green-500">✓</span>}
          {invalid('email') && <span className="text-red-500">{errors.email}</span>}

          <button type="submit">Create</button>
        </>
      )}
    </Form>
  )
}

服务端:

# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
  config.precognition_prevent_writes = true  # Prevent DB writes during validation
end

记住表单状态

使用 useRemember 在浏览器历史记录导航时保留表单数据。

useRemember Hook
import { useRemember } from '@inertiajs/vue3'

// Form state persists across back/forward navigation
const form = useRemember({
  name: '',
  email: '',
  message: '',
})
同一页面上的多个组件

多个组件使用 remember 时,请提供唯一键:

// Contact form
const contactForm = useRemember({
  email: '',
  message: '',
}, 'ContactForm')

// Newsletter form
const newsletterForm = useRemember({
  email: '',
}, 'NewsletterForm')
与 useForm 辅助函数搭配使用

表单辅助函数内置了记住状态的支持:

// Pass a unique key as first argument
const form = useForm('CreateUser', {
  name: '',
  email: '',
  password: '',
})

// For edit forms, include the ID for uniqueness
const form = useForm(`EditUser:${props.user.id}`, {
  name: props.user.name,
  email: props.user.email,
})
手动状态管理
import { router } from '@inertiajs/vue3'

// Save state manually
router.remember({ step: 2, selections: ['a', 'b'] }, 'wizard-state')

// Restore state
const savedState = router.restore('wizard-state')
if (savedState) {
  // Restore component state from savedState
}
React 示例
import { useRemember } from '@inertiajs/react'

export default function ContactForm() {
  const [form, setForm] = useRemember({
    name: '',
    email: '',
    message: '',
  }, 'ContactForm')

  return (
    <form>
      <input
        value={form.name}
        onChange={(e) => setForm({ ...form, name: e.target.value })}
      />
      {/* ... */}
    </form>
  )
}

useHttp Hook(v3)

发送独立 HTTP 请求,而不触发 Inertia 页面访问。适用于 API 调用、后台操作以及无需导航的 AJAX 请求。

React
import { useHttp } from '@inertiajs/react'

export default function SearchUsers() {
  const { data, post, processing, errors } = useHttp()
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  function search() {
    post('/api/search', {
      data: { query },
      onSuccess: (response) => setResults(response.data),
    })
  }

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button onClick={search} disabled={processing}>
        {processing ? 'Searching...' : 'Search'}
      </button>
      {results.map(user => <div key={user.id}>{user.name}</div>)}
    </div>
  )
}
与 useForm 的主要区别
功能useFormuseHttp
触发导航是否
返回 Promise否是
更新页面属性是否
响应式状态是是
文件上传进度是是
Precognition 支持是是

如果需要发起不会导致页面跳转的 API 请求,请使用 useHttp。

乐观更新(v3)

在服务器响应前立即更新 UI,失败时自动回滚:

使用 router
import { router } from '@inertiajs/react'

function toggleLike(post) {
  router
    .optimistic((page) => {
      // Optimistically update the page props
      const postIndex = page.props.posts.findIndex(p => p.id === post.id)
      page.props.posts[postIndex].liked = !post.liked
      page.props.posts[postIndex].likes_count += post.liked ? -1 : 1
    })
    .post(`/posts/${post.id}/toggle-like`)
}
使用 Form 组件
<Form
  action={`/posts/${post.id}/toggle-like`}
  method="post"
  optimistic={(page) => {
    const p = page.props.posts.find(p => p.id === post.id)
    p.liked = !p.liked
  }}
>
  <button type="submit">
    {post.liked ? '❤️' : '🤍'} {post.likes_count}
  </button>
</Form>
使用 useForm
const form = useForm({})

form
  .optimistic((page) => {
    page.props.posts[0].title = 'Updated title'
  })
  .patch(`/posts/${post.id}`)

如果服务器返回验证错误或非 2xx 响应,乐观更新会自动回滚。

最佳实践

1. 始终使用 PRG 模式
# Incorrect - renders on POST
def create
  @user = User.create(user_params)
  render inertia: { user: @user }
end

# Correct - redirect after action
def create
  user = User.create(user_params)
  redirect_to user_url(user)
end
2. 仅返回必要的错误数据
# Only include field errors, not full model
redirect_to new_user_url, inertia: { errors: user.errors.to_hash }
3. 在客户端和服务器端处理验证
// Client-side for UX
const validateEmail = (email) => {
  if (!email.includes('@')) {
    form.setError('email', 'Invalid email format')
    return false
  }
  return true
}

function submit() {
  if (validateEmail(form.email)) {
    form.post('/users')  // Server validates too
  }
}
4. 出现错误时保留滚动位置
form.post('/users', {
  preserveScroll: 'errors',  // Only preserve on validation errors
})
5. 成功后重置敏感字段
form.post('/users', {
  onSuccess: () => form.reset('password', 'password_confirmation'),
})
6. 显示加载状态
<button type="submit" :disabled="form.processing">
  <span v-if="form.processing">
    <Spinner /> Saving...
  </span>
  <span v-else>Save</span>
</button>
7. 确认破坏性操作
function deleteUser() {
  if (confirm('Are you sure?')) {
    router.delete(`/users/${user.id}`)
  }
}

评论

支持 Markdown