inertia-rails-forms

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Inertia 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
useForm
helper provides reactive form state management with built-in features for validation, file uploads, and submission handling.
useForm辅助函数提供响应式的表单状态管理,内置验证、文件上传和提交处理功能。

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
end

useForm Properties and Methods

useForm 的属性与方法

Properties

属性

PropertyTypeDescription
data
ObjectCurrent form data
errors
ObjectValidation errors from server
hasErrors
BooleanWhether errors exist
processing
BooleanWhether form is submitting
progress
ObjectFile upload progress
wasSuccessful
BooleanTrue after successful submission
recentlySuccessful
BooleanTrue for 2 seconds after success
isDirty
BooleanWhether form data has changed
属性类型说明
data
Object当前表单数据
errors
Object服务器返回的验证错误
hasErrors
Boolean是否存在错误
processing
Boolean表单是否正在提交
progress
Object文件上传进度
wasSuccessful
Boolean提交成功后为true
recentlySuccessful
Boolean提交成功后的2秒内为true
isDirty
Boolean表单数据是否已修改

Methods

方法

MethodDescription
setData(key, value)
Set a single field value
setData(values)
Set multiple field values
reset()
Reset all fields to initial values
reset(...fields)
Reset specific fields
clearErrors()
Clear all validation errors
clearErrors(...fields)
Clear specific field errors
setError(field, message)
Set a custom error
setError(errors)
Set multiple errors
transform(callback)
Transform data before submission
defaults()
Update default values for reset
get(url, options)
Submit GET request
post(url, options)
Submit POST request
put(url, options)
Submit PUT request
patch(url, options)
Submit PATCH request
delete(url, options)
Submit DELETE request
方法说明
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请求

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
FormData
:
javascript
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会自动将包含文件的表单转换为
FormData
格式:
javascript
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' } }
  }
end
Access 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.email

Form 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
.
使用
useRemember
钩子在浏览器历史导航中保存表单数据。

The 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
undefined
ruby
undefined

Incorrect - 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
undefined
def create user = User.create(user_params) redirect_to user_url(user) end
undefined

2. Return Minimal Error Data

2. 返回精简的错误数据

ruby
undefined
ruby
undefined

Only include field errors, not full model

仅返回字段错误,而非完整模型信息

redirect_to new_user_url, inertia: { errors: user.errors.to_hash }
undefined
redirect_to new_user_url, inertia: { errors: user.errors.to_hash }
undefined

3. 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}`)
  }
}