Inertia Rails Forms logo

Inertia Rails Forms

Organization
inertia-rails
inertia-rails-forms

Full-stack form handling for Inertia Rails: create, edit, delete, multi-step wizard, and file upload forms with validation errors and progress tracking. React examples inline; Vue and Svelte equivalents in references. Use when building any form, handling file uploads, multi-step forms, client-side validation, or wiring form submission to Rails controllers. NEVER react-hook-form. Use `<Form>` for simple forms, useForm for dynamic/programmatic control.

Overview

Publisherinertia-rails
Repositoryskills
Skill nameinertia-rails-forms
Stars
68
Forks
2
Bundled files
4
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 4 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by inertia-rails on GitHub. Read the source before you install it.

Installation

Install the Inertia Rails Forms AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/inertia-rails/skills.git /tmp/skills
mkdir -p .claude/skills
cp -r /tmp/skills/skills/inertia-rails-forms .claude/skills/inertia-rails-forms
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Inertia Rails Forms in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Inertia Rails Forms on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Inertia Rails Forms is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Inertia Rails Forms

Full-stack form handling for Inertia.js + Rails.

Before building a form, ask:

  • Simple create/edit?<Form> component (no state management needed)
  • Requires per-field UI elements? → Still <Form>. React useState for UI state (preview URL, file size display) is independent of form data — <Form> handles the submission; useState handles the UI.
  • Multi-step wizard, dynamic fields (add/remove inputs), or form data shared with sibling components (e.g., live preview panel)?useForm hook
  • Tempted by react-hook-form? → Don't. Inertia's <Form> already handles CSRF tokens, redirect following, error mapping from Rails, processing state, file upload detection, and history state. react-hook-form would duplicate or fight all of this.

When NOT to use <Form> or useForm:

  • Data lookups — not a form submission. Use router.get with debounce + preserveState, or raw fetch for large datasets
  • Inline single-field edits without navigationrouter.patch directly, or useForm if you need error display on the field

NEVER:

  • Use react-hook-form, vee-validate, or sveltekit-superforms — Inertia <Form> already handles CSRF, redirect following, error mapping, processing state, and file detection. These libraries fight Inertia's request lifecycle.
  • Pass data={...} to <Form> — it has no data prop. Data comes from input name attributes automatically. data is a useForm concept.
  • Use useForm for simple create/edit — <Form> handles these without state management. Reserve useForm for multi-step wizards, dynamic add/remove fields, or form data shared with sibling components.
  • Use controlled value= instead of defaultValue on inputs — controlled inputs bypass <Form>'s dirty tracking, making isDirty always false.
  • Omit value="1" on checkboxes — without it, the browser submits "on" and Rails won't cast to boolean correctly.
  • Call useForm inside a loop or conditional — it's a hook (React rules apply). Create one form instance per logical form.

<Form> Component (Preferred)

The simplest way to handle forms. Collects data from input name attributes automatically — no manual state management needed. <Form> has NO data prop — do NOT pass data={...} (that's a useForm concept). For edit forms, use defaultValue on inputs.

Use render function children {({ errors, processing }) => (...)} to access form state. Plain children work but give no access to errors, processing, or progress.

tsx
import { Form } from '@inertiajs/react'

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

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

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

// Plain children — valid but no access to errors/processing/progress:
// <Form method="post" action="/users">
//   <input name="name" />
//   <button type="submit">Create</button>
// </Form>

Delete Form

tsx
<Form method="delete" action={`/posts/${post.id}`}>
  {({ processing }) => (
    <button type="submit" disabled={processing}>
      {processing ? 'Deleting...' : 'Delete Post'}
    </button>
  )}
</Form>

Key Render Function Properties

PropertyTypePurpose
errorsRecord<string, string>Validation errors keyed by field name
processingbooleanTrue while request is in flight
progress{ percentage: number } | nullUpload progress (file uploads only)
hasErrorsbooleanTrue if any errors exist
wasSuccessfulbooleanTrue after last submit succeeded
recentlySuccessfulbooleanTrue for 2s after success — ideal for "Saved!" feedback
isDirtybooleanTrue if any input changed from initial value
reset(...fields) => voidReset specific fields or all fields
clearErrors(...fields) => voidClear specific errors or all errors

Additional <Form> props (errorBag, only, resetOnSuccess, event callbacks like onBefore, onSuccess, onError, onProgress) are documented in references/advanced-forms.md — see loading trigger below.

Edit Form (Pre-populated)

Use method="patch" and uncontrolled defaults:

  • Text/textarea → defaultValue
  • Checkbox/radio → defaultChecked
  • Select → defaultValue on <select>

Checkbox without explicit value submits "on" — set value="1" so Rails casts to boolean correctly.

tsx
<Form method="patch" action={`/posts/${post.id}`}>
  {({ errors, processing }) => (
    <>
      <input type="text" name="title" defaultValue={post.title} />
      {errors.title && <span className="error">{errors.title}</span>}

      <label>
        <input type="checkbox" name="published" value="1"
          defaultChecked={post.published} />
        Published
      </label>

      <button type="submit" disabled={processing}>
        {processing ? 'Saving...' : 'Update Post'}
      </button>
    </>
  )}
</Form>

Transforming Data

Use the transform prop to reshape data before submission without useForm.

For advanced transform with useForm, see references/advanced-forms.md.

External Access with formRef

The ref exposes the same methods and state as render function props (FormComponentSlotProps). Use when you need to interact with the form from outside <Form>. Key ref methods: submit(), reset(), clearErrors(), setError(), getData(), getFormData(), validate(), touch(), defaults(). State: errors, processing, progress, isDirty, hasErrors, wasSuccessful, recentlySuccessful.

tsx
import {useRef} from 'react'
import {Form} from '@inertiajs/react'
import type {FormComponentRef} from '@inertiajs/core'

export default function CreateUser() {
  const formRef = useRef<FormComponentRef>(null)

  return (
    <>
      <Form ref={formRef} method='post' action='/users'>
        {({errors}) => (
          <>
            <input type='text' name='name'/>
            {errors.name && <span className='error'>{errors.name}</span>}
          </>
        )}
      </Form>
      <button onClick={() => formRef.current?.submit()}>Submit</button>
      <button onClick={() => formRef.current?.reset()}>Reset</button>
    </>
  )
}

useForm Hook

Use useForm only for multi-step wizards, dynamic add/remove fields, or form data shared with sibling components.

MANDATORY — READ ENTIRE FILE when using useForm hook, transform, errorBag, resetOnSuccess, multi-step forms, or client-side validation with setError: references/advanced-forms.md (~330 lines) — full useForm API, transform examples, error bag scoping, multi-step wizard patterns, and client-side validation.

Do NOT load advanced-forms.md when using <Form> component for simple create/edit forms — the examples above are sufficient.

File Uploads

Both <Form> and useForm auto-detect files and switch to FormData. Upload progress is built into the render function — destructure progress alongside errors and processing:

tsx
type Props = { user: User }

export default function EditProfile({ user }: Props) {
  return (
    <Form method="patch" action="/profile">
      {({ errors, processing, progress }) => (
        <>
          <input type="text" name="name" defaultValue={user.name} />
          {errors.name && <span className="error">{errors.name}</span>}

          <input type="file" name="avatar" />
          {errors.avatar && <span className="error">{errors.avatar}</span>}

          {progress && (
            <progress value={progress.percentage ?? 0} max="100" />
          )}

          <button type="submit" disabled={processing}>
            {processing ? 'Uploading...' : 'Save'}
          </button>
        </>
      )}
    </Form>
  )
}

Choosing <Form> vs useForm for uploads:

  • File submits with other fields (avatar + name, one Save button) → file input inside <Form>
  • Standalone immediate upload (uploads on select, no Save button) → <Form> + formRef.submit() on change
  • Drag-and-drop uploaduseForm (dropped files aren't in DOM inputs, setData is cleaner)

Preview / validation → useState alongside either approach, see references/file-uploads.md.

Vue / Svelte

All examples above use React syntax. For Vue 3 or Svelte equivalents:

  • Vue 3: references/vue.md<Form> with #default scoped slot, useForm returns reactive proxy (form.email not setData), v-model binding
  • Svelte: references/svelte.md<Form> with {#snippet} syntax, useForm returns Writable store ($form.email), bind:value, ref exposes methods only (not reactive state)

MANDATORY — READ THE MATCHING FILE when the project uses Vue or Svelte.

Troubleshooting

SymptomCauseFix
No access to errors/processingPlain children instead of render function<Form> children should be {({ errors, processing }) => (...)}
Form sends GET instead of POSTMissing method propAdd method="post" (or "patch", "delete")
File upload sends empty bodyPUT/PATCH with fileMultipart limitation — Inertia auto-adds _method field to convert to POST
Errors don't clear after fixing fieldStale error stateErrors auto-clear on next submit; use clearErrors('field') for immediate clearing
isDirty always falseUsing value instead of defaultValueControlled inputs (value=) bypass dirty tracking — use defaultValue
progress is always nullNo file input in formProgress tracking only activates when <Form> detects a file input
Checkbox sends "on"No explicit valueAdd value="1" to checkbox inputs
Form submits twice in devReact StrictMode double-invocationNormal in development — StrictMode remounts components. Only fires once in production
Used useForm for file upload with previewonChange + useState mistaken for "programmatic data manipulation"<Form> + useState for preview UI. useForm is only needed when form submission data must live in React state (multi-step, dynamic add/remove fields). File preview is local UI state, not form data

Related Skills

  • Server-side PRG & errorsinertia-rails-controllers (redirect_back, to_hash, flash)
  • shadcn inputsshadcn-inertia (Input/Select adaptation, toast UI)
  • Page props typinginertia-rails-typescript (type Props not interface, TS2344)

MANDATORY — READ ENTIRE FILE when handling file uploads with image preview, Active Storage, or direct uploads: references/file-uploads.md (~200 lines) — image preview with <Form>, Active Storage integration, direct upload setup, multiple files, and progress tracking.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Inertia Rails Forms AI skill do?

Full-stack form handling for Inertia Rails: create, edit, delete, multi-step wizard, and file upload forms with validation errors and progress tracking. React examples inline; Vue and Svelte equivalents in references. Use when building any form, handling file uploads, multi-step forms, client-side validation, or wiring form submission to Rails controllers. NEVER react-hook-form. Use `<Form>` for simple forms, useForm for dynamic/programmatic control.

Why use Inertia Rails Forms on TypingMind?

Because you install it once and use it with any model. Inertia Rails Forms is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Inertia Rails Forms in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/inertia-rails/skills/tree/main/skills/inertia-rails-forms. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Inertia Rails Forms?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Inertia Rails Forms?

As many as you like. As long as a model supports skills, you can use Inertia Rails Forms with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Inertia Rails Forms AI skill free?

Yes. It is published on GitHub by inertia-rails under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇