Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions .changeset/silver-bikes-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@snapwp/blocks": patch
"@snapwp/types": patch
---

feat: Add support for overloading Synced Pattern blocks.
89 changes: 89 additions & 0 deletions docs/overloading-wp-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,92 @@ export default function Page() {

- **Local Styles**: Use CSS Modules (as shown with `styles.module.css`) or any other CSS-in-JS solution for component-specific styling.
- **Global and Theme Styles**: Classes like `wp-block-heading`, `has-text-align-center`, and `has-x-large-font-size` (likely from your WordPress `theme.json` and/or global CSS) are automatically available.

## Overloading Synced Patterns

SnapWP allows you to customize **Synced Patterns** by mapping them to custom React components. This is useful for modifying the structure, design, or behavior of reusable block patterns while keeping WordPress as the content source.

> [!TIP]
> Synced Patterns are reusable block patterns in WordPress that stay in sync across all instances. When you edit a synced pattern, all instances of that pattern are updated.

### 1. Creating a Custom Component

Create a new React component to modify the rendering of a specific Synced Pattern:

```tsx
import React from 'react';
import { BlockData, cn, getClassNamesFromString } from '@snapwp/core';

export default function MyCustomSyncedPattern( {
renderedHtml,
attributes,
children,
}: BlockData ) {
const safeAttributes = attributes || {}; // Ensure attributes are not undefined.
const { style } = safeAttributes;

const classNamesFromString = renderedHtml
? getClassNamesFromString( renderedHtml )
: '';
const classNames = cn( classNamesFromString );

return (
<div className={ classNames } style={ style }>
{ /* Your custom rendering logic here */ }
{ children }
</div>
);
}
```

### 2. Registering the Custom Component

You can register your custom component either globally or per-route:

#### Global Registration

Add your custom component to the `blockDefinitions` in your `snapwp.config.ts`:

```ts
import { defineConfig } from '@snapwp/config';
import MyCustomSyncedPattern from './components/MyCustomSyncedPattern';

export default defineConfig( {
blockDefinitions: {
CoreSyncedPattern: MyCustomSyncedPattern,
},
} );
```

#### Per-Route Registration

Override the component for specific routes:

```tsx
import { EditorBlocksRenderer } from '@snapwp/blocks';
import MyCustomSyncedPattern from './components/MyCustomSyncedPattern';

const pageBlockDefinitions = {
CoreSyncedPattern: MyCustomSyncedPattern,
};

export default function Page() {
return (
<TemplateRenderer>
{ ( editorBlocks ) => (
<EditorBlocksRenderer
editorBlocks={ editorBlocks }
blockDefinitions={ pageBlockDefinitions }
/>
) }
</TemplateRenderer>
);
}
```

This allows you to apply the override only on specific routes while using the default block rendering elsewhere.

Now, whenever a `core/synced-pattern` block is encountered, your `MyCustomSyncedPattern` component will be used to render it. Any other blocks will use the default rendering unless you provide a custom component in `blockDefinitions`.

> [!TIP]
> If a block is overridden both globally (`snapwp.config.ts`) and per-route (`EditorBlocksRenderer` prop), the per-route override takes precedence.
63 changes: 32 additions & 31 deletions packages/blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,37 +49,38 @@ const blockDefinitions = {

These components provide developer-friendly APIs for rendering core WordPress blocks. If a WordPress block does not have a corresponding component, it will fallback to the `Default` block component, which uses `html-react-parser` under the hood.

| Type | Component |
| ------------------ | ---------------- |
| core-audio | CoreAudio |
| core-button | CoreButton |
| core-buttons | CoreButtons |
| core-code | CoreCode |
| core-column | CoreColumn |
| core-columns | CoreColumns |
| core-cover | CoreCover |
| core-details | CoreDetails |
| core-file | CoreFile |
| core-freeform | CoreFreeform |
| core-gallery | CoreGallery |
| core-group | CoreGroup |
| core-heading | CoreHeading |
| core-html | CoreHtml |
| core-image | CoreImage |
| core-list | CoreList |
| core-list-item | CoreListItem |
| core-media-text | CoreMediaText |
| core-paragraph | CoreParagraph |
| core-post-content | CorePostContent |
| core-preformatted | CorePreformatted |
| core-pullquote | CorePullquote |
| core-quote | CoreQuote |
| core-separator | CoreSeparator |
| core-spacer | CoreSpacer |
| core-template-part | CoreTemplatePart |
| core-verse | CoreVerse |
| core-video | CoreVideo |
| default | Default |
| Type | Component |
| ------------------- | ----------------- |
| core-audio | CoreAudio |
| core-button | CoreButton |
| core-buttons | CoreButtons |
| core-code | CoreCode |
| core-column | CoreColumn |
| core-columns | CoreColumns |
| core-cover | CoreCover |
| core-details | CoreDetails |
| core-file | CoreFile |
| core-freeform | CoreFreeform |
| core-gallery | CoreGallery |
| core-group | CoreGroup |
| core-heading | CoreHeading |
| core-html | CoreHtml |
| core-image | CoreImage |
| core-list | CoreList |
| core-list-item | CoreListItem |
| core-media-text | CoreMediaText |
| core-paragraph | CoreParagraph |
| core-post-content | CorePostContent |
| core-preformatted | CorePreformatted |
| core-pullquote | CorePullquote |
| core-quote | CoreQuote |
| core-separator | CoreSeparator |
| core-spacer | CoreSpacer |
| core-synced-pattern | CoreSyncedPattern |
| core-template-part | CoreTemplatePart |
| core-verse | CoreVerse |
| core-video | CoreVideo |
| default | Default |

## Known Limitations

Expand Down
46 changes: 46 additions & 0 deletions packages/blocks/src/blocks/core-synced-pattern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
cn,
getClassNamesFromString,
getStylesFromAttributes,
} from '@snapwp/core';
import type {
CoreSyncedPattern as CoreSyncedPatternType,
CoreSyncedPatternProps,
} from '@snapwp/types';
import type { ReactNode } from 'react';

/**
* Renders the core/synced-pattern block.
*
* @param {Object} props The props for the block component.
* @param {CoreSyncedPatternProps['attributes']} props.attributes Block attributes.
* @param {ReactNode} props.children The block's children.
* @param {CoreSyncedPatternProps['renderedHtml']} props.renderedHtml The block's rendered HTML.
*
* @return The rendered block.
*/
export const CoreSyncedPattern: CoreSyncedPatternType = ( {
attributes,
children,
renderedHtml,
}: CoreSyncedPatternProps ): ReactNode => {
const { style } = attributes || {};
const styleObject = getStylesFromAttributes( { style } );

/**
* @todo replace with cssClassName once it's supported.
*/
const classNamesFromString = renderedHtml
? getClassNamesFromString( renderedHtml )
: '';
const classNames = cn( classNamesFromString );

return (
<div
className={ classNames }
{ ...( styleObject && { style: styleObject } ) }
>
{ children }
</div>
);
};
2 changes: 2 additions & 0 deletions packages/blocks/src/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { CorePullquote } from './core-pullquote';
import { CoreQuote } from './core-quote';
import { CoreSeparator } from './core-separator';
import { CoreSpacer } from './core-spacer';
import { CoreSyncedPattern } from './core-synced-pattern';
import { CoreTemplatePart } from './core-template-part';
import { CoreVerse } from './core-verse';
import { CoreVideo } from './core-video';
Expand Down Expand Up @@ -58,6 +59,7 @@ export const blocks: BlockDefinitions = {
CoreQuote,
CoreSeparator,
CoreSpacer,
CoreSyncedPattern,
CoreTemplatePart,
CoreVerse,
CoreVideo,
Expand Down
81 changes: 81 additions & 0 deletions packages/blocks/src/blocks/tests/core-synced-pattern.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { render } from '@testing-library/react';

import { CoreSyncedPattern } from '../core-synced-pattern';

describe( 'CoreSyncedPattern Component', () => {
it( 'renders children correctly', () => {
const { container } = render(
<CoreSyncedPattern>
<div>Test Child</div>
</CoreSyncedPattern>
);

expect( container ).toHaveTextContent( 'Test Child' );
} );

it( 'applies className from renderedHtml', () => {
const renderedHtml = '<div class="test-class another-class"></div>';
const { container } = render(
<CoreSyncedPattern renderedHtml={ renderedHtml }>
<div>Test Child</div>
</CoreSyncedPattern>
);

const wrapper = container.firstChild;
expect( wrapper ).toHaveClass( 'test-class', 'another-class' );
} );

it( 'applies style from attributes', () => {
const style = JSON.stringify( { color: 'red', padding: '10px' } );
const { container } = render(
<CoreSyncedPattern attributes={ { style } }>
<div>Test Child</div>
</CoreSyncedPattern>
);

const wrapper = container.firstChild;
expect( wrapper ).toHaveStyle( {
color: 'red',
padding: '10px',
} );
} );

it( 'handles empty attributes gracefully', () => {
const { container } = render(
//@ts-ignore to test undefined props
<CoreSyncedPattern attributes={ undefined }>
<div>Test Child</div>
</CoreSyncedPattern>
);

expect( container ).toHaveTextContent( 'Test Child' );
} );

it( 'handles empty renderedHtml gracefully', () => {
const { container } = render(
// @ts-ignore to test undefined props
<CoreSyncedPattern renderedHtml="">
<div>Test Child</div>
</CoreSyncedPattern>
);

expect( container ).toHaveTextContent( 'Test Child' );
} );

it( 'combines className and style correctly', () => {
const renderedHtml = '<div class="test-class"></div>';
const style = JSON.stringify( { color: 'blue' } );
const { container } = render(
<CoreSyncedPattern
renderedHtml={ renderedHtml }
attributes={ { style } }
>
<div>Test Child</div>
</CoreSyncedPattern>
);

const wrapper = container.firstChild;
expect( wrapper ).toHaveClass( 'test-class' );
expect( wrapper ).toHaveStyle( { color: 'blue' } );
} );
} );
10 changes: 10 additions & 0 deletions packages/types/src/blocks/props/core-synced-pattern.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { BaseProps } from '../base';
import type { ComponentType, PropsWithChildren } from 'react';

export type CoreSyncedPatternProps = PropsWithChildren<
BaseProps< {
style?: string;
} >
>;

export type CoreSyncedPattern = ComponentType< CoreSyncedPatternProps >;
1 change: 1 addition & 0 deletions packages/types/src/blocks/props/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export * from './core-spacer';
export * from './core-template-part';
export * from './core-verse';
export * from './core-video';
export * from './core-synced-pattern';
export * from './default';
Loading