Skip to content

Instantly share code, notes, and snippets.

@dlh01
Last active February 24, 2025 04:56
Show Gist options
  • Save dlh01/1b18743cb03e26b5e094ec470aca2690 to your computer and use it in GitHub Desktop.
Save dlh01/1b18743cb03e26b5e094ec470aca2690 to your computer and use it in GitHub Desktop.
Block editor

Block editor things to remember.

import { store as blocksStore } from '@wordpress/blocks';
const blockType = useSelect((select) => select(blocksStore).getBlockType(name), [name]);
const map = blockType.attributes.myAttribute.enum.map((e) => e);
// https://l.alley.dev/2551c5bf3d
// Global dependencies.
import useSWR from 'swr';
// WordPress dependencies.
import apiFetch from '@wordpress/api-fetch';
import { ComboboxControl } from '@wordpress/components';
import { useDebouncedInput } from '@wordpress/compose';
import { __, sprintf } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
// Local interfaces.
interface BlockAttributes {
id?: string,
}
/**
* Internal component for displaying the setup state, including the currently selected item.
*/
function Setup({
attributes,
setAttributes,
}: {
attributes: BlockAttributes,
setAttributes: (next: any) => void,
}) {
/**
* Using the currently selected item as the search term has the effect of causing that item to
* be searched for when the component loads, which populates the options list with that item.
*/
const [searchTerm, setSearchTerm, debouncedSearchTerm] = useDebouncedInput(attributes.id);
/**
* TODO: The matching item should be captured so that it's always available to use as an option,
* since the label shown in the input is drawn from the options list.
*/
const { data, isLoading }: { data: any, isLoading: boolean } = useSWR(
addQueryArgs('alley/v1/blocks/block-name/search', { search: debouncedSearchTerm }),
(key) => apiFetch({ path: key }),
);
return (
<ComboboxControl
label={__('Search for items', 'alley')}
value={attributes.item}
onChange={(next) => setAttributes({ item: next || '' })}
onFilterValueChange={(next) => setSearchTerm(next)}
options={isLoading
// Small hack. See https://github.com/WordPress/gutenberg/issues/25798.
? [{
/* translators: %s: search term */
label: searchTerm ? sprintf(__('Searching for "%s"…', 'alley'), searchTerm) : __('Loading…', 'alley'),
value: '',
disabled: true,
}]
: data.items.map((item: any) => ({
label: item.title,
value: item.id,
}))}
// @ts-ignore
expandOnFocus={attributes.item === ''}
/>
);
}
import { createInterpolateElement } from '@wordpress/element';
const c = <CheckboxControl
// @ts-ignore - `createInterpolateElement()` is fine here.
label={createInterpolateElement(
sprintf(
/* translators: %s: HTML attribute */
__('Include %s', 'foo'),
'<code>data-whatever="true"</code>',
),
{
code: <code />,
},
)}
checked={attributes.whatever === 'true'}
onChange={(next: boolean) => setAttributes({ whatever: next ? 'true' : '' })}
/>
// Global dependencies.
import { useState } from 'react';
// WordPress dependencies.
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps, store as blocksStore } from '@wordpress/blocks';
import {
Button,
Placeholder,
TextControl,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { format as formattedDate } from '@wordpress/date';
import { View } from '@wordpress/primitives';
interface EditProps extends BlockEditProps<any> {
name: string;
}
const dateFormattedForInput = (date: any) => formattedDate('Y-m-d\\TH:i', date);
const dateFormattedForAttribute = (date: any) => formattedDate('Y-m-d\\TH:i:sP', date);
/**
* @return {WPElement} Element to render.
*/
export default function Edit({
name,
isSelected,
attributes,
setAttributes,
}: EditProps) {
const {
date: dateAttribute,
} = attributes;
// @ts-ignore
const blockType = useSelect((select) => select(blocksStore).getBlockType(name), [name]);
const [showSetup, setShowSetup] = useState(true);
const [dateInput, setDateInput] = useState(dateFormattedForInput(new Date()));
return (
<View {...useBlockProps()}>
{showSetup ? (
<Placeholder
label={blockType.title}
icon={blockType?.icon?.src}
instructions={blockType.description}
>
<form
className="setup-form"
onSubmit={(event) => {
event.preventDefault();
setAttributes({
date: dateFormattedForAttribute(dateInput),
});
setShowSetup(false);
}}
>
<TextControl
label="End Date"
value={dateInput}
// @ts-ignore
type="datetime-local"
onChange={(value) => setDateInput(value)}
required
/>
<div>
<Button
variant="primary"
type="submit"
>
Embed
</Button>
</div>
</form>
</Placeholder>
) : (
<>
{isSelected ? (
<div className="controls">
<div>
<TextControl
label="End Date"
value={dateFormattedForInput(dateAttribute)}
// @ts-ignore
type="datetime-local"
onChange={(value) => setAttributes({ date: dateFormattedForAttribute(value) })}
/>
</div>
</div>
) : null}
</>
)}
</View>
);
}
/*
attributes
clientId
context
insertBlocksAfter (fn)
isSelected
isSelectionEnabled
mergeBlocks (fn)
name
onRemove (fn)
onReplace (fn)
setAttributes
toggleSelection (fn)
*/
interface EditProps extends BlockEditProps<any> {
name: string;
}
<?php
/**
* Register REST API routes to support the editing interface.
*/
function my_namespace_my_block_name_block_rest_api_init(): void {
/*
* Parse the raw embed code for use as attributes.
* Accepts an embed code like:
*
* ...
*/
register_rest_route(
'my-namespace/v1/blocks',
'/my-block/attributes',
[
'methods' => 'POST',
'permission_callback' => fn () => current_user_can( 'edit_posts' ),
'show_in_index' => false,
'args' => [
'embed_code' => [
'type' => 'string',
'required' => true,
],
],
'callback' => function ( $request ) {
$id = '';
// Parse the ID from the embed code.
// ...
if ( strlen( $id ) === 0 || ! is_numeric( $id ) ) {
return new WP_Error(
'invalid_embed_code',
__( 'Sorry, the ID could not be found in the embed code.', 'my-namespace' ),
[ 'status' => 400 ],
);
}
return rest_ensure_response(
[
'id' => $id,
],
);
},
],
);
}
add_action( 'rest_api_init', 'my_namespace_my_block_name_block_rest_api_init' );
// Global dependencies.
import {
Dispatch,
Reducer,
useEffect,
useReducer,
} from 'react';
// WordPress dependencies.
import apiFetch from '@wordpress/api-fetch';
import { BlockControls, useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import {
Button,
Disabled,
Placeholder,
SandBox,
TextareaControl,
ToolbarButton,
ToolbarGroup,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __, _x } from '@wordpress/i18n';
import { edit, cancelCircleFilled } from '@wordpress/icons';
// Internal dependencies.
import './index.scss';
// Internal types.
interface EditProps extends BlockEditProps<any> {
name: string;
}
// Internal component state.
type EditState = {
step: 'setup' | 'pending' | 'preview';
input: string;
error: string;
};
type EditAction = {
type: 'submitted_form' | 'received_attributes' | 'began_replacement' | 'canceled_replacement';
} | {
type: 'input_changed' | 'submission_failed';
payload: string;
};
// Internal reducer.
const reducer: Reducer<EditState, EditAction> = (state, action) => {
let next = { ...state };
switch (action.type) {
case 'submitted_form': {
next.step = 'pending';
break;
}
case 'received_attributes':
case 'canceled_replacement': {
next.step = 'preview';
break;
}
case 'began_replacement': {
next = {
step: 'setup',
input: '',
error: '',
};
break;
}
case 'input_changed': {
next.input = action.payload;
break;
}
case 'submission_failed': {
next.step = 'setup';
next.error = action.payload;
break;
}
default: {
// @ts-ignore
throw new Error(`Invalid action type: ${action.type}`);
}
}
return next;
};
/**
* The block edit function.
*
* @return {WPElement} Element to render.
*/
export default function Edit({ name, attributes, setAttributes }: EditProps) {
// This block type.
const blockType = useSelect((select) => (select('core/blocks') as any).getBlockType(name), [name]);
// Assume that the block has the necessary data as long as it has an ID.
const hasEmbedCode = attributes.id !== '';
// Component state.
const [state, dispatch]: [EditState, Dispatch<EditAction>] = useReducer(reducer, {
step: hasEmbedCode ? 'preview' : 'setup',
input: '',
error: '',
});
// Submit the embed code to the server for parsing after changing to the 'pending' state.
useEffect(() => {
if (state.step === 'pending') {
apiFetch({
path: '/my-namespace/v1/blocks/my-block/attributes',
method: 'POST',
data: {
embed_code: state.input,
},
}).then((data: any) => {
setAttributes(data);
dispatch({ type: 'received_attributes' });
}).catch((err) => {
dispatch({
type: 'submission_failed',
payload: err?.message || __('Sorry, this content could not be embedded.', 'my-namespace'),
});
});
}
}, [state.step, state.input, setAttributes]);
return (
<div {...(useBlockProps())}>
<BlockControls>
<ToolbarGroup>
{state.step === 'preview' ? (
<ToolbarButton
label={__('Replace', 'my-namespace')}
onClick={() => dispatch({ type: 'began_replacement' })}
icon={edit}
/>
) : null}
{state.step === 'setup' && hasEmbedCode ? (
<ToolbarButton
label={__('Cancel', 'my-namespace')}
onClick={() => dispatch({ type: 'canceled_replacement' })}
icon={cancelCircleFilled}
/>
) : null}
</ToolbarGroup>
</BlockControls>
{state.step === 'setup' || state.step === 'pending' ? (
<Placeholder
label={blockType.title}
icon={blockType?.icon?.src}
instructions={blockType.description}
isColumnLayout
>
<form
onSubmit={(event) => {
event.preventDefault();
dispatch({ type: 'submitted_form' });
}}
>
<TextareaControl
style={{ fontFamily: 'var(--wp--preset--font-family--mono)' }}
value={state.input}
placeholder={__('Enter embed code here…', 'my-namespace')}
onChange={(next) => {
dispatch({
type: 'input_changed',
payload: next,
});
}}
disabled={state.step === 'pending'}
/>
{state.error ? (
<p>{state.error}</p>
) : null }
<Button
style={{ width: 'fit-content' }}
variant="primary"
type="submit"
disabled={!state.input || state.step === 'pending'}
>
{_x('Embed', 'button label', 'my-namespace')}
</Button>
</form>
</Placeholder>
) : null}
{state.step === 'preview' ? (
<Disabled>
<SandBox
html={`...`}
scripts={[`...`]}
type="embed"
/>
</Disabled>
) : null}
</div>
);
}
// Take a string of HTML, create blocks out of it, and inject the result as inner blocks.
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { rawHandler } from '@wordpress/blocks';
import { useDispatch } from '@wordpress/data';
const Edit = ({ clientId }) => {
const { replaceInnerBlocks } = useDispatch('core/block-editor');
const blockProps = useBlockProps();
const rawHtml = '<p>Foo bar</p>';
// When the block is mounted, convert the raw meta HTML to inner blocks of this block.
useEffect(() => {
const blocks = rawHandler({ HTML: rawHtml });
replaceInnerBlocks(clientId, blocks);
}, []);
return (
<div {...blockProps}>
<InnerBlocks />
</div>
);
};
wp.blocks.getBlockTypes().forEach(block => {
wp.data.dispatch('core/block-editor').insertBlock(
wp.blocks.createBlock(block.name)
);
});
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps, store as blocksStore } from '@wordpress/blocks';
import { Placeholder } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import './index.scss';
interface EditProps extends BlockEditProps<any> {
context: object;
name: string;
}
/**
* The block edit function.
*
* @return {WPElement} Element to render.
*/
export default function Edit({ name }: EditProps) {
// @ts-ignore
const blockType = useSelect((select) => select(blocksStore).getBlockType(name), [name]);
return (
<div {...(useBlockProps())}>
<Placeholder
label={blockType.title}
icon={blockType?.icon?.src}
instructions={blockType.description}
/>
</div>
);
}
import { serialize } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
export default function Edit({ clientId }) => {
const blockObj = useSelect((select) => select('core/block-editor').getBlock(clientId));
const innerBlockHtml = serialize(blockObj.innerBlocks);
}

Designing a [block] is a small tractable task that can be finished and called done. You’re creating a perfect, beautiful, reusable jewel. All engineers really want to do is write [blocks]. The top-down approach doesn’t have this nice property; the product is forever incomplete. Yes, top-down can be less appealing to some people. Do it anyway.

Adapted


block descriptions:

Embed a [service] [noun].

Embed a YouTube playlist.

import { getSettings as dateSettings } from '@wordpress/date';
const { timezone } = dateSettings();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment