590 lines
16 KiB
Markdown
590 lines
16 KiB
Markdown
---
|
|
title: Form
|
|
description: Flexible form container with support for field layout, validation, and multi-column layouts.
|
|
---
|
|
|
|
# Form
|
|
|
|
A comprehensive form component that provides structured layout for form fields with support for vertical/horizontal layouts, validation, field groups, and responsive multi-column layouts.
|
|
|
|
## Import
|
|
|
|
```rust
|
|
use gpui_component::form::{form_field, v_form, h_form, Form, FormField};
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Basic Form
|
|
|
|
```rust
|
|
v_form()
|
|
.child(
|
|
form_field()
|
|
.label("Name")
|
|
.child(TextInput::new(&name_input))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Email")
|
|
.child(TextInput::new(&email_input))
|
|
.required(true)
|
|
)
|
|
```
|
|
|
|
### Horizontal Form Layout
|
|
|
|
```rust
|
|
h_form()
|
|
.label_width(px(120.))
|
|
.child(
|
|
form_field()
|
|
.label("First Name")
|
|
.child(TextInput::new(&first_name))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Last Name")
|
|
.child(TextInput::new(&last_name))
|
|
)
|
|
```
|
|
|
|
### Multi-Column Form
|
|
|
|
```rust
|
|
v_form()
|
|
.column(2) // Two-column layout
|
|
.child(
|
|
form_field()
|
|
.label("First Name")
|
|
.child(TextInput::new(&first_name))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Last Name")
|
|
.child(TextInput::new(&last_name))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Bio")
|
|
.col_span(2) // Span across both columns
|
|
.child(TextInput::new(&bio_input))
|
|
)
|
|
```
|
|
|
|
## Form Container and Layout
|
|
|
|
### Vertical Layout (Default)
|
|
|
|
```rust
|
|
v_form()
|
|
.gap(px(12.))
|
|
.child(form_field().label("Name").child(input))
|
|
.child(form_field().label("Email").child(email_input))
|
|
```
|
|
|
|
### Horizontal Layout
|
|
|
|
```rust
|
|
h_form()
|
|
.label_width(px(100.))
|
|
.child(form_field().label("Name").child(input))
|
|
.child(form_field().label("Email").child(email_input))
|
|
```
|
|
|
|
### Custom Sizing
|
|
|
|
```rust
|
|
v_form()
|
|
.large() // Large form size
|
|
.label_text_size(rems(1.2))
|
|
.child(form_field().label("Title").child(input))
|
|
|
|
v_form()
|
|
.small() // Small form size
|
|
.child(form_field().label("Code").child(input))
|
|
```
|
|
|
|
## Form Validation
|
|
|
|
### Required Fields
|
|
|
|
```rust
|
|
form_field()
|
|
.label("Email")
|
|
.required(true) // Shows asterisk (*) next to label
|
|
.child(TextInput::new(&email_input))
|
|
```
|
|
|
|
### Field Descriptions
|
|
|
|
```rust
|
|
form_field()
|
|
.label("Password")
|
|
.description("Must be at least 8 characters long")
|
|
.child(TextInput::new(&password_input))
|
|
```
|
|
|
|
### Dynamic Descriptions
|
|
|
|
```rust
|
|
form_field()
|
|
.label("Bio")
|
|
.description_fn(|_, _| {
|
|
div().child("Use at most 100 words to describe yourself.")
|
|
})
|
|
.child(TextInput::new(&bio_input))
|
|
```
|
|
|
|
### Field Visibility
|
|
|
|
```rust
|
|
form_field()
|
|
.label("Admin Settings")
|
|
.visible(user.is_admin()) // Conditionally show field
|
|
.child(Switch::new("admin-mode"))
|
|
```
|
|
|
|
## Submit Handling
|
|
|
|
### Basic Submit Pattern
|
|
|
|
```rust
|
|
struct FormView {
|
|
name_input: Entity<InputState>,
|
|
email_input: Entity<InputState>,
|
|
}
|
|
|
|
impl FormView {
|
|
fn submit(&mut self, cx: &mut Context<Self>) {
|
|
let name = self.name_input.read(cx).value();
|
|
let email = self.email_input.read(cx).value();
|
|
|
|
// Validate inputs
|
|
if name.is_empty() || email.is_empty() {
|
|
// Show validation error
|
|
return;
|
|
}
|
|
|
|
// Submit form data
|
|
self.handle_submit(name, email, cx);
|
|
}
|
|
}
|
|
|
|
// Form with submit button
|
|
v_form()
|
|
.child(form_field().label("Name").child(TextInput::new(&self.name_input)))
|
|
.child(form_field().label("Email").child(TextInput::new(&self.email_input)))
|
|
.child(
|
|
form_field()
|
|
.no_label_indent()
|
|
.child(
|
|
Button::new("submit")
|
|
.primary()
|
|
.child("Submit")
|
|
.on_click(cx.listener(|this, _, _, cx| this.submit(cx)))
|
|
)
|
|
)
|
|
```
|
|
|
|
### Form with Action Buttons
|
|
|
|
```rust
|
|
v_form()
|
|
.child(form_field().label("Title").child(TextInput::new(&title)))
|
|
.child(form_field().label("Content").child(TextInput::new(&content)))
|
|
.child(
|
|
form_field()
|
|
.no_label_indent()
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(Button::new("save").primary().child("Save"))
|
|
.child(Button::new("cancel").child("Cancel"))
|
|
.child(Button::new("preview").outline().child("Preview"))
|
|
)
|
|
)
|
|
```
|
|
|
|
## Field Groups
|
|
|
|
### Related Fields
|
|
|
|
```rust
|
|
v_form()
|
|
.child(
|
|
form_field()
|
|
.label("Name")
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(div().flex_1().child(TextInput::new(&first_name)))
|
|
.child(div().flex_1().child(TextInput::new(&last_name)))
|
|
)
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Address")
|
|
.items_start() // Align to start for multi-line content
|
|
.child(
|
|
v_flex()
|
|
.gap_2()
|
|
.child(TextInput::new(&street))
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(div().flex_1().child(TextInput::new(&city)))
|
|
.child(div().w(px(100.)).child(TextInput::new(&zip)))
|
|
)
|
|
)
|
|
)
|
|
```
|
|
|
|
### Custom Field Components
|
|
|
|
```rust
|
|
form_field()
|
|
.label("Theme Color")
|
|
.child(ColorPicker::new(&color_state).small())
|
|
|
|
form_field()
|
|
.label("Birth Date")
|
|
.description("We'll send you a birthday gift!")
|
|
.child(DatePicker::new(&date_state))
|
|
|
|
form_field()
|
|
.label("Notifications")
|
|
.child(
|
|
v_flex()
|
|
.gap_2()
|
|
.child(Switch::new("email").label("Email notifications"))
|
|
.child(Switch::new("push").label("Push notifications"))
|
|
.child(Switch::new("sms").label("SMS notifications"))
|
|
)
|
|
```
|
|
|
|
### Conditional Fields
|
|
|
|
```rust
|
|
v_form()
|
|
.child(
|
|
form_field()
|
|
.label("Account Type")
|
|
.child(Dropdown::new(&account_type))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Company Name")
|
|
.visible(is_business_account) // Show only for business accounts
|
|
.child(TextInput::new(&company_name))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Tax ID")
|
|
.visible(is_business_account)
|
|
.required(is_business_account)
|
|
.child(TextInput::new(&tax_id))
|
|
)
|
|
```
|
|
|
|
## Grid Layout and Positioning
|
|
|
|
### Column Spanning
|
|
|
|
```rust
|
|
v_form()
|
|
.column(3) // Three-column grid
|
|
.child(form_field().label("First").child(input1))
|
|
.child(form_field().label("Second").child(input2))
|
|
.child(form_field().label("Third").child(input3))
|
|
.child(
|
|
form_field()
|
|
.label("Full Width")
|
|
.col_span(3) // Spans all three columns
|
|
.child(TextInput::new(&full_width))
|
|
)
|
|
```
|
|
|
|
### Column Positioning
|
|
|
|
```rust
|
|
v_form()
|
|
.column(4)
|
|
.child(form_field().label("A").child(input_a))
|
|
.child(form_field().label("B").child(input_b))
|
|
.child(
|
|
form_field()
|
|
.label("Positioned")
|
|
.col_start(1) // Start at column 1
|
|
.col_span(2) // Span 2 columns
|
|
.child(input_positioned)
|
|
)
|
|
```
|
|
|
|
### Responsive Layout
|
|
|
|
```rust
|
|
v_form()
|
|
.column(if is_mobile { 1 } else { 2 })
|
|
.child(form_field().label("Name").child(name_input))
|
|
.child(form_field().label("Email").child(email_input))
|
|
.child(
|
|
form_field()
|
|
.label("Bio")
|
|
.when(!is_mobile, |field| field.col_span(2))
|
|
.child(bio_input)
|
|
)
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### Form
|
|
|
|
| Method | Description |
|
|
| ----------------------- | ------------------------------------- |
|
|
| `vertical()` | Create vertical layout form |
|
|
| `horizontal()` | Create horizontal layout form |
|
|
| `layout(Axis)` | Set form layout direction |
|
|
| `label_width(Pixels)` | Set label width (horizontal layout) |
|
|
| `label_text_size(Rems)` | Set label text size |
|
|
| `gap(Pixels)` | Set gap between fields |
|
|
| `column(u16)` | Set number of columns for grid layout |
|
|
| `child(FormField)` | Add form field |
|
|
| `children(iter)` | Add multiple form fields |
|
|
| `large()` | Set large size |
|
|
| `small()` | Set small size |
|
|
|
|
### FormField
|
|
|
|
| Method | Description |
|
|
| --------------------- | -------------------------------- |
|
|
| `label(str)` | Set field label |
|
|
| `label_fn(fn)` | Set dynamic label using function |
|
|
| `description(str)` | Set field description |
|
|
| `description_fn(fn)` | Set dynamic description |
|
|
| `required(bool)` | Mark field as required |
|
|
| `visible(bool)` | Set field visibility |
|
|
| `no_label_indent()` | Disable label indentation |
|
|
| `items_start()` | Align items to start |
|
|
| `items_center()` | Align items to center |
|
|
| `items_end()` | Align items to end |
|
|
| `col_span(u16)` | Set column span |
|
|
| `col_start(i16)` | Set starting column |
|
|
| `col_end(i16)` | Set ending column |
|
|
| `track_focus(handle)` | Track focus for field |
|
|
|
|
## Examples
|
|
|
|
### User Registration Form
|
|
|
|
```rust
|
|
struct RegistrationForm {
|
|
first_name: Entity<InputState>,
|
|
last_name: Entity<InputState>,
|
|
email: Entity<InputState>,
|
|
password: Entity<InputState>,
|
|
confirm_password: Entity<InputState>,
|
|
terms_accepted: bool,
|
|
}
|
|
|
|
impl Render for RegistrationForm {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
v_form()
|
|
.large()
|
|
.child(
|
|
form_field()
|
|
.label("Personal Information")
|
|
.no_label_indent()
|
|
.child(
|
|
h_flex()
|
|
.gap_3()
|
|
.child(
|
|
div().flex_1().child(
|
|
TextInput::new(&self.first_name)
|
|
.placeholder("First name")
|
|
)
|
|
)
|
|
.child(
|
|
div().flex_1().child(
|
|
TextInput::new(&self.last_name)
|
|
.placeholder("Last name")
|
|
)
|
|
)
|
|
)
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Email")
|
|
.required(true)
|
|
.child(TextInput::new(&self.email))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Password")
|
|
.required(true)
|
|
.description("Must be at least 8 characters")
|
|
.child(TextInput::new(&self.password))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Confirm Password")
|
|
.required(true)
|
|
.child(TextInput::new(&self.confirm_password))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.no_label_indent()
|
|
.child(
|
|
Checkbox::new("terms")
|
|
.label("I agree to the Terms of Service")
|
|
.checked(self.terms_accepted)
|
|
.on_click(cx.listener(|this, checked, _, cx| {
|
|
this.terms_accepted = *checked;
|
|
cx.notify();
|
|
}))
|
|
)
|
|
)
|
|
.child(
|
|
form_field()
|
|
.no_label_indent()
|
|
.child(
|
|
Button::new("register")
|
|
.primary()
|
|
.large()
|
|
.w_full()
|
|
.child("Create Account")
|
|
)
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Settings Form with Sections
|
|
|
|
```rust
|
|
v_form()
|
|
.column(2)
|
|
.child(
|
|
form_field()
|
|
.label("Profile")
|
|
.no_label_indent()
|
|
.col_span(2)
|
|
.child(Divider::horizontal())
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Display Name")
|
|
.child(TextInput::new(&display_name))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Email")
|
|
.child(TextInput::new(&email))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Bio")
|
|
.col_span(2)
|
|
.items_start()
|
|
.child(TextInput::new(&bio))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Preferences")
|
|
.no_label_indent()
|
|
.col_span(2)
|
|
.child(Divider::horizontal())
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Theme")
|
|
.child(Dropdown::new(&theme_state))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Language")
|
|
.child(Dropdown::new(&language_state))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.no_label_indent()
|
|
.child(Switch::new("notifications").label("Enable notifications"))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.no_label_indent()
|
|
.child(Switch::new("marketing").label("Marketing emails"))
|
|
)
|
|
```
|
|
|
|
### Contact Form
|
|
|
|
```rust
|
|
v_form()
|
|
.child(
|
|
form_field()
|
|
.label("Contact Information")
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
Dropdown::new(&prefix_state)
|
|
.w(px(80.))
|
|
)
|
|
.child(
|
|
div().flex_1().child(
|
|
TextInput::new(&name_input)
|
|
.placeholder("Your name")
|
|
)
|
|
)
|
|
)
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Email")
|
|
.required(true)
|
|
.child(TextInput::new(&email_input))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Subject")
|
|
.child(Dropdown::new(&subject_state))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.label("Message")
|
|
.required(true)
|
|
.items_start()
|
|
.description("Please describe your inquiry in detail")
|
|
.child(TextInput::new(&message_input))
|
|
)
|
|
.child(
|
|
form_field()
|
|
.no_label_indent()
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.justify_between()
|
|
.child(
|
|
Checkbox::new("copy")
|
|
.label("Send me a copy")
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(Button::new("cancel").child("Cancel"))
|
|
.child(Button::new("send").primary().child("Send Message"))
|
|
)
|
|
)
|
|
)
|
|
```
|
|
|
|
## Accessibility
|
|
|
|
- **Keyboard Navigation**: Full keyboard support for all form elements
|
|
- **Focus Management**: Proper focus indicators and tab order
|
|
- **Screen Readers**: Labels properly associated with form controls
|
|
- **Required Fields**: Visual and semantic indication of required fields
|
|
- **Error States**: Clear error messaging and validation feedback
|
|
- **ARIA Labels**: Proper ARIA attributes for complex form structures
|
|
- **Semantic HTML**: Uses appropriate form semantics for accessibility tools
|