A fluent, immutable PHP library for rendering form fields bound to a form model.
Labels, hints, errors, and controls composed through application-scoped configuration.
composer require ui-awesome/html-field:^0.1The field resolves the id, name, and label from the form model and renders a text input by default.
use App\Model\BasicForm;
use UIAwesome\Html\Field\Field;
echo Field::tag()
->formModel(new BasicForm())
->property('username')
->render();Html output:
<div>
<label for="basicform-username">Username</label>
<input id="basicform-username" name="BasicForm[username]" type="text">
</div>Hints link to the input through aria-describedby; property errors render after the control.
The automatic hint association requires nonempty content, a nonempty ID without HTML whitespace, a supported
hint tag configured through hintTag(), and {hint} in the field template. The outer containerTag() does not
control this association. Block and inline hint elements are supported; hintTag(false) renders bare content
without adding a reference. hintId() determines the emitted ID, ahead of hintAttributes().
Existing control description tokens retain their first-occurrence order, with the hint ID appended only when
absent. Explicit field attributes override this derived association, and the model's final field configuration
overrides both. These overrides can replace or remove aria-describedby; the hint ID is not appended again.
use App\Model\BasicForm;
use UIAwesome\Html\Field\Field;
$form = new BasicForm();
$form->addError('username', 'Username is required.');
echo Field::tag()
->formModel($form)
->property('username')
->hintContent('Choose a unique username.')
->render();Html output:
<div>
<label for="basicform-username">Username</label>
<input id="basicform-username" name="BasicForm[username]" type="text" aria-describedby="basicform-username-help">
<div id="basicform-username-help">
Choose a unique username.
</div>
<div>
Username is required.
</div>
</div>Any form control can replace the default input; the form model field config is applied to the replacement.
use App\Model\BasicForm;
use UIAwesome\Html\Field\Field;
use UIAwesome\Html\Form\InputEmail;
echo Field::tag()
->formModel(new BasicForm())
->property('username')
->input(InputEmail::tag())
->containerClass('form-group')
->render();Html output:
<div class="form-group">
<label for="basicform-username">Username</label>
<input id="basicform-username" name="BasicForm[username]" type="email">
</div>Field selects controls through ControlFactory and applies application-scoped recipes to the field, container,
control-specific input container and label, control, hint, error, prefix, and suffix contexts. The package does not
select a theme: replace $theme with any ThemeInterface implementation, such as Bootstrap5Theme or TailwindTheme.
use UIAwesome\Html\Core\Config\Config;
use UIAwesome\Html\Field\Factory\ControlFactory;
use UIAwesome\Html\Field\Field;
$config = new Config(theme: $theme, factory: new ControlFactory());
echo Field::tag()
->config($config)
->control('email')
->formModel($form)
->property('email')
->render();The default registry supports checkbox, checkbox-list, email, password, radio, radio-list, select,
text, and textarea. Registrations are immutable — derive a factory with with() to replace or extend them.
Select and its typed Option objects come from ui-awesome/html: compose the control there and the field passes
the resolved model value to Select::value().
CheckboxList, RadioList, and ChoiceItem also come from ui-awesome/html; Field supplies their checked values,
name, validation state, label, hint, and errors. See the usage guide for examples, slot contexts, and recipe methods.
UIAwesome\Html\Field\Values\FieldComponentName declares every component identifier the field emits with the default
field base context and the default control registry: field, field.container, field.control, field.hint,
field.error, field.prefix, field.suffix, field.label, and field.control.<type>,
field.input-container.<type>, and field.label.<type> for each default control type. Match the backing values in a
theme instead of retyping the strings:
use UIAwesome\Html\Core\Config\{Call, ComponentContext, Cookbook, Recipe};
use UIAwesome\Html\Core\Theme\ThemeInterface;
use UIAwesome\Html\Field\Values\FieldComponentName;
final class AppTheme implements ThemeInterface
{
public function getName(): string
{
return 'app';
}
public function getRecipes(ComponentContext $context): iterable
{
$calls = match ($context->component) {
FieldComponentName::FIELD_CONTAINER->value => [new Call('containerClass', 'mb-3')],
FieldComponentName::FIELD_CONTROL_EMAIL->value => [new Call('class', 'form-control')],
FieldComponentName::FIELD_LABEL->value => [new Call('labelClass', 'form-label')],
default => [],
};
if ($calls !== []) {
yield new Recipe("app.{$context->component}", new Cookbook(...$calls));
}
}
}Identifiers built from a custom base context, such as app.field, or from control types registered through
ControlFactory::with() are not declared and remain plain strings. The installed ui-awesome/html-core release accepts
only strings in ComponentContext, so pass FieldComponentName::FIELD->value rather than the case itself.
Omitting value() keeps model fallback. Calling value(null) explicitly clears the bound value or selection instead
of falling back to the model; checkbox and radio option values remain intact. For controls implementing
ClearableContentInterface, including TextArea, explicit null also clears previously composed content.
Other content implementations keep their existing contract. Non-null content binding retains its append behavior.
Field values are immutable, and the form model's final field configuration still takes precedence.
Form model field configurations are applied through the core config applier in strict mode, once per render, against
the control the field finally resolves — so the fluent call order never changes the outcome. An entry naming a method
the resolved control does not expose throws ConfigException instead of being skipped, so typos such as maxlenght
fail at render time. The model binding runs first and the config last, so entries such as value, id, name,
checked, and placeholder act as explicit per-property overrides rather than being silently overwritten. See the
upgrade guide for the precedence rules and supported entry shapes.
For detailed usage, testing, and quality workflows.