Using WordPress JavaScript APIs: The core/editor store
Welcome to part four of our series on using the WordPress JavaScript API’s, in which we explore the API’s that were introduced in WordPress 5.0 and how we can use them to better integrate with other plugins, in a reliable and safe way.
In the previous chapter, we migrated over from our own bare bone Redux implementation to one that uses the WordPress-style of data management. This allowed other plugins to access the data we have stored in our own Redux store. In this chapter, we’ll be deep-diving into WordPress’ own core/editor
store, which contains the data WordPress keeps track of when you’re writing or editing a post. By utilizing the core/editor
store, it will bring our demo application one step closer to becoming completely interoperable with WordPress.
Before we begin
A small notice before we begin: in this post we’ll be showing you one approach on how to interact with WordPress. This example is very much an oversimplification of all that WordPress has to offer, but it’s a good place to start. The names of the selectors we use in the code samples are identical to the ones available in WordPress and the data that is stored and retrieved, is all structured similarly to WordPress’ structure.
The package
WordPress 5.0 introduced a bunch of new JavaScript packages, each with their own specific set of features and tasks. The core/editor
package is the one that manages the editor state. Anything you change when you write content for your post (title, slug, excerpt, content and more), passes through this package and is persisted within the package’s store.
Implementing the package
I know what you might be thinking “This project doesn’t even use WordPress! How will you retrieve information from the editor?!”. You are correct in thinking this. So, to illustrate an approach that will work with WordPress, we’ll be making a ‘dummy’ version of the core/editor
package which will simulate some of the basic action calls and expose selectors that are identical to those WordPress ships (or very close to it). The added benefit of this is that this approach will also show you a way to replace the WordPress editor altogether, without your page builder accidentally breaking plugins such as Yoast SEO!
As the package does a lot of different things, trying to implement all of these separate parts would require a lot of additional code. Most of this additional code isn’t directly related to our project, so we’ll be creating a very simplistic version of the package.
Preparing the actions
WordPress calls two main actions while editing content. One of them is the EDIT_POST
action, which is called whenever the user edits the title, slug or excerpt. The other action, called UPDATE_BLOCK_ATTRIBUTES
, needs to be added due to how WordPress now uses blocks for the various parts of content. This action is called whenever you edit an aspect of the post’s content. As the name suggests, it keeps track of multiple attributes related to the block that is being edited, such as the content of the block or its position on the screen.
Let’s add these actions and discuss the structure of them in a bit more detail.
In the js/src/actions
directory, create a file named core-editor.js
and add the following code:
export const EDIT_POST = 'EDIT_POST'; export const UPDATE_BLOCK_ATTRIBUTES = 'UPDATE_BLOCK_ATTRIBUTES'; /** * Sets the post title attribute. * * @param {string} title The title. * * @returns {{edits: {}, type: string}} The edit post action. */ export function setPostTitle( title ) { return setPostAttribute( 'title', title ); } /** * Sets the post slug attribute. * * @param {string} slug The slug. * * @returns {{edits: {}, type: string}} The edit post action. */ export function setPostSlug( slug ) { return setPostAttribute( 'slug', slug ); } /** * Sets the post excerpt attribute. * * @param {string} excerpt The excerpt. * * @returns {{edits: {}, type: string}} The edit post action. */ export function setPostExcerpt( excerpt ) { return setPostAttribute( 'excerpt', excerpt ); } /** * Sets the passed post attribute to the passed value. * * @param {string} attribute The attribute to set. * @param {string} value The value to set the attribute to. * * @returns {{edits: {}, type: string}} The edit post action. */ function setPostAttribute( attribute, value ) { return { type: EDIT_POST, edits: { [attribute]: value } }; } /** * Sets the post's content. * * @param {string} content The content. * * @returns {{clientId: string, attributes: {content: *}, type: string}} */ export function setPostContent( content ) { return { type: UPDATE_BLOCK_ATTRIBUTES, clientId: 'some-random-block-id', attributes: { content } } }
The above code looks somewhat familiar in terms of how it’s set up when compared to our own actions that we defined for the yoast/api-example
store.
In the above code, we’ve written a small, generic function called setPostAttribute
that allows us to quickly set values in our action. This isn’t necessarily how WordPress approaches this; it’s purely to make our lives easier while simulating WordPress. The functions we export above are just simple wrappers to extract a single property from our store later on.
Last, but not least, the setPostContent
function handles the editing of our content, which in a WordPress context would be a block, but for simplicity’s sake, we’ll just imagine our ‘content text area’ field is a paragraph block. The clientId
property is usually generated by WordPress and differs per block. Because we’re not focusing on a ‘block style’ type of implementation for our post’s content, inserting some random string like in the example above is sufficient.
Preparing the reducer
After defining the actions, our next step is to introduce a new reducer that is similar to the one WordPress uses. In your js/src/reducers
directory, create a file called core-editor.js
and add the following code to it:
import { EDIT_POST, UPDATE_BLOCK_ATTRIBUTES } from "../actions/core-editor"; const INITIAL_STATE = { present: { edits: {}, blocks: {} } }; /** * Reducer for the editor data. * * @param {Object} state The current state. * @param {Object} action The current action. * * @returns {Object} The new state. */ export default function editorReducer( state = INITIAL_STATE, action ) { switch ( action.type ) { case EDIT_POST: return { present: { ...state.present, edits: { ...state.present.edits, ...action.edits, } } }; case UPDATE_BLOCK_ATTRIBUTES: return { present: { ...state.present, blocks: { ...state.present.blocks, byClientId: { [action.clientId]: { attributes: { ...action.attributes } } } } } }; } return state; }
The above code isn’t much different in terms of setup compared to the reducer we’ve written in part two. We import all the defined actions, ensure we have an initial state set and then let the reducer interpret the incoming action to update the state accordingly. Due to the larger amount of data that WordPress stores within the state, there is a lot of nesting going on in the editorReducer
. For clarity sake, we’ve limited to only the properties that are necessary for this project.
Creating the selectors
In the src/selectors
directory we’ll add a new selector by creating a file named core-editor.js
with the following two functions:
/** * Gets the edited post attribute by it's name. * * @param {Object} state The state. * @param {string} attribute The attribute to retrieve. * * @returns {string} The attributes value or an empty string if it doesn't exist. */ export function getEditedPostAttribute( state, attribute ) { switch( attribute ) { case 'title': return state.editor.present.edits.title || ''; case 'slug': return state.editor.present.edits.slug || ''; case 'excerpt': return state.editor.present.edits.excerpt || ''; case 'content': return getEditedPostContent( state ) || ''; default: return ''; } } /** * Gets the edited post content. * * @param {Object} state The state. * * @returns {string} The edited post content. */ export function getEditedPostContent( state ) { const blocks = getBlocksForSerialization( state ); if ( Object.entries( blocks ).length === 0 ) { return ''; } return blocks.byClientId['some-random-block-id'].attributes.content; } /** * Returns a set of blocks which are to be used in consideration of the post's * generated save content. * * @param {Object} state Editor state. * * @return {WPBlock[]} Filtered set of blocks for save. */ function getBlocksForSerialization( state ) { // This is an overly simplified version of the original method. return state.editor.present.blocks; }
The getEditedPostAttribute
selector is rather simple and just retrieves the specific attributes that aren’t considered part of any blocks within the WordPress editor. Please note that they default to empty strings in our code as the various properties aren’t always present in the state, resulting in possible undefined
values later on. Also, note that this function allows you to collect the content (i.e. all blocks) by passing along content
as the property to retrieve. This is just a shortcut to calling the getEditedPostContent
function, which does all the heavy lifting.
In the above example, the getEditedPostContent
selector is responsible for retrieving ‘blocks’ from the content. However, because we’re not actually simulating blocks, our version of this function simply checks if there is a blocks property and retrieves the content of our ‘random block’.
Please note: In the real implementation of this selector, you’re working with the WordPress-based output of the various blocks. This is before it’s parsed into HTML, and does not look like plain text like we are using in this example. If you want to then parse said content into HTML, you can pass the content of the block to wp.blocks.parse()
, which will return the actual HTML output of the block.
Updating the forms
React
First, let’s open up the src/js/simple-form-react.js
file and add the necessary changes to ensure we’re using the correct stores and selectors.
Replace the event handlers on lines 24 to 66 with the following:
/** * Handles the changing of the title field. * * @param {Object} event The event that took place. * * @returns void */ onTitleChange( event ) { this.props.setPostTitle( event.target.value ); } /** * Handles the changing of the slug field. * * @param {Object} event The event that took place. * * @returns void */ onSlugChange( event ) { this.props.setPostSlug( event.target.value ); } /** * Handles the changing of the content field. * * @param {Object} event The event that took place. * * @returns void */ onContentChange( event ) { this.props.setPostContent( event.target.value ); } /** * Handles the changing of the excerpt field. * * @param {Object} event The event that took place. * * @returns void */ onExcerptChange( event ) { this.props.setPostExcerpt( event.target.value ); }
You might be wondering why we decided to rename the setForm<Property>
methods. The idea behind this is that this way we’ll have a better separation between our own custom store and the simulated core/editor
store, were we to implement both of them in a single form.
Next, scroll down to the mapStateToProps
method and replace it with the following snippet:
const mapStateToProps = ( select ) => { const store = select( "core/editor" ); return { title: store.getEditedPostAttribute( 'title' ), slug: store.getEditedPostAttribute( 'slug' ), content: store.getEditedPostContent(), excerpt: store.getEditedPostAttribute( 'excerpt' ), }; }
The above changes replace our custom selectors with those associated with the core/editor
store and sets the proper selector methods.
Last, but certainly not least, scroll down to the mapDispatchToProps
method and replace it with the following:
const mapDispatchToProps = ( dispatch ) => { const store = dispatch( "core/editor" ); return { setPostTitle: store.setPostTitle, setPostSlug: store.setPostSlug, setPostContent: store.setPostContent, setPostExcerpt: store.setPostExcerpt, } }
Again, the changes are minimal: We call our simulated store and ensure we assign the methods within that store to the props.
We’re not out of the woods just yet: One last thing we need to do to prepare the React version of our form, is replace the registerStore
in our src/js/app.js
file so that the WordPress API is aware of our fake core/editor
store.
Replace the imports of the root reducer, selectors and actions with the following:
import editorReducer from "./reducers/core-editor"; import * as editorActions from "./actions/core-editor"; import * as editorSelectors from "./selectors/core-editor";
And replace the registration of our own store, with the following:
registerStore( "core/editor", { reducer: combineReducers( { editor: editorReducer } ), selectors: editorSelectors, actions: editorActions, } );
Make sure you save the changes and rebuild the files with yarn build-dev
before testing out the application.
jQuery
To get the jQuery version of our form compatible with the new store and selectors, we’ll have to make a few minor changes. In your src/js/simple-form.js file, remove the imports of the root reducer, selectors and actions and replace these lines with the following snippet:
import editorReducer from "./reducers/core-editor"; import * as editorSelectors from "./selectors/core-editor"; import * as editorActions from "./actions/core-editor";
Directly below that, we’ll also have to alter the NAMESPACE
constant and register the core/editor
store.
const NAMESPACE = "core/editor"; registerStore( NAMESPACE, { reducer: combineReducers( { editor: editorReducer } ), selectors: editorSelectors, actions: editorActions, } );
Next, we’ll have to attach the events and ensure they dispatch to the right actions, as per the following code:
// Attach events $( '#title' ).on( 'keyup', function() { dispatch( NAMESPACE ).setPostTitle( this.value ); } ); $( '#slug' ).on( 'keyup', function() { dispatch( NAMESPACE ).setPostSlug( this.value ); } ); $( '#content' ).on( 'keyup', function() { dispatch( NAMESPACE ).setPostContent( this.value ); } ); $( '#excerpt' ).on( 'keyup', function() { dispatch( NAMESPACE ).setPostExcerpt( this.value ); } );
Last, we’ll change the section where we subscribe to the store, so it uses the proper methods for data retrieval. This results in the following code:
// Subscribe to changes subscribe( function() { const store = select( NAMESPACE ); $( '#title' ).val( store.getEditedPostAttribute( 'title' ) ); $( '#slug' ).val( store.getEditedPostAttribute( 'slug' ) ); $( '#content' ).val( store.getEditedPostContent() ); $( '#excerpt' ).val( store.getEditedPostAttribute( 'excerpt' ) ); } );
That’s it for the jQuery version of our form! Make sure you run yarn build-dev
, otherwise the changes won’t be reflected in our application.
What about our own reducer?!
In the previous chapter, we were using our own reducer, store and selectors to persist and retrieve our data. You might have noticed that after the changes made in this chapter, we’re no longer referencing yoast/api-example
anywhere. So why keep it? Well, in one of our future chapters, we’ll be expanding the form once again and be reintroducing the custom store to do some stuff for us, allow us to keep our custom data separate from that of the core/editor
store.
Conclusion
That’s it for chapter four in our series on how to use the WordPress JavaScript APIs to better integrate plugins and page builders. In the following chapter, we’ll be looking into the slot/fill API, allowing us to integrate with the WordPress sidebar!
Check out the entire series (up until now):
Using the WordPress JavaScript APIs for fun and profit – The beginning
Using the WordPress JavaScript APIs for fun and profit part two – Introducing Redux
Using the WordPress JavaScript APIs for fun and profit part three – Connecting to the WordPress API
Coming up next!
-
Event
WordCamp Netherlands 2024
November 29 - 30, 2024 Team Yoast is at Sponsoring WordCamp Netherlands 2024! Click through to see who will be there, what we will do, and more! See where you can find us next » -
SEO webinar
The SEO update by Yoast - October & November 2024 Edition
26 November 2024 Get expert analysis on the latest SEO news developments with Carolyn Shelby and Alex Moss. Join our upcoming update! ?️ All Yoast SEO webinars »