mcpField
S
shadcn-vue-inertia

shadcn-vue-inertia

github.com/inertia-rails開発70115 回インストール37 週間前に更新
リポジトリ

shadcn-vue-inertia について

Inertia.js と Vue 3 を使う Rails アプリ向けに、shadcn-vue の適用方法をまとめたコーディングスキルです。フレームワークごとの差異やコンポーネントの使い方、注意点を扱い、Nuxt は対象外です。

主な機能

  1. 01Inertia.js のフォームとナビゲーション
  2. 02shadcn-vue コンポーネントの指針
  3. 03ダークモードとトースト設定の注意点
  4. 04Vue のリアクティビティとダイアログの注意点

活用シーン

  1. 01Nuxt 向け例を Rails 用に置き換える
  2. 02Inertia のエラー処理を使ったフォーム構築
  3. 03古いプロパティやダイアログの挙動の調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/inertia-rails/skills --skill shadcn-vue-inertia

インストール後、Claude Code で/shadcn-vue-inertiaを入力して読み込まれたか確認します。

同梱ファイルreferences/components.mdreferences/flash-toast.md

詳細

Inertia Rails 向け shadcn-vue

Inertia.js + Rails + Vue 3 向けに調整した shadcn-vue のパターンです。Nuxt ではありません。

shadcn-vue の例を使う前に、次の点を確認してください。

  • Nuxt 固有の API を使っていますか?(useRouter、useFetch、<NuxtLink>)→ Inertia の router、サーバープロパティ、<Link> に置き換えます。
  • vee-validate + zod を使っていますか? → Inertia の <Form> + name 属性に置き換えます。Inertia が CSRF、エラー、リダイレクト、処理状態を処理します。

Nuxt のデフォルトとの主な違い

shadcn-vue のデフォルト(Nuxt)Inertia での相当機能
useFetch / useAsyncDataコントローラー経由でサーバーレンダリングされるプロパティ
useRouter()(Nuxt)@inertiajs/vue3 の router
<NuxtLink>@inertiajs/vue3 の <Link>
vee-validate + zodInertia の <Form> コンポーネント
FormField、FormItem、FormMessageプレーンな <Input name="..."> + errors.field
useHead()(Nuxt)@inertiajs/vue3 の <Head>

shadcn-vue の FormField、FormItem、FormLabel、FormMessage コンポーネントは絶対に使わないでください — これらは内部で vee-validate のフォームコンテキストに依存しており、コンテキストがないとクラッシュします。 Inertia の <Form> 内では、name 属性を持つプレーンな shadcn-vue の Input/Label/Select を使い、 スコープ付きスロットの errors オブジェクトからエラーを表示してください。

セットアップ

npx shadcn-vue@latest init を実行します。存在しない場合は、tsconfig.json に @/ の解決エイリアスを追加します。 vite.config.ts に @/ の解決エイリアスを追加しないでください — vite-plugin-ruby がすでに提供しています。

Inertia の <Form> で使う shadcn-vue の入力コンポーネント

Inertia の <Form> 内では、name 属性を持つプレーンな shadcn-vue の Input/Label/Button を使います。 <Form> API の詳細については、inertia-rails-forms スキル(および references/vue.md)を参照してください。

基本パターン: shadcn-vue の FormField/FormItem/FormMessage を、プレーンな コンポーネント + 手動でのエラー表示に置き換えます。

<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
</script>

<template>
  <Form method="post" action="/users">
    <template #default="{ errors, processing }">
      <div class="space-y-4">
        <div>
          <Label for="name">Name</Label>
          <Input id="name" name="name" />
          <p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
        </div>

        <div>
          <Label for="email">Email</Label>
          <Input id="email" name="email" type="email" />
          <p v-if="errors.email" class="text-sm text-destructive">{{ errors.email }}</p>
        </div>

        <Button type="submit" :disabled="processing">
          {{ processing ? 'Creating...' : 'Create User' }}
        </Button>
      </div>
    </template>
  </Form>
</template>

Inertia の <Form> との連携には、<Select> に name プロパティが必要です。

<template>
  <Select name="role" default-value="member">
    <SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
    <SelectContent>
      <SelectItem value="admin">Admin</SelectItem>
      <SelectItem value="member">Member</SelectItem>
    </SelectContent>
  </Select>
</template>

Inertia ナビゲーションを使うダイアログ

<script setup lang="ts">
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { router } from '@inertiajs/vue3'

defineProps<{ open: boolean; user: User }>()
</script>

<template>
  <Dialog
    :open="open"
    @update:open="(isOpen) => { if (!isOpen) router.replaceProp('show_dialog', false) }"
  >
    <DialogContent>
      <DialogHeader>
        <DialogTitle>{{ user.name }}</DialogTitle>
      </DialogHeader>
      
    </DialogContent>
  </Dialog>
</template>

サーバーサイドソートを使うテーブル

<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { router } from '@inertiajs/vue3'

defineProps<{ users: User[]; sort: string }>()

const handleSort = (column: string) => {
  router.get('/users', { sort: column }, { preserveState: true })
}
</script>

<template>
  <Table>
    <TableHeader>
      <TableRow>
        <TableHead class="cursor-pointer" @click="handleSort('name')">
          Name {{ sort === 'name' ? '↑' : '' }}
        </TableHead>
        <TableHead>Email</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow v-for="user in users" :key="user.id">
        <TableCell>{{ user.name }}</TableCell>
        <TableCell>{{ user.email }}</TableCell>
      </TableRow>
    </TableBody>
  </Table>
</template>

SPA ナビゲーションを維持するため、行リンクには <a> ではなく <Link> を使います。

フラッシュメッセージを使うトースト

フラッシュの設定(flash_keys)は inertia-rails-controllers にあります。フラッシュへのアクセス (usePage().flash)は inertia-rails-pages にあります。このセクションでは、トースト UI の接続方法のみを説明します。

必須 — ファイル全体を読むこと。Sonner でフラッシュベースのトーストを実装する場合は、次を参照してください。 references/flash-toast.md(約80行)— useFlash コンポーザブルと Sonner トーストプロバイダーの全体です。トースト UI なしでフラッシュ値のみを読む場合は、読み込まないでください。

ダークモード(Nuxt color-mode なし)

npx shadcn-vue@latest init は、ライト/ダーク用の CSS 変数と、 CSS(Tailwind v4)内の @custom-variant dark (&:is(.dark *)); を生成します。変数自体に追加の セットアップは必要ありません。

重要 — 間違ったテーマのちらつき(FOUC)を防ぐ: Vue のハイドレーション前に、 <head> にインラインスクリプトを追加します。

<%# app/views/layouts/application.html.erb — in <head>, before any stylesheets %>
<script>
  document.documentElement.classList.toggle(
    "dark",
    localStorage.appearance === "dark" ||
      (!("appearance" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
  );
</script>

Nuxt color-mode の代わりに、useAppearance コンポーザブル(ライト/ダーク/システムモード、localStorage への永続化、 matchMedia リスナー)を使います。<html> の .dark クラスで切り替えます — プロバイダーは不要です。

Vue 固有の注意点

Inertia の <Form> では v-model は動作しません — <Form> は送信時に Vue のリアクティビティシステムではなく、入力の name 属性から値を読み取ります。v-model を使うと、 <Form> が無視する第2の信頼できる情報源が作られます。


<Form method="post" action="/users">
  <Input v-model="name" />
</Form>


<Form method="post" action="/users">
  <Input name="name" />
</Form>

v-model は useForm と組み合わせる場合にのみ使います(その場合は form.name を明示的に管理します)。

usePage() はリアクティブなオブジェクトを返します — 派生値には computed() を使います。

<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'

const page = usePage()

// BAD — not reactive, won't update when page changes:
// const user = page.props.auth.user

// GOOD — reactive, updates on navigation:
const user = computed(() => page.props.auth.user)
</script>

computed() を使わないと、分割代入した値は初期状態で固定され、Inertia のナビゲーション後に更新されません。

ダイアログでは @update:open と @close のどちらを使うか — shadcn-vue の Dialog が発行するのは close ではなく update:open です。@close を使っても何も起こりません。


<Dialog @close="handleClose">


<Dialog :open="open" @update:open="(isOpen) => { if (!isOpen) handleClose() }">

トラブルシューティング

症状原因修正方法
FormField/FormMessage がクラッシュするvee-validate に依存する shadcn-vue のフォームコンポーネントを使用している通常の Input/Label と errors.field の表示に置き換えます
Select の値が送信されないname プロパティが指定されていない<Select> に name="field" を追加します
ダイアログが予期せず閉じる@update:open ハンドラーが指定されていない、または誤っている@update:open="(open) => { if (!open) closeHandler() }" を使用します
誤ったテーマが一瞬表示される(FOUC)<head> にインラインの <script> がないスタイルシートより前にダークモード用のスクリプトを追加します
v-model の値が送信されない<Form> は Vue のリアクティブな状態ではなく、name 属性を読み取るname 属性を使用し、v-model は useForm の場合にのみ使用します
ナビゲーション後、共有プロパティが古いままになるcomputed() を使わずに usePage() を分割代入している派生値を computed(() => ...) でラップします

関連スキル

  • フォームコンポーネント → inertia-rails-forms + references/vue.md(<Form> のスコープ付きスロット、useForm)
  • Flash の設定 → inertia-rails-controllers(flash_keys イニシャライザー)
  • Flash へのアクセス → inertia-rails-pages + references/vue.md(usePage().flash)
  • URL によるダイアログ → inertia-rails-pages + references/vue.md(router.get パターン)

参考資料

上記に示されていない shadcn-vue コンポーネント(Accordion、Sheet、Tabs、DropdownMenu、 Inertia パターンを使用する AlertDialog)を構築する場合は、references/components.md(約200行)を読み込みます。

基本的な Form、Select、Dialog、Table の使い方では、components.md を読み込まないでください — 上記の例で十分です。

コメント