動的フォームフィールド——Select選択内容で他フィールドが変わるリアクティブフォーム
リアクティブフォームとは
ユーザーの入力に応じてフォームの構成が変化する「リアクティブフォーム」は、管理画面のUXを大きく向上させます。例えば、「都道府県を選択すると市区町村の選択肢が絞られる」「カテゴリに応じて追加フィールドが表示される」といったパターンです。
FilamentはLivewireをベースにしているため、->live()メソッド1つで動的フォームが実現できます。
->live() の基本
use Filament\Forms\Components\Select;
use Filament\Forms\Get;
Select::make('country')
->label('国')
->options([
'JP' => '日本',
'US' => 'アメリカ',
'GB' => 'イギリス',
])
->live() // 変更のたびにLivewireがフォームを再描画
->live()を付けると、フィールドの値が変わるたびにサーバーに通信してフォーム全体を再レンダリングします。デフォルトはonChange(即座)ですが、->live(debounce: 500)でデバウンスも設定できます。
->afterStateUpdated() で値変更時の処理
値が変わったときに他のフィールドの値をリセットしたい場合:
Select::make('country')
->options([...])
->live()
->afterStateUpdated(function (callable $set): void {
$set('prefecture', null); // 都道府県をリセット
$set('city', null); // 市区町村もリセット
})
連鎖するSelectフィールド(都道府県→市区町村)
use Filament\Forms\Get;
use Filament\Forms\Set;
public static function form(Form $form): Form
{
return $form
->schema([
Select::make('prefecture')
->label('都道府県')
->options(Prefecture::all()->pluck('name', 'id'))
->searchable()
->live()
->afterStateUpdated(fn (Set $set) => $set('city_id', null))
->required(),
Select::make('city_id')
->label('市区町村')
->options(function (Get $get): array {
$prefectureId = $get('prefecture');
if (! $prefectureId) {
return [];
}
return City::where('prefecture_id', $prefectureId)
->pluck('name', 'id')
->toArray();
})
->searchable()
->live()
->required()
->disabled(fn (Get $get): bool => ! $get('prefecture')),
]);
}
$get()で他フィールドの現在値を取得し、$set()で他フィールドの値を変更できます。これがリアクティブフォームの核心です。
カテゴリによるフィールドの動的表示
カテゴリ選択によって追加フィールドを表示/非表示にする例:
Select::make('product_type')
->label('商品タイプ')
->options([
'physical' => '物理商品',
'digital' => 'デジタル商品',
'service' => 'サービス',
])
->live()
->required(),
// 物理商品のみ表示
TextInput::make('weight')
->label('重量(g)')
->numeric()
->visible(fn (Get $get): bool => $get('product_type') === 'physical'),
TextInput::make('dimensions')
->label('サイズ(cm)')
->visible(fn (Get $get): bool => $get('product_type') === 'physical'),
// デジタル商品のみ表示
TextInput::make('download_url')
->label('ダウンロードURL')
->url()
->visible(fn (Get $get): bool => $get('product_type') === 'digital'),
// サービスのみ表示
Select::make('service_duration')
->label('提供期間')
->options([
'1month' => '1ヶ月',
'3months' => '3ヶ月',
'1year' => '1年',
])
->visible(fn (Get $get): bool => $get('product_type') === 'service'),
->required() の動的変更
フィールドの必須/任意も動的に切り替えられます。
Toggle::make('has_expiry')
->label('有効期限あり')
->live(),
DatePicker::make('expires_at')
->label('有効期限')
->required(fn (Get $get): bool => (bool) $get('has_expiry'))
->visible(fn (Get $get): bool => (bool) $get('has_expiry')),
価格計算フォームの実例
入力値から合計を自動計算して表示するフォームです。
TextInput::make('unit_price')
->label('単価')
->numeric()
->prefix('¥')
->live(debounce: 300)
->afterStateUpdated(function (Get $get, Set $set): void {
$qty = (float) ($get('quantity') ?? 0);
$price = (float) ($get('unit_price') ?? 0);
$set('subtotal', $qty * $price);
}),
TextInput::make('quantity')
->label('数量')
->numeric()
->default(1)
->live(debounce: 300)
->afterStateUpdated(function (Get $get, Set $set): void {
$qty = (float) ($get('quantity') ?? 0);
$price = (float) ($get('unit_price') ?? 0);
$set('subtotal', $qty * $price);
}),
Placeholder::make('subtotal')
->label('小計')
->content(fn (Get $get): string =>
'¥' . number_format((float)($get('subtotal') ?? 0))
),
Repeater内でのリアクティブフォーム
Repeater内のフィールドでも$get()・$set()は機能しますが、相対パスを考慮する必要があります。
Repeater::make('order_items')
->schema([
Select::make('product_id')
->label('商品')
->options(Product::pluck('name', 'id'))
->live()
->afterStateUpdated(function (Get $get, Set $set): void {
// Repeater内の同じ行の 'unit_price' を更新
$product = Product::find($get('product_id'));
$set('unit_price', $product?->price ?? 0);
}),
TextInput::make('unit_price')
->label('単価')
->numeric()
->readOnly(),
TextInput::make('quantity')
->label('数量')
->numeric()
->default(1),
])
コツ・注意点・ハマりポイント
コツ: ->live()はフィールドの変更ごとにLivewireのネットワークリクエストが発生します。テキスト入力に->live()を付ける場合は必ずdebounceを設定してください。->live(debounce: 500)で500ms後にリクエストが発生します。
注意点: ->visible()はHTMLの表示/非表示を制御しますが、非表示フィールドの値もフォームデータに含まれる場合があります。送信時に不要なデータを除外するには->dehydrated(fn (Get $get) => ...)を使います。
ハマりポイント: Repeater内の$get()はデフォルトで同じ行のフィールドを参照します。Repeaterの外のフィールドを参照したい場合は$get('../../parent_field')のような相対パスか、$get(fn (Get $get) => ...)コールバック形式を使う必要があります。
まとめ
->live()と->afterStateUpdated()・$get()・$set()を組み合わせることで、Filamentのフォームは非常に高いインタラクティビティを実現できます。連鎖Select・条件付きフィールド表示・リアルタイム計算など、複雑なフォームUIもLivewireベースで直感的に実装できます。