Skip to content

Bootstrap field collections - #76332

Closed
oandregal wants to merge 18 commits into
trunkfrom
try/register-field-collections
Closed

oandregal wants to merge 18 commits into
trunkfrom
try/register-field-collections

Conversation

@oandregal

@oandregal oandregal commented Mar 9, 2026 •

Copy link
Copy Markdown
Member

Part of #74865

What?

Introduces a Field Collections API — a server-side registration system for defining a collection of fields for an entity.

A field collection is a PHP structure that includes:

  • An identifier for the collection: id, kind, name.
  • A list of fields:
    • Fields are defined in PHP, via the declarative Fields API (works across DataViews/DataForm).
    • Optionally, fields can provide JavaScript code to extend the core functionality for properties that cannot be serializable (e.g., a function defining a render method of a field).
  • A list of strings representing WordPress module scripts. These scripts contain the JavaScript extensions to fields.
data-flow

Why

Part of #74865

How

gutenberg_register_field_collection( $id, $kind, $name, $fields, $fields_modules );
gutenberg_get_field_collections( $kind, $name );
gutenberg_get_all_field_collections();
gutenberg_enqueue_field_collections_loader();

http://<site>/wp-json/wp/v2/field-collections?kind=KIND&name=NAME

  • A useFieldCollections( kind, name ) JavaScript hook (code) that request the collection data (REST Endpoint), imports dynamically the WordPress script modules for the collection (JavaScript extensions), and merges it together for consumers to use.

Field Collection definitions for core/Gutenberg:

Extensibility

The goal is to make it easy to tweak the small things and the complex ones possible.

The API introduces a way for 3rd parties to register their own collections, see example for postType-attachment. Additionally, we wanted to auto-register fields for all (Custom) Post Types, while providing a mechanism to extend the defaults. The API supports three use cases:

  1. A plugin registers a CPT and wants to register its own collection. Uses gutenberg_register_field_collection. Example.

  2. A plugin wants to add/update/remove an existing field of a collection. For example, the postType/page wants to disable hiding the title field. It hooks into the gutenberg_field_collection_fields filter and updates its output:

add_filter(
	'gutenberg_field_collection_fields',
	function ( $fields, $id, $kind, $name ) {
		if ( 'postType' !== $kind || 'page' !== $name ) {
			return $fields;
		}

		foreach ( $fields as &$field ) {
			if ( 'title' === $field['id'] ) {
				$field['enableHiding'] = false;
			}
		}
		unset( $field );

		return $fields;
	},
	10,
	4
);
  1. A plugin wants to add/update some JavaScript extension. For example, adding a different render method for the title field. It needs to prepare a module script and attach it to the list of filter modules for that collection:
add_filter(
	'gutenberg_field_collection_modules',
	function ( $modules, $id, $kind, $name ) {
		if ( 'postType' === $kind && 'page' === $name ) {
			$modules[] = '@wordpress/field-collections/postType-page';
		}

		return $modules;
	},
	10,
	4
);

Status

Prototype, not ready to ship.

This API is inspired by prior prototypes implemented by @youknowriad, @gigitux, and other folks in the Woo realm. It's also vibe coded and not fully human reviewed yet. The focus right now is to understand how to support all the use cases we want via the API surface.

Open questions:

  • Migration/Deprecation:
    • Field definitions: from @wordpress/fields and @wordpress/media-fields to @wordpress/field-collections.
    • Data store: from editor to a dedicated field-collections store (e.g., registerEntityField API that was available only when Gutenberg was active).

Test

  • Site Editor: Pages, Templates, Patterns.
  • Enable experiment for DataForm in Editor Inspector.

@github-actions

github-actions Bot commented Mar 9, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: oandregal <oandregal@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions Bot added [Package] Core data /packages/core-data [Package] Editor /packages/editor labels Mar 9, 2026
@oandregal oandregal added the [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible label Mar 9, 2026
@github-actions

github-actions Bot commented Mar 9, 2026 •

Copy link
Copy Markdown

Size Change: +120 kB (+1.39%)

Total Size: 8.72 MB

📦 View Changed
Filename Size Change
build/modules/field-collections/loader.min.js 59 B +59 B (new file) 🆕
build/modules/field-collections/postType-attachment.min.js 39.4 kB +39.4 kB (new file) 🆕
build/modules/field-collections/postType-default.min.js 67.6 kB +67.6 kB (new file) 🆕
build/modules/field-collections/postType-page.min.js 1.41 kB +1.41 kB (new file) 🆕
build/modules/field-collections/postType-wp_block.min.js 37.4 kB +37.4 kB (new file) 🆕
build/modules/field-collections/postType-wp_template.min.js 1.02 kB +1.02 kB (new file) 🆕
build/scripts/editor/index.min.js 445 kB -28.9 kB (-6.11%) ✅
build/scripts/field-collections/index.min.js 1.63 kB +1.63 kB (new file) 🆕
build/scripts/private-apis/index.min.js 1.13 kB +8 B (+0.71%)

compressed-size-action

Comment thread lib/compat/wordpress-7.1/field-collections-register-core-collections.php Outdated
Comment thread packages/editor/src/dataviews/store/private-actions.ts
@ntsekouras
ntsekouras force-pushed the try/register-field-collections branch from 1758f48 to 6271427 Compare March 13, 2026 08:36
@oandregal
oandregal marked this pull request as draft March 13, 2026 08:55
* REST API: Gutenberg_REST_Field_Collections_Controller class
*
* @package gutenberg
* @since 20.8.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We should update the proper version before mergin.

* @since 20.8.0
*/

if ( ! defined( 'ABSPATH' ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this check.

* @return array Collection parameters.
*/
public function get_collection_params() {
$params = parent::get_collection_params();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We probably only want 'context' => $this->get_context_param(), from get_collection_params. We don't support search or pagination.

Comment thread packages/core-data/src/entities.js Outdated
supportsPagination: true,
},
{
label: __( 'Field Collection' ),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be plural?

Comment thread packages/core-data/src/entities.js Outdated
label: __( 'Field Collection' ),
name: 'fieldCollection',
kind: 'root',
baseURL: '/wp/v2/field-collections',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though view context seems to be the default, should we be explicit like icons and fonts(baseURLParams: { context: 'view' },)?

'filterBy' => false,
);

gutenberg_register_field_collection(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we wrap this in a rest_api_init hook?

// Check if collection with same ID already exists.
foreach ( $gutenberg_field_collections as $existing_collection ) {
if ( $existing_collection['id'] === $id ) {
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a _doing_it_wrong here?

}

// Global storage for registered field collections.
global $gutenberg_field_collections;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely fine to start with a global, but have you considered a registry?

);
} );
( fieldCollections ?? [] ).forEach( ( collection: any ) => {
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine if we don't handle the universal (name:null) in here, but we should add a TODO comment not to forget it with the follow ups.

@oandregal

Copy link
Copy Markdown
Member Author

I'm going to leave this one in the backburner for a few days and will focus on the view/form config first.

@oandregal
oandregal force-pushed the try/register-field-collections branch 2 times, most recently from 227da21 to cf8a102 Compare June 10, 2026 14:54
oandregal and others added 14 commits June 16, 2026 17:17
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All fields except the content preview are now provided by server-side
field collections: hand-written collections for the core post types and
the dynamic postType-default registrar for every other REST-enabled
post type. The preview field stays client-side because its render
depends on editor internals the collections' script modules cannot
import.

Post types registered after init priority 100 no longer get any
fields, as they have no server-side collection and the client-side
fallback is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the field collections out of @wordpress/fields into a new
@wordpress/field-collections package that owns a dedicated, editor-free
data store (core/field-collections) and a useFieldCollections hook.

Previously the merged field registry lived in the core/editor store, so any
screen needing field definitions had to depend on @wordpress/editor. The new
package fetches definitions from GET /wp/v2/field-collections, lazily imports
each collection's extension script module, and merges by field id — depending
only on data/api-fetch/element/compose/url/private-apis (+ dataviews types).

- Move collections/ to the new package; re-point extensions.ts to named
  @wordpress/fields imports; rename module handles to
  @wordpress/field-collections/{loader,postType-*}.
- Move wpScriptModuleExports + wpCopyFiles to the new package; update the PHP
  glob path and loader id. Registration API, REST controller, and loader
  enqueue stay in lib/.
- Carve the collection-field merge out of registerPostTypeSchema (keeps actions
  + the editor-only preview field); usePostFields now combines editor fields
  with useFieldCollections, preview-first.
- Remove the now-unused root/fieldCollection core-data entity.
- Add the package to the private-apis allowlist and root tsconfig.

Tests: 12 JS unit tests (reducer/selector/resolver/hook) and the existing
field-collections PHPUnit suite (24 tests) pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns onto generator

Add two filters so consumers can tailor any field collection — including an
auto-generated one — without re-registering it:

- gutenberg_field_collection_fields ( $fields, $id, $kind, $name ): remove,
  reorder, relabel, or add serializable field definitions.
- gutenberg_field_collection_modules ( $modules, $id, $kind, $name ): append
  script modules of non-serializable extensions.

To support multiple contributors, a collection's single fields_module string
becomes a fields_modules array (REST, store types, hook, preload). The client
loads every module, concatenates their extensions, and merges all entries
matching a field id so a later module overrides an earlier one property by
property (last wins).

Refactor the post type collections onto the default generator as the single
baseline:

- Delete post, wp_navigation, wp_template_part — the generator reproduces them
  exactly and the default union module already covers their extensions.
- Convert page, wp_template, wp_block to filter-based overrides that only tweak
  the title field plus a slim extensions module for its title behavior.
- Keep attachment standalone (bespoke; opts out by registering its own
  collection, which the generator then skips).

Update tests (new coverage for both filters and multi-module merge) and the
README. The generic REST controller tests move off real post-type names onto a
synthetic entity, since the override files register persistent filters that
survive the test's collection-global reset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oandregal
oandregal force-pushed the try/register-field-collections branch from cf8a102 to 77a42d4 Compare June 17, 2026 10:00
@github-actions github-actions Bot added [Package] Private APIs /packages/private-apis and removed [Package] Core data /packages/core-data labels Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026 •

Copy link
Copy Markdown

Flaky tests detected in 045514f.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27697541734
📝 Reported issues:

Add an SVG (source) and PNG (rendered, for GitHub) diagram illustrating
how a field is defined in two halves — serializable definitions in PHP and
non-serializable extensions in JavaScript — registered server-side, exposed
via REST, and re-merged in the client by useFieldCollections before screens
consume the resulting Field[]. Embed the PNG near the top of the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oandregal
oandregal requested a review from gigitux June 17, 2026 15:00
*
* @see packages/vips/src/loader.ts — the reference pattern
*/
export {};

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentionally empty. It only exists so that there's a handle (@wordpress/field-collections/loader) to which "attach" the scripts that will be dynamically loaded.

In use-field-collections.ts, the collections load their non-serializable field extensions with a dynamic import:

const module = await import( handle );

where handle is a string like 'core/media-fields-module'.

For the browser to resolve that specifier to an actual URL at runtime, the handle must be present in the page's import map. And for WordPress to print import-map entries for them, the modules need to be reachable from the dependency graph of an enqueued module. Hence, this empty loader.

This is another place where we do the same.

The reason I went for an empty handle instead of making the modules a dependency of some site-editor's script is that the field modules can be loaded outside the site editor. Think of examples like the settings page, connectors, comments page, etc. We cannot assume every place that needs fields collections will be part of the site editor.

Comment on lines +354 to +371
function gutenberg_register_default_post_type_field_collections() {
$post_types = get_post_types( array( 'show_in_rest' => true ) );

foreach ( $post_types as $post_type ) {
if ( ! empty( gutenberg_get_field_collections( 'postType', $post_type ) ) ) {
continue;
}

gutenberg_register_field_collection(
"core/{$post_type}-fields",
'postType',
$post_type,
gutenberg_get_default_post_type_fields( $post_type ),
'@wordpress/field-collections/postType-default'
);
}
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no collection defined for an existing post type, we automatically register a collection for it based on its declared supports.

The extensibility section of the PR describes the mechanisms a 3rd party can use to create or tweak collecitons. Essentially:

  • A 3rd party can register a collection for an existing post type and this default would be ignored.
  • A 3rd party can hook into any of the available filters to tweak an existing collection (e.g., to add/update/remove a field).

Comment on lines -22 to -42
excerptField,
featuredImageField,
dateField,
parentField,
passwordField,
commentStatusField,
pingStatusField,
discussionField,
slugField,
statusField,
authorField,
titleField,
templateField,
templateTitleField,
pageTitleField,
patternTitleField,
notesField,
scheduledDateField,
formatField,
postContentInfoField,
stickyField,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the logic about whether to load this fields now lives in PHP at packages/field-collections/src/collections/postType-default/fields.php.

@oandregal

Copy link
Copy Markdown
Member Author

I've pushed the latest I had and updated the PR description with the current state as well as documented some parts via comments.

This API has been inspired by prior prototypes implemented by @youknowriad, @gigitux, and other folks in the Woo realm. It's also vibe coded and not fully human reviewed yet. The focus right now is to understand how to support all the use cases we want via the API surface.

I won't have the availability to push this forward to make it part of WordPress 7.1, so, unless someone else picks it up it should have to wait until next cycle. @ntsekouras is taking care of coordinating everything for DataViews/DataForm in WordPress 7.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Editor /packages/editor [Package] Private APIs /packages/private-apis [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants