From 940f4a55b7b7a9c1d18fdc6444847c9e0f210f8b Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 16 Oct 2025 14:41:58 +0200 Subject: [PATCH 1/5] Pattern Overrides: Infer partial syncing supported blocks from the server --- packages/editor/src/hooks/pattern-overrides.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/hooks/pattern-overrides.js b/packages/editor/src/hooks/pattern-overrides.js index 828e98bb940f69..58d892b5346f1c 100644 --- a/packages/editor/src/hooks/pattern-overrides.js +++ b/packages/editor/src/hooks/pattern-overrides.js @@ -4,7 +4,10 @@ import { addFilter } from '@wordpress/hooks'; import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { useBlockEditingMode } from '@wordpress/block-editor'; +import { + store as blockEditorStore, + useBlockEditingMode, +} from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { getBlockBindingsSource } from '@wordpress/blocks'; @@ -20,7 +23,6 @@ const { PatternOverridesControls, ResetOverridesControl, PATTERN_TYPES, - PARTIAL_SYNCING_SUPPORTED_BLOCKS, PATTERN_SYNC_TYPES, } = unlock( patternsPrivateApis ); @@ -35,8 +37,14 @@ const { */ const withPatternOverrideControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { - const isSupportedBlock = - !! PARTIAL_SYNCING_SUPPORTED_BLOCKS[ props.name ]; + const isSupportedBlock = useSelect( + ( select ) => { + const { __experimentalBlockBindingsSupportedAttributes } = + select( blockEditorStore ).getSettings(); + return !! __experimentalBlockBindingsSupportedAttributes?.[ props.name ]; + }, + [ props.name ] + ); return ( <> From 77a554bb9a206d5ff1797271d25ea2290c2fb531 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 16 Oct 2025 14:46:43 +0200 Subject: [PATCH 2/5] Remove now-obsolete comment --- packages/editor/src/hooks/pattern-overrides.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/editor/src/hooks/pattern-overrides.js b/packages/editor/src/hooks/pattern-overrides.js index 58d892b5346f1c..bcccd3952b9569 100644 --- a/packages/editor/src/hooks/pattern-overrides.js +++ b/packages/editor/src/hooks/pattern-overrides.js @@ -29,7 +29,6 @@ const { /** * Override the default edit UI to include a new block inspector control for * assigning a partial syncing controls to supported blocks in the pattern editor. - * Currently, only the `core/paragraph` block is supported. * * @param {Component} BlockEdit Original component. * From a3f56e21c2bcddd17b8672dec4a3e71c6aafdadb Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Fri, 17 Oct 2025 14:42:02 +0200 Subject: [PATCH 3/5] Remove remaining instances of hardcoded list --- docs/private-apis.md | 2 - packages/block-library/src/block/edit.js | 47 ++++++++++++------- packages/patterns/src/api/index.js | 24 ---------- .../src/components/overrides-panel.js | 18 +++++-- packages/patterns/src/constants.js | 8 ---- packages/patterns/src/private-apis.js | 5 +- 6 files changed, 46 insertions(+), 58 deletions(-) diff --git a/docs/private-apis.md b/docs/private-apis.md index 4e9929bee84afe..2b684769ed8f0b 100644 --- a/docs/private-apis.md +++ b/docs/private-apis.md @@ -213,7 +213,6 @@ Private exports: - `CreatePatternModalContents` - `DuplicatePatternModal` - `isOverridableBlock` -- `hasOverridableBlocks` - `useDuplicatePatternProps` - `RenamePatternModal` - `PatternsMenuItems` @@ -227,7 +226,6 @@ Private exports: - `PATTERN_USER_CATEGORY` - `EXCLUDED_PATTERN_SOURCES` - `PATTERN_SYNC_TYPES` -- `PARTIAL_SYNCING_SUPPORTED_BLOCKS` ### `core/patterns` store diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index ab7f8adf24c134..13b7029f5f04a7 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -40,7 +40,7 @@ import { getBlockBindingsSource } from '@wordpress/blocks'; import { unlock } from '../lock-unlock'; const { useLayoutClasses } = unlock( blockEditorPrivateApis ); -const { hasOverridableBlocks } = unlock( patternsPrivateApis ); +const { isOverridableBlock } = unlock( patternsPrivateApis ); const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; @@ -168,24 +168,39 @@ function ReusableBlockEdit( { const { __unstableMarkLastChangeAsPersistent } = useDispatch( blockEditorStore ); - const { onNavigateToEntityRecord, hasPatternOverridesSource } = useSelect( - ( select ) => { - const { getSettings } = select( blockEditorStore ); - // For editing link to the site editor if the theme and user permissions support it. - return { - onNavigateToEntityRecord: - getSettings().onNavigateToEntityRecord, - hasPatternOverridesSource: !! getBlockBindingsSource( - 'core/pattern-overrides' - ), - }; - }, - [] - ); + const { + onNavigateToEntityRecord, + hasPatternOverridesSource, + supportedBlockTypes, + } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + // For editing link to the site editor if the theme and user permissions support it. + return { + onNavigateToEntityRecord: getSettings().onNavigateToEntityRecord, + hasPatternOverridesSource: !! getBlockBindingsSource( + 'core/pattern-overrides' + ), + supportedBlockTypes: Object.keys( + getSettings().__experimentalBlockBindingsSupportedAttributes || + {} + ), + }; + }, [] ); + + const hasOverridableBlocks = ( _blocks ) => + _blocks.some( ( block ) => { + if ( + supportedBlockTypes.includes( block.name ) && + isOverridableBlock( block ) + ) { + return true; + } + return hasOverridableBlocks( block.innerBlocks ); + } ); const canOverrideBlocks = useMemo( () => hasPatternOverridesSource && hasOverridableBlocks( blocks ), - [ hasPatternOverridesSource, blocks ] + [ hasPatternOverridesSource, hasOverridableBlocks, blocks ] ); const { alignment, layout } = useInferredLayout( blocks, parentLayout ); diff --git a/packages/patterns/src/api/index.js b/packages/patterns/src/api/index.js index 4321dc4262145a..ed142e22179a95 100644 --- a/packages/patterns/src/api/index.js +++ b/packages/patterns/src/api/index.js @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants'; - /** * Determines whether a block is overridable. * @@ -12,9 +7,6 @@ import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants'; */ export function isOverridableBlock( block ) { return ( - Object.keys( PARTIAL_SYNCING_SUPPORTED_BLOCKS ).includes( - block.name - ) && !! block.attributes.metadata?.name && !! block.attributes.metadata?.bindings && Object.values( block.attributes.metadata.bindings ).some( @@ -22,19 +14,3 @@ export function isOverridableBlock( block ) { ) ); } - -/** - * Determines whether the blocks list has overridable blocks. - * - * @param {WPBlock[]} blocks The blocks list. - * - * @return {boolean} `true` if the list has overridable blocks, `false` otherwise. - */ -export function hasOverridableBlocks( blocks ) { - return blocks.some( ( block ) => { - if ( isOverridableBlock( block ) ) { - return true; - } - return hasOverridableBlocks( block.innerBlocks ); - } ); -} diff --git a/packages/patterns/src/components/overrides-panel.js b/packages/patterns/src/components/overrides-panel.js index b567b3c372daa2..7210ad35755dbe 100644 --- a/packages/patterns/src/components/overrides-panel.js +++ b/packages/patterns/src/components/overrides-panel.js @@ -19,8 +19,15 @@ import { unlock } from '../lock-unlock'; const { BlockQuickNavigation } = unlock( blockEditorPrivateApis ); export default function OverridesPanel() { - const allClientIds = useSelect( - ( select ) => select( blockEditorStore ).getClientIdsWithDescendants(), + const { allClientIds, supportedBlockTypes } = useSelect( + ( select ) => ( { + allClientIds: + select( blockEditorStore ).getClientIdsWithDescendants(), + supportedBlockTypes: Object.keys( + select( blockEditorStore ).getSettings() + ?.__experimentalBlockBindingsSupportedAttributes || {} + ), + } ), [] ); const { getBlock } = useSelect( blockEditorStore ); @@ -28,9 +35,12 @@ export default function OverridesPanel() { () => allClientIds.filter( ( clientId ) => { const block = getBlock( clientId ); - return isOverridableBlock( block ); + return ( + supportedBlockTypes.includes( block.name ) && + isOverridableBlock( block ) + ); } ), - [ allClientIds, getBlock ] + [ allClientIds, getBlock, supportedBlockTypes ] ); if ( ! clientIdsWithOverrides?.length ) { diff --git a/packages/patterns/src/constants.js b/packages/patterns/src/constants.js index 9f35b719949c42..0bcfeb99d4d020 100644 --- a/packages/patterns/src/constants.js +++ b/packages/patterns/src/constants.js @@ -15,12 +15,4 @@ export const PATTERN_SYNC_TYPES = { unsynced: 'unsynced', }; -// TODO: This should not be hardcoded. Maybe there should be a config and/or an UI. -export const PARTIAL_SYNCING_SUPPORTED_BLOCKS = { - 'core/paragraph': [ 'content' ], - 'core/heading': [ 'content' ], - 'core/button': [ 'text', 'url', 'linkTarget', 'rel' ], - 'core/image': [ 'id', 'url', 'title', 'alt', 'caption' ], -}; - export const PATTERN_OVERRIDES_BINDING_SOURCE = 'core/pattern-overrides'; diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 0553378cb56043..972527ad8d3fb3 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -11,7 +11,7 @@ import { default as DuplicatePatternModal, useDuplicatePatternProps, } from './components/duplicate-pattern-modal'; -import { isOverridableBlock, hasOverridableBlocks } from './api'; +import { isOverridableBlock } from './api'; import RenamePatternModal from './components/rename-pattern-modal'; import PatternsMenuItems from './components'; import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; @@ -24,7 +24,6 @@ import { PATTERN_USER_CATEGORY, EXCLUDED_PATTERN_SOURCES, PATTERN_SYNC_TYPES, - PARTIAL_SYNCING_SUPPORTED_BLOCKS, } from './constants'; export const privateApis = {}; @@ -34,7 +33,6 @@ lock( privateApis, { CreatePatternModalContents, DuplicatePatternModal, isOverridableBlock, - hasOverridableBlocks, useDuplicatePatternProps, RenamePatternModal, PatternsMenuItems, @@ -47,5 +45,4 @@ lock( privateApis, { PATTERN_USER_CATEGORY, EXCLUDED_PATTERN_SOURCES, PATTERN_SYNC_TYPES, - PARTIAL_SYNCING_SUPPORTED_BLOCKS, } ); From f0372081cca4be172df2935b7aae953e189a93c1 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Fri, 17 Oct 2025 14:49:02 +0200 Subject: [PATCH 4/5] Prettier --- packages/editor/src/hooks/pattern-overrides.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/hooks/pattern-overrides.js b/packages/editor/src/hooks/pattern-overrides.js index bcccd3952b9569..074ab909d6eaa0 100644 --- a/packages/editor/src/hooks/pattern-overrides.js +++ b/packages/editor/src/hooks/pattern-overrides.js @@ -40,7 +40,9 @@ const withPatternOverrideControls = createHigherOrderComponent( ( select ) => { const { __experimentalBlockBindingsSupportedAttributes } = select( blockEditorStore ).getSettings(); - return !! __experimentalBlockBindingsSupportedAttributes?.[ props.name ]; + return !! __experimentalBlockBindingsSupportedAttributes?.[ + props.name + ]; }, [ props.name ] ); From 518805a0228bf4cefa5ef288577d2f1f56d18e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Tue, 6 Jan 2026 15:58:22 +0100 Subject: [PATCH 5/5] Docs: Document block_bindings_supported_attributes filter and pattern overrides in dynamic blocks Adds comprehensive documentation for: - The block_bindings_supported_attributes filter for extending supported block attributes - The block_bindings_supported_attributes_{$block_type} dynamic filter with examples - How to access pattern override values in a dynamic block's render_callback - Step-by-step guide with complete code examples for implementing pattern overrides in custom blocks --- .../block-api/block-bindings.md | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/docs/reference-guides/block-api/block-bindings.md b/docs/reference-guides/block-api/block-bindings.md index dc4e397f1a9d42..6497026c03808a 100644 --- a/docs/reference-guides/block-api/block-bindings.md +++ b/docs/reference-guides/block-api/block-bindings.md @@ -23,7 +23,7 @@ An example could be connecting an Image block `url` attribute to a function that ## Compatible blocks and their attributes -Right now, not all block attributes are compatible with block bindings. There is some ongoing effort to increase this compatibility, but for now, this is the list: +Right now, not all block attributes are compatible with block bindings. There is some ongoing effort to increase this compatibility, but for now, this is the default list: | Supported Blocks | Supported Attributes | | ---------------- | -------------------- | @@ -32,6 +32,117 @@ Right now, not all block attributes are compatible with block bindings. There is | Image | id, url, title, alt | | Button | text, url, linkTarget, rel | +### Extending supported attributes + +_**Note:** Since WordPress 6.9._ + +Developers can extend the list of supported attributes using the `block_bindings_supported_attributes` filter. This filter allows adding support for additional block attributes. + +There are two filters available: + +- `block_bindings_supported_attributes`: A general filter that receives the supported attributes array and the block type name. +- `block_bindings_supported_attributes_{$block_type}`: A dynamic filter specific to a block type (e.g., `block_bindings_supported_attributes_core/image`). + +Example of adding support for the `caption` attribute on the Image block: + +```php +add_filter( + 'block_bindings_supported_attributes_core/image', + function ( $supported_attributes ) { + $supported_attributes[] = 'caption'; + return $supported_attributes; + } +); +``` + +Example of adding support for a custom block: + +```php +add_filter( + 'block_bindings_supported_attributes_my-plugin/my-block', + function ( $supported_attributes ) { + $supported_attributes[] = 'title'; + $supported_attributes[] = 'description'; + return $supported_attributes; + } +); +``` + +This filter also affects which blocks and attributes are available for Pattern Overrides, as both features share the same underlying supported attributes configuration. + +### Accessing Pattern Override values in dynamic blocks + +When creating a dynamic block that supports Pattern Overrides, you can access the override values within your `render_callback` function. The Pattern block (`core/block`) provides override values to nested blocks via the `pattern/overrides` context. + +**Step 1: Register your block with the required context and supported attributes** + +```php +add_action( 'init', function() { + // Register supported attributes for pattern overrides + add_filter( + 'block_bindings_supported_attributes_my-plugin/my-block', + function ( $supported_attributes ) { + $supported_attributes[] = 'title'; + $supported_attributes[] = 'description'; + return $supported_attributes; + } + ); + + register_block_type( 'my-plugin/my-block', array( + 'attributes' => array( + 'title' => array( 'type' => 'string', 'default' => '' ), + 'description' => array( 'type' => 'string', 'default' => '' ), + 'metadata' => array( 'type' => 'object' ), + ), + // Declare that you need the pattern/overrides context + 'uses_context' => array( 'pattern/overrides' ), + 'render_callback' => 'my_block_render_callback', + ) ); +} ); +``` + +**Step 2: Access override values in your render callback** + +The override values are stored in `$block->context['pattern/overrides']` as an associative array. The keys are block metadata names (assigned when enabling overrides), and the values are arrays of attribute overrides. + +```php +function my_block_render_callback( $attributes, $content, $block ) { + // Get the block's metadata name (set when enabling overrides) + $block_name = $attributes['metadata']['name'] ?? null; + + // Get the pattern overrides from context + $overrides = array(); + if ( $block_name && isset( $block->context['pattern/overrides'][ $block_name ] ) ) { + $overrides = $block->context['pattern/overrides'][ $block_name ]; + } + + // Get attribute values, preferring overrides when available + // Note: An empty string in overrides means "reset to default" + $title = $attributes['title']; + if ( isset( $overrides['title'] ) && $overrides['title'] !== '' ) { + $title = $overrides['title']; + } + + $description = $attributes['description']; + if ( isset( $overrides['description'] ) && $overrides['description'] !== '' ) { + $description = $overrides['description']; + } + + return sprintf( + '

%s

%s

', + esc_html( $title ), + esc_html( $description ) + ); +} +``` + +**Key points to keep in mind:** + +- **`uses_context`**: Your block must declare `pattern/overrides` in its `uses_context` to receive override data from parent Pattern blocks. +- **Block metadata name**: Each overridable block instance has a unique name stored in `$attributes['metadata']['name']`. This name is assigned when the user enables overrides on the block in the editor. +- **Empty string convention**: An empty string (`""`) in the overrides represents a reset to the default value. Your code should handle this appropriately. +- **Fallback behavior**: Always provide fallback values from `$attributes` in case the block is not inside a pattern or overrides are not set. + ## Registering a custom source Registering a source requires defining at least `name`, a `label` and a `callback` function that gets a value from the source and passes it back to a block attribute.