Forms

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:

FormatValidationTransformation
Emailvalidator::ValidateEmailLowercased
Urlvalidator::ValidateUrl
PasswordStandardArgon2 hash in finalize(), value cleared on render()
RichTextStandardXSS sanitization (sanitize()) before validation
CsrfSession 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 $argon2 to avoid double hashing. In a login form, do not rely on is_valid() to check the password — fetch the user from the DB first, then call verify() 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:

MethodDestination
.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: .svg files are always rejected by default (XSS risk). Image validation uses the image crate to check the real file format. Empty submissions (no file selected) are handled correctly — the required constraint 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 need HiddenField::new_csrf() unless you are building a fully custom form.


Field types summary

StructConstructorsSpecial validation
TextFieldtext(), email(), url(), password(), textarea(), richtext()Email/URL via validator, Argon2, XSS sanitization
NumericFieldinteger(), float(), decimal(), percent(), range()Min/max bounds, decimal precision
BooleanFieldnew(), radio()Required = must be checked
ChoiceFieldnew() + .multiple()Value must be in declared choices
RadioFieldnew()Value must be in declared choices
CheckboxFieldnew()All values must be in choices
DateFieldnew()YYYY-MM-DD format, min/max bounds
TimeFieldnew()HH:MM format, min/max bounds
DateTimeFieldnew()YYYY-MM-DDTHH:MM format, min/max bounds
DurationFieldnew()Seconds, min/max bounds
FileFieldimage(), document(), any()Extensions, size, dimensions, anti-SVG
ColorFieldnew()#RRGGBB or #RGB format
SlugFieldnew()ASCII/unicode, no hyphen at start/end
UUIDFieldnew()Valid UUID format
JSONFieldnew()Valid JSON via serde_json
IPAddressFieldnew() + .ipv4_only() / .ipv6_only()IPv4/IPv6 via std::net::IpAddr
HiddenFieldnew(), new_csrf()CSRF token validation if name == "csrf_token"

Typed conversion helpers | Database errors