Skip to content

Guide Recipes Settings Form

MeowLynxSea edited this page Jun 18, 2026 · 3 revisions

Recipe: Settings form

A real settings form using all 7 text inputs, Form, FormField, and inline validation.

State

#[derive(Default)]
pub struct SettingsState {
    pub email:           String,
    pub email_error:     Option<String>,
    pub password:        String,
    pub number:          f64,
    pub search:          String,
    pub workspace_path:  Option<PathBuf>,
    pub keybinding:      String,
    pub notes:           String,
}

Full form

use std::collections::HashMap;
use yororen_ui::headless::file_path_input::file_path_input;
use yororen_ui::headless::form::form;
use yororen_ui::headless::form_field::form_field;
use yororen_ui::headless::keybinding_input::{KeybindingInputMode, keybinding_input};
use yororen_ui::headless::number_input::number_input;
use yororen_ui::headless::password_input::password_input;
use yororen_ui::headless::search_input::search_input;
use yororen_ui::headless::text_area::text_area;
use yororen_ui::headless::text_input::text_input;

impl Render for SettingsPage {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let entity = cx.entity();

        // Snapshot once
        let state = entity.read(cx);
        let email = state.email.clone();
        let pw = state.password.clone();
        let number = state.number;
        let email_error = state.email_error.clone();
        drop(state);

        // Form scaffold
        let entity_for_form = entity.clone();
        let form_el = form("settings-form", &mut *cx)
            .value("email", email.clone())
            .value("password", pw.clone())
            .value("number", number.to_string())
            .error("email", email_error.as_deref())
            .submit("Save")
            .on_submit(move |vals, _w, cx| {
                entity_for_form.update(cx, |s, _| {
                    if let Some(e) = vals.get("email") {
                        s.email = e.to_string();
                        s.email_error = if e.contains('@') { None } else { Some("must contain @".into()) };
                    }
                    if let Some(p) = vals.get("password") { s.password = p.to_string(); }
                    if let Some(n) = vals.get("number")   { s.number   = n.parse().unwrap_or(0.0); }
                });
            });

        let submit_btn = form_el.submit_button(&mut *cx).unwrap();

        // Email field
        let entity_for_email = entity.clone();
        let email_field = form_field("settings-email", "email", &mut *cx)
            .label("Email").required(true)
            .help("Used for sign-in.")
            .input(text_input("settings-email-input")
                .placeholder("you@example.com")
                .on_change(move |new: &str, _, cx| {
                    entity_for_email.update(cx, |s, _| s.email = new.to_string());
                })
                .render(cx, _w))
            .render(cx);

        // Password field
        let entity_for_pw = entity.clone();
        let password_field = form_field("settings-password", "password", &mut *cx)
            .label("Password")
            .input(password_input("settings-password-input")
                .placeholder("••••••••")
                .on_change(move |new: &str, _, cx| {
                    entity_for_pw.update(cx, |s, _| s.password = new.to_string());
                })
                .render(cx, _w))
            .render(cx);

        // Number field
        let entity_for_num = entity.clone();
        let entity_for_inc = entity.clone();
        let entity_for_dec = entity.clone();
        let number_field = form_field("settings-number", "number", &mut *cx)
            .label("Retries")
            .input(number_input("settings-number-input")
                .min(0.0).max(10.0).step(1.0)
                .value(number)
                .on_change(move |new: f64, _, cx| entity_for_num.update(cx, |s, _| s.number = new))
                .on_increment(move |next: f64, _, cx| entity_for_inc.update(cx, |s, _| s.number = next))
                .on_decrement(move |next: f64, _, cx| entity_for_dec.update(cx, |s, _| s.number = next))
                .render(cx, _w))
            .render(cx);

        // … other fields follow the same shape …

        div()
            .size_full().p_4().flex().flex_col().gap_4()
            .id("settings-scroll").overflow_y_scroll()
            .child(email_field)
            .child(password_field)
            .child(number_field)
            // …
            .child(div().pt_2().child(submit_btn))
    }
}

Notes

  • cx.entity().clone() per closure. Every on_change / on_clear / on_browse / on_increment / on_decrement closure needs its own clone.
  • All inputs render via .render(cx, window). The renderer registers an IME handler on window. form_field.input(el) accepts an AnyElement, so pass the result directly.
  • .value(seed) is set once at construction. Don't echo the current value back every render — the input owns its own caret/selection state and reading back resets both.
  • number_input has three callbacks: on_change(f64, …) for the typed value, on_increment(f64, …) and on_decrement(f64, …) for the stepper buttons. All three write to the same field.
  • keybinding_input has mode: Idle | Capturing. Wire on_start_capture / on_cancel_capture to update your status line.
  • search_input.on_clear is Fn(&mut Window, &mut App) — no &str payload; the renderer has already cleared the value.
  • form_el.submit_button(cx) returns a pre-wired Primary button that fires on_submit with a snapshot. Place it where it makes sense.
  • Validation lives in on_submit. The renderer's job is layout; your job is validation. Set .error(field, msg) on the props for any field that should display an error.
  • text_input::init(cx) must be called at boot. See Quick start §3.

See also

  • Form — full API.
  • FormField — full API.
  • crates/yororen-ui-demos/inputs_demo/ — the canonical example.

Clone this wiki locally