Sitelet https://github.com/statamic/cms/pull/15255
Skip to content

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

Draft
duncanmcclean wants to merge 23 commits into
forms-2from
forms-2-unique-instances
Draft

[6.x] Forms 2: Unique Instances#15255
duncanmcclean wants to merge 23 commits into
forms-2from
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 (#15063) will be overridable per entry in the same way, so each entry can have its own notifications.

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

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

Depends on: #15063

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
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