Skip to main content

FormInput

Derive Macro FormInput 

Source
#[derive(FormInput)]
{
    // Attributes available to this derive:
    #[form]
}
Expand description

Derives FormInput for a typed value a form posts under its own keys: what an Action asks for before it runs, what a sign-up reads, or one item of a repeater.

#[derive(tablo_core::FormInput)]
pub struct Rejection {
    #[form(multiline = 4)]
    pub reason: String,
    pub notify_author: bool,
}

struct Reject;

impl Action<PostResource> for Reject {
    type Input = Rejection;
    const NAME: &'static str = "reject";

    async fn run(
        _cx: &Cx,
        posts: &[Post],
        rejection: Rejection,
        ex: &mut dyn toasty::Executor,
    ) -> Result<tablo_core::Outcome, tablo_core::WriteError> {
        // Store `rejection.reason`, notify when `rejection.notify_author` holds…
        Ok(tablo_core::Outcome::done())
    }
}

Each field posts its own name, which no column binds, and renders the control its type picks: a bool a checkbox, #[form(options)] a choice over the field type’s Options, #[form(options = T)] one over T’s, and any other FormScalar a text input of the type’s input type.

One item of a repeater is an embedded struct a #[document] list stores, which a RecordForm edits with #[form(repeat)]:

#[derive(Debug, Clone, toasty::Embed, tablo_core::FormInput)]
pub struct Link {
    pub label: String,
    #[form(label = "URL")]
    pub url: String,
}

#[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
pub struct Post {
    #[key]
    #[auto]
    id: uuid::Uuid,
    #[document]
    #[default(Vec::<Link>::new())]
    links: Vec<Link>,
}

#[derive(tablo_core::RecordForm)]
#[form(model = Post)]
pub struct PostForm {
    #[form(repeat)]
    pub links: Vec<Link>,
}

Each row posts the item’s keys under its own prefix (links.0.url) and answers to its controls’ own rules, such as a choice offering only its options. A repeater takes at most MAX_REPEATER_ROWS rows.

A field’s blank answer is its #[form(blank = ..)], "" for an #[form(optional)] String, None for an Option, or false for a bool. A field with none is required: its control renders required and the parse refuses an empty submission.

§Per-field attributes

  • #[form(label = "Reason")] — the control’s label (default: the field name in sentence case).
  • #[form(multiline = 4)] — a <textarea> of 4 rows.
  • #[form(placeholder = "rust, async")] — a text input’s placeholder.
  • #[form(email)] — a text input that refuses a malformed email address.
  • #[form(url)] — a text input that refuses what is not an http or https URL: TextField::url.
  • #[form(tel)] — a telephone text input, in no checked format: TextField::tel.
  • #[form(password)] — a masked text input on a String, read untrimmed, that a refused form renders empty. A repeater refuses one in its item at mount: an edit could not render the stored secret back, so each save would overwrite it.
  • #[form(blank = ..)], #[form(optional)] — the blank answer.
  • #[form(options)], #[form(options = T)] — a choice. An Option<T> field names its options type: #[form(options = T)].

A generic struct, a tuple struct, an empty struct (name () instead), an unknown key, a text input’s key with options or on a bool, password with multiline, two of email, url, tel and password, and a field that is not a FormScalar are compile errors. RouterBuilderPanelExt::panel refuses an action’s input with a field named csrf_token, confirm or ids, and a sign-up’s with one named csrf_token or next: the keys each POST carries besides its input.

A hand-written impl builds its fields with Field::text_input, Field::choice_input and Field::toggle_input; its controls render without the required mark, though its parse still refuses what it requires. Its write fills a repeater row from a stored item, and writes nothing for a type no repeater holds. Derive FormInput for a typed value a form posts under its own keys; documented on the tablo-core re-export.