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'sfieldintable.->in($allowedValues)— the value (or each item, for arrays) must appear in the given in-memory allow-list. No DB query. Use this to guardselect/multi-selectfields against tampered POST payloads where the client sent an option that wasn't in the rendered dropdown. Works on plainselect(single-value) andmulti-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.valueis a boolean (the checked state). Assigning acceptstrue/false,1/0,"1"/"0","true"/"false", or"on".- On submit the box always sends its state —
1when ticked,0when not. (Unlike a native HTML checkbox it is never omitted.) This meansupdateDatabase/insertDatabasewrite a clean0/1to 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.valuereturns 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 — nojson_decodeneeded 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/updateDatabaseautomaticallyjson_encodearray field values before binding, so aJSON(orTEXT/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_encodedumped 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.