Field types
TextField — Text fields
TextField supports 6 special formats via the SpecialFormat enum:
// Plain text
form.field(&TextField::text("username").label("Name").required());
// Email — validated via `validator::ValidateEmail`
form.field(&TextField::email("email").label("Email").required());
// URL — validated via `validator::ValidateUrl`
form.field(&TextField::url("website").label("Website"));
// Password — automatic Argon2 hashing in finalize(), never re-displayed in HTML
form.field(
&TextField::password("password")
.label("Password")
.required()
.min_length(8, "Min 8 characters"),
);
// Textarea
form.field(&TextField::textarea("summary").label("Summary"));
// RichText — automatic XSS sanitization before validation
form.field(&TextField::richtext("content").label("Content"));
Builder options:
TextField::text("name")
.label("My field") // Display label
.placeholder("Enter...") // Placeholder
.required() // Required (default message)
.min_length(3, "Too short") // Min length with message
.max_length(100, "Too long") // Max length with message
.readonly("Read-only") // Read-only
.disabled("Disabled") // Disabled
Automatic behavior per format:
| Format | Validation | Transformation |
|---|---|---|
Email | validator::ValidateEmail | Lowercased |
Url | validator::ValidateUrl | — |
Password | Standard | Argon2 hash in finalize(), value cleared on render() |
RichText | Standard | XSS sanitization (sanitize()) before validation |
Csrf | Session token | — |
Password utilities:
Hashing and verification are delegated to PasswordConfig, initialized at startup via password_init():
use runique::prelude::{hash, verify};
// Hash manually (e.g. account creation outside a form)
let hashed = hash("my_password")?;
// Verify a plain password against a stored hash (e.g. login)
let ok = verify("plain_pwd", &user.password_hash);
if !ok {
// incorrect password
}
Automatic hashing in
finalize()detects if the value already starts with$argon2to avoid double hashing. In a login form, do not rely onis_valid()to check the password — fetch the user from the DB first, then callverify()manually.
NumericField — Numeric fields
5 variants via the NumericConfig enum:
// Integer with bounds
form.field(
&NumericField::integer("age")
.label("Age")
.min(0.0, "Min 0")
.max(150.0, "Max 150"),
);
// Float number
form.field(&NumericField::float("price").label("Price"));
// Decimal with precision
form.field(
&NumericField::decimal("amount")
.label("Amount")
.digits(2, 4), // min 2, max 4 digits after the decimal separator
);
// Percentage (0–100 by default)
form.field(&NumericField::percent("rate").label("Rate"));
// Range slider with min, max, default value
form.field(
&NumericField::range("volume", 0.0, 100.0, 50.0)
.label("Volume")
.step(5.0),
);
Options: .min(val, msg), .max(val, msg), .step(val), .digits(min, max), .label(l), .placeholder(p)
BooleanField — Checkboxes / Single radio
// Simple checkbox
form.field(
&BooleanField::new("accept_terms")
.label("I accept the terms")
.required(),
);
// Single radio (yes/no)
form.field(&BooleanField::radio("newsletter").label("Newsletter"));
// Pre-checked
form.field(&BooleanField::new("remember_me").label("Remember me").checked());
ChoiceField — Select / Dropdown
use runique::forms::fields::choice::ChoiceOption;
let choices = vec![
ChoiceOption::new("fr", "France"),
ChoiceOption::new("be", "Belgium"),
ChoiceOption::new("ch", "Switzerland"),
];
// Single select
form.field(
&ChoiceField::new("country")
.label("Country")
.choices(choices.clone())
.required(),
);
// Multiple select
form.field(
&ChoiceField::new("languages")
.label("Languages")
.choices(choices)
.multiple(),
);
Validation automatically checks that the submitted value is among the declared choices.
RadioField — Radio buttons
form.field(
&RadioField::new("gender")
.label("Gender")
.choices(vec![
ChoiceOption::new("m", "Male"),
ChoiceOption::new("f", "Female"),
ChoiceOption::new("o", "Other"),
])
.required(),
);
CheckboxField — Multiple checkboxes
form.field(
&CheckboxField::new("hobbies")
.label("Hobbies")
.choices(vec![
ChoiceOption::new("sport", "Sports"),
ChoiceOption::new("music", "Music"),
ChoiceOption::new("reading", "Reading"),
]),
);
Submitted values are in the form
"val1,val2,val3". Validation checks that each value exists in the choices.
DateField, TimeField, DateTimeField — Date / Time
use chrono::NaiveDate;
// Date (format: YYYY-MM-DD)
form.field(
&DateField::new("birthday")
.label("Birth date")
.min(NaiveDate::from_ymd_opt(1900, 1, 1).unwrap(), "Too old")
.max(NaiveDate::from_ymd_opt(2010, 12, 31).unwrap(), "Too recent"),
);
// Time (format: HH:MM)
form.field(&TimeField::new("meeting_time").label("Meeting time"));
// Date + Time (format: YYYY-MM-DDTHH:MM)
form.field(&DateTimeField::new("event_start").label("Event start"));
DurationField — Duration
form.field(
&DurationField::new("timeout")
.label("Delay (seconds)")
.min_seconds(60, "Minimum 1 minute")
.max_seconds(3600, "Maximum 1 hour"),
);
FileField — File uploads
// Image with full constraints — explicit directory
form.field(
&FileField::image("avatar")
.label("Profile picture")
.upload_to("uploads/avatars") // → uploads/avatars/
.max_size_mb(5)
.max_files(1)
.max_dimensions(1920, 1080)
.allowed_extensions(vec!["png", "jpg", "jpeg", "webp", "avif"]),
);
// Image — automatic directory from MEDIA_ROOT (.env)
// Files go to {MEDIA_ROOT}/{field_name}/ e.g. media/photo/
form.field(
&FileField::image("photo")
.label("Photo")
.upload_to_env()
.max_size_mb(5),
);
// Without upload_to — files stored directly in MEDIA_ROOT
form.field(
&FileField::image("image")
.label("Image")
.max_size_mb(5),
);
// Document
form.field(
&FileField::document("cv")
.label("Resume")
.upload_to("uploads/cv")
.max_size_mb(10),
);
// Any file (multi-file)
form.field(
&FileField::any("attachments")
.label("Attachments")
.max_files(5),
);
File destination:
| Method | Destination |
|---|---|
.upload_to("uploads/images") | uploads/images/ (exact path) |
.upload_to_env() | {MEDIA_ROOT}/{field_name}/ (from .env) |
| (none) | MEDIA_ROOT directly (no subdirectory) |
The move to the final destination happens in finalize(), only if validation passes. The directory is created automatically if it does not already exist.
Security:
.svgfiles are always rejected by default (XSS risk). Image validation uses theimagecrate to check the real file format. Empty submissions (no file selected) are handled correctly — therequiredconstraint works as expected.
Associated JS files
fn register_fields(form: &mut Forms) {
// ... fields ...
form.add_js(&["js/my_script.js", "js/other.js"]);
}
JS files are automatically included in the form's HTML rendering.
ColorField — Color picker
form.field(
&ColorField::new("theme_color")
.label("Theme color")
.default_color("#3498db"), // Validates #RGB or #RRGGBB format
);
SlugField — URL-friendly slug
form.field(
&SlugField::new("slug")
.label("Slug")
.placeholder("my-url-article")
.allow_unicode(), // Optional: allow unicode characters
);
Validation: letters, digits, hyphens, underscores only. Cannot start or end with a hyphen.
UUIDField
form.field(
&UUIDField::new("external_id")
.label("External ID")
.placeholder("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),
);
JSONField — Textarea with JSON validation
form.field(
&JSONField::new("metadata")
.label("Metadata")
.placeholder(r#"{"key": "value"}"#)
.rows(10), // Number of textarea rows
);
IPAddressField — IP address
// IPv4 + IPv6
form.field(&IPAddressField::new("server_ip").label("Server IP"));
// IPv4 only
form.field(&IPAddressField::new("gateway").label("Gateway").ipv4_only());
// IPv6 only
form.field(&IPAddressField::new("ipv6").label("IPv6 address").ipv6_only());
HiddenField — Hidden field
An invisible field in the HTML form (<input type="hidden">). Two main uses: pass technical data without showing it to the user, or manually validate a CSRF token.
// Generic hidden field (e.g. linked entity ID)
form.field(
&HiddenField::new("entity_id")
.label("Entity ID"),
);
// Internal CSRF field (managed automatically by Runique — advanced use only)
form.field(&HiddenField::new_csrf());
In standard Runique forms, CSRF is handled automatically via
{% csrf %}in the template. You don't needHiddenField::new_csrf()unless you are building a fully custom form.
Field types summary
| Struct | Constructors | Special validation |
|---|---|---|
TextField | text(), email(), url(), password(), textarea(), richtext() | Email/URL via validator, Argon2, XSS sanitization |
NumericField | integer(), float(), decimal(), percent(), range() | Min/max bounds, decimal precision |
BooleanField | new(), radio() | Required = must be checked |
ChoiceField | new() + .multiple() | Value must be in declared choices |
RadioField | new() | Value must be in declared choices |
CheckboxField | new() | All values must be in choices |
DateField | new() | YYYY-MM-DD format, min/max bounds |
TimeField | new() | HH:MM format, min/max bounds |
DateTimeField | new() | YYYY-MM-DDTHH:MM format, min/max bounds |
DurationField | new() | Seconds, min/max bounds |
FileField | image(), document(), any() | Extensions, size, dimensions, anti-SVG |
ColorField | new() | #RRGGBB or #RGB format |
SlugField | new() | ASCII/unicode, no hyphen at start/end |
UUIDField | new() | Valid UUID format |
JSONField | new() | Valid JSON via serde_json |
IPAddressField | new() + .ipv4_only() / .ipv6_only() | IPv4/IPv6 via std::net::IpAddr |
HiddenField | new(), new_csrf() | CSRF token validation if name == "csrf_token" |