Skip to content

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

Open
duncanmcclean wants to merge 216 commits into
forms-2from
forms-2-unique-instances
Open

duncanmcclean wants to merge 216 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:

duncanmcclean and others added 30 commits July 21, 2026 12:33
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…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 11 commits October 7, 2026 13:01
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A connector is the integration (Email, Webhook, Log…) and a connection is one configured instance of a connector on a form. The base class, repository, facade, addon/extension registration, CP controller props and route params, Vue components and public JS exports now use that vocabulary, and "row" naming in the base-class hooks and the list components is replaced with "connection".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connector gains a public blueprint() method. When it returns a blueprint, the base class pre-processes and processes each connection through its fields and merges the top-level fields' validation rules into rules(). connectionFields(), connectionMeta() and blueprintProps() let connectors adjust values and meta per connection and build the blueprint, meta and defaults props for their Vue component. Email and Webhook use the new hooks instead of hand-building them.

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

ConnectionList now accepts the connector's blueprint and meta. Without a default slot it renders the logic builder and the connection's fields, so connector components only need a header. The new ConnectionFields component renders one connection's fields and is exported for custom layouts. ConnectionList also keeps a copy of each connection's meta, so new and duplicated connections no longer share and mutate the default meta, and validation errors keep their full path so nested fields like grids show them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Laravel only compiles rules like Rule::forEach() per item when they're a key's entire value. Wrapping them in an array made them validate against the whole data set, so they silently stopped failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connectors now hold their form and connections, set with setForm() and setConnections() (or forForm() for a form's stored connections), so render(), blueprint(), count(), rules() and the per-connection hooks no longer take the form as an argument. render() and count() use the connections they were given instead of reading the form's stored ones, so callers like entry-level overrides can pass their own. setConfig() and config() are replaced by setConnections() and connections().

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>
…l pipeline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from forms-2-connections to forms-2 October 8, 2026 22:19
jasonvarga added a commit that referenced this pull request Oct 12, 2026
Internal submission keys are now prefixed with an underscore (_partial, _site, _spam), and form field handles can't start with one, so the entry, partial, site and spam handles no longer need reserving. entry was reserved ahead of #15255, which should store _entry instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jasonvarga and others added 17 commits October 13, 2026 09:51
Ports this branch's connection code to the connector rename: ConnectionsListing is now ConnectorsListing, the form_connections fieldtype uses FormConnector, and Submission::connectionJobs() resolves connections through the entry's form instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connections (#15063) was squash-merged into forms-2, so this branch's copy of its history conflicted everywhere. The merge result is forms-2 plus this branch's own changes since the final Connections head. forms-2's honeypot behavior and config section positioning now live in ConfigFields, and instance submission limits count finalized submissions only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
forms-2 now prefixes internal submission keys with an underscore (_partial, _site, _marked_spam_at), so entry follows suit and is no longer a reserved form field handle. The entry key is still exposed in toArray(), augmentation and emails, but only when the form doesn't have its own field named entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Queries filter on the _entry data key, so the entry index was never used and only ever held nulls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Presets were disabled unconditionally to protect the stack's auto-applied submission_entry filter. Only disable them when viewing in a stack.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Restrictions were computed from the raw context id while _entry was only rendered when the entry referenced the form. Resolve the submittable entry once and use it for both. With no submittable entry, form-level restrictions apply.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
It re-read and decoded composer.lock on every call, including per-row paths.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ittable entry

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…pdate

The connections fieldtype now posts to a dedicated endpoint that validates and processes a connector's config without saving. The connector update endpoint always saves again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Users who can edit an entry but not the form get no Configure stack, no stored config sent to the client, and keep the entry's stored config on save, at any nesting depth. The stored config lookup is shared with Instance.

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

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

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