In part 1 of this guide, we set up a new WordPress plugin with an empty options page. In this part, we will add a basic text input field to our options page, as well as a save button to save our changes.
Step 1: Add some content to our options page
Let’s start by adding some code to our admin.js
file:
JSimport { __ } from '@wordpress/i18n'; import { createRoot } from 'react-dom/client'; const OptionsPage = () => { return ( <div> <h1>{ __( 'Options Page', 'wop' ) }</h1> </div> ); }; const rootElement = document.getElementById( 'wop-admin-page' ); if ( rootElement ) { createRoot( rootElement ).render( <OptionsPage /> ); }
In the terminal, run the following command:
Terminalnpm run start
This command will keep running in your terminal window, and rebuild our files whenever it detects a change. If you go back to your WordPress admin area, you should now see something like this:
Let’s walk to this code step by step. First, we’re importing some of the dependencies we need:
JSimport { __ } from '@wordpress/i18n'; import { createRoot } from 'react-dom/client'; const OptionsPage = () => { return ( <div> <h1>{ __( 'Options Page', 'wop' ) }</h1> </div> ); }; const rootElement = document.getElementById( 'wop-admin-page' ); if ( rootElement ) { createRoot( rootElement ).render( <OptionsPage /> ); }
The first thing we’re doing is importing the __
function from the @wordpress/i18n
package. This is the JavaScript equivalent of the PHP __
function, and makes any text you render translatable. Getting translations to work is beyond the scope of this guide, but it is always a good idea to make sure any strings you add to your code are translatable.
We’re also importing createRoot
, which is a React function that essentially creates a blank canvas (or “root”) that we can add React components to.
A neat thing about the @wordpress/scripts
package is that it automatically updates our asset file. Take a look at the build/admin.asset.php
file, and you should see something like this:
PHP<?php return array('dependencies' => array('react', 'react-dom', 'wp-i18n'), 'version' => 'e322b079c34ae8c29300');
As you can see, the dependencies we added in our JavaScript are listed here, as well as a randomly generated version number. We are using this in the wop_enqueue_scripts
function from Part 1, so that we don’t need to manually update the dependencies of our script every time we add or remove something in our JavaScript file.
By using a random version number, the browser will automatically use the new version instead of the cached version.
Next, we’re creating a very simple React component, that just has a <div>
and a heading. This will serve as a container for our option input that we’ll add later.
JSimport { __ } from '@wordpress/i18n'; import { createRoot } from 'react-dom/client'; const OptionsPage = () => { return ( <div> <h1>{ __( 'Options Page', 'wop' ) }</h1> </div> ); }; const rootElement = document.getElementById( 'wop-admin-page' ); if ( rootElement ) { createRoot( rootElement ).render( <OptionsPage /> ); }
Finally, we’re rendering this React component onto our options page. To do this, we use the getElementById
function to find the empty <div>
that we added to our options page. If it exists, we turn it into the root of our React application and render the <OptionsPage>
component that we just created.
JSimport { __ } from '@wordpress/i18n'; import { createRoot } from 'react-dom/client'; const OptionsPage = () => { return ( <div> <h1>{ __( 'Options Page', 'wop' ) }</h1> </div> ); }; const rootElement = document.getElementById( 'wop-admin-page' ); if ( rootElement ) { createRoot( rootElement ).render( <OptionsPage /> ); }
Step 2: Add a text input to our options page
Obviously, an empty options page isn’t exactly useful. What we really want to do is add some inputs that are synced with our site’s options. Let’s start by adding a simple text input:
JSimport { __ } from '@wordpress/i18n'; import { createRoot } from 'react-dom/client'; import { Card, CardBody, TextControl } from '@wordpress/components'; const OptionsPage = () => { return ( <div> <h1>{ __( 'Options Page', 'wop' ) }</h1> <Card> <CardBody> <TextControl label={ __( 'Custom Field', 'wop' ) } help={ __( 'This is a custom field.', 'wop' ) } /> </CardBody> </Card> </div> ); }; const rootElement = document.getElementById( 'wop-admin-page' ); if ( rootElement ) { createRoot( rootElement ).render( <OptionsPage /> ); }
We import the <TextControl>
component from the @wordpress/components
package, which adds a basic text input field with a label and description. We also wrap it in the <Card>
and <CardBody>
components to make it look a bit nicer.
I highly recommend checking out the Gutenberg Storybook, which has interactive examples of all components in the @wordpress/components
package with documentation and an overview of the props for each component.
When you refresh the page, you’ll see a working but empty text input with a label and description. You can type in it, but it won’t do anything just yet.
Step 3: Adding a new option to our plugin
To register a setting for our options page, head back to our plugin file and add the following code:
PHPfunction wop_register_settings() { register_setting( 'wop', 'wop_custom_field', array( 'type' => 'string', 'description' => __( 'A custom field.', 'wop' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); } add_action( 'admin_init', 'wop_register_settings' ); add_action( 'rest_api_init', 'wop_register_settings' );
Here, we’re using the register_setting
function to register a new option named wop_custom_field
. In the options array, there’s a few things to pay attention to:
- We’re setting the
type
tostring
. This tells WordPress what type of data should be in this option. - We’re adding
sanitize_text_field
as oursanitize_callback
. When saving the option, WordPress will automatically run the new value through this function before saving it to the database, to prevent saving any unwanted or dangerous content. You can also use a custom function as your callback. - By setting
show_in_rest
totrue
, we ensure that the option is visible in the REST API, which is what we will use in the next step. - Finally, setting
default
to an empty string ensures we’ll always have a value when retrieving the option.
We’re registering our setting in two separate hooks—admin_init
and rest_api_init
. This way, the setting will be registered on any admin pages as well as on any API requests.
Step 4: Adding the new option to our text input
With our new option registered in WordPress, let’s add it to our text input:
JSimport { __ } from '@wordpress/i18n'; import { useState, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import { Card, CardBody, TextControl } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; const OptionsPage = () => { const [ customField, setCustomField ] = useState( '' ); useEffect( () => { const fetchSettings = async () => { const { wop_custom_field } = await apiFetch( { path: '/wp/v2/settings?_fields=wop_custom_field', } ); setCustomField( wop_custom_field ); }; fetchSettings().catch( ( error ) => { console.error( error ); } ); }, [] ); return ( <div> <h1>{ __( 'Options Page', 'wop' ) }</h1> <Card> <CardBody> <TextControl label={ __( 'Custom Field', 'wop' ) } help={ __( 'This is a custom field.', 'wop' ) } value={ customField } onChange={ setCustomField } /> </CardBody> </Card> </div> ); }; const rootElement = document.getElementById( 'wop-admin-page' ); if ( rootElement ) { createRoot( rootElement ).render( <OptionsPage /> ); }
There’s a lot going on here, so let’s walk through it step by step. First, we’re importing some new dependencies.
JSimport { useState, useEffect } from 'react'; import apiFetch from '@wordpress/api-fetch';
useState
and useEffect
are both React hooks. useState
is used to manage state in a functional component, and useEffect
is used to manage side effects, for example to fetch data when a component is loaded. If you want to learn more about React hooks and how they work, I highly recommend checking out the documentation.
apiFetch
is a small but useful wrapper around window.fetch
that’s used to make requests to the WordPress REST API.
Up until recently, @wordpress/element
was used as a wrapper around React components and was the default way to import methods such as useState
in your components, but WordPress is moving away from that in favor of importing React directly.
Next, we’re setting the default values of customField
and setCustomField
:
JSconst [ customField, setCustomField ] = useState( '' );
This sets customField
to ''
and creates a new function setCustomField
that can be called to update the value of customField
.
Right after this, we’re using useEffect
and apiFetch
to fetch our custom field from the database:
JSuseEffect( () => { const fetchSettings = async () => { const { wop_custom_field } = await apiFetch( { path: '/wp/v2/settings?_fields=wop_custom_field', } ); setCustomField( wop_custom_field ); }; fetchSettings(); }, [] );
useEffect
takes an array of dependencies, and it will call the function each time one of the dependencies changes. By setting the dependency array to []
, we’re telling our component to run the function once when the component is loaded and never again.
In our function, we create and immediately call an asynchronous function named fetchSettings
that makes a call to the REST API settings endpoint to get the value of our custom option (named wop_custom_field
), and updates the state with setCustomField
. This means that whenever the page is loaded, the REST API is called to get the option value from the database, and customField
will be set to this value.
Finally, we’re updating our <TextControl>
component to use our new state:
JS<TextControl label={ __( 'Custom Field', 'wop' ) } help={ __( 'This is a custom field.', 'wop' ) } value={ customField } onChange={ setCustomField } />
With these changes, our <TextControl>
will get its value from the database. You can test it by running this WP-CLI command in your terminal:
Terminalwp option add wop_custom_field "A test message"
After running this command and refreshing the page, the text input should say A test message
. In the next step, we will add a save button to actually save any changes from the text input.
Step 5: Saving the text input
At this point, saving the text input is actually quite straightforward:
JSimport { __ } from '@wordpress/i18n'; import { useState, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import { Card, CardBody, TextControl } from '@wordpress/components'; import { Button, Card, CardBody, TextControl } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; const OptionsPage = () => { const [ customField, setCustomField ] = useState( '' ); useEffect( () => { const fetchSettings = async () => { const { wop_custom_field } = await apiFetch( { path: '/wp/v2/settings?_fields=wop_custom_field', } ); setCustomField( wop_custom_field ); }; fetchSettings().catch( ( error ) => { console.error( error ); } ); }, [] ); const handleSubmit = async ( event ) => { event.preventDefault(); const { wop_custom_field } = await apiFetch( { path: '/wp/v2/settings', method: 'POST', data: { wop_custom_field: customField, }, } ); setCustomField( wop_custom_field ); }; return ( <div> <h1>{ __( 'Options Page', 'wop' ) }</h1> <form onSubmit={ handleSubmit }> <Card> <CardBody> <TextControl label={ __( 'Custom Field', 'wop' ) } help={ __( 'This is a custom field.', 'wop' ) } value={ customField } onChange={ setCustomField } /> <Button variant="primary" type="submit"> { __( 'Save', 'wop' ) } </Button> </CardBody> </Card> </form> </div> ); }; const rootElement = document.getElementById( 'wop-admin-page' ); if ( rootElement ) { createRoot( rootElement ).render( <OptionsPage /> ); }
First, we’re importing the <Button>
component. This is a fairly standard component that can be used to render a button in various styles and run some code when the button is clicked.
JSimport { Card, CardBody, TextControl } from '@wordpress/components'; import { Button, Card, CardBody, TextControl } from '@wordpress/components';
In the body of our component, we’re wrapping everything in a <form>
tag and adding our button:
JS<form onSubmit={ handleSubmit }> <Card> <CardBody> <TextControl label={ __( 'Custom Field', 'wop' ) } help={ __( 'This is a custom field.', 'wop' ) } value={ customField } onChange={ ( value ) => { setCustomField( value ); } } /> <Button variant="primary" type="submit"> { __( 'Save', 'wop' ) } </Button> </CardBody> </Card> </form>
We’re also adding a submit handler to our form named handleSubmit
, which will run when the user submits the form by pressing the save button:
JSconst handleSubmit = async ( event ) => { event.preventDefault(); const { wop_custom_field } = await apiFetch( { path: '/wp/v2/settings', method: 'POST', data: { wop_custom_field: customField, }, } ); setCustomField( wop_custom_field ); };
This code is fairly similar to the code we used to get the option from the database. First off, we’re using event.preventDefault()
to prevent the default form submission logic from reloading the page.
Next, we’re using apiFetch
again, this time to make a POST
request to the REST API settings endpoint. We pass in the value of customField
with the key wop_custom_field
, which is the name of our option in register_setting
.
apiFetch
will return the value of the updated option in the database, and we use that to update our local state again by calling setCustomField
.
And with that, we have a fully working options page! You can give it a test by updating the value and pressing the save button. Refreshing the page should show you the new value in the text input. You can also use WP-CLI to get the value of the option:
Terminalwp option get wop_custom_field
When using this code in a real project, you will most likely want to add some extra things, such as styling, error handing, and a notification when the settings have saved successfully.
I hope that this guide serves as a great starting point to start building your own option pages in WordPress! If you have any questions or comments, please feel free to leave a comment or tweet/DM me on X! I’d love to hear your feedback.
Leave a Reply