Forms
JWeb's Form DSL provides Java methods for form elements with HTML5 validation. Handle form events with state or submit to server handlers.
Overview
Create forms with the form() element and various input elements. Attribute shortcuts like name, type, and validation rules are plain arguments.
form(action("/submit"), method("POST"),
input(type("text"), name("name"), required()),
input(type("email"), name("email"), required()),
button(type("submit"), "Submit")
)Basic Form
Create forms with action and method attributes.
form(action("/submit"), method("POST"),
div(
label(for_("name"), "Name"),
input(type("text"), id("name"), name("name"),
placeholder("Enter your name"), required())
),
div(
label(for_("email"), "Email"),
input(type("email"), id("email"), name("email"),
placeholder("you@example.com"), required())
),
button(type("submit"), "Submit")
)Form Structure
Group form fields with fieldset and legend.
form(action("/register"), method("POST"),
fieldset(
legend("Personal Information"),
div(
label(for_("fname"), "First Name"),
input(type("text"), id("fname"), name("firstName"))
),
div(
label(for_("lname"), "Last Name"),
input(type("text"), id("lname"), name("lastName"))
)
),
fieldset(
legend("Account"),
div(
label(for_("uname"), "Username"),
input(type("text"), id("uname"), name("username"))
)
),
button(type("submit"), "Register")
)Input Types
All HTML5 input types are supported.
// Text inputs
input(type("text"), name("name"))
input(type("email"), name("email"))
input(type("password"), name("password"))
input(type("tel"), name("phone"))
input(type("url"), name("website"))
input(type("search"), name("query"))
// Number inputs
input(type("number"), name("age"), attrs().min("0").max("120"))
input(type("range"), name("volume"), attrs().min("0").max("100"))
// Date/time inputs
input(type("date"), name("birthdate"))
input(type("time"), name("appointment"))
input(type("datetime-local"), name("meeting"))
// Other inputs
input(type("color"), name("favorite"))
input(type("file"), name("avatar"), attrs().accept("image/*"))
input(type("hidden"), name("userId"), value("123"))Textarea
textarea(name("bio"), placeholder("Tell us about yourself..."),
attrs().rows(4).cols(50).maxlength(500))A Form Is a Record
form(SomeRecord.class) renders the whole form: one labelled field per record component, the control type read from the Java type, the CSRF hidden field taken from the current request, and a submit button. The same record validates the submission on the server, so the rules are written once.
public record Contact(
@Form.Required String name,
@Form.Required @Form.Email String email,
@Form.Required @Form.Multiline(rows = 4) String message) {}
// Render
form(Contact.class)
.action("/contact")
.submit("Send message")Component Type to Control
The Java type picks the control; the hints refine it.
String -> <input type="text">
@Form.Email String -> <input type="email"> + email validation
@Form.Password String -> <input type="password">
@Form.Multiline String -> <textarea>
int, long, Integer, Long -> <input type="number">
double, float, BigDecimal -> <input type="number" step="any">
boolean, Boolean -> <input type="checkbox">
an enum -> <select> over its constants
LocalDate / LocalTime -> <input type="date"> / type="time">
LocalDateTime -> <input type="datetime-local">
UploadedFile -> <input type="file">, form becomes multipartOverriding a Field
field(name, f -> ...) changes how a field is presented — label, placeholder, help text, control type, rows. The rules (required, email, length) stay on the record, so the browser and the server can never disagree about them.
record Contact(
@Form.Required String name,
@Form.Required @Form.Email String email,
@Form.Required @Form.Multiline(rows = 4) String message) {}
form(Contact.class)
.action("/contact")
.field("email", f -> f.label("Your email")
.placeholder("you@example.com")
.autocomplete("email"))
.field("message", f -> f.rows(6).help("Markdown is fine."))
.submit("Send message")Binding and Errors
Form.bind reads the submitted values, validates them against the same record, and hands back the value or the messages. Passing the result to errors(...) re-renders the form with the user's input, per-field messages, aria-invalid on the controls, and a summary.
record Contact(
@Form.Required String name,
@Form.Required @Form.Email String email,
@Form.Required @Form.Multiline(rows = 4) String message) {}
class MessageStore { void save(Contact c) {} }
MessageStore messages = new MessageStore();
app.post("/contact", req -> {
Form.Bound<Contact> submitted = Form.bind(Contact.class, req);
if (!submitted.ok()) {
return form(Contact.class)
.action("/contact")
.errors(submitted) // messages + the values as typed
.submit("Send message");
}
Contact contact = submitted.value();
messages.save(contact);
return p("Thanks!");
});Progressive Submission
swapForm posts over fetch and swaps the returned fragment into a target; the plain action() keeps working with JavaScript off.
record Contact(
@Form.Required String name,
@Form.Required @Form.Email String email,
@Form.Required @Form.Multiline(rows = 4) String message) {}
form(Contact.class)
.id("contact-form")
.action("/contact/submit") // no-JS fallback
.swapForm("/contact/submit", "#form-status") // progressive swap
.submit("Send Message")Styling
The form emits stable class names — jweb-form, jweb-field, jweb-label, jweb-control, jweb-help, jweb-error, jweb-errors, jweb-submit. Form.styles() is a ready-made stylesheet for them; drop it in the head once and override what you like.
Cls JWEB_SUBMIT = cls("jweb-submit");
CSSValue BRAND = hex("#6366f1");
head(
style(Form.styles()),
style(stylesheet()
.rule(JWEB_SUBMIT, style().backgroundColor(BRAND))
.build())
)Selects Without a Record
Outside a record form, a select is elements all the way down.
List<String> skillsList = List.of("Java", "SQL", "Docker");
select(name("car"), id("car"),
optgroup(attrs().label("Swedish Cars"),
option(value("volvo"), "Volvo"),
option(value("saab"), "Saab")
),
optgroup(attrs().label("German Cars"),
option(value("mercedes"), "Mercedes"),
option(value("audi"), "Audi")
)
)
// Multi-select
select(name("skills"), attrs().multiple(),
each(skillsList, s -> option(value(s), s))
)Select Dropdowns
Create select elements with options.
// Basic select
select(name("country"),
option(value(""), "Select a country"),
option(value("us"), "United States"),
option(value("uk"), "United Kingdom"),
option(value("ca"), "Canada")
)
// With selected option
select(name("status"),
option(value("active"), selected(), "Active"),
option(value("inactive"), "Inactive")
)
// Option groups
select(name("car"),
optgroup(attrs().label("Swedish Cars"),
option(value("volvo"), "Volvo"),
option(value("saab"), "Saab")
),
optgroup(attrs().label("German Cars"),
option(value("mercedes"), "Mercedes"),
option(value("audi"), "Audi")
)
)Dynamic Options
record Country(String code, String name) {}
List<Country> countries = List.of(new Country("us", "United States"), new Country("ca", "Canada"));
select(name("country"),
option(value(""), "Select..."),
each(countries, c ->
option(value(c.code()), c.name())
)
)Checkboxes
Single and multiple checkboxes.
// Single checkbox
div(
input(type("checkbox"), id("agree"), name("agree"), required()),
label(for_("agree"), "I agree to the terms")
)
// Checkbox group
fieldset(
legend("Select interests"),
div(
input(type("checkbox"), id("tech"), name("interests"), value("tech")),
label(for_("tech"), "Technology")
),
div(
input(type("checkbox"), id("sports"), name("interests"), value("sports")),
label(for_("sports"), "Sports")
),
div(
input(type("checkbox"), id("music"), name("interests"), value("music")),
label(for_("music"), "Music")
)
)Radio Buttons
fieldset(
legend("Select plan"),
div(
input(type("radio"), id("free"), name("plan"), value("free")),
label(for_("free"), "Free")
),
div(
input(type("radio"), id("pro"), name("plan"), value("pro"), checked()),
label(for_("pro"), "Pro - $9/month")
),
div(
input(type("radio"), id("enterprise"), name("plan"), value("enterprise")),
label(for_("enterprise"), "Enterprise - $99/month")
)
)HTML5 Validation
Use built-in validation attributes.
// Required field
input(type("text"), name("name"), required())
// Email validation
input(type("email"), name("email"), required())
// Min/max length
input(type("text"), name("username"),
attrs().minlength(3).maxlength(20))
// Number range
input(type("number"), name("age"),
attrs().min("18").max("100"))
// Pattern validation
input(type("text"), name("phone"),
attrs().pattern("[0-9]{3}-[0-9]{3}-[0-9]{4}")
.title("Format: 123-456-7890"))Custom Error Messages
input(type("email"), name("email"), required(),
attrs().title("Please enter a valid email address"))
// Custom validation message via JavaScript
input(type("text"), name("username"),
attrs().pattern("[a-z0-9_]+"),
data("error", "Username can only contain lowercase letters, numbers, and underscores"))Server-Side Validation
class UserService { void register(String email, String password) {} }
UserService userService = new UserService();
Element registerForm(List<String> errors) { return div(); }
app.post("/register", req -> {
String email = req.formParam("email");
String password = req.formParam("password");
List<String> errors = new ArrayList<>();
if (email == null || !email.contains("@")) {
errors.add("Invalid email address");
}
if (password == null || password.length() < 8) {
errors.add("Password must be at least 8 characters");
}
if (!errors.isEmpty()) {
return registerForm(errors);
}
userService.register(email, password);
return Response.redirect("/login");
});Event Handlers
Handle form events with state.
State<String> searchTerm = useState("");
State<String> email = useState("");
void validateEmail(String value) {}
void showHint() {}
void hideHint() {}
// Input event (fires on every keystroke)
input(
type("text"),
value(searchTerm.get()),
onInput(e -> searchTerm.set(e.value())))
// Change event (fires on blur)
input(
type("email"),
value(email.get()),
onChange(e -> {
email.set(e.value());
validateEmail(e.value());
}))
// Focus and blur
input(
type("text"),
onFocus(e -> showHint()),
onBlur(e -> hideHint()))Form Submit
import java.util.concurrent.CompletableFuture;
State<String> name = useState("");
State<String> email = useState("");
State<Boolean> loading = useState(false);
CompletableFuture<Void> submitForm(String n, String e) { return CompletableFuture.completedFuture(null); }
form(onSubmit(e -> {
e.preventDefault();
loading.set(true);
// Submit data
submitForm(name.get(), email.get())
.thenRun(() -> loading.set(false));
}),
input(type("text"), onInput(e -> name.set(e.value()))),
input(type("email"), onInput(e -> email.set(e.value()))),
button(type("submit"), attrs().disabled(loading.get()),
loading.get() ? "Submitting..." : "Submit")
)File Upload
Handle file uploads with validation and storage.
import jweb.FileUpload;
import jweb.UploadedFile;
import java.io.IOException;
import java.nio.file.Path;
app.post("/upload", req -> {
UploadedFile file = FileUpload.getFile(req, "document");
if (file.isEmpty()) {
return Response.badRequest("No file provided");
}
try {
Path saved = file.saveTo(Path.of("uploads")); // saveTo throws IOException
return Response.json(Map.of("path", saved.toString()));
} catch (IOException e) {
return Response.error(500, "Could not store the upload");
}
});Upload Form
form(action("/upload"), method("post"), attrs().enctype("multipart/form-data"),
input(type("file"), name("document")),
button(type("submit"), "Upload")
)File Properties
import java.io.InputStream;
UploadedFile file = FileUpload.getFile(req, "document");
// Check if file was uploaded
if (file.isEmpty()) { /* no file was uploaded */ }
// File info
String name = file.getFilename(); // "report.pdf"
String type = file.getContentType(); // "application/pdf"
long size = file.getSize(); // bytes
// Get content
byte[] bytes = file.getBytes();
InputStream stream = file.getInputStream();
// Type checks
if (file.isImage()) { /* PNG, JPG, GIF */ }
if (file.hasExtension("pdf", "doc")) { /* document */ }Validation
UploadedFile file = FileUpload.getFile(req, "document");
var validation = FileUpload.validate(file)
.required() // must be present
.maxSizeMB(10) // max 10MB
.imagesOnly(); // PNG, JPG, GIF only
Object result = validation.isValid()
? Response.ok()
: Response.badRequest(validation.getFirstError());
// Or with specific extensions
FileUpload.validate(file)
.required()
.maxSizeMB(5)
.allowedExtensions("pdf", "doc", "docx")Multiple Files
import java.nio.file.Path;
// HTML
input(type("file"), name("images"), attrs().multiple().accept("image/*"))
// Handler
List<UploadedFile> images = FileUpload.getFiles(req, "images");
for (UploadedFile image : images) {
var v = FileUpload.validate(image).maxSizeMB(5).imagesOnly();
if (v.isValid()) {
String filename = UUID.randomUUID() + "_" + image.getFilename();
image.saveTo(Path.of("uploads/gallery"), filename);
}
}Save Options
import java.nio.file.Path;
UploadedFile file = FileUpload.getFile(req, "document");
// Save to directory (generates a unique name)
file.saveTo(Path.of("uploads"));
// Save with custom name
file.saveTo(Path.of("uploads"), "custom-name.pdf");
// Save preserving the original filename
file.saveWithOriginalName(Path.of("uploads"));