Skip to content

Auto form validation and database updates

This framework has the ability to create automatic generated forms with user feedback. These forms can be validated automatically on the server and if errors occur the feedback is sent back to the user. If no errors occur the data can be used to update a database table.

Front-end

To create the form on the frontend, use the Z.js library. An example form can be created with this code:

var form = Z.Forms.create({dom: "form"});

var inputFirstName = form.createField({
    name: "first_name",
    type: "text",
    text: "First name",
    required: true
});

var inputLastName = form.createField({
    name: "last_name",
    type: "text",
    text: "Last name",
    required: true
});

Creating a form

The form is created with Z.Forms.create. The dom attribute takes the id of an html element in which the form in embedded. For this to work the element needs to be loaded so this script should not execute directly at the start of the page. If the element will spawns later the form can be added to it with the dom attribute element.appendChild(form.dom);.

Creating fields

Attribute Description
name Corresponds to the name in the post request.
type The input type. Any valid HTML standard type is accepted, as well as textarea, select, multi-select, and autocomplete. This attribute does not affect server-side value parsing.
text Label text for the input field.
value Default value for the input field. For multi-select, pass an array (e.g. ["1","4"]) or <?= json_encode([...]) ?>.
food Required for select and multi-select types. It defines the options available and is formatted as an array: [{value: 1, text: "one"}, {value: 2, text: "two"}, ...]. This array can be generated via $controller->makeFood.
required Specifies whether the field is required. When set to true, the input must be filled before form submission.
width Defines the width of the form element in 1/12 units of the total width. Effective on medium or larger devices. On small devices, the width is always 100%.
attributes Allows adding additional attributes for the generated input element (e.g., min, max for number inputs). Example usage: attributes: {'min': 1, 'max': 10}.
prepend Adds a visual element before the input field. This can be used for prefixes, labels, or other indicators. Example usage: prepend: 'Prefix' creates an input field with a preceding text or symbol.
disabled Starts the field disabled. The input is greyed out and cannot be edited until field.enable() is called.
hidden Starts the field hidden. It is left out of the layout until field.show() is called. The field still exists and submits its value.

Simple input example

var inputAmount = $form.createElement({
    name: "amount",
    type: "number",
    attributes: {
        min: 0,
        max: 100,
        step: 10
    }
});

Manipulating fields

The return value if form.createField is the created field. It has an attribute called input which can be used to access the dom input element directly. It also has on which is an alias for addEventListener on the input dom element. The value can be read and set with value.

form.addCustomHTML(). With this you can add Html inside of the form.

form.addSeperator(). This inserts a simple <hr> element at the end of the current builded form.

Disabling fields

A field can be disabled and re-enabled at runtime. A disabled field is greyed out and cannot be edited, but still submits its value.

field.disable();        // disable a single field
field.enable();         // re-enable it
field.isDisabled();     // true while disabled

form.disable();         // disable the whole form (all fields + submit button)
form.enable();          // re-enable it

form.disable() and a field's own field.disable() are independent: re-enabling the form does not re-enable a field that was disabled on its own. While a form is submitting it disables itself automatically and restores the previous state when the request finishes.

Showing and hiding fields

Fields can be removed from and added back to the layout at runtime. A hidden field still exists and submits its value — it is just not rendered.

field.hide();           // remove from the layout
field.show();           // add it back in its original position
field.isHidden();       // true while hidden

Custom HTML (addCustomHTML) and separators (addSeperator) keep their place when fields are shown or hidden. Do not mix show() / hide() with manual DOM manipulation of a field's wrapper (e.g. $(field.dom).parent().hide()) — the two will fight over the layout.

The built-in submit button can be hidden too — useful for live forms that have no submit:

form.hideSubmit();
form.showSubmit();

Reading and writing all values

form.getValues();                       // { fieldName: value, ... }
form.setValues({ first_name: "Ada" });  // set the named fields
form.setValues(data, { resetUnknown: true }); // reset fields not present in data first

CED fields are skipped by getValues / setValues; their data round-trips through the normal submit instead.

Keys passed to setValues that don't match any field are kept on form.meta and returned by getValues, but are never submitted to the backend. This lets a value like an id ride along with the form data:

form.setValues({ first_name: "Ada", id: 42 }); // id has no field -> stored as meta
form.getValues();                               // { first_name: "Ada", id: 42 }
form.meta;                                      // { id: 42 }   (inspectable)

Live / client-only forms

With collectOnly: true the form never posts to the backend. Clicking the submit button hands the collected values (getValues(), meta included) straight to saveHook instead. Combined with inputHook — called with getValues() on every change — the form becomes a live value source you can wire into the page without a round-trip.

var form = Z.Forms.create({
    dom: "form",
    collectOnly: true,                  // submit -> saveHook(getValues()), no POST
    saveHook: (values) => { /* values incl. meta */ },
    inputHook: (values) => { /* fires on every keystroke / select change */ },
});

Back-end

When the form is submitted, it will send an asynchronous post request to the current action specified by the current users url. To check in the action if the current request is from a form, $req->hasFormData() can be used. This is example code for handling a form:

Backend validation

if ($req->hasFormData()) {
    $formResult = $req->validateForm([
        (new FormField("first_name"))
            ->required()->length(1, 255),
        (new FormField("last_name"))
            ->required()->length(1, 255)
    ]);

    if ($formResult->hasErrors) {
        return $res->formErrors($formResult->errors);
    }
}

Validation structure

$req->hasFormData() checks if there is any data in the request.

$req->validateForm() validates the values. As the first parameter it takes an array of fields to validate. To these field, rules can be attached.

$formResult->hasErrors returns true or false depended on the validation result of $req->validateForm(). If the validation fails, $res->formErrors($formResult->errors) will return the errors to the frontend, where they will be displayed.

Rules on array values (multi-select)

A multi-select field's value arrives at the server as a real PHP array. Three existing rules are list-aware — they adapt automatically when the field value is an array — and one new rule (->in()) covers the in-memory allow-list case:

  • ->length($min, $max)strlen() for scalars; count() for arrays. So ->length(1, 3) on a multi-select means "between 1 and 3 selections."
  • ->regex($pattern, $exceptions) — runs the regex against each item; the field fails as soon as any item fails.
  • ->exists("table", "field") — applied per item: every picked entry must exist as a row's field in table.
  • ->in($allowedValues) — the value (or each item, for arrays) must appear in the given in-memory allow-list. No DB query. Use this to guard select / multi-select fields against tampered POST payloads where the client sent an option that wasn't in the rendered dropdown. Works on plain select (single-value) and multi-select (per-item).
$formResult = $req->validateForm([
    (new FormField("skills"))
        ->required()
        ->length(1, 5)
        ->in($availableSkillIds),   // every picked id must be in the rendered list
]);

Saving functions

Success will exit the current action. So before calling it the data should be processed by a model or $res->updateDatabase(). Update database will take the result object from the form validation to get the names in the database. If the name in the database column and the post differ, the database name can be set by the second parameter of the constructor of FormField.

$res->insertDatabase() can also be used to process the data. This method will create a dataset in a table given as argument. It works similar to $res->updateDatabase().

insertOrUpdateDatabase Example

$req->insertOrUpdateDatabase() Adds a logic to check if the dataset already exists.

public function action_manage(Request $req, Response $res) {
    $req->checkPermission("employee.edit");

    // Get the employeeid from the URL
    $employeeId = $req->getParameters(0, 1);
    $employee = null;

    if(!empty($employeeId)) {
        $employee = $req->getModel("Employee")->getById($employeeId);
    }

    if($req->hasFormData()) {
        $formResult = $req->validateForm([
            (new FormField("first_name"))
                ->required()->length(1, 255),
            (new FormField("last_name"))
                ->required()->length(1, 255),
            (new FormField("contact_email"))
                ->required()->filter(FILTER_VALIDATE_EMAIL)->length(1, 255),
            (new FormField("birthday"))
                ->date(),
            (new FormField("notes")),
            (new FormField("type"))
        ]);

        if ($formResult->hasErrors) {
            return $res->formErrors($formResult->errors);
        }

        // Insert the Employee with their datas or updates them if exist
        $employeeId = $res->insertOrUpdateDatabase(
            "employee",
            "id", "i", $employee["id"] ?? null,
            $formResult,
        );

        // Send a response with the inserted/updated EmployeeId
        return $res->success([
            "employeeId" => $employeeId,
        ]);
    }

    return $res->render("employee/employee_edit.php", [
        "employee" => $employee,
        "types" => $this->makeFood(
            $req->getModel("Employee")->getTypes(),
            "id", "label",
        ),
    ]);
}

Example advanced layout

<div id="form"></div>

<script>
    var form = Z.Forms.create({
        dom: "form"
    });

    form.addCustomHTML("<h2>These Fields are required</h2>");

    form.createField({
        name: "first_name",
        type: "text",
        text: "First name",
        required: true,
        width: 6,
        value: <?= json_encode($opt["employee"]["first_name"] ?? "") ?>,
    });

    form.createField({
        name: "last_name",
        type: "text",
        text: "Last name",
        required: true,
        width: 6,
        value: <?= json_encode($opt["employee"]["last_name"] ?? "") ?>,
    });

    form.createField({
        name: "contact_email",
        type: "email",
        text: "Email Address",
        required: true,
        width: 6,
        value: <?= json_encode($opt["employee"]["contact_email"] ?? "") ?>,
    });

    form.createField({
        name: "type",
        type: "select",
        text: "Type",
        food: "<?= $opt['types'] ?>"
    });

    form.addSeperator();
    form.addCustomHTML("<h2>These Fields are optional</h2>");

    form.createField({
        name: "birthday",
        type: "date",
        text: "Geburtstag",
        width: 6,
        value: <?= json_encode($opt["employee"]["birthday"] ?? "") ?>
    });

    form.createField({
        name: "notes",
        type: "textarea",
        text: "Notizen",
        attributes: {
            rows: 5,
        }
    });

    form.saveHook = (res) => {
        location.href = "<?= $opt["root"] ?>employee/manage/" + res.employeeId;
    };

    $(form.buttonSubmit).html("Send");
</script>

Supported types:

  • All default HTML types
  • Additionally supported::
    • textarea
    • select
    • multi-select
    • autocomplete
  • Specially rendered:
    • checkbox

Checkbox

The checkbox type renders with a Bootstrap form-check layout: the box sits to the left of its label, and clicking the label toggles the box. text becomes the label.

form.createField({
    name: "send_notifications",
    type: "checkbox",
    text: "Send me notifications",
    default: true,         // optional; start checked
});

Value semantics differ from text inputs:

  • field.value is a boolean (the checked state). Assigning accepts true/false, 1/0, "1"/"0", "true"/"false", or "on".
  • On submit the box always sends its state — 1 when ticked, 0 when not. (Unlike a native HTML checkbox it is never omitted.) This means updateDatabase / insertDatabase write a clean 0/1 to a binary column, so toggling a setting off persists just like toggling it on.

Because a checkbox always submits a value, ->required() is effectively a no-op on it. To enforce that a box must be ticked (e.g. accept-terms), use ->checked():

$formResult = $req->validateForm([
    (new FormField("accept_terms"))
        ->checked(),   // errors unless the box is ticked
]);

->checked() is sugar over ->in(["1", "true", "on"]) — the box's value must be a ticked representation.

Autocomplete

The autocomplete type creates a text input with an additional feature: it displays suggestions based on predefined data as the user types.

Example

<div id="form"></div>
<script>
    var form = Z.Forms.create({
        dom: "form"
    });

    form.createField({
        name: 'favoriteFruit',
        type: 'autocomplete',
        autocompleteData: ['Apple', 'Banana', 'Cherry', 'Dragonfruit'],
    });
</script>

Advanced Example

public function action_favourite(Request $req, Response $res) {
    $value = $req->getPost("value");

    return $res->generateRest([
        "data" => $req->getModel("Fruits")->getByValue($value)
    ]);
}
// View
<div id="form"></div>
<script>
    var form = Z.Forms.create({
        dom: "form"
    });

    let placeSearch = form.createField({
        name: "favouriteFruits",
        type: "autocomplete",
        autocompleteData: "fruits/favourite",   // The Endpoint you're trying to get your data from
        autocompleteMinCharacters: 2,           // The number of characters you need to type in to get the first autocomplete data
        autocompleteTextCB: (text, value) => {  // This will be called when autocomplete data is found
            let json = JSON.parse(value);
            return json.text;
        },

        autocompleteCB: (text) => {             // This will be called when you click on the word suggestion
            let json = JSON.parse(text);
            placeSearch.input.value = json.text;
        },
    });
</script>

Select

The select type generates a dropdown menu and automatically populates it with options.
This feature leverages a function called makeFood, which is used in the controllers.

Example

// Controller
return $res->render("employee/employee_edit.php", [
    "types" => $this->makeFood(
        $req->getModel("Employee")->getTypes(),
        "id", "label",
    ),
]);
// View
<div id="form"></div>
<script>
    var form = Z.Forms.create({
        dom: "form"
    });

    // Manually written food
    form.createField({
        name: 'favoriteFruit',
        type: 'select',
        food: [
            {value: "1", text: "Apple"},
            {value: "2", text: "Banana"},
            {value: "3", text: "Cherry"},
            {value: "4", text: "Dragonfruit"}
        ],
    });

    // Auto generated food
    form.createField({
        name: 'types',
        type: 'select',
        food: <?= $opt["types"] ?>,
    });

    // Advanced Example with optgroups
    form.createField({
        name: 'favoriteFruitVegetables',
        type: 'select',
        food: [
            {type: "optgroup", text: "Vegetables"},
            {value: "1", text: "Carrot"},
            {value: "2", text: "Broccoli"},
            {value: "3", text: "Spinach"},
            {type: "optgroup", text: "Fruits"},
            {value: "4", text: "Apple"},
            {value: "5", text: "Banana"},
            {value: "6", text: "Orange"},
        ],
    });
</script>

Multi Select

The multi-select type behaves like select but lets the user pick more than one value. Picked entries appear as removable badges under the dropdown and the picked option is hidden from the dropdown until the badge is removed. Uses the same food shape (and same makeFood helper) as select.

Value semantics

  • field.value returns the picked entries as a JavaScript array of values.
  • Assigning field.value = ["a", "b"] replaces the selection. Non-array input is treated as an empty selection.
  • On submit, each pick is sent as a native PHP array entry (name[]=a&name[]=b), so $_POST[name] arrives as a real PHP array — no json_decode needed in user code. An empty multi-select is omitted from the POST entirely — just like an unchecked checkbox — so the existing ->required() rule (isset($_POST[$name])) catches it without any special-casing.
  • insertDatabase / updateDatabase automatically json_encode array field values before binding, so a JSON (or TEXT/VARCHAR) column receives a real JSON string. User code never has to encode manually.
  • Pre-fill from PHP with either an inline JS literal or json_encode dumped directly (no surrounding quotes):
form.createField({
    name: "skills",
    type: "multi-select",
    food: <?= $opt["skillsFood"] ?>,
    value: <?= json_encode($opt["employee"]["skills"] ?? []) ?>,
});

Food shapes

  • {value, text} — value is what gets posted, text is what's shown in the dropdown and the badge.
  • {value} alone — value is also used as the label.
  • {text} alone — text doubles as the value.
  • {type: "optgroup", text} — groups the options that follow (until the next optgroup) under an <optgroup> label. Optgroups are collapsed automatically when every option below them has been picked, and come back when one is removed.

Example

// Controller
return $res->render("project/manage.php", [
    "skills" => $this->makeFood(
        $req->getModel("Skill")->getAll(),
        "id", "name",
    ),
]);
// View
<div id="form"></div>
<script>
    var form = Z.Forms.create({ dom: "form" });

    form.createField({
        name: "skills",
        type: "multi-select",
        text: "Skills",
        placeholder: "Add a skill...",   // replaces the default "---"
        food: <?= $opt["skills"] ?>,
        required: true,                  // empty selection fails ->required()
        default: ["1"],                  // pre-fill; reset() returns to this
    });
</script>

Backend

$formResult = $req->validateForm([
    (new FormField("skills", "skills_json"))
        ->required(),
]);

if ($formResult->hasErrors) {
    return $res->formErrors($formResult->errors);
}

$res->insertDatabase("project", $formResult);

$req->getPost("skills") and $formResult->fields[0]->value both come back as real PHP arrays (e.g. ["1", "4"]). insertDatabase then auto-json_encodes the array before binding, so the column stores a JSON string (["1","4"]) — a JSON, TEXT, or VARCHAR column all work. Nothing in user code needs to special-case the multi-select.