Skip to content

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

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

duncanmcclean wants to merge 254 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 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();

Depends on:

…need to use antlers

Using Antlers in email address fields is still supported, but this is a slightly easier approach for end-users.
jasonvarga and others added 28 commits October 15, 2026 22:11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form fieldtype goes back to its forms-2 version.

The View Submissions button only appeared in certain ui modes. To support it in others, it would require even more changes. This PR is already big enough. Now that there's a top level form instances field, there's already a simple way to view submissions. We can bring back the button in a future PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Submissions, limits, partials, overrides and the CP's View Submissions filters now resolve to the root entry, so every localization of an entry shares one instance. The submission's _site still records where it came from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It only works inside the form_instances fieldtype, so name it and its injection key accordingly.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lost when FormFieldtype.test.js was deleted. Covers the submissionsMeta guard in the form_instances fieldtype.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The field holds per-entry form configuration rather than content, so it augments to null in templates and API responses. Augmenting it to forms listed stored handles the runtime no longer honours, and the shape (forms vs instances) can be decided later without a breaking change. The CP listing column still lists the stored forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The entry's Configure → Connections editor was preloaded with the form's connector components, so entry-only connections got no meta and overridden ones got the form's. Meta-dependent fields like Grid lost their row state. Opening a connector now posts the field's current value to a new editor endpoint, which renders the component and meta from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The entry lookup and localization now only run when a template or email actually uses `entry`, so looping over submissions doesn't pay for it otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening an entry serialized the submission entry filter for every form its collection offers, plucking _entry from all of each form's submissions and loading every matching entry, three times per form since toArray() builds the fields thrice. When the context provides an entry, the filter now only offers that entry, and its options are computed once per instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Malformed bodies caused a 500 from a TypeError in process(). Only id, enabled and conditions are checked, so stored connections that fail the connector's own rules can still be opened and fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entry saves stored per-entry overrides without running any rules, so a crafted request could bypass connector validation (e.g. a namespaced email view) and the Access field rules. The Forms field now validates posted overrides for forms the user can configure, using the override blueprint's field rules and each connector's rules(), and reports a single error on the field. Stored config for forms the user can't edit is still kept without validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applying a connector read the editing handle when the validate response arrived, so closing the editor mid-request wrote the result under a null key and silently dropped it, and opening another connector meanwhile overwrote that connector's connections and showed the old request's errors on it. The editor now can't be closed while Apply is in flight, and Apply captures its handle and request so a response from a closed or replaced editor is ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stored overrides are re-posted on every entry save, so one that drifted out of validity (a renamed form field, an unresolvable webhook host, a changed email view folder) blocked unrelated saves. Only overrides that differ from what's stored are validated now, and connections are compared per connection id so a drifted row doesn't block edits to its siblings. Localizations skip override validation entirely since they never store the field. An invalid date override's error now lands on the form instances field instead of a bare close_date key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…editor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jasonvarga

Copy link
Copy Markdown
Member

Storing per-entry connection config overrides is getting complicated. Going to revisit after forms 2 releases.

@jasonvarga
jasonvarga marked this pull request as draft October 21, 2026 16:30

This branch has not been deployed

No deployments
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.

3 participants