The FormField Component System

Introduction

Every form-control render (the admin model edit form, the module settings page, and public FormBuilder contact forms) used to be its own hand-rolled if/elseif chain of raw HTML, each with its own slightly different value-casting logic. Zero\Support\Forms unifies all three behind one small set of PHP classes — TextInput, NumberInput, Select, Checkbox, Textarea, and others — each of which both renders itself (via Template::renderFile() against a template in src/Views/components/forms/) and owns its own submitted-value casting, so every caller gets identical, correct behavior for free instead of re-implementing it.

The FormField Contract

The FormField Contract php
namespace Zero\Interfaces;

interface FormField
{
    public function __construct(string $name, array $config);

    // Reads its own $name key out of $source ($_POST, or a decoded JSON payload) and returns
    // the correctly-typed PHP value -- e.g. a checkbox's absence means false, a number is
    // clamped to its configured min/max, a select is validated against its options allow-list.
    public function castSubmittedValue(array $source);

    public function render(): string;
}

Rendering a Field in a View

Rendering a Field in a View php
echo App::makeFormField('number', 'posts_per_page', [
    'label' => 'Posts Per Page',
    'value' => $currentValue,
    'min' => 1,
    'max' => 100,
    'required' => true,
])->render();

Casting on Submission

A controller collecting POST data does the same thing regardless of which model, module, or form it's handling:

$field = App::makeFormField($fieldConfig['type'], $fieldName, $fieldConfig);
$value = $field->castSubmittedValue($_POST);

This is exactly what ModelController, ModuleSettingsController, and FormBuilder's FormApiController each do internally — one shared implementation instead of three that could quietly drift apart.

Registering a Custom Field Type

The type registry follows the same App::registerX() pattern used by blocks and models. Core types are registered once during bootstrap; a module can register its own additional type from inside its own Module::init():

App::registerFormFieldType('sku_lookup', \Zero\Modules\Shop\Support\Forms\SkuLookupField::class);

Any class implementing Zero\Interfaces\FormField works — see Per-Module Site Settings for the array-config schema shape a field's constructor receives.