Skip to content

Instantly share code, notes, and snippets.

@Mamaduka
Last active May 26, 2023 09:26
Show Gist options
  • Save Mamaduka/3ce2cbeabe1a3295b8d134a0b15c7664 to your computer and use it in GitHub Desktop.
Save Mamaduka/3ce2cbeabe1a3295b8d134a0b15c7664 to your computer and use it in GitHub Desktop.

Disclaimer: The document is based on the proposed API by @ellatrix and @gziolo. See #48809.

Summary

Introduce a new API, registerBlockSupport, for describing the behavior and rendering of block features that utilize support properties.

Problem

As the block editor started absorbing the complexities of block features like color, fonts, and dimension controls, it became apparent that the current method of integrating UI controls is not very sustainable and may sometimes hinder performance.

The present approach to add the "block support" features requires fragmenting the logic across multiple filters. Each filter is applied separately, overriding whole components multiple times. Additionally, it requires specific checks to ensure controls are only rendered when necessary and workarounds to prevent redundant re-mounting of the block's "Edit" component.

Design in #48809

Alignment support
function addAttribute( settings ) {
	if ( 'type' in ( settings.attributes?.align ?? {} ) ) {
		return settings;
	}
	if ( hasBlockSupport( settings, 'align' ) ) {
		// Gracefully handle if settings.attributes is undefined.
		settings.attributes = {
			...settings.attributes,
			align: {
				type: 'string',
				// Allow for '' since it is used by updateAlignment function
				// in withToolbarControls for special cases with defined default values.
				enum: [ ...ALL_ALIGNMENTS, '' ],
			},
		};
	}

	return settings;
}

export const withToolbarControls = createHigherOrderComponent(
	( BlockEdit ) => ( props ) => {
		const blockEdit = <BlockEdit key="edit" { ...props } />;

		return (
			<>
				<BlockControls group="block" __experimentalShareWithChildBlocks>
					<BlockAlignmentControl
						value={ props.attributes.align }
						onChange={ ( nextAlign ) =>
							props.setAttributes( { align: nextAlign } )
						}
					/>
				</BlockControls>
				{ blockEdit }
			</>
		);
	},
	'withToolbarControls'
);

export const withDataAlign = createHigherOrderComponent(
	( BlockListBlock ) => ( props ) => {
		const { attributes } = props;
		const { align } = attributes;

		// If an alignment is not assigned, there's no need to go through the
		// effort to validate or assign its value.
		if ( align === undefined ) {
			return <BlockListBlock { ...props } />;
		}

		let wrapperProps = props.wrapperProps;
		wrapperProps = { ...wrapperProps, 'data-align': align };

		return <BlockListBlock { ...props } wrapperProps={ wrapperProps } />;
	},
	'withDataAlign'
);

export function addAssignedAlign( props, blockType, attributes ) {
	const { align } = attributes;

	props.className = classnames( `align${ align }`, props.className );

	return props;
}

addFilter(
	'blocks.registerBlockType',
	'core/align/addAttribute',
	addAttribute
);
addFilter(
	'editor.BlockListBlock',
	'core/editor/align/with-data-align',
	withDataAlign
);
addFilter(
	'editor.BlockEdit',
	'core/editor/align/with-toolbar-controls',
	withToolbarControls
);
addFilter(
	'blocks.getSaveContent.extraProps',
	'core/align/addAssignedAlign',
	addAssignedAlign
);

Proposal

The new first-class API will shift the implementation burden to the Block Editor, reducing the complexity for developers.

Initial design

registerBlockSupport( 'align', {
	blockSettings: {
		attributes: {
			align: { type: 'string' },
		},
	},
	Controls( props ) {
		return (
			<BlockControls group="block" __experimentalShareWithChildBlocks>
				<BlockAlignmentControl
					value={ props.attributes.align }
					onChange={ ( nextAlign ) =>
						props.setAttributes( { align: nextAlign } )
					}
				/>
			</BlockControls>
		);
	},
	useBlockProps( attributes ) {
		const { align } = attributes;
		return { 'data-align': align };
	},
	getSaveProps( attributes ) {
		const { align } = attributes;
		return {
			className: `align${ align }`,
		};
	},
} );

A support key based registrar; doesn't require a separate isSupported check. The new settings object is merged into the block type definition.

The API is easy to use but lacks flexibility. e.g., Some block support features must consider multiple support keys before rendering UI.

Proposed Design

registerBlockSupport( 'core/align', {
	isSupported( blockType ) {
		return hasBlockSupport( blockType, 'align' );
	},
	blockSettings( settings ) {
		if ( 'type' in ( settings.attributes?.align ?? {} ) ) {
			return settings;
		}

		settings.attributes = {
			...settings.attributes,
			align: {
				type: 'string',
				enum: [ ...ALL_ALIGNMENTS, '' ],
			},
		};

		return settings;
	},
	EditControls( props ) {
		return (
			<BlockControls group="block" __experimentalShareWithChildBlocks>
				<BlockAlignmentControl
					value={ props.attributes.align }
					onChange={ ( nextAlign ) =>
						props.setAttributes( { align: nextAlign } )
					}
				/>
			</BlockControls>
		);
	},
	useBlockProps( { attributes, blockType } ) {
		const { align } = attributes;
		return { 'data-align': align };
	},
	getSaveProps( { attributes, blockType } ) {
		const { align } = attributes;
		return {
			className: `align${ align }`,
		};
	},
} );

This results from "migrating" current block support implementations to the proposed API. It builds on the initial design but uses functions to determine block support and update block settings, as they seem more flexible.

In the future, we could potentially offer "shortcut" options for both isSupported and blockSettings.

Unresolved questions

  • Should new API support adding inline styles and SVGs to the editor? See #48884
  • Some features change the block transformation logic. Are we considering supporting it on the API level?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment