关于 inertia-rails-forms
面向 Inertia Rails 开发者的表单构建指南,涵盖 React、Vue 和 Svelte。介绍表单状态、验证、文件上传与 Rails 控制器处理模式。
主要功能
- 01管理响应式表单状态
- 02处理验证错误与提交状态
- 03上传文件并查看进度
- 04支持嵌套数据和多表单
- 05提供实时验证与乐观更新
适用场景
- 01构建用户注册或编辑表单
- 02显示服务器返回的验证错误
- 03提交包含文件的表单
- 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 属性和方法
属性
| 属性 | 类型 | 描述 |
|---|---|---|
data | Object | 当前表单数据 |
errors | Object | 来自服务端的验证错误 |
hasErrors | Boolean | 是否存在错误 |
processing | Boolean | 表单是否正在提交 |
progress | Object | 文件上传进度 |
wasSuccessful | Boolean | 成功提交后为 True |
recentlySuccessful | Boolean | 成功后的 2 秒内为 True |
isDirty | Boolean | 表单数据是否已更改 |
方法
| 方法 | 描述 |
|---|---|
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 的主要区别
| 功能 | useForm | useHttp |
|---|---|---|
| 触发导航 | 是 | 否 |
| 返回 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