Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docs/private-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ Private exports:
- `CreatePatternModalContents`
- `DuplicatePatternModal`
- `isOverridableBlock`
- `hasOverridableBlocks`
- `useDuplicatePatternProps`
- `RenamePatternModal`
- `PatternsMenuItems`
Expand All @@ -227,7 +226,6 @@ Private exports:
- `PATTERN_USER_CATEGORY`
- `EXCLUDED_PATTERN_SOURCES`
- `PATTERN_SYNC_TYPES`
- `PARTIAL_SYNCING_SUPPORTED_BLOCKS`

### `core/patterns` store

Expand Down
113 changes: 112 additions & 1 deletion docs/reference-guides/block-api/block-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| ---------------- | -------------------- |
Expand All @@ -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(
'<div class="my-block"><h3>%s</h3><p>%s</p></div>',
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.

Comment thread
ockham marked this conversation as resolved.
## 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.
Expand Down
47 changes: 31 additions & 16 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ];

Expand Down Expand Up @@ -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 );
Expand Down
19 changes: 14 additions & 5 deletions packages/editor/src/hooks/pattern-overrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -20,23 +23,29 @@ const {
PatternOverridesControls,
ResetOverridesControl,
PATTERN_TYPES,
PARTIAL_SYNCING_SUPPORTED_BLOCKS,
PATTERN_SYNC_TYPES,
} = unlock( patternsPrivateApis );

/**
* 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.
*
* @return {Component} Wrapped component.
*/
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 (
<>
Expand Down
24 changes: 0 additions & 24 deletions packages/patterns/src/api/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* Internal dependencies
*/
import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants';

/**
* Determines whether a block is overridable.
*
Expand All @@ -12,29 +7,10 @@ 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(
( binding ) => binding.source === 'core/pattern-overrides'
)
);
}

/**
* 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 );
} );
}
18 changes: 14 additions & 4 deletions packages/patterns/src/components/overrides-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,28 @@ 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 );
const clientIdsWithOverrides = useMemo(
() =>
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 ) {
Expand Down
8 changes: 0 additions & 8 deletions packages/patterns/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
5 changes: 1 addition & 4 deletions packages/patterns/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,7 +24,6 @@ import {
PATTERN_USER_CATEGORY,
EXCLUDED_PATTERN_SOURCES,
PATTERN_SYNC_TYPES,
PARTIAL_SYNCING_SUPPORTED_BLOCKS,
} from './constants';

export const privateApis = {};
Expand All @@ -34,7 +33,6 @@ lock( privateApis, {
CreatePatternModalContents,
DuplicatePatternModal,
isOverridableBlock,
hasOverridableBlocks,
useDuplicatePatternProps,
RenamePatternModal,
PatternsMenuItems,
Expand All @@ -47,5 +45,4 @@ lock( privateApis, {
PATTERN_USER_CATEGORY,
EXCLUDED_PATTERN_SOURCES,
PATTERN_SYNC_TYPES,
PARTIAL_SYNCING_SUPPORTED_BLOCKS,
} );
Loading