Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/boost/resources/boost/docs/core/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ Action::make('edit')

The closure may hand back an enum case straight off a cast attribute (`fn ($record) => ['role' => $record->role]`): the seeded bag collapses every enum to its backing value, because that is what Livewire state carries to the browser and what a `Select` matches its `<option>` values against. The choice still saves back through the cast.

The `$data` the callback receives is what the form **would have persisted**, not the raw widget state: on submit the modal runs the same field-level dehydration as `Form::save()` (see [Custom fields](../forms/custom-fields.md#shaping-the-value-a-field-stores)). A cleared `Select` arrives as `null` rather than `''`, a cleared `numeric()` `TextInput` as `null`, a `DateTimePicker` in its storage format and zone, and a `FileUpload` as its stored path. A wizard dehydrates every step, not only the one on screen at submit. Footer actions are deliberately excluded — they read the form mid-edit, before it is submitted.

A `HeaderAction` form modal has **no record**, so its `fillFormUsing` closure takes no arguments. Use it to seed initial state — and always seed array-typed fields (`CheckboxList`, `Tags`, multiple `Select`) with an empty array so they bind correctly from the first interaction:

```php
Expand Down
13 changes: 12 additions & 1 deletion packages/boost/resources/boost/docs/forms/custom-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ directions. They are independent — implement only the one you need:
| Contract | Method | Runs |
|---|---|---|
| `HydratesState` | `hydrateState($value, ?Model $record)` | model value → state, after the `getStateType()` cast |
| `DehydratesState` | `dehydrateState($state, ?Model $record)` | state → stored value, during save |
| `DehydratesState` | `dehydrateState($state, ?Model $record)` | state → stored value, on every write path |

Note that the [`MoneyInput`](#building-a-custom-field) above needs *neither*: its
state is already the integer it stores, which `getStateType(): 'int'` is enough to
Expand Down Expand Up @@ -376,6 +376,17 @@ The same two contracts drive [editable table columns](../table/columns/editing.m
`TextInputColumn` uses them for its trim/case/number pipeline — so a component
that implements them behaves the same in a form and in an inline-edited cell.

**Three hosts run the write path, and they must agree.** `Form::save()` runs it
through `SaveHandler`; an editable cell runs it in `updateTableCell()`; an
[action modal](../core/actions.md#form-modal) runs it on submit, so the `$data`
an action callback receives is what the form would have persisted rather than
raw Livewire state. A host that skipped it would make the same schema write
`null` through one path and `''` through another. The one deliberate exception
is a [footer action](../core/actions.md#footer-actions): it reads the form
mid-edit and writes back into the same bag, so dehydrating there would hand the
callback a value the form no longer holds — and would run a `FileUpload`'s store
on a form the user has not submitted.

> **Both directions, or neither.** If a transform moves the value (a timezone
> conversion, a unit change), implementing only `hydrateState()` means the shifted
> state gets written straight back on save, moving the value a little further on
Expand Down
18 changes: 18 additions & 0 deletions packages/boost/resources/boost/docs/forms/fields/text-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ TextInput::make('age')->integer()
| `search()` | `search` | Search input |
| `type(string)` | Custom | Set HTML input type directly |

## Cleared Values

A cleared number input submits `''`, and no numeric column can hold that: MySQL
in strict mode refuses the write outright (`Incorrect decimal value: ''`) and a
lenient driver silently stores `0`. So a field whose HTML type is `number` —
`numeric()`, `integer()`, `type('number')` — **stores `null` when it is left
empty**, on every write path: `Form::save()`, an
[action modal](../../core/actions.md#form-modal) submit, an editable cell.

Every other type is left alone. `''` is a legitimate string value, and a
non-nullable text column holds one; turning it into `null` would break the write
rather than save it.

```php
TextInput::make('discount')->numeric() // cleared → null
TextInput::make('note') // cleared → ''
```

## Constraints

```php
Expand Down
Loading