Tutorial

Build a Contact Form

A step-by-step walkthrough of assembling a real, validated Wicket page with Oat's form components — from an empty form to a working submit handler.

This picks up right after the Quick Start steps on the homepage — we'll assume you've already called WicketOats.install(this) in your application's init(). Oat's form fields work like plain Wicket form components, so the page reads like any other Wicket form. Every snippet below is taken from a page we compiled and ran against the library.

1

The model

Start with a plain model object and a Form with a CompoundPropertyModel — nothing Oat-specific yet, just standard Wicket. The form gets a markup id so the Ajax submit in step 5 can re-render it.

// A simple model object
public static class ContactMessage implements Serializable {
    public String name;
    public String email;
    public String message;
}
// Inside your page's constructor
Form<ContactMessage> form = new Form<>("contactForm",
        new CompoundPropertyModel<>(new ContactMessage()));
form.setOutputMarkupId(true);
add(form);
2

Add the name field

OatTextField renders a label, the input and a feedback slot. Just like a Wicket TextField, it takes its model from the CompoundPropertyModel by id ("name" → ContactMessage.name), and its label from your page's .properties file by the same id. Required fields get an asterisk and aria-required.

form.add(new OatTextField<String>("name").setRequired(true));
# ContactPage.properties
name=Your Name
3

Add the email field

Same shape, a different field type — OatEmailField renders a native <input type="email"> and validates the address.

form.add(new OatEmailField("email").setRequired(true));
# ContactPage.properties
email=Email Address
4

Add the message field

OatTextArea follows the exact same pattern, just backed by a <textarea> instead of an <input>.

form.add(new OatTextArea<String>("message").setRequired(true));
# ContactPage.properties
message=Message
5

Add the submit button and feedback

Oat.Components.submitButton creates an Oat-styled AjaxButton: the first handler runs when the form is valid, the second when it isn't. Report the outcome with Wicket's own success() and error(), and let Oat.Behaviors.feedbackToasts() show them as toasts — on Ajax requests too, without touching the AjaxRequestTarget. Re-rendering the form with target.add(form) shows the fields' inline errors.

// Show success()/error() messages as toasts
add(Oat.Behaviors.feedbackToasts());

form.add(Oat.Components.submitButton("submit", "Send Message",
        target -> {
            success("Thanks, " + form.getModelObject().name + "! We'll be in touch.");
            target.add(form);
        },
        target -> {
            error("Please fix the errors below.");
            target.add(form); // show the inline errors
        }));
<!-- The label comes from Java, so the markup is just -->
<button wicket:id="submit"></button>
6

Validation and accessibility, for free

Submit with an empty name and the field shows Wicket's validation message right where the hint was, using the label from your properties file. The input gets aria-invalid and is linked to the message with aria-describedby, so screen readers announce it. The toast only says what the inline errors don't: Oat leaves out the field errors already shown inline, so nothing appears twice.

'Your Name' is required.

Put it all together

The finished page

public class ContactPage extends BasePage {

    public static class ContactMessage implements Serializable {
        public String name;
        public String email;
        public String message;
    }

    public ContactPage() {
        // Show success()/error() messages as toasts
        add(Oat.Behaviors.feedbackToasts());

        Form<ContactMessage> form = new Form<>("contactForm",
                new CompoundPropertyModel<>(new ContactMessage()));
        form.setOutputMarkupId(true);
        add(form);

        // Models come from the CompoundPropertyModel, labels from ContactPage.properties - both by id
        form.add(new OatTextField<String>("name").setRequired(true));
        form.add(new OatEmailField("email").setRequired(true));
        form.add(new OatTextArea<String>("message").setRequired(true));

        form.add(Oat.Components.submitButton("submit", "Send Message",
                target -> {
                    success("Thanks, " + form.getModelObject().name + "! We'll be in touch.");
                    target.add(form);
                },
                target -> {
                    error("Please fix the errors below.");
                    target.add(form); // show the inline errors
                }));
    }
}
# ContactPage.properties
name=Your Name
email=Email Address
message=Message
<!-- ContactPage.html -->
<wicket:extend>
    <form wicket:id="contactForm" class="vstack gap-4">
        <div wicket:id="name"></div>
        <div wicket:id="email"></div>
        <div wicket:id="message"></div>
        <footer class="hstack justify-end mt-2">
            <button wicket:id="submit"></button>
        </footer>
    </form>
</wicket:extend>

vstack, hstack, gap-4, justify-end and mt-2 are Oat's own layout utilities, not Tailwind: Wicket Oat uses no CSS library besides Oat. The README lists all of them.

Going further

Where to next

A more advanced form

The examples app's EventRegistrationPage builds on the same pattern with a dropdown, a toggle switch and placeholder text. Clone the repo, run ./mvnw install -DskipTests, then ./mvnw spring-boot:run -pl wicket-oat-examples -Dspring-boot.run.arguments=--spring.docker.compose.file=../compose.yaml (it starts MongoDB with Docker Compose) to see it at http://localhost:8080.

Edit in a dialog

Put the same fields in an OatDialog's body and open it from any Ajax handler with dialog.open(target). Its Confirm button validates them, and the dialog stays open to show any errors.

More field types

Dates, times, numbers, dropdowns, radio groups, checkbox groups, multi-selects, tag inputs and file uploads all follow the same pattern. Prefer factories? Each also has an Oat.Components method, e.g. Oat.Components.textField("name").