The plugin extends the WordPress block editor (Gutenberg) with a custom Album Parent Selector component that allows editors to assign an artist to an album directly from the document sidebar.
Overview
The parent selector is implemented as a React component using WordPress’s @wordpress/plugins API. It appears in the document settings panel when editing an Album post.
Component Implementation
Location: src/editor.js:10
Complete Source Code
import { registerPlugin } from '@wordpress/plugins' ;
import { PluginDocumentSettingPanel } from '@wordpress/editor' ;
import { SelectControl , Spinner } from '@wordpress/components' ;
import { useSelect , useDispatch } from '@wordpress/data' ;
import { store as editorStore } from '@wordpress/editor' ;
import { store as coreStore } from '@wordpress/core-data' ;
import { __ } from '@wordpress/i18n' ;
import { useMemo } from '@wordpress/element' ;
const AlbumParentSelector = () => {
const { postType , currentParent } = useSelect (( select ) => ({
postType: select ( editorStore ). getCurrentPostType (),
currentParent: select ( editorStore ). getEditedPostAttribute ( 'parent' ),
}), []);
const { artists , isResolving } = useSelect (( select ) => {
if ( postType !== 'music_album' ) {
return { artists: null , isResolving: false };
}
return {
artists: select ( coreStore ). getEntityRecords ( 'postType' , 'music_artist' , {
per_page: - 1 ,
orderby: 'title' ,
order: 'asc' ,
status: 'publish,draft' ,
}),
isResolving: select ( coreStore ). isResolving ( 'getEntityRecords' , [
'postType' ,
'music_artist' ,
{ per_page: - 1 , orderby: 'title' , order: 'asc' , status: 'publish,draft' },
]),
};
}, [ postType ]);
const { editPost } = useDispatch ( editorStore );
const options = useMemo (() => {
if ( ! artists ) return [];
return [
{ label: __ ( '— Select Artist —' , 'bifrost-music' ), value: 0 },
... artists . map (( artist ) => ({
label: artist . title . rendered ,
value: artist . id ,
})),
];
}, [ artists ]);
if ( postType !== 'music_album' ) {
return null ;
}
if ( isResolving ) {
return (
< PluginDocumentSettingPanel
name = "album-parent-artist"
title = { __ ( 'Artist' , 'bifrost-music' ) }
>
< Spinner />
</ PluginDocumentSettingPanel >
);
}
return (
< PluginDocumentSettingPanel
name = "album-parent-artist"
title = { __ ( 'Artist' , 'bifrost-music' ) }
>
< SelectControl
__next40pxDefaultSize
__nextHasNoMarginBottom
value = { currentParent || 0 }
options = { options }
onChange = { ( value ) => editPost ({ parent: parseInt ( value , 10 ) }) }
/>
</ PluginDocumentSettingPanel >
);
};
registerPlugin ( 'album-parent-selector' , {
render: AlbumParentSelector ,
});
Component Breakdown
Data Selection with useSelect
The component uses two useSelect hooks to retrieve data from WordPress stores.
Current Post Data
Location: src/editor.js:11
const { postType , currentParent } = useSelect (( select ) => ({
postType: select ( editorStore ). getCurrentPostType (),
currentParent: select ( editorStore ). getEditedPostAttribute ( 'parent' ),
}), []);
This retrieves:
postType: The current post type being edited (e.g., 'music_album')
currentParent: The currently selected parent artist ID
Artist Posts Data
Location: src/editor.js:16
const { artists , isResolving } = useSelect (( select ) => {
if ( postType !== 'music_album' ) {
return { artists: null , isResolving: false };
}
return {
artists: select ( coreStore ). getEntityRecords ( 'postType' , 'music_artist' , {
per_page: - 1 ,
orderby: 'title' ,
order: 'asc' ,
status: 'publish,draft' ,
}),
isResolving: select ( coreStore ). isResolving ( 'getEntityRecords' , [
'postType' ,
'music_artist' ,
{ per_page: - 1 , orderby: 'title' , order: 'asc' , status: 'publish,draft' },
]),
};
}, [ postType ]);
This:
Early returns if not editing an album
Fetches all artist posts via getEntityRecords
Includes both published and draft artists
Sorts alphabetically by title
Tracks loading state with isResolving
The per_page: -1 parameter fetches all artists without pagination.
Updating Post Data with useDispatch
Location: src/editor.js:36
const { editPost } = useDispatch ( editorStore );
The editPost function is used to update the post’s parent attribute when the user makes a selection.
Memoized Options
Location: src/editor.js:38
const options = useMemo (() => {
if ( ! artists ) return [];
return [
{ label: __ ( '— Select Artist —' , 'bifrost-music' ), value: 0 },
... artists . map (( artist ) => ({
label: artist . title . rendered ,
value: artist . id ,
})),
];
}, [ artists ]);
This creates the dropdown options array:
First option is ”— Select Artist —” with value 0 (no parent)
Remaining options map each artist to { label, value } format
Uses useMemo to avoid recreating the array on every render
The title.rendered property contains the artist title with HTML entities decoded and rendered.
Conditional Rendering
Only Show for Albums
Location: src/editor.js:50
if ( postType !== 'music_album' ) {
return null ;
}
The component only renders when editing an Album post.
Loading State
Location: src/editor.js:54
if ( isResolving ) {
return (
< PluginDocumentSettingPanel
name = "album-parent-artist"
title = { __ ( 'Artist' , 'bifrost-music' ) }
>
< Spinner />
</ PluginDocumentSettingPanel >
);
}
While artists are loading, a spinner is displayed.
The Select Control
Location: src/editor.js:65
return (
< PluginDocumentSettingPanel
name = "album-parent-artist"
title = { __ ( 'Artist' , 'bifrost-music' ) }
>
< SelectControl
__next40pxDefaultSize
__nextHasNoMarginBottom
value = { currentParent || 0 }
options = { options }
onChange = { ( value ) => editPost ({ parent: parseInt ( value , 10 ) }) }
/>
</ PluginDocumentSettingPanel >
);
Key props:
value: The current parent ID (defaults to 0 if none)
options: The memoized array of artists
onChange: Updates the post’s parent when selection changes
The __next40pxDefaultSize and __nextHasNoMarginBottom props are WordPress design system flags for styling consistency.
Plugin Registration
Location: src/editor.js:81
registerPlugin ( 'album-parent-selector' , {
render: AlbumParentSelector ,
});
This registers the component as a WordPress editor plugin with the ID 'album-parent-selector'.
Asset Enqueuing
The compiled JavaScript is enqueued via the EditorAssets service.
Location: inc/Editor/EditorAssets.php:36
private function enqueue () : void
{
$script_asset = include PLUGIN_DIR . '/build/editor.asset.php' ;
wp_enqueue_script (
'bifrost-music-editor' ,
plugin_dir_url ( PLUGIN_FILE ) . 'build/editor.js' ,
$script_asset [ 'dependencies' ],
$script_asset [ 'version' ],
true
);
// Set translations for editor scripts.
wp_set_script_translations ( 'bifrost-music-editor' , 'bifrost-music' );
}
The editor.asset.php file is auto-generated by @wordpress/scripts and contains dependency and version information.
Translation Support
Location: inc/Editor/EditorAssets.php:48
wp_set_script_translations ( 'bifrost-music-editor' , 'bifrost-music' );
This enables JavaScript internationalization using the __() function in the React component.
REST API Integration
The component works because the Album post type registers a custom parent field with the REST API:
Location: inc/Content/Album.php:150
private function restRegister () : void
{
register_rest_field ( Definitions :: POST_TYPE_ALBUM , 'parent' , [
'get_callback' => fn ( $post ) => ( int ) $post [ 'parent' ],
'update_callback' => fn ( $value , $post ) => wp_update_post ([
'ID' => $post -> ID ,
'post_parent' => ( int ) $value
]),
'schema' => [
'description' => __ ( 'Parent Artist ID' , 'bifrost-music' ),
'type' => 'integer' ,
'context' => [ 'view' , 'edit' ]
]
]);
}
Without this registration, the non-hierarchical Album post type wouldn’t expose the parent field in REST responses.
Build Process
The JavaScript is built using @wordpress/scripts:
This:
Compiles JSX to JavaScript
Bundles dependencies
Generates build/editor.js and build/editor.asset.php
User Experience Flow
Editor opens an Album post
Component checks if post type is music_album
Component fetches all Artist posts via REST API
Loading spinner appears while data loads
Dropdown appears with all artists sorted alphabetically
User selects an artist from the dropdown
onChange fires , calling editPost({ parent: artistId })
WordPress updates the post’s parent via REST API
Component re-renders with new parent selected
Best Practices Demonstrated
Performance Optimization Uses useMemo to avoid recreating options array on every render
Loading States Shows a spinner while data is being fetched
Conditional Rendering Only renders for the correct post type to avoid unnecessary API calls
Internationalization All user-facing strings use __() for translation support
Extending the Component
Developers can extend this pattern for other relationships:
// Example: Add a "Record Label" selector
const AlbumLabelSelector = () => {
const { postType , currentLabel } = useSelect (( select ) => ({
postType: select ( editorStore ). getCurrentPostType (),
currentLabel: select ( editorStore ). getEditedPostAttribute ( 'meta' )?. record_label ,
}), []);
const { editPost } = useDispatch ( editorStore );
// ... similar implementation
};
Next Steps
Custom Post Types Learn how the Album REST API integration works
Architecture Understand how editor assets are loaded via service providers