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. Use attrs() to set attributes like name, type, and validation rules.

form(attrs().action("/submit").method("POST"),
    input(attrs().type("text").name("name").required()),
    input(attrs().type("email").name("email").required()),
    button(attrs().type("submit"), text("Submit"))
)

Basic Form

Create forms with action and method attributes.

form(attrs().action("/submit").method("POST"),
    div(
        label(attrs().for_("name"), text("Name")),
        input(attrs().type("text").id("name").name("name")
            .placeholder("Enter your name").required())
    ),
    div(
        label(attrs().for_("email"), text("Email")),
        input(attrs().type("email").id("email").name("email")
            .placeholder("you@example.com").required())
    ),
    button(attrs().type("submit"), text("Submit"))
)

Form Structure

Group form fields with fieldset and legend.

form(attrs().action("/register").method("POST"),
    fieldset(
        legend("Personal Information"),
        div(
            label(attrs().for_("fname"), text("First Name")),
            input(attrs().type("text").id("fname").name("firstName"))
        ),
        div(
            label(attrs().for_("lname"), text("Last Name")),
            input(attrs().type("text").id("lname").name("lastName"))
        )
    ),
    fieldset(
        legend("Account"),
        div(
            label(attrs().for_("uname"), text("Username")),
            input(attrs().type("text").id("uname").name("username"))
        )
    ),
    button(attrs().type("submit"), text("Register"))
)

Input Types

All HTML5 input types are supported.

// Text inputs
input(attrs().type("text").name("name"))
input(attrs().type("email").name("email"))
input(attrs().type("password").name("password"))
input(attrs().type("tel").name("phone"))
input(attrs().type("url").name("website"))
input(attrs().type("search").name("query"))

// Number inputs
input(attrs().type("number").name("age").min("0").max("120"))
input(attrs().type("range").name("volume").min("0").max("100"))

// Date/time inputs
input(attrs().type("date").name("birthdate"))
input(attrs().type("time").name("appointment"))
input(attrs().type("datetime-local").name("meeting"))

// Other inputs
input(attrs().type("color").name("favorite"))
input(attrs().type("file").name("avatar").accept("image/*"))
input(attrs().type("hidden").name("userId").value("123"))

Textarea

textarea(attrs()
    .name("bio")
    .rows(4)
    .cols(50)
    .placeholder("Tell us about yourself...")
    .maxlength("500"))

Form Input Builders

JWeb provides type-safe input builders with validation and styling built-in.

import static com.osmig.Jweb.framework.elements.Elements.*;

// Text inputs with labels
textInput("username", "Username")
textInput("fullName", "Full Name").placeholder("John Doe")

// Email with validation
emailInput("email", "Email Address").required()

// Password with constraints
passwordInput("password", "Password")
    .minLength(8)
    .required()

// Number inputs
numberInput("age", "Age").min(0).max(120)
numberInput("price", "Price").step(0.01)

Complete Field Builder

The field() builder creates a full form field with label, input, and error container.

// Basic text field
field("name").label("Full Name").text().required()

// Email field with placeholder
field("email")
    .label("Email Address")
    .email()
    .placeholder("user@example.com")
    .required()

// Password field
field("password")
    .label("Password")
    .password()
    .minLength(8)
    .required()

// Number with range
field("quantity")
    .label("Quantity")
    .number()
    .min(1)
    .max(100)
    .value(1)

Checkbox and Radio

// Single checkbox
checkbox("terms", "I agree to the terms")
checkbox("newsletter", "Subscribe to newsletter").checked()

// Radio group
radioGroup("gender",
    radio("male", "Male"),
    radio("female", "Female"),
    radio("other", "Other")
)

// Inline radio
radioGroup("size",
    radio("s", "Small"),
    radio("m", "Medium"),
    radio("l", "Large")
).inline()

Select Dropdown

// Simple select
selectField("country", "Country",
    option("us", "United States"),
    option("uk", "United Kingdom"),
    option("ca", "Canada")
)

// With groups
selectField("car", "Choose a car",
    optgroup("Swedish Cars",
        option("volvo", "Volvo"),
        option("saab", "Saab")
    ),
    optgroup("German Cars",
        option("mercedes", "Mercedes"),
        option("audi", "Audi")
    )
)

// Multi-select
selectField("skills", "Skills").multiple()
    .options(skillsList)
Input builders automatically generate IDs and wire up labels for accessibility.

Select Dropdowns

Create select elements with options.

// Basic select
select(attrs().name("country"),
    option(attrs().value(""), text("Select a country")),
    option(attrs().value("us"), text("United States")),
    option(attrs().value("uk"), text("United Kingdom")),
    option(attrs().value("ca"), text("Canada"))
)

// With selected option
select(attrs().name("status"),
    option(attrs().value("active").selected(), text("Active")),
    option(attrs().value("inactive"), text("Inactive"))
)

// Option groups
select(attrs().name("car"),
    optgroup(attrs().label("Swedish Cars"),
        option(attrs().value("volvo"), text("Volvo")),
        option(attrs().value("saab"), text("Saab"))
    ),
    optgroup(attrs().label("German Cars"),
        option(attrs().value("mercedes"), text("Mercedes")),
        option(attrs().value("audi"), text("Audi"))
    )
)

Dynamic Options

List<Country> countries = countryService.findAll();

select(attrs().name("country"),
    option(attrs().value(""), text("Select...")),
    each(countries, c ->
        option(attrs().value(c.code()), text(c.name()))
    )
)

Checkboxes

Single and multiple checkboxes.

// Single checkbox
div(
    input(attrs().type("checkbox").id("agree").name("agree").required()),
    label(attrs().for_("agree"), text("I agree to the terms"))
)

// Checkbox group
fieldset(
    legend("Select interests"),
    div(
        input(attrs().type("checkbox").id("tech").name("interests").value("tech")),
        label(attrs().for_("tech"), text("Technology"))
    ),
    div(
        input(attrs().type("checkbox").id("sports").name("interests").value("sports")),
        label(attrs().for_("sports"), text("Sports"))
    ),
    div(
        input(attrs().type("checkbox").id("music").name("interests").value("music")),
        label(attrs().for_("music"), text("Music"))
    )
)

Radio Buttons

fieldset(
    legend("Select plan"),
    div(
        input(attrs().type("radio").id("free").name("plan").value("free")),
        label(attrs().for_("free"), text("Free"))
    ),
    div(
        input(attrs().type("radio").id("pro").name("plan").value("pro").checked()),
        label(attrs().for_("pro"), text("Pro - $9/month"))
    ),
    div(
        input(attrs().type("radio").id("enterprise").name("plan").value("enterprise")),
        label(attrs().for_("enterprise"), text("Enterprise - $99/month"))
    )
)

HTML5 Validation

Use built-in validation attributes.

// Required field
input(attrs().type("text").name("name").required())

// Email validation
input(attrs().type("email").name("email").required())

// Min/max length
input(attrs().type("text").name("username")
    .minlength("3").maxlength("20"))

// Number range
input(attrs().type("number").name("age")
    .min("18").max("100"))

// Pattern validation
input(attrs().type("text").name("phone")
    .pattern("[0-9]{3}-[0-9]{3}-[0-9]{4}")
    .title("Format: 123-456-7890"))

Custom Error Messages

input(attrs()
    .type("email")
    .name("email")
    .required()
    .title("Please enter a valid email address"))

// Custom validation message via JavaScript
input(attrs()
    .type("text")
    .name("username")
    .pattern("[a-z0-9_]+")
    .data("error", "Username can only contain lowercase letters, numbers, and underscores"))

Server-Side Validation

app.post("/register", req -> {
    String email = req.body("email");
    String password = req.body("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("");

// Input event (fires on every keystroke)
input(attrs()
    .type("text")
    .value(searchTerm.get())
    .onInput(e -> searchTerm.set(e.value())))

// Change event (fires on blur)
input(attrs()
    .type("email")
    .value(email.get())
    .onChange(e -> {
        email.set(e.value());
        validateEmail(e.value());
    }))

// Focus and blur
input(attrs()
    .type("text")
    .onFocus(e -> showHint())
    .onBlur(e -> hideHint()))

Form Submit

State<String> name = useState("");
State<String> email = useState("");
State<Boolean> loading = useState(false);

form(attrs().onSubmit(e -> {
    e.preventDefault();
    loading.set(true);

    // Submit data
    submitForm(name.get(), email.get())
        .thenRun(() -> loading.set(false));
}),
    input(attrs().type("text").onInput(e -> name.set(e.value()))),
    input(attrs().type("email").onInput(e -> email.set(e.value()))),
    button(attrs().type("submit").disabled(loading.get()),
        text(loading.get() ? "Submitting..." : "Submit"))
)

File Upload

Handle file uploads with validation and storage.

import com.osmig.Jweb.framework.upload.FileUpload;
import com.osmig.Jweb.framework.upload.UploadedFile;

app.post("/upload", req -> {
    UploadedFile file = FileUpload.getFile(req, "document");

    if (file.isEmpty()) {
        return Response.badRequest("No file provided");
    }

    Path saved = file.saveTo(Path.of("uploads"));
    return Response.json(Map.of("path", saved.toString()));
});

Upload Form

form(
    action("/upload"),
    method("post"),
    enctype("multipart/form-data"),

    input(type("file"), name("document")),
    button(type("submit"), "Upload")
)

File Properties

UploadedFile file = FileUpload.getFile(req, "document");

// Check if file was uploaded
if (file.isEmpty()) return error("No file");

// File info
String name = file.getOriginalFilename();  // "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

var validation = FileUpload.validate(file)
    .required()                     // must be present
    .maxSizeMB(10)                 // max 10MB
    .imagesOnly();                 // PNG, JPG, GIF only

if (!validation.isValid()) {
    return Response.badRequest(validation.getFirstError());
}

// Or with specific extensions
FileUpload.validate(file)
    .required()
    .maxSizeMB(5)
    .allowedExtensions("pdf", "doc", "docx")

Multiple Files

// HTML
input(type("file"), name("images"), 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.getOriginalFilename();
        image.saveAs(Path.of("uploads/gallery", filename));
    }
}

Save Options

// Save to directory (keeps original name)
file.saveTo(Path.of("uploads"));

// Save with custom name
file.saveAs(Path.of("uploads/custom-name.pdf"));

// Save with unique name
String unique = UUID.randomUUID() + "_" + file.getOriginalFilename();
file.saveAs(Path.of("uploads", unique));
Configure max file size in application.yaml: spring.servlet.multipart.max-file-size