Skip to content

[6.x] Forms 2: Unique Instances - #15255

Open
duncanmcclean wants to merge 30 commits into
forms-2-connectionsfrom
forms-2-unique-instances
Open

[6.x] Forms 2: Unique Instances#15255
duncanmcclean wants to merge 30 commits into
forms-2-connectionsfrom
forms-2-unique-instances

Conversation

@duncanmcclean

@duncanmcclean duncanmcclean commented Aug 24, 2026

Copy link
Copy Markdown
Member

This pull request implements "unique instances" for forms: a Forms Pro feature which lets a form be treated as its own instance per entry, with submissions tracked against the entry they were submitted from, rather than in aggregate.

The classic use case is an Events collection where every event has its own RSVP form — each event gets its own submissions, its own capacity and its own close date, all from a single rsvp form.

Like multi-page forms, everything lives in core but is only available when the statamic/forms-pro addon is installed.

Enabling unique instances

Toggle on "Unique Instances" in the form's configure screen, then attach the form to entries using the form fieldtype.

CleanShot 2026-08-24 at 21 43 53

The frontend

When {{ form:create }} is rendered in an entry's context, it outputs a hidden _entry input, and the submission is tracked against that entry.

Per-entry restrictions & overrides

When unique instances is enabled, the form's Access settings are evaluated per entry:

  • Submission limits count only the entry's submissions (eg. "100 spots per event").
  • Each entry can override the Access settings (close date, submission limit & period, closed message, require login & message) via the form fieldtype's "Configure" option. Fields are synced with the form's settings by default — editing a field desyncs it and stores an override, while synced fields keep following the form.

Connections are overridable in the same way, so each entry can have its own notifications. A warning banner is visible when configuring connections on a form with "unique instances" enabled.

CleanShot.2026-08-24.at.21.42.58.mp4

In the Control Panel

The submissions listing shows which entry each submission came from, and can be filtered down to a single entry.

On the entry's publish form, the form fieldtype has a "View Submissions" option which opens the entry's submissions in a stack, with individual submissions opening in a nested stack. The fieldtype's index view links to the pre-filtered submissions listing, and the submission page links back to the entry.

CleanShot 2026-08-24 at 21 12 42

PHP API

The form fieldtype's value is stored as a plain handle until an entry has overrides, at which point it becomes an array with form and config keys.

Restriction logic lives on a new Instance class, since "is this form open?" becomes a per-entry question when unique instances is enabled:

$form->instance($entryId)->status(); // open, closed, limit_reached
$form->instance($entryId)->restricted();
$form->instance($entryId)->restrictionMessage();
$form->instance($entryId)->config('submission_limit'); // entry override, falling back to the form
$form->instance($entryId)->connections(); // entry override, falling back to the form's own connections

// Every form has a "default" instance, which these delegate to:
$form->status();
$form->restricted();
$form->restrictionMessage();

duncanmcclean and others added 23 commits August 18, 2026 15:38
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWzPmQoku8u1dA6VMp51RS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWzPmQoku8u1dA6VMp51RS
The configurable value shape is a non-empty array even when no form is
selected, so `required` and `max_items` rules never failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhgpB3C65heLNJs7YCSZqV
Localizations inheriting the form field from their origin were getting
no overrides, since `get()` only reads local data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhgpB3C65heLNJs7YCSZqV
The `_entry` input is user-editable, so submissions could be attached to
any entry at all. Requiring the entry to reference the form keeps
tampering equivalent to submitting from that entry's own page.

Also translates the validation message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhgpB3C65heLNJs7YCSZqV
`pluck` reads from the Stache's indexed values (or a plain select on
eloquent), so building the options no longer hydrates every submission.
The badge now looks up a single entry instead of the whole options map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhgpB3C65heLNJs7YCSZqV
`FormFieldValues::on($entry)` walks the entry's blueprint and values, so
entry overrides and submission validation find form fields inside nested
fieldtypes rather than only at the top level.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhgpB3C65heLNJs7YCSZqV
`Form::instance()` drops the entry unless the form has unique instances,
which also covers the tag's hidden `_entry` input. The submission page
only exposes the attached entry under the same conditions, matching the
listing column and filter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhgpB3C65heLNJs7YCSZqV
The configure stack shows the form's own values as synced origin values
and tracks which fields have been deliberately desynced, so a toggle can
be overridden off and only desynced fields are stored. This replaces the
placeholder approach, and lets `required` and `max_items` validate
against the actual form handles.

The override value shape now also requires forms pro, which reaches the
Vue component through preload meta.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhgpB3C65heLNJs7YCSZqV
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWzPmQoku8u1dA6VMp51RS
@duncanmcclean
duncanmcclean marked this pull request as draft August 24, 2026 22:27
adds a `form_connections` fieldtype (used internally by the `form`
fieldtype's Configure stack) alongside the existing Access override
section, so a `Connections` field lets an entry override a form's
connections independently of the form's own configuration, following the
same origin/sync pattern already used for Access overrides.

`Instance::connections()` resolves the entry's override when present,
falling back to the form's own connections otherwise - no merging within a
connection type, the entry's config for a type fully replaces the form's.
`Submission::finalize()` now resolves connections through the form's
instance instead of reading the form's connections directly.

extracted `ConnectionsListing` and `ConnectionEditor` out of the connect
pages so the new field's Connections stack can reuse them for browsing and
editing a connection - editing goes through `FormConnectController::update`
with `?_save=false` so it's validated and processed the same way as saving,
without persisting to the form.

also adds a warning alert to the Connect pages when a form has unique
instances enabled, since connections configured there may be overridden
per-entry.
@duncanmcclean
duncanmcclean marked this pull request as ready for review August 31, 2026 12:12
@duncanmcclean
duncanmcclean changed the base branch from forms-2 to forms-2-connections August 31, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant