mcpField
I
inertia-rails-cookbook

inertia-rails-cookbook

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

关于 inertia-rails-cookbook

Inertia Rails Cookbook 为开发 Inertia Rails 应用的人员提供常见模式和集成方案。它通过实用示例涵盖前端集成、页面交互和 Rails 工作流。

主要功能

  1. 01资源与路由配置
  2. 02布局属性和自定义
  3. 03模态页面与对话框
  4. 04搜索、筛选和向导
  5. 05闪存消息与验证
  6. 06ActionCable 和文件下载

适用场景

  1. 01为 Rails 应用添加资源页面
  2. 02构建可搜索和筛选的数据表
  3. 03创建多步骤引导流程
  4. 04在模态框中打开页面并保留 URL 历史

安装

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

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

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

详细介绍

Inertia Rails 实用指南

Inertia Rails 应用中常见模式和集成的实用方案。

使用官方入门套件

官方入门套件提供了完整的基础框架。以下介绍如何根据需要进行自定义。

入门套件结构(React)
app/
├── controllers/
│   ├── application_controller.rb    # Shared data setup
│   ├── dashboard_controller.rb      # Example authenticated page
│   ├── home_controller.rb           # Public landing page
│   ├── sessions_controller.rb       # Login/logout
│   ├── users_controller.rb          # Registration
│   ├── identity/                    # Password reset
│   └── settings/                    # User settings
├── frontend/
│   ├── components/
│   │   ├── ui/                      # shadcn/ui components
│   │   ├── nav-main.tsx             # Main navigation
│   │   ├── app-sidebar.tsx          # Sidebar component
│   │   └── user-menu-content.tsx    # User dropdown
│   ├── hooks/
│   │   ├── use-flash.tsx            # Flash message hook
│   │   └── use-appearance.tsx       # Dark mode hook
│   ├── layouts/
│   │   ├── app-layout.tsx           # Main app layout
│   │   ├── auth-layout.tsx          # Auth pages layout
│   │   └── app/
│   │       ├── app-sidebar-layout.tsx
│   │       └── app-header-layout.tsx
│   ├── pages/
│   │   ├── dashboard/index.tsx      # Dashboard page
│   │   ├── home/index.tsx           # Landing page
│   │   ├── sessions/new.tsx         # Login page
│   │   ├── users/new.tsx            # Registration page
│   │   └── settings/                # Settings pages
│   └── types/
│       └── index.ts                 # Shared TypeScript types
添加新资源

1. 生成控制器:

bin/rails generate controller Products index show new create edit update destroy

2. 创建页面组件:

// app/frontend/pages/products/index.tsx
import { Head, Link } from '@inertiajs/react'
import AppLayout from '@/layouts/app-layout'
import { Button } from '@/components/ui/button'
import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableRow
} from '@/components/ui/table'

interface Product {
  id: number
  name: string
  price: number
}

interface Props {
  products: Product[]
}

export default function ProductsIndex({ products }: Props) {
  return (
    <AppLayout>
      <Head title="Products" />

      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">Products</h1>
        <Button asChild>
          <Link href="/products/new">Add Product</Link>
        </Button>
      </div>

      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Name</TableHead>
            <TableHead>Price</TableHead>
            <TableHead>Actions</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {products.map((product) => (
            <TableRow key={product.id}>
              <TableCell>{product.name}</TableCell>
              <TableCell>${product.price}</TableCell>
              <TableCell>
                <Link href={`/products/${product.id}/edit`}>Edit</Link>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </AppLayout>
  )
}

3. 更新导航:

// app/frontend/components/nav-main.tsx
const navItems = [
  { title: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
  { title: 'Products', href: '/products', icon: Package },  // Add this
  // ...
]

4. 添加路由:

# config/routes.rb
resources :products
添加新的 shadcn/ui 组件

入门套件包含许多组件,但你也可以添加更多组件:

# Add a specific component
npx shadcn@latest add toast
npx shadcn@latest add calendar
npx shadcn@latest add data-table

# See all available components
npx shadcn@latest add
自定义布局

在侧边栏布局和页眉布局之间切换:

// app/frontend/layouts/app-layout.tsx
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout'
import AppHeaderLayout from '@/layouts/app/app-header-layout'

// Use sidebar (default)
export default function AppLayout({ children }: Props) {
  return <AppSidebarLayout>{children}</AppSidebarLayout>
}

// Or use header layout
export default function AppLayout({ children }: Props) {
  return <AppHeaderLayout>{children}</AppHeaderLayout>
}
扩展类型
// app/frontend/types/index.ts
export interface User {
  id: number
  name: string
  email: string
  avatar_url: string | null
}

// Add your own types
export interface Product {
  id: number
  name: string
  description: string
  price: number
  created_at: string
}

export interface PageProps {
  auth: {
    user: User | null
  }
  flash: {
    success?: string
    error?: string
  }
}
使用 Flash Hook

入门套件包含一个使用 Sonner toast 的 Flash 消息系统:

// Already set up in the layout, just use flash in your controller
class ProductsController < ApplicationController
  def create
    @product = Product.create(product_params)
    redirect_to products_path, notice: 'Product created!'
  end
end

use-flash Hook 会自动将 Flash 消息显示为 toast。

移除不需要的功能

移除设置页面:

rm -rf app/frontend/pages/settings
rm -rf app/controllers/settings
# Remove routes in config/routes.rb

移除身份验证(用于内部工具):

rm -rf app/frontend/pages/sessions
rm -rf app/frontend/pages/users
rm -rf app/frontend/pages/identity
rm app/controllers/sessions_controller.rb
rm app/controllers/users_controller.rb
rm -rf app/controllers/identity
# Update routes and ApplicationController

布局 Props(v3)

使用 useLayoutProps 在页面及其持久化布局之间共享数据:

静态布局 Props
// app/frontend/pages/dashboard/index.tsx
import AppLayout from '@/layouts/app-layout'

export default function Dashboard({ stats }) {
  return <div>Dashboard content</div>
}

// Pass static props to the layout
Dashboard.layout = [AppLayout, { title: 'Dashboard', breadcrumbs: ['Home', 'Dashboard'] }]
动态布局 Props
import { useLayoutProps } from '@inertiajs/react'
import AppLayout from '@/layouts/app-layout'

export default function UserProfile({ user }) {
  // Set layout props dynamically
  useLayoutProps({ title: user.name, breadcrumbs: ['Users', user.name] })

  return <div>{user.name}'s profile</div>
}

UserProfile.layout = AppLayout
在布局中
// app/frontend/layouts/app-layout.tsx
import { useLayoutProps } from '@inertiajs/react'

export default function AppLayout({ children }) {
  const { title, breadcrumbs } = useLayoutProps()

  return (
    <div>
      <header>
        <h1>{title}</h1>
        <nav>{breadcrumbs?.join(' > ')}</nav>
      </header>
      <main>{children}</main>
    </div>
  )
}

Inertia Modal — 将页面渲染为对话框

inertia_rails-contrib gem 和 @inertiaui/modal 包可让你将任意 Inertia 页面渲染为模态对话框。

安装
# Ruby gem (optional, for base_url helper)
bundle add inertia_rails-contrib

# NPM package (Vue)
npm install @inertiaui/modal-vue

# NPM package (React)
npm install @inertiaui/modal-react
设置(React)
// app/frontend/entrypoints/application.js
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import { renderApp } from '@inertiaui/modal-react'

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('../pages/**/*.tsx', { eager: true })
    return pages[`../pages/${name}.tsx`]
  },
  setup({ el, App, props }) {
    createRoot(el).render(renderApp(App, props))
  },
})
Tailwind 配置
// tailwind.config.js (v3)
module.exports = {
  content: [
    // ... your content paths
    './node_modules/@inertiaui/modal-react/src/**/*.{js,jsx,ts,tsx}',
  ],
}
/* For Tailwind v4 */
@import "tailwindcss";
@source '../../../node_modules/@inertiaui/modal-react';
基本用法

以模态框形式打开页面:

import { ModalLink } from '@inertiaui/modal-react'

export default function UsersList() {
  return (
    <ModalLink href="/users/create">
      Create User
    </ModalLink>
  )
}

在 Modal 中包裹页面内容:

// pages/users/create.tsx
import { Modal } from '@inertiaui/modal-react'

export default function CreateUser({ roles }) {
  return (
    <Modal>
      <h2>Create User</h2>
      <UserForm roles={roles} />
    </Modal>
  )
}
带基础 URL 的模态框

启用 URL 更新和浏览器历史记录:

控制器:

class UsersController < ApplicationController
  def create
    render inertia_modal: {
      roles: Role.all.as_json
    }, base_url: users_path
  end
end

带导航的链接:

<ModalLink href="/users/create" navigate>
  Create User
</ModalLink>

打开时,URL 将变为 /users/create,支持浏览器后退按钮,也可以添加书签。

侧滑面板变体
<Modal slideover>
  <h2>User Details</h2>
  {/* Content slides in from the side */}
</Modal>
嵌套模态框
<Modal>
  <h2>Edit User</h2>
  <UserForm />

  {/* Open another modal from within */}
  <ModalLink href="/roles/create">
    Add New Role
  </ModalLink>
</Modal>
关闭模态框
import { Modal } from '@inertiaui/modal-react'

export default function EditUser({ onClose }) {
  return (
    <Modal onClose={onClose}>
      <button onClick={onClose}>Cancel</button>
    </Modal>
  )
}

集成 shadcn/ui

在 Inertia Rails 中使用 shadcn/ui 组件,打造精致的 UI。

设置(React)
# Initialize shadcn/ui
npx shadcn@latest init

# Add components
npx shadcn@latest add button input card form
使用 shadcn/ui 的表单
import { useForm } from '@inertiajs/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

export default function LoginForm() {
  const { data, setData, post, processing, errors } = useForm({
    email: '',
    password: '',
  })

  function submit(e) {
    e.preventDefault()
    post('/login')
  }

  return (
    <Card className="w-[400px]">
      <CardHeader>
        <CardTitle>Login</CardTitle>
      </CardHeader>
      <CardContent>
        <form onSubmit={submit} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              type="email"
              value={data.email}
              onChange={(e) => setData('email', e.target.value)}
              placeholder="[email protected]"
            />
            {errors.email && (
              <p className="text-sm text-red-500">{errors.email}</p>
            )}
          </div>

          <div className="space-y-2">
            <Label htmlFor="password">Password</Label>
            <Input
              id="password"
              type="password"
              value={data.password}
              onChange={(e) => setData('password', e.target.value)}
            />
          </div>

          <Button type="submit" disabled={processing} className="w-full">
            {processing ? 'Signing in...' : 'Sign in'}
          </Button>
        </form>
      </CardContent>
    </Card>
  )
}
支持排序和筛选的数据表
import { router, usePage } from '@inertiajs/react'
import { useState, useCallback } from 'react'
import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableRow
} from '@/components/ui/table'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Link } from '@inertiajs/react'
import { useDebouncedCallback } from 'use-debounce'

export default function UsersTable({ users, filters }) {
  const [search, setSearch] = useState(filters.search || '')
  const [sort, setSort] = useState(filters.sort || 'name')
  const [direction, setDirection] = useState(filters.direction || 'asc')

  const debouncedSearch = useDebouncedCallback((value) => {
    router.get('/users', { search: value, sort, direction }, {
      preserveState: true,
      replace: true,
    })
  }, 300)

  function toggleSort(column) {
    const newDirection = sort === column && direction === 'asc' ? 'desc' : 'asc'
    setSort(column)
    setDirection(newDirection)
    router.get('/users', { search, sort: column, direction: newDirection }, {
      preserveState: true,
      replace: true,
    })
  }

  return (
    <div className="space-y-4">
      <Input
        value={search}
        onChange={(e) => {
          setSearch(e.target.value)
          debouncedSearch(e.target.value)
        }}
        placeholder="Search users..."
        className="max-w-sm"
      />

      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>
              <Button variant="ghost" onClick={() => toggleSort('name')}>
                Name {sort === 'name' && (direction === 'asc' ? '↑' : '↓')}
              </Button>
            </TableHead>
            <TableHead>
              <Button variant="ghost" onClick={() => toggleSort('email')}>
                Email {sort === 'email' && (direction === 'asc' ? '↑' : '↓')}
              </Button>
            </TableHead>
            <TableHead>Actions</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {users.map((user) => (
            <TableRow key={user.id}>
              <TableCell>{user.name}</TableCell>
              <TableCell>{user.email}</TableCell>
              <TableCell>
                <Link href={`/users/${user.id}/edit`}>Edit</Link>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}

带筛选条件的搜索

控制器
class UsersController < ApplicationController
  def index
    users = User.all

    # Apply search
    if params[:search].present?
      users = users.where('name ILIKE ? OR email ILIKE ?',
        "%#{params[:search]}%", "%#{params[:search]}%")
    end

    # Apply filters
    users = users.where(role: params[:role]) if params[:role].present?
    users = users.where(active: params[:active]) if params[:active].present?

    # Apply sorting
    sort_column = %w[name email created_at].include?(params[:sort]) ? params[:sort] : 'name'
    sort_direction = params[:direction] == 'desc' ? 'desc' : 'asc'
    users = users.order("#{sort_column} #{sort_direction}")

    # Paginate
    users = users.page(params[:page]).per(20)

    render inertia: {
      users: users.as_json(only: [:id, :name, :email, :role, :active]),
      filters: {
        search: params[:search],
        role: params[:role],
        active: params[:active],
        sort: sort_column,
        direction: sort_direction,
      },
      pagination: {
        current_page: users.current_page,
        total_pages: users.total_pages,
        total_count: users.total_count,
      }
    }
  end
end
支持 URL 同步的前端
import { router } from '@inertiajs/react'
import { useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'

export default function UsersIndex({ users, filters, pagination }) {
  const [search, setSearch] = useState(filters.search || '')
  const [role, setRole] = useState(filters.role || '')
  const [active, setActive] = useState(filters.active || '')

  function applyFilters(overrides = {}) {
    router.get('/users', {
      search: search || undefined,
      role: role || undefined,
      active: active || undefined,
      ...overrides,
    }, {
      preserveState: true,
      replace: true,
    })
  }

  const debouncedSearch = useDebouncedCallback(() => applyFilters(), 300)

  function clearFilters() {
    setSearch('')
    setRole('')
    setActive('')
    router.get('/users', {}, { preserveState: true, replace: true })
  }

  return (
    <div className="space-y-4">
      <div className="flex gap-4">
        <input
          value={search}
          onChange={(e) => { setSearch(e.target.value); debouncedSearch() }}
          placeholder="Search..."
          className="input"
        />

        <select value={role} onChange={(e) => { setRole(e.target.value); applyFilters({ role: e.target.value }) }} className="select">
          <option value="">All Roles</option>
          <option value="admin">Admin</option>
          <option value="user">User</option>
        </select>

        <select value={active} onChange={(e) => { setActive(e.target.value); applyFilters({ active: e.target.value }) }} className="select">
          <option value="">All Status</option>
          <option value="true">Active</option>
          <option value="false">Inactive</option>
        </select>

        <button onClick={clearFilters}>Clear</button>
      </div>

      <UserTable users={users} />
      <Pagination pagination={pagination} />
    </div>
  )
}

多步骤向导

控制器
class OnboardingController < ApplicationController
  def show
    step = params[:step]&.to_i || 1

    render inertia: "onboarding/step#{step}", props: {
      step: step,
      total_steps: 4,
      data: session[:onboarding] || {}
    }
  end

  def update
    step = params[:step].to_i

    # Merge step data into session
    session[:onboarding] ||= {}
    session[:onboarding].merge!(step_params.to_h)

    if step < 4
      redirect_to onboarding_path(step: step + 1)
    else
      # Complete onboarding
      User.create!(session[:onboarding])
      session.delete(:onboarding)
      redirect_to dashboard_path, notice: 'Welcome!'
    end
  end

  private

  def step_params
    case params[:step].to_i
    when 1 then params.permit(:name, :email)
    when 2 then params.permit(:company, :role)
    when 3 then params.permit(:preferences)
    when 4 then params.permit(:terms_accepted)
    end
  end
end
向导组件
import { useForm, router } from '@inertiajs/react'

export default function WizardStep({ step, total_steps, data, children }) {
  const form = useForm(data)

  function next(e) {
    e.preventDefault()
    form.post(`/onboarding?step=${step}`)
  }

  function back() {
    router.get(`/onboarding?step=${step - 1}`)
  }

  return (
    <div>
      {/* Progress indicator */}
      <div className="flex gap-2 mb-8">
        {Array.from({ length: total_steps }, (_, i) => i + 1).map((i) => (
          <div
            key={i}
            className={`w-8 h-8 rounded-full flex items-center justify-center ${
              i <= step ? 'bg-blue-500 text-white' : 'bg-gray-200'
            }`}
          >
            {i}
          </div>
        ))}
      </div>

      <form onSubmit={next}>
        {typeof children === 'function' ? children({ form }) : children}

        <div className="flex gap-4 mt-8">
          {step > 1 && (
            <button type="button" onClick={back}>Back</button>
          )}
          <button type="submit" disabled={form.processing}>
            {step === total_steps ? 'Complete' : 'Next'}
          </button>
        </div>
      </form>
    </div>
  )
}

使用 Toast 显示 Flash 消息

共享数据设置
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  inertia_share flash: -> {
    {
      success: flash.notice,
      error: flash.alert,
      info: flash[:info],
      warning: flash[:warning]
    }.compact
  }
end
Toast 组件(React)
// components/FlashMessages.tsx
import { usePage } from '@inertiajs/react'
import { useEffect, useState } from 'react'

interface Toast {
  id: number
  type: string
  message: string
}

export default function FlashMessages() {
  const { flash } = usePage().props
  const [toasts, setToasts] = useState<Toast[]>([])

  useEffect(() => {
    if (!flash) return

    Object.entries(flash).forEach(([type, message]) => {
      if (message) {
        const id = Date.now() + Math.random()
        setToasts((prev) => [...prev, { id, type, message: message as string }])

        setTimeout(() => {
          setToasts((prev) => prev.filter((t) => t.id !== id))
        }, 5000)
      }
    })
  }, [flash])

  const colorMap: Record<string, string> = {
    success: 'bg-green-500 text-white',
    error: 'bg-red-500 text-white',
    info: 'bg-blue-500 text-white',
    warning: 'bg-yellow-500 text-black',
  }

  return (
    <div className="fixed top-4 right-4 space-y-2 z-50">
      {toasts.map((toast) => (
        <div
          key={toast.id}
          className={`px-4 py-3 rounded-lg shadow-lg transition-all ${colorMap[toast.type] || 'bg-gray-500 text-white'}`}
        >
          {toast.message}
        </div>
      ))}
    </div>
  )
}
在布局中使用
// layouts/AppLayout.tsx
import FlashMessages from '@/components/FlashMessages'

export default function AppLayout({ children }) {
  return (
    <div>
      <FlashMessages />
      <nav>{/* ... */}</nav>
      <main>{children}</main>
    </div>
  )
}

Flash API(v3)

Inertia.js v3 引入了专用的 Flash API,可支持比简单键值字符串更丰富的 Flash 数据。

服务端
class PostsController < ApplicationController
  def create
    post = Post.create!(post_params)

    # Standard Rails flash (exposed via flash_keys config)
    redirect_to posts_path, notice: 'Post created!'
  end

  def update
    post = Post.find(params[:id])

    # Rich flash data via flash.inertia
    flash.inertia[:undo] = { url: undo_post_path(post), expires_in: 30 }

    # Current-request-only flash
    flash.now.inertia[:notification] = { type: 'info', message: 'Saving...' }

    redirect_to post_path(post)
  end
end
前端
import { usePage, router } from '@inertiajs/react'

function FlashHandler() {
  const { flash } = usePage().props

  // Access standard flash
  if (flash.notice) showToast(flash.notice)

  // Access rich flash data
  if (flash.undo) {
    showUndoToast(flash.undo.message, () => {
      router.post(flash.undo.url)
    })
  }
}

// Client-side flash (no server roundtrip)
router.flash({ notice: 'Saved locally' })
配置 Flash 键
InertiaRails.configure do |config|
  config.flash_keys = %i[notice alert success error warning info]
end

确认对话框

可复用的 Confirm 组件
// components/ConfirmDialog.tsx
import { useState, useCallback, useRef } from 'react'

interface ConfirmOptions {
  title?: string
  message?: string
  confirmText?: string
  cancelText?: string
  destructive?: boolean
}

export function useConfirm() {
  const [isOpen, setIsOpen] = useState(false)
  const [options, setOptions] = useState<ConfirmOptions>({})
  const resolveRef = useRef<((value: boolean) => void) | null>(null)

  const confirm = useCallback((opts: ConfirmOptions = {}) => {
    setOptions({
      title: 'Are you sure?',
      message: 'This action cannot be undone.',
      confirmText: 'Confirm',
      cancelText: 'Cancel',
      destructive: false,
      ...opts,
    })
    setIsOpen(true)

    return new Promise<boolean>((resolve) => {
      resolveRef.current = resolve
    })
  }, [])

  function handleConfirm() {
    setIsOpen(false)
    resolveRef.current?.(true)
  }

  function handleCancel() {
    setIsOpen(false)
    resolveRef.current?.(false)
  }

  const ConfirmDialog = isOpen ? (
    <div className="fixed inset-0 z-50 flex items-center justify-center">
      <div className="absolute inset-0 bg-black/50" onClick={handleCancel} />
      <div className="relative bg-white rounded-lg p-6 max-w-md w-full mx-4">
        <h3 className="text-lg font-semibold">{options.title}</h3>
        <p className="mt-2 text-gray-600">{options.message}</p>
        <div className="mt-6 flex gap-3 justify-end">
          <button onClick={handleCancel} className="btn-secondary">
            {options.cancelText}
          </button>
          <button
            onClick={handleConfirm}
            className={options.destructive ? 'btn-danger' : 'btn-primary'}
          >
            {options.confirmText}
          </button>
        </div>
      </div>
    </div>
  ) : null

  return { confirm, ConfirmDialog }
}
用法
import { router } from '@inertiajs/react'
import { useConfirm } from '@/components/ConfirmDialog'

export default function UserRow({ user }) {
  const { confirm, ConfirmDialog } = useConfirm()

  async function deleteUser() {
    const confirmed = await confirm({
      title: 'Delete User',
      message: `Are you sure you want to delete ${user.name}?`,
      confirmText: 'Delete',
      destructive: true,
    })

    if (confirmed) {
      router.delete(`/users/${user.id}`)
    }
  }

  return (
    <div>
      <button onClick={deleteUser}>Delete</button>
      {ConfirmDialog}
    </div>
  )
}

处理 Rails 验证错误类型

Rails 会返回不同格式的错误。请统一处理:

# Controller helper
def format_errors(model)
  model.errors.to_hash.transform_values { |messages| messages.first }
end

# Usage
redirect_to edit_user_url(user), inertia: { errors: format_errors(user) }
// Frontend - errors are now { field: 'message' } format
form.errors.email  // "can't be blank"
嵌套模型错误
# For nested attributes
def format_nested_errors(model)
  errors = {}

  model.errors.each do |error|
    key = error.attribute.to_s.gsub('.', '_')
    errors[key] = error.message
  end

  errors
end

使用 ActionCable 实现实时功能

设置 Turbo Streams 替代方案
// channels/notifications_channel.js
import { router } from '@inertiajs/react'
import consumer from './consumer'

consumer.subscriptions.create('NotificationsChannel', {
  received(data) {
    if (data.reload) {
      router.reload({ only: ['notifications'] })
    }
  }
})
控制器广播
class NotificationsController < ApplicationController
  def create
    notification = current_user.notifications.create!(notification_params)

    ActionCable.server.broadcast(
      "notifications_#{current_user.id}",
      { reload: true }
    )

    redirect_to notifications_path
  end
end

文件下载

触发下载
def download
  report = Report.find(params[:id])

  # Return download URL as prop
  render inertia: {
    download_url: rails_blob_path(report.file, disposition: 'attachment')
  }
end
// Trigger download without navigation
function downloadFile(url) {
  window.location.href = url
}

// Or use inertia_location for non-Inertia responses
router.visit(url, { method: 'get' })
下载时进行外部重定向
def export
  # Generate file...
  inertia_location export_download_path(token: token)
end

评论

支持 Markdown