HTML Forms + Sage Integration
Stateless form rendering via the HTML Forms WordPress plugin, bridged into Sage by log1x/sage-html-forms. Forms live as a CPT (html-form), are referenced from ACF blocks via addPostObject, and render through a Blade filter that routes to project-controlled form views.
When to Use
| Approach | Best for |
|---|---|
| HTML Forms + sage-html-forms | Stateless contact/lead forms; editors pick the form from a dropdown; submissions handled by the HF plugin |
| Livewire form | Reactive state, multi-step wizards, inline validation tied to server state |
Blade + native <form> |
Single-field or trivial forms with no submission pipeline |
When NOT to Use
- Form requires multi-step state → use Livewire (
acorn-livewireskill) - Form payload drives a Laravel controller action → Acorn Routes + Blade form, not HTML Forms
- Form must be embedded outside a block context (e.g. header newsletter) → Blade component + native
<form>is simpler
Prerequisites
- Sage 11+ / Acorn 4+ on Bedrock with Lando
- ACF Composer installed (for the block field declaration)
- Shared
x-form.*components present inresources/views/components/form/(the design-system form primitives)
Installation Summary
lando composer require wpackagist-plugin/html-forms
lando theme-composer require log1x/sage-html-forms
Acorn's package discovery auto-registers the service provider — no manual wiring. Full details: references/installation.md.
Integration Pattern Summary
The form is a html-form CPT post. A block exposes an ACF addPostObject field scoped to that CPT so the editor picks the form. The block view renders it via hf_get_form($form->ID)->get_html(), which the sage-html-forms provider intercepts and routes to a Blade view at resources/views/forms/{form-slug}.blade.php. The Blade form view uses the project's x-form.* components.
{{-- Block view (snippet) --}}
@if ($form)
{!! hf_get_form($form->ID)->get_html() !!}
@endif
{{-- resources/views/forms/{form-slug}.blade.php --}}
<x-html-forms :form="$form">
<x-form.field label="Name" for="name" :required="true">
<x-form.input type="text" name="name" :required="true" minlength="2" />
</x-form.field>
{{-- ...more fields... --}}
<x-button type="submit" variant="primary" class="w-full">Send</x-button>
</x-html-forms>
Full walkthrough: references/blade-form-views.md.
Validation Module Summary
Client-side validation is a reusable ES module imported by the block's JS — never globally enqueued. The module exposes one function:
initHfValidation(formEl, { messages, validators, onSuccess, onError });
Four layers: native HTML5 constraints, blur validation, input lazy re-validation while aria-invalid="true", and post-submit scroll via the HTML Forms plugin's DOM events (hf-success, hf-error, hf-submitted).
Full API and implementation: references/hf-validation.md.
Traps (Critical)
Three documented bugs that silently break forms in this stack. Full symptom/root cause/fix for each: references/traps.md.
- T1 —
patternattribute backslash escaping in Blade components.$attributes->merge()double-escapes backslashes;patternMismatchnever fires. Symptom: form submits even thoughpatternregex doesn't match user input. Workaround: use a JS validator, not apatternattribute. - T2 —
type="tel"skipspatternMismatchin Chrome. Usetype="text" inputmode="tel"instead. Symptom: phone format validator silently no-ops in Chrome (works in Firefox/Safari). - T3 —
ValidityStateis non-enumerable. Spread ({ ...field.validity }) andObject.keys()return empty. Symptom: your error-message lookup object is always empty. Access named properties directly:validity.valueMissing,validity.patternMismatch,validity.tooShort,validity.customError, etc.
File Map (canonical per form integration)
| File | Role |
|---|---|
app/Blocks/{ClassName}.php |
ACF block — addPostObject field + with() mapping |
resources/views/blocks/{slug}.blade.php |
Block view — calls hf_get_form($form->ID)->get_html() |
resources/views/forms/{form-slug}.blade.php |
Form view — x-html-forms + x-form.* structure |
resources/js/blocks/{slug}.js |
Block custom element — imports initHfValidation, configures per form |
resources/js/modules/hf-validation.js |
Reusable validation module (one per project, not per form) |
Consumers
agents/forms.md(user-invocable specialist — analyzes and refactors existing forms, or scaffolds new ones)skills/block-scaffolding/SKILL.mdPhase 0c (coordinated scaffold when a block embeds a form)
Both consumers treat this skill as authoritative — no pattern, template, or trap is documented outside this skill.