Component 23 of 60
Form
Coordinates validation and submission for fields.
Starting the preview. GPUI compiles to WebAssembly, so the first load takes a few seconds. This browser has neither WebGPU nor WebGL 2, so the live preview cannot run. The code on this page still works. The preview did not start.
fn form_preview(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let validation = window.use_keyed_state("subscribe.validation", cx, |_, _| {
(false, false, false, false)
});
let fruit = window.use_keyed_state("subscribe.fruit", cx, |window, cx| {
SelectState::new(
[
SelectItem::new("apple", "Apple"),
SelectItem::new("pear", "Pear"),
],
window,
cx,
)
});
let quantity = window.use_keyed_state("subscribe.quantity", cx, |window, cx| {
InputState::new(window, cx)
.default_value("0")
.min(0.)
.max(20.)
.step(1.)
});
let enter_validation = validation.clone();
let enter_fruit = fruit.clone();
let enter_quantity = quantity.clone();
let email = live_input(
"subscribe.email",
"",
"you@example.com",
window,
cx,
move |this, input, event, cx| {
if matches!(event, InputEvent::PressEnter { .. }) {
this.submit_form(
input.read(cx).value(),
enter_fruit.read(cx).value().cloned(),
enter_quantity.read(cx).value(),
&enter_validation,
cx,
);
}
},
);
let result = *validation.read(cx);
let submit_email = email.clone();
let submit_fruit = fruit.clone();
let submit_quantity = quantity.clone();
form("subscribe", cx)
.w(px(320.))
.aria_label("Fruit order")
.child(
Field::new("subscribe-email", &email)
.label("Email")
.required(true)
.when(result.0, |f| f.error("Enter a valid email address.")),
)
.child(
Field::from_control(
"subscribe-fruit",
Select::new(&fruit).placeholder("Select a fruit…"),
)
.label("Fruit")
.required(true)
.when(result.1, |f| f.error("Select a fruit.")),
)
.child(
Field::from_control("subscribe-quantity", NumberField::new(&quantity))
.label("Quantity")
.required(true)
.when(result.2, |f| f.error("Enter a quantity from 1 to 20.")),
)
.child(
Button::new("subscribe-submit")
.label("Place order")
.on_click(cx.listener(move |this, _, _, cx| {
this.submit_form(
submit_email.read(cx).value(),
submit_fruit.read(cx).value().cloned(),
submit_quantity.read(cx).value(),
&validation,
cx,
);
})),
)
.when(result.3, |f| f.child("Order submitted."))
}
Installation
After the one-time setup, run the command from your app root. The CLI
writes the modules and keeps src/ui/mod.rs in step.
gpuicn add form button number-field select
If you prefer to copy the files by hand, declare them yourself:
// src/ui/mod.rs
pub mod theme;
pub mod form;
pub mod button;
pub mod input;
pub mod field;
pub mod number_field;
pub mod select;
// src/main.rs
mod ui;
Usage
Put this in a view module and return example(email, cx) from its render method.
use crate::ui::button::Button;
use crate::ui::field::Field;
use crate::ui::form::*;
use crate::ui::input::InputState;
use crate::ui::number_field::NumberField;
use crate::ui::select::{Select, SelectState};
use gpui_kit::Entity;
use gpui_kit::{App, IntoElement, ParentElement, StatefulInteractiveElement};
fn example(email: &Entity<InputState>, cx: &App) -> impl IntoElement {
// Validate in the owning view and share its submit handler with InputEvent::PressEnter.
form("subscribe", cx)
.aria_label("Subscribe")
.child(Field::new("email", email).label("Email").required(true))
.child(Button::new("subscribe.submit").label("Subscribe"))
}
fn mixed_example(
email: &Entity<InputState>,
fruit: &Entity<SelectState>,
quantity: &Entity<InputState>,
cx: &App,
) -> impl IntoElement {
form("order", cx)
.aria_label("Fruit order")
.child(Field::new("email", email).label("Email").required(true))
.child(
Field::from_control("fruit", Select::new(fruit))
.label("Fruit")
.required(true),
)
.child(
Field::from_control("quantity", NumberField::new(quantity))
.label("Quantity")
.required(true),
)
.child(Button::new("order.submit").label("Place order"))
}
API reference
pub fn form(id: impl Into<ElementId>, cx: &App) -> Stateful<Div>
Groups fields semantically without an implicit global form context. Subscribe to each input's `InputEvent` and submit through the application's state.
Installed source, 24 lines
//! Nova form layout. The application owns values, validation and submission.
use super::theme::UiTheme;
use gpui_kit::{
App, Div, ElementId, InteractiveElement as _, Role, Stateful, StatefulInteractiveElement as _,
Styled, div,
};
/// Groups fields semantically without an implicit global form context.
/// Subscribe to each input's `InputEvent` and submit through the application's state.
pub fn form(id: impl Into<ElementId>, cx: &App) -> Stateful<Div> {
let theme = UiTheme::read(cx);
div()
.id(id)
.role(Role::Form)
.flex()
.flex_col()
.w_full()
.gap(theme.space(6.))
.font_family(theme.fonts.body.clone())
.text_size(theme.text(14.))
.text_color(theme.colors.foreground)
}
Platform notes
Visual reference: shadcn/ui 4.19.0, Neutral Nova. Implementation:
crates/ui/registry/form/form.rs, using GPUI Kit 0.6.4.
The form helper supplies a named semantic group and layout. The application owns retained input states, validation errors and submission. Subscribe to InputEvent::PressEnter and use the same submit handler as the submit button. There is no implicit serialization or validation context.
The Rust source and Usage example define the supported API. Browser DOM/CSS behavior, exact exit animations and cross-platform screen-reader parity are not implied by the native component. See migration qualification for the checks completed on this revision.