inertia-rails-forms
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseInertia Rails Forms
Inertia Rails 表单
Comprehensive guide to building forms in Inertia Rails applications with React, Vue, or Svelte.
这是一份在Inertia Rails应用中结合React、Vue或Svelte构建表单的综合指南。
The useForm Helper
useForm 辅助函数
The helper provides reactive form state management with built-in features for validation, file uploads, and submission handling.
useFormuseForm辅助函数提供响应式的表单状态管理,内置验证、文件上传和提交处理功能。
React
React 实现
jsx
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>
)
}jsx
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
Vue 3 实现
vue
<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>vue
<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
Svelte 实现
svelte
<script>
import { useForm } from '@inertiajs/svelte'
let form = useForm({
name: '',
email: '',
password: '',
avatar: null,
})
function submit() {
$form.post('/users', {
onSuccess: () => $form.reset('password'),
preserveScroll: true,
})
}
</script>
<form on:submit|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" on:change={(e) => ($form.avatar = e.target.files[0])} />
</div>
<button type="submit" disabled={$form.processing}>
{$form.processing ? 'Creating...' : 'Create User'}
</button>
</form>svelte
<script>
import { useForm } from '@inertiajs/svelte'
let form = useForm({
name: '',
email: '',
password: '',
avatar: null,
})
function submit() {
$form.post('/users', {
onSuccess: () => $form.reset('password'),
preserveScroll: true,
})
}
</script>
<form on:submit|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" on:change={(e) => ($form.avatar = e.target.files[0])} />
</div>
<button type="submit" disabled={$form.processing}>
{$form.processing ? 'Creating...' : 'Create User'}
</button>
</form>Rails Controller Pattern
Rails 控制器模式
The standard pattern for handling form submissions:
ruby
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处理表单提交的标准模式:
ruby
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
enduseForm Properties and Methods
useForm 的属性与方法
Properties
属性
| Property | Type | Description |
|---|---|---|
| Object | Current form data |
| Object | Validation errors from server |
| Boolean | Whether errors exist |
| Boolean | Whether form is submitting |
| Object | File upload progress |
| Boolean | True after successful submission |
| Boolean | True for 2 seconds after success |
| Boolean | Whether form data has changed |
| 属性 | 类型 | 说明 |
|---|---|---|
| Object | 当前表单数据 |
| Object | 服务器返回的验证错误 |
| Boolean | 是否存在错误 |
| Boolean | 表单是否正在提交 |
| Object | 文件上传进度 |
| Boolean | 提交成功后为true |
| Boolean | 提交成功后的2秒内为true |
| Boolean | 表单数据是否已修改 |
Methods
方法
| Method | Description |
|---|---|
| Set a single field value |
| Set multiple field values |
| Reset all fields to initial values |
| Reset specific fields |
| Clear all validation errors |
| Clear specific field errors |
| Set a custom error |
| Set multiple errors |
| Transform data before submission |
| Update default values for reset |
| Submit GET request |
| Submit POST request |
| Submit PUT request |
| Submit PATCH request |
| Submit DELETE request |
| 方法 | 说明 |
|---|---|
| 设置单个字段的值 |
| 设置多个字段的值 |
| 将所有字段重置为初始值 |
| 重置指定字段 |
| 清除所有验证错误 |
| 清除指定字段的错误 |
| 设置自定义错误 |
| 设置多个错误 |
| 提交前转换数据 |
| 更新重置时的默认值 |
| 提交GET请求 |
| 提交POST请求 |
| 提交PUT请求 |
| 提交PATCH请求 |
| 提交DELETE请求 |
Submission Options
提交选项
javascript
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: () => {},
})javascript
form.post('/users', {
// 验证错误时保留组件状态
preserveState: true, // 或设为'errors'仅在错误时保留
// 保留滚动位置
preserveScroll: true, // 或设为'errors'仅在错误时保留
// 自定义请求头
headers: { 'X-Custom': 'value' },
// 即使没有文件也强制使用FormData
forceFormData: true,
// 同一页面多表单的错误命名空间
errorBag: 'createUser',
// 事件回调
onBefore: (visit) => confirm('Submit form?'),
onStart: (visit) => {},
onProgress: (progress) => {},
onSuccess: (page) => form.reset(),
onError: (errors) => console.log(errors),
onCancel: () => {},
onFinish: () => {},
})File Uploads
文件上传
Inertia automatically converts forms with files to :
FormDatajavascript
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))}
/>Inertia会自动将包含文件的表单转换为格式:
FormDatajavascript
const form = useForm({
name: '',
avatar: null,
documents: [], // 多文件上传
})
// 单文件上传
<input type="file" onChange={(e) => setData('avatar', e.target.files[0])} />
// 多文件上传
<input
type="file"
multiple
onChange={(e) => setData('documents', Array.from(e.target.files))}
/>Upload Progress
上传进度显示
vue
<template>
<div v-if="form.progress">
<progress :value="form.progress.percentage" max="100" />
<span>{{ form.progress.percentage }}%</span>
</div>
</template>vue
<template>
<div v-if="form.progress">
<progress :value="form.progress.percentage" max="100" />
<span>{{ form.progress.percentage }}%</span>
</div>
</template>File Uploads with PUT/PATCH
使用PUT/PATCH进行文件上传
Some servers don't support multipart PUT/PATCH. Use method spoofing:
javascript
// Instead of form.put()
form.post(`/users/${user.id}`, {
_method: 'put', // Rails recognizes this
})部分服务器不支持multipart格式的PUT/PATCH请求,可使用方法伪装:
javascript
// 替代form.put()
form.post(`/users/${user.id}`, {
_method: 'put', // Rails可识别该参数
})Nested Data
嵌套数据
Use bracket notation for nested attributes:
javascript
const form = useForm({
user: {
name: '',
profile: {
bio: '',
},
},
})
// Access errors
form.errors['user.name']
form.errors['user.profile.bio']Or with Rails-style params:
vue
<input name="user[name]" v-model="form.user.name" />
<input name="user[profile][bio]" v-model="form.user.profile.bio" />使用括号语法处理嵌套属性:
javascript
const form = useForm({
user: {
name: '',
profile: {
bio: '',
},
},
})
// 访问错误信息
form.errors['user.name']
form.errors['user.profile.bio']或使用Rails风格的参数名:
vue
<input name="user[name]" v-model="form.user.name" />
<input name="user[profile][bio]" v-model="form.user.profile.bio" />Multiple Forms on Same Page
同一页面多表单处理
Use error bags to isolate validation errors:
javascript
// 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' })Server-side:
ruby
def create
# ...
redirect_to root_url, inertia: {
errors: { login: { email: 'Invalid credentials' } }
}
endAccess errors:
page.props.errors.login.email使用errorBag隔离不同表单的验证错误:
javascript
// 登录表单
const loginForm = useForm({ email: '', password: '' })
loginForm.post('/login', { errorBag: 'login' })
// 注册表单
const registerForm = useForm({ name: '', email: '', password: '' })
registerForm.post('/register', { errorBag: 'register' })服务器端处理:
ruby
def create
# ...
redirect_to root_url, inertia: {
errors: { login: { email: 'Invalid credentials' } }
}
end访问错误信息:
page.props.errors.login.emailForm Transforms
表单数据转换
Transform data before submission:
javascript
const form = useForm({
first_name: 'John',
last_name: 'Doe',
})
form
.transform((data) => ({
...data,
full_name: `${data.first_name} ${data.last_name}`,
}))
.post('/users')提交前可对数据进行转换:
javascript
const form = useForm({
first_name: 'John',
last_name: 'Doe',
})
form
.transform((data) => ({
...data,
full_name: `${data.first_name} ${data.last_name}`,
}))
.post('/users')The Form Component (Declarative)
Form 组件(声明式写法)
For simpler forms, use the Form component:
对于简单表单,可使用Form组件:
Vue
Vue 实现
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>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">
提交
</button>
</Form>
</template>React
React 实现
jsx
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>
)
}jsx
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}>
提交
</button>
</>
)}
</Form>
)
}Remembering Form State
表单状态持久化
Preserve form data across browser history navigation using .
useRemember使用钩子在浏览器历史导航中保存表单数据。
useRememberThe useRemember Hook
useRemember 钩子
javascript
import { useRemember } from '@inertiajs/vue3'
// Form state persists across back/forward navigation
const form = useRemember({
name: '',
email: '',
message: '',
})javascript
import { useRemember } from '@inertiajs/vue3'
// 表单状态在前进/后退导航中保持
const form = useRemember({
name: '',
email: '',
message: '',
})Multiple Components on Same Page
同一页面多组件状态持久化
Provide a unique key when multiple components use remember:
javascript
// Contact form
const contactForm = useRemember({
email: '',
message: '',
}, 'ContactForm')
// Newsletter form
const newsletterForm = useRemember({
email: '',
}, 'NewsletterForm')当多个组件使用状态持久化时,需提供唯一标识:
javascript
// 联系表单
const contactForm = useRemember({
email: '',
message: '',
}, 'ContactForm')
// 订阅表单
const newsletterForm = useRemember({
email: '',
}, 'NewsletterForm')With useForm Helper
结合useForm辅助函数使用
The form helper has built-in remember support:
javascript
// 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,
})useForm辅助函数内置状态持久化支持:
javascript
// 第一个参数传入唯一标识
const form = useForm('CreateUser', {
name: '',
email: '',
password: '',
})
// 编辑表单中,需包含ID以保证唯一性
const form = useForm(`EditUser:${props.user.id}`, {
name: props.user.name,
email: props.user.email,
})Manual State Management
手动状态管理
javascript
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
}javascript
import { router } from '@inertiajs/vue3'
// 手动保存状态
router.remember({ step: 2, selections: ['a', 'b'] }, 'wizard-state')
// 恢复状态
const savedState = router.restore('wizard-state')
if (savedState) {
// 从savedState恢复组件状态
}React Example
React 示例
jsx
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>
)
}jsx
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>
)
}Best Practices
最佳实践
1. Always Use the PRG Pattern
1. 始终使用PRG模式
ruby
undefinedruby
undefinedIncorrect - renders on POST
错误示例 - 在POST请求中直接渲染
def create
@user = User.create(user_params)
render inertia: { user: @user }
end
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
undefineddef create
user = User.create(user_params)
redirect_to user_url(user)
end
undefined2. Return Minimal Error Data
2. 返回精简的错误数据
ruby
undefinedruby
undefinedOnly include field errors, not full model
仅返回字段错误,而非完整模型信息
redirect_to new_user_url, inertia: { errors: user.errors.to_hash }
undefinedredirect_to new_user_url, inertia: { errors: user.errors.to_hash }
undefined3. Handle Validation on Client and Server
3. 同时在客户端和服务端进行验证
javascript
// 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
}
}javascript
// 客户端验证提升用户体验
const validateEmail = (email) => {
if (!email.includes('@')) {
form.setError('email', '邮箱格式无效')
return false
}
return true
}
function submit() {
if (validateEmail(form.email)) {
form.post('/users') // 服务端也会进行验证
}
}4. Preserve Scroll on Errors
4. 错误时保留滚动位置
javascript
form.post('/users', {
preserveScroll: 'errors', // Only preserve on validation errors
})javascript
form.post('/users', {
preserveScroll: 'errors', // 仅在验证错误时保留滚动位置
})5. Reset Sensitive Fields on Success
5. 成功提交后重置敏感字段
javascript
form.post('/users', {
onSuccess: () => form.reset('password', 'password_confirmation'),
})javascript
form.post('/users', {
onSuccess: () => form.reset('password', 'password_confirmation'),
})6. Show Loading State
6. 显示加载状态
vue
<button type="submit" :disabled="form.processing">
<span v-if="form.processing">
<Spinner /> Saving...
</span>
<span v-else>Save</span>
</button>vue
<button type="submit" :disabled="form.processing">
<span v-if="form.processing">
<Spinner /> 保存中...
</span>
<span v-else>保存</span>
</button>7. Confirm Destructive Actions
7. 危险操作添加确认提示
javascript
function deleteUser() {
if (confirm('Are you sure?')) {
router.delete(`/users/${user.id}`)
}
}javascript
function deleteUser() {
if (confirm('确定要删除吗?')) {
router.delete(`/users/${user.id}`)
}
}