Skip to content

Actions

An action runs on the server in response to a click and returns effects the client dispatches: a toast, a redirect, a component or page refresh, opening a modal. An action can both change data and drive the UI that follows.

A click posts the action’s signed reference to its endpoint. The server verifies the reference (and the trusted context baked into it), authorizes, runs handle(), and returns an ActionResult whose effects the client then dispatches:

Click to server-run work to client effects

Extend ActionDefinition and implement two methods: definition() describes the trigger (label, icon, variant, confirmation), and handle() runs the work and returns an ActionResult. The #[AsAction] attribute gives the action a stable id so it can be discovered and addressed by its endpoint.

use App\Models\Product;
use Illuminate\Http\Request;
use Lattice\Actions\ActionDefinition;
use Lattice\Actions\ActionResult;
use Lattice\Actions\Components\Action;
use Lattice\Core\Attributes\AsAction;
use Lattice\Core\Concerns\ResolvesContextModels;
use Lattice\Ui\Enums\Emphasis;
use Lattice\Ui\Enums\Variant;
#[AsAction('app.products.archive')]
class ArchiveProductAction extends ActionDefinition
{
use ResolvesContextModels;
public function definition(Action $action): Action
{
return $action
->label('Archive')
->variant(Variant::Danger)
->confirm('Archive product?', 'This hides it from the catalogue.');
}
public function handle(Request $request): ActionResult
{
$product = $this->contextModel('product_id', Product::class);
$product->update(['status' => 'archived']);
return ActionResult::success()
->toast('Product archived.', Variant::Success)
->reloadComponent('app.products');
}
}

contextModel() reads the sealed context and resolves it into the model through its own route binding — it aborts with a 404 when the key is missing or nothing matches. It comes from the opt-in ResolvesContextModels trait rather than context() itself, which stays untyped on every definition.

Reference an action anywhere a component is accepted with Action::use(). The most common spot is a table’s row actions, where ->context() scopes it to the record:

Action::use(ArchiveProductAction::class)
->context(['product_id' => $row['id']]);

->context() carries data from the page to the action; handle() reads it back server-side, typically through a typed accessor like contextModel() (see Defining an action). The context is signed into the action’s reference, so it can’t be tampered with on the way back.

Group related actions behind a single trigger with ActionGroup:

use Lattice\Actions\Components\ActionGroup;
ActionGroup::make('row-actions')->actions([
Action::use(EditProductAction::class)->context(['product_id' => $row['id']]),
Action::use(ArchiveProductAction::class)->context(['product_id' => $row['id']]),
]);

Render the same group inline when the actions should stay visible:

use Lattice\Ui\Enums\Orientation;
ActionGroup::make('locale-switcher')
->label('Language')
->inline(Orientation::Horizontal)
->actions([
Action::use(SetLocaleAction::class)->context(['locale' => 'en']),
Action::use(SetLocaleAction::class)->context(['locale' => 'de']),
]);

handle() returns an ActionResult. Start from ActionResult::success(), optionally attaching data, then chain effects:

return ActionResult::success(['id' => $product->id])
->toast('Saved.')
->reloadComponent('app.products');

Override authorize() to gate an action. It receives the request — the signed context is available through $this->context() — and returns a boolean; a denied action never reaches handle().

public function authorize(Request $request): bool
{
return $this->contextModel('product_id', Product::class)->status !== 'archived';
}

An action’s authorize() also runs at render time — when it’s hidden from the page rather than rejected outright, a strict accessor’s 404 takes the whole page down with it. See Authorization for the OrNull variants to reach for there instead.