WordPress' Block Editor is great for creating User Interfaces for configuring content that in the past would have been created via a shortcode. I have a plugin called A-Z Listing that I have ported to the Block Editor. This plugin doesn’t pay for itself, however, so I also need to create addons or extensions that augment the Open Core plugin from WordPress.org to provide extra functionality for a modest fee.
How do we augment a Block in the WordPress block editor with extensions?
Thankfully we can add our extensions using the inbuilt Block Editor features and its supporting libraries with “Filters” and “SlotFills”.
Registering the plugin via JS is fairly typical. We won’t use the blocks.json
automatic registration mechamism but instead do it programmatically so that we can use filters when registering everything.
We need to ensure that the extensions have fully loaded, so we wait for the domReady
event.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { __ } from '@wordpress/i18n';
import domReady from '@wordpress/dom-ready';
import { createBlock, registerBlockType } from '@wordpress/blocks';
import edit from './edit';
import globalAttributes from './attributes.json';
domReady( () => {
const attributes = applyFilters( 'a_z_listing_attributes', globalAttributes );
registerBlockType( 'a-z-listing/block', {
title: __( 'A-Z Listing', 'a-z-listing' ),
category: 'widgets',
icon,
supports: {
align: true,
html: false,
},
attributes,
edit,
save: () => null,
} );
} );
|
Our extensions will use the filter a_z_listing_attributes
to add extra supported parameters/attributes to the block so that everything gets saved correctly. Without this filter and augmentation by the extensions their settings will be silently ignored when saving.
We will create a Redux store
to allow access to our slotfills:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { registerStore } from '@wordpress/data';
import DisplayOptions from '../components/DisplayOptions';
import ItemSelection from '../components/ItemSelection';
import AZInspectorControls from '../components/AZInspectorControls';
registerStore( 'a-z-listing/slotfills', {
reducer( state = {} ) {
return state;
},
actions: {},
selectors: {
getDisplayOptions() {
return DisplayOptions;
},
getItemSelection() {
return ItemSelection;
},
getInspectorControls() {
return AZInspectorControls;
},
},
} );
|
Each of the three files imported here are very similar, so I’ll only show one:
1
2
3
4
5
6
7
8
9
|
import { createSlotFill } from '@wordpress/components';
export const { Fill, Slot } = createSlotFill( 'AZListingDisplayOptions' );
const DisplayOptions = ( { children } ) => <Fill>{ children }</Fill>;
DisplayOptions.Slot = Slot;
export default DisplayOptions;
|
In our block’s edit component we include the SlotFills:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
import { __ } from '@wordpress/i18n';
const A_Z_Listing_Edit = ( { attributes, setAttributes } ) => {
const inspectorControls = (
<InspectorControls>
<AZInspectorControls.Slot>
{ ( fills ) => (
<>
{ /* ... Other controls here ... */ }
{ /* The next line includes the components from the extension plugins: */ }
{ fills }
</OtherComponentsHere>
) }
</AZInspectorControls.Slot>
</InspectorControls>
);
return (
<>
{ inspectorControls }
<ServerSideRender
block="a-z-listing/block"
attributes={ attributes }
LoadingResponsePlaceholder={ () => <Spinner /> }
ErrorResponsePlaceholder={ () => (
<Placeholder
icon={ pin }
label={ __( 'A-Z Listing', 'a-z-listing' ) }
>
{ __( 'Error Loading the listing...', 'a-z-listing' ) }
</Placeholder>
) }
EmptyResponsePlaceholder={ () => (
<Placeholder
icon={ pin }
label={ __( 'A-Z Listing', 'a-z-listing' ) }
>
{ __(
'The listing has returned an empty page. This is likely an error.',
'a-z-listing'
) }
</Placeholder>
) }
/>
</>
);
} );
export default A_Z_Listing_Edit;
|
Each of our extension plugins then hooks into the filter to add their own parameters:
1
2
3
|
import { addFilter } from '@wordpress/hooks';
addFilter( 'a_z_listing_attributes', 'a-z-listing/proper-nouns', ( globalAttributes ) => ( { ...globalAttributes, ...attributes } ) );
|
And uses a higher-order functional component to access the SlotFill to insert the configuration UI for the extensions' parameters:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import { __ } from '@wordpress/i18n';
import { withSelect } from '@wordpress/data';
import { createHigherOrderComponent } from '@wordpress/compose';
import attributes from './attributes.json';
const ProperNounsPlugin = createHigherOrderComponent( ( HostElement ) => {
return withSelect( (select) => {
const { getDisplayOptions } = select( 'a-z-listing/slotfills' );
return { getDisplayOptions };
} )( ( props ) => {
if ( props.name !== 'a-z-listing/block' ) {
return (
<HostElement { ...props }/>
);
}
const { getDisplayOptions, attributes, setAttributes } = props;
const DisplayOptions = getDisplayOptions();
return (
<>
<HostElement { ...props }/>
<AZInspectorControls>
{ /* Anything added here will be added to the main plugin's slot named `AZInspectorControls` */ }
<ToggleControl
label={ __( 'Enable proper nouns extension' ) }
checked={ !!attributes['proper-nouns'] }
onChange={ ( value ) =>
setAttributes( { 'proper-nouns': value } )
}
/>
{ /* ... Other controls here ... */ }
</AZInspectorControls>
</>
);
} );
} );
addFilter( 'editor.BlockEdit', 'a-z-listing/block', ProperNounsPlugin );
|
The PHP side is similar; we augment the attributes via a filter.
Main plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
$attributes = json_decode( file_get_contents( dirname( A_Z_LISTING_PLUGIN_FILE ) . '/scripts/blocks/attributes.json' ), true );
$attributes = apply_filters( 'a_z_listing_get_gutenberg_attributes', $attributes );
register_block_type(
'a-z-listing/block',
array(
'editor_script' => 'a-z-listing-block-editor',
'editor_style' => 'a-z-listing-block-editor',
'style' => 'a-z-listing-block',
'render_callback' => array( $this, 'render' ),
'attributes' => $attributes,
)
);
|
1
2
3
4
5
6
7
|
<?php
add_filter( 'a_z_listing_get_gutenberg_attributes', 'add_proper_nouns_gutenberg_attributes' );
function add_proper_nouns_gutenberg_attributes( $attributes ) {
$additional_attributes = json_decode( file_get_contents( trailingslashit( plugin_dir_path( __DIR__ ) ) . 'scripts/blocks/attributes.json' ), true );
return wp_parse_args( $attributes, $additional_attributes );
}
|
Handling the attributes
Main plugin
We pass the attributes into another filter in PHP so that the extensions may use their parameters accordingly:
1
2
3
4
5
6
|
<?php
function handle_a_z_shortcode_and_block( $attributes = array() ) {
do_action( 'a_z_listing_shortcode_start', $attributes );
... // rest of shortcode function
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?php
add_action( 'a_z_listing_shortcode_start', 'a_z_proper_nouns_handler', 10, 1 );
function a_z_proper_nouns_handler( $attributes = array() ) {
$attributes = shortcode_atts(
array(
'fullname-suffixes' => '',
'proper-nouns' => 'off',
),
$attributes,
'a-z-listing-proper-nouns'
);
... // Do something useful here
}
|