Block editor things to remember.
Last active
February 24, 2025 04:56
-
-
Save dlh01/1b18743cb03e26b5e094ec470aca2690 to your computer and use it in GitHub Desktop.
Block editor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 === ''} | |
/> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' : '' })} | |
/> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
attributes | |
clientId | |
context | |
insertBlocksAfter (fn) | |
isSelected | |
isSelectionEnabled | |
mergeBlocks (fn) | |
name | |
onRemove (fn) | |
onReplace (fn) | |
setAttributes | |
toggleSelection (fn) | |
*/ | |
interface EditProps extends BlockEditProps<any> { | |
name: string; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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' ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
wp.blocks.getBlockTypes().forEach(block => { | |
wp.data.dispatch('core/block-editor').insertBlock( | |
wp.blocks.createBlock(block.name) | |
); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} |
<Disabled><Sandbox /></Disabled>
: https://l.alley.dev/e34d333015
^ although that should have used scripts
prop instead of script tag
<View><Sandbox /></View>
: https://l.alley.dev/e8dd1c2fc7
^ sandbox with an iframe
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
block descriptions:
Embed a [service] [noun].
Embed a YouTube playlist.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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