Validation in the browser

The rules a form's fields carry are checked on the server, after the post. With client on the {form} the browser checks them too, as the visitor types and before the form leaves - with the same rules and the same messages, so the two never disagree. The server still checks every post: the browser's check only saves a round trip.

PHP
fragments/client_validation_1.php
HTML
fragments/client_validation_1.pad
<?php

// The server checks every post against the same rules - the browser's check only saves
// the visitor a round trip.

if ( padPosted ( 'signup' ) ) {

padFlash ( 'Welcome aboard.' );

padRedirect ();

}

?>
{form 'signup', client, error}
{input 'name', label='Name', rules='required|max:40'}
{input 'email', type='email', label='E-mail', rules='required|email'}
{input 'age', label='Age', rules='integer|min:18'}
<button>Sign up</button>
{/form}

Forms a component draws

A React, Vue or Alpine form is no {form} of the template. Keep the rules in the page's PHP instead - one list for both sides: padValidate ( $rules ) checks the post, and padValidateClient ( $rules ) exports the same list for the browser, every rule with the message padValidate would give:

PHP
fragments/client_validation_2.php
HTML
fragments/client_validation_2.pad
Result
<?php

// One list of rules for both sides: padValidate checks the post with it, and the
// component that draws the form gets it as padValidateClient exports it.

$rules = [ 'email' => 'required|email', 'qty' => 'integer|min:1|max:10' ];

$client = json_encode ( padValidateClient ( $rules ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );

?>
{$client}
{
    "email": {
        "label": "Email",
        "number": false,
        "list": false,
        "single": ":label must be a single value",
        "rules": [
            [
                "required",
                "",
                ":label is required",
                ""
            ],
            [
                "email",
                "",
                ":label must be a valid e-mail address",
                ""
            ]
        ]
    },
    "qty": {
        "label": "Qty",
        "number": true,
        "list": false,
        "single": ":label must be a single value",
        "rules": [
            [
                "integer",
                "",
                ":label must be a whole number",
                ""
            ],
            [
                "min",
                "1",
                ":label must be at least :n",
                "1"
            ],
            [
                "max",
                "10",
                ":label must be at most :n",
                "10"
            ]
        ]
    }
}

{validator} in the template brings the checker without a form, and the component asks it:

<div x-data="{ rules: {^rules}, form: {}, errors: {} }">
  <input x-model="form.email" @blur="errors = padValidate.check(rules, form)">
  <span class="error" x-text="errors.email"></span>
</div>
{validator}

padValidate.check ( rules, values ) answers the message of every field that broke a rule; padValidate.field ( rules.email, value, values ) one field's, empty when it holds. A field named tags[] takes a list.