Actions

Actions connect UI elements to Ferro handlers for navigation, form submission, and destructive operations.

How Actions Work

Every interactive element in JSON-UI uses an Action to declare what happens when the user interacts with it. Actions reference handler names (e.g., "users.store") instead of raw URLs. The framework resolves handler names to URLs at render time using the route registry.

  • GET actions render as links (navigation)
  • Non-GET actions (POST, PUT, PATCH, DELETE) render as form submissions
  • Actions can require confirmation before executing
  • Success and error outcomes control what happens after the server responds

Creating Actions

The Action struct provides builder methods for common HTTP methods:

#![allow(unused)]
fn main() {
use ferro_rs::Action;

// Form submission (POST, the default)
Action::new("users.store")

// Navigation (GET)
Action::get("users.index")

// Deletion (DELETE)
Action::delete("users.destroy")
}

To override the HTTP method explicitly:

#![allow(unused)]
fn main() {
use ferro_rs::{Action, HttpMethod};

Action::new("users.update").method(HttpMethod::Put)
}

Available methods: Get, Post, Put, Patch, Delete.

JSON Equivalent

{
  "handler": "users.store",
  "method": "POST"
}

The method field serializes as uppercase ("GET", "POST", "DELETE", etc.) and defaults to "POST" when omitted.

Route Parameters

Pass route parameters using the handler's registered route pattern. The framework resolves "users.show" to its registered path (e.g., /users/{user}), and the frontend renderer substitutes parameters from row data.

#![allow(unused)]
fn main() {
// Table row actions receive parameters from each row's data
Action::get("users.show")
Action::delete("users.destroy")
    .confirm_danger("Delete this user?")
}

Confirmations

Actions can show a confirmation dialog before executing. Two variants are available:

#![allow(unused)]
fn main() {
use ferro_rs::Action;

// Standard confirmation
Action::new("users.store")
    .confirm("Save changes?")

// Destructive confirmation (danger styling)
Action::delete("users.destroy")
    .confirm_danger("Delete this user?")
}

The ConfirmDialog struct behind these builders has three fields:

FieldTypeDescription
titleStringDialog heading text
messageOption<String>Optional detail text
variantDialogVariantDefault or Danger

JSON Equivalent

{
  "handler": "users.destroy",
  "method": "DELETE",
  "confirm": {
    "title": "Delete this user?",
    "variant": "danger"
  }
}

Success Outcomes

The on_success field controls what happens after the action completes. Four outcome types are available:

OutcomeDescription
Redirect { url }Navigate to a URL
RefreshReload the current page
ShowErrorsDisplay validation errors on form fields
Notify { message, variant }Show a notification toast
#![allow(unused)]
fn main() {
use ferro_rs::{Action, ActionOutcome, NotifyVariant};

// Redirect after successful creation
Action::new("users.store")
    .on_success(ActionOutcome::Redirect {
        url: "/users".to_string(),
    })

// Refresh the current page
Action::new("settings.update")
    .on_success(ActionOutcome::Refresh)

// Show a notification
Action::new("users.store")
    .on_success(ActionOutcome::Notify {
        message: "User created".to_string(),
        variant: NotifyVariant::Success,
    })
}

Notification variants: Success, Info, Warning, Error.

Error Outcomes

The on_error field works identically and controls behavior when the action fails:

#![allow(unused)]
fn main() {
Action::new("users.store")
    .on_error(ActionOutcome::ShowErrors)
}

JSON Equivalent

{
  "handler": "users.store",
  "method": "POST",
  "on_success": {
    "type": "redirect",
    "url": "/users"
  },
  "on_error": {
    "type": "show_errors"
  }
}

Outcome types serialize with a type discriminator: "redirect", "refresh", "show_errors", "notify".

Actions on Components

Actions attach to components in three places:

ComponentNode Action

Any component can have an action via the action field on ComponentNode. This makes the entire component interactive:

#![allow(unused)]
fn main() {
use ferro_rs::{
    ComponentNode, Component, ButtonProps, ButtonVariant, Size, Action,
};

ComponentNode {
    key: "create-btn".to_string(),
    component: Component::Button(ButtonProps {
        label: "Create User".to_string(),
        variant: ButtonVariant::Default,
        size: Size::Default,
        disabled: None,
        icon: None,
        icon_position: None,
    }),
    action: Some(Action::get("users.create")),
    visibility: None,
}
}

Form Action

Forms have a dedicated action field on FormProps that defines the submission endpoint:

#![allow(unused)]
fn main() {
use ferro_rs::{Component, FormProps, Action};

Component::Form(FormProps {
    action: Action::new("users.store"),
    fields: vec![/* ... */],
    method: None,
})
}

Table Row Actions

Tables support per-row actions via row_actions on TableProps:

#![allow(unused)]
fn main() {
use ferro_rs::Action;

let row_actions = vec![
    Action::get("users.show"),
    Action::delete("users.destroy")
        .confirm_danger("Delete this user?"),
];
}

URL Resolution

The framework resolves action handler names to URLs automatically during rendering. When JsonUi::render() or JsonUi::render_json() is called, the view is cloned and all actions are walked recursively. Each handler name (e.g., "users.store") is looked up in the route registry and the resolved URL is set on the action's url field.

If a handler cannot be resolved, its url remains None. The original view is never mutated.

#![allow(unused)]
fn main() {
use ferro_rs::{JsonUi, JsonUiView};

// Actions are resolved automatically during render
let view = JsonUiView::new()
    .title("Users")
    .component(/* component with action */);

JsonUi::render(&view, &serde_json::json!({}))
}