Model Binding
Model binding populates a form with data from an Eloquent model. Call bind() with a model instance, and each field's value is filled from the corresponding model attribute.
Basic Binding
use App\Forms\EditUserForm;
class UserController
{
public function edit(User $user)
{
return Inertia::render('Users/Edit', [
'form' => EditUserForm::make()->bind($user),
]);
}
}The form automatically reads the model's attributes and uses them as the initial field values.
Saving Models
Use save() when the form fields map directly to model attributes and any media-backed fields declare their storage destination:
use App\Forms\EditPostForm;
use InertiaUI\Forms\Validate;
public function update(#[Validate] EditPostForm $form, Post $post)
{
$form->save($post);
return back();
}save() returns the same model instance. It writes only declared fields that are authorized, visible, model-bound, present in the validated data, and not excluded through bind(..., except: ...). Absent fields leave their model attributes untouched. Explicitly validated null and empty values are saved normally.
Eloquent remains responsible for persistence. The method calls fill(), so $fillable, $guarded, casts, accessors, mutators, and normal model events all apply. A field also needs validation rules before Laravel includes it in the validated data.
Array And JSON Attributes
Structured fields submit structured values. A multiple Combobox, for example, submits an array:
Combobox::make('audiences')
->options([
'customers' => 'Customers',
'partners' => 'Partners',
'internal' => 'Internal',
])
->multiple()
->required();The matching model attribute needs an Eloquent array or JSON-compatible cast:
protected function casts(): array
{
return [
'audiences' => 'array',
];
}The same rule applies to values submitted by Repeaters and Blocks, as well as Checkbox Groups, multiple dates, and other fields whose saved value is an array. save() does not infer casts or persist Eloquent relationships from nested values.
Media Fields Inside Repeater And Blocks
save() also processes File uploads, RichText images, and Composer attachments declared directly inside a Repeater or Blocks schema:
Repeater::make('sections')->schema([
File::make('attachments')
->multiple()
->mediaCollection(
collection: 'section-attachments',
disk: 'media',
responsiveImages: true,
),
RichText::make('body')
->imageUploads()
->storeImagesInMediaLibrary(
'content-images',
deleteUnused: true,
),
Composer::make('message')
->allowAttachments()
->mediaCollection('message-attachments'),
]);The parent attribute still needs an array or JSON-compatible Eloquent cast:
protected function casts(): array
{
return [
'sections' => 'array',
];
}Each configured collection is shared by every submitted instance of that one schema field. save() stores all rows first, writes durable internal media references to the structured attribute, and then cleans and reorders the collection once. Binding the model resolves those references to normal existing file values and editor-ready RichText HTML. Temporary upload tokens and expiring existing-file tokens are never written to the model.
File and Composer mediaCollection() options are applied to top-level and directly nested automatic writes. The destination disk, responsive-image flag, and FileAdder configuration closure remain server-only. Manual upload helpers and direct MediaLibraryUploads calls remain available for application-owned persistence.
An absent upload value does not clear its collection. A submitted empty value participates in collection cleanup. Give every File or Composer field its own collection. A RichText field using deleteUnused: true also needs an exclusive collection.
Automatic nested media persistence uses temporary or direct-to-storage upload tokens. Fields using storeWithForm() still need manual multipart processing. Uploads below another nested Repeater or Blocks level also use validated() and the upload helpers because generated validation currently covers one structural schema level.
Creating Models
New models with only ordinary attributes are saved once. Media Library needs a persisted owner, so forms with media-backed fields use two phases:
$post = $form->save(new Post);The first save creates the model with its ordinary attributes. Media is then stored, deferred structured and RichText attributes are assigned, and the model is saved again. The created event therefore runs before those attributes contain their final stored values. Later saving and updating observers may run during the second save.
Deferred RichText and structured columns must accept the model's initial value during creation. Make the column nullable, give it a safe default, or initialize the model attribute before calling save(). The package never writes raw submitted HTML or upload tokens as a temporary placeholder.
Media Library and filesystem changes are not part of one database transaction. See RichText Media Library storage and File Media Library storage for the failure and cleanup boundaries. Manual validated() and upload helpers remain available when a form needs relationships, custom storage, or another write workflow.
Skipping Bound Values
Use withoutModelBinding() when a declared field should still render, submit, and validate normally, but should not read its initial value from the bound model:
TextInput::make('api_token')
->withoutModelBinding()
->required();Password text inputs call withoutModelBinding() by default. This keeps stored password hashes out of the serialized form data on edit screens:
TextInput::make('password')->password();For forms with several sensitive fields, pass an exclusion list to bind():
EditUserForm::make()->bind($user, except: [
'password',
'api_token',
]);The except list affects model hydration for matching declared field names. It also tells Form::save() not to write those fields. It does not remove fields from the form and does not change submission or validation. A field configured with withoutModelBinding() is omitted from both hydration and Form::save().
If an application truly needs to hydrate a password field from the model, opt in deliberately:
TextInput::make('temporary_password')
->password()
->withModelBinding();Binding Lookup Order
Bound models and arrays are read with Laravel's data_get() helper. The field name is the lookup path, and default() is used only when that path is missing. Only values for fields declared by the form are serialized. Binding a model does not send the full model or unused attributes to the frontend.
Field names therefore support dot notation for nested data:
TextInput::make('address.city'), // data_get($model, 'address.city')
TextInput::make('settings.theme'), // data_get($model, 'settings.theme')Default Values
Without a bound model, or without a value at the bound path, the field falls back to its default value:
TextInput::make('nickname')->default('Anonymous'),
Combobox::make('theme')->options([...])->default('light'),
Toggle::make('notifications')->default(true),Without bind(), base field value resolution starts from each field's default value (which is null unless set with default()). Field types such as Checkbox and Toggle may then normalize that value.
A bound attribute with a null value makes data_get() return null. The field does not replace that value with default().
Works with Eloquent Features
Binding reads values through the model, so normal Eloquent behavior applies. Accessors may provide field values, casts return their cast values before the field normalizes them, and JSON or array casts can be read with dot notation.
data_get() also follows relationship accessors, so you may bind to related model data such as a profile field.
Eager-load relations used by field names when you want predictable query behavior. Eloquent may lazy-load an unloaded relationship during binding. Prevented lazy loading or a missing relation resolves the value to
null.
RichText Casts
Cast a RichText attribute to RichTextContent to load stored Media Library images back into the editor automatically:
use InertiaUI\Forms\RichText\RichTextContent;
protected function casts(): array
{
return [
'body' => RichTextContent::class,
];
}EditPostForm::make()->bind($post);The RichText field preserves the signed image references required for later submissions. Public rendering through $post->body?->toHtml() removes those internal references. Use only RichTextContent::toHtml() for user-facing output. toStoredHtml(), toEditorHtml(), and preserveStoredImageReferences()->toHtml() may intentionally retain them. See Rendering Saved Content for the storage and display distinction, and Loading Stored Images Into The Editor for custom image resolution and manual binding examples.
Binding Arrays
You may also bind an array instead of a model:
$form = EditSettingsForm::make()->bind([
'site_name' => 'My App',
'contact_email' => 'admin@example.com',
'features' => [
'registration' => true,
'dark_mode' => false,
],
]);This is useful for forms that do not map directly to an Eloquent model, like settings pages or config editors.
Edit Forms
bind() fills the form with model values. It does not change where the form submits. For edit screens, set the action route on the same form instance:
EditUserForm::make()
->bind($user)
->route('users.update', ['user' => $user]);Route parameters may include the model. The form's HTTP method is still detected from the named route definition.
Checkbox and Toggle Binding
The Checkbox and Toggle fields preserve bound or default values that exactly match their configured checked/on or unchecked/off values. Other values fall back to their boolean meaning so boolean-backed attributes hydrate predictably:
// If $user->is_admin is truthy, the toggle returns its on value
Toggle::make('is_admin'),
// Custom values
Checkbox::make('status')
->trueValue('active')
->falseValue('inactive'),
// "active" and "inactive" are preserved exactly. Other values use the
// truthiness fallback and return the configured true or false value.File Field Binding
The File field converts bound values to existing file representations. Model file path strings become preview-friendly objects. With Spatie Media Library integration, media items are loaded from the specified collection:
File::make('avatar')->image(),
// Reads the file path from $user->avatar and creates a preview URL
File::make('documents')
->multiple()
->mediaCollection('documents'),
// Loads media from $user->getMedia('documents')Path-backed values are converted for previews only. Binding them does not make Form::save() write a submitted file to the model's path attribute. Use manual file storage for that workflow. A File field configured with mediaCollection() uses the automatic Media Library workflow.