Forms and input

A form is a single call. UiForm::ActionForm renders the fields and runs your handler on submit, all at the same URL. The handler validates the posted data, then redirects with a success or error message, and any validation errors come back attached to the fields that caused them.

One form, one URL, two closures

UiForm::ActionForm takes a form name, a closure that renders the fields, and a closure that runs when the form is submitted. It posts back to the same URL with a query parameter carrying the form name, so on the request that submits it the library runs your action closure; on every other request it renders the fields. That means the form and its handler live side by side in one place, and the form name only has to be unique on the page:

UiForm::ActionForm('shop_product_add', function() {
    UiForm::Text('name', 'Name');
    UiForm::Number('price', 'Price (pence)');
    UiForm::Submit('Add');
}, function() {
    // runs on submit
});

The field methods are the input half of the Ui library: UiForm::Text, UiForm::TextArea, UiForm::Number, UiForm::Dropdown, UiForm::Checkbox, UiForm::Password, UiForm::File and the hidden fields UiForm::Hidden and UiForm::HiddenID. UiForm::Submit ends the form.

Validate the posted data

In the action closure, Status::Validate checks the posted values against a map of field name to validation rule. It returns the cleaned values in order, so you destructure them straight into variables. The rules are ready-made: Status::ValidateString (with optional minimum and maximum length), Status::ValidateInteger and Status::ValidateFloat (with optional bounds), Status::ValidateBoolean, Status::ValidateEmail and Status::ValidatePhoneNumber:

function() {
    [$name, $price] = Status::Validate($_POST, null, [
        'name'  => Status::ValidateString(1),
        'price' => Status::ValidateInteger(0),
    ]);

    $productId = Shop::AddProduct($name, $price);
    Status::Success('/shop/products', 'Product added', [$productId]);
}

The middle argument is the URL to return to if validation fails; passing null means the current page. If any rule reports an error, Status::Validate stops the request there and redirects back with the errors, so the code after it only runs when every field was valid. That keeps the handler on a single happy path.

Redirect with a message

A handler ends by redirecting, never by rendering a page itself. On success, Status::Success redirects to a URL and carries a message; on a handled failure, Status::Error does the same with an error message. Both encode the message into the target URL and stop the request, which is the standard post-then-redirect pattern that stops a refresh from re-submitting the form. The message is signed, so it cannot be forged by editing the URL by hand.

Errors come back on the fields

When Status::Validate redirects on failure, it carries both the message for each field and the value the visitor typed. The field methods read those back automatically: a field with an error renders with its error text beside it, and it refills with the previous value so the visitor does not lose their input. Under the hood the methods call Status::GetValidationError and Status::GetValidationPreviousValue, but you do not call those yourself, using the standard field methods is enough.

To show the overall success or error banner at the top of the page, place StatusUi::Message near the top of the page body. It renders the message from the redirect once per page.

A button that runs an action

For an action that needs no fields, such as a delete or a toggle, UiForm::ButtonAction renders a single button whose closure runs on click. It is a one-field action form under the hood, so the same validate-then-redirect handler style applies:

UiForm::ButtonAction('Delete', function() {
    Shop::DeleteProduct($productId);
    Status::Success('/shop/products', 'Product deleted');
});

Next: Routing.