Building a Create page form

In the previous part we created an Edit page feature, and in this part we will add a Create page feature. Here’s a glimpse of what we’re going to build:

Step 1: Add a Create a new page button

Let’s start by building a button to display the create page form. It’s similar to an Edit button we have built in the part 3:

import { useDispatch } from '@wordpress/data';
import { Button, Modal, TextControl } from '@wordpress/components';

function CreatePageButton() {
    const [isOpen, setOpen] = useState( false );
    const openModal = () => setOpen( true );
    const closeModal = () => setOpen( false );
    return (
        <>
            <Button onClick={ openModal } variant="primary">
                Create a new Page
            </Button>
            { isOpen && (
                <Modal onRequestClose={ closeModal } title="Create a new page">
                    <CreatePageForm
                        onCancel={ closeModal }
                        onSaveFinished={ closeModal }
                    />
                </Modal>
            ) }
        </>
    );
}

function CreatePageForm() {
    // Empty for now
    return <div/>;
}

Great! Now let’s make MyFirstApp display our shiny new button:

function MyFirstApp() {
    // ...
    return (
        <div>
            <div className="list-controls">
                <SearchControl onChange={ setSearchTerm } value={ searchTerm }/>
                <CreatePageButton/>
            </div>
            <PagesList hasResolved={ hasResolved } pages={ pages }/>
        </div>
    );
}

The final result should look as follows:

Step 2: Extract a controlled PageForm

Now that the button is in place, we can focus entirely on building the form. This tutorial is about managing data, so we will not build a complete page editor. Instead, the form will only contain one field: post title.

Luckily, the EditPageForm we built in part three already takes us 80% of the way there. The bulk of the user interface is already available, and we will reuse it in the CreatePageForm. Let’s start by extracting the form UI into a separate component:

function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
    // ...
    return (
        <PageForm
            title={ page.title }
            onChangeTitle={ handleChange }
            hasEdits={ hasEdits }
            lastError={ lastError }
            isSaving={ isSaving }
            onCancel={ onCancel }
            onSave={ handleSave }
        />
    );
}

function PageForm( { title, onChangeTitle, hasEdits, lastError, isSaving, onCancel, onSave } ) {
    return (
        <div className="my-gutenberg-form">
            <TextControl
                __nextHasNoMarginBottom
                __next40pxDefaultSize
                label="Page title:"
                value={ title }
                onChange={ onChangeTitle }
            />
            { lastError ? (
                <div className="form-error">Error: { lastError.message }</div>
            ) : (
                false
            ) }
            <div className="form-buttons">
                <Button
                    onClick={ onSave }
                    variant="primary"
                    disabled={ !hasEdits || isSaving }
                >
                    { isSaving ? (
                        <>
                            <Spinner/>
                            Saving
                        </>
                    ) : 'Save' }
                </Button>
                <Button
                    onClick={ onCancel }
                    variant="tertiary"
                    disabled={ isSaving }
                >
                    Cancel
                </Button>
            </div>
        </div>
    );
}

This code quality change should not alter anything about how the application works. Let’s try to edit a page just to be sure:

Great! The edit form is still there, and now we have a building block to power the new CreatePageForm.

Step 3: Build a CreatePageForm

The only thing that CreatePageForm component must do is to provide the following seven properties needed to render the PageForm component:

  • title
  • onChangeTitle
  • hasEdits
  • lastError
  • isSaving
  • onCancel
  • onSave

Let’s see how we can do that:

Title, onChangeTitle, hasEdits

The EditPageForm updated and saved an existing entity record that lived in the Redux state. Because of that, we relied on the editedEntityRecords selector.

In case of the CreatePageForm however, there is no pre-existing entity record. There is only an empty form. Anything that the user types is local to that form, which means we can keep track of it using the React’s useState hook:

function CreatePageForm( { onCancel, onSaveFinished } ) {
    const [title, setTitle] = useState();
    const handleChange = ( title ) => setTitle( title );
    return (
        <PageForm
            title={ title }
            onChangeTitle={ setTitle }
            hasEdits={ !!title }
            { /* ... */ }
        />
    );
}

onSave, onCancel

In the EditPageForm, we dispatched the saveEditedEntityRecord('postType', 'page', pageId ) action to save the edits that lived in the Redux state.

In the CreatePageForm however, we do not have any edits in the Redux state, nor we do have a pageId. The action we need to dispatch in this case is called saveEntityRecord (without the word Edited in the name) and it accepts an object representing the new entity record instead of a pageId.

The data passed to saveEntityRecord is sent via a POST request to the appropriate REST API endpoint. For example, dispatching the following action:

saveEntityRecord( 'postType', 'page', { title: "Test" } );

Triggers a POST request to the /wp/v2/pages WordPress REST API endpoint with a single field in the request body: title=Test.

Now that we know more about saveEntityRecord, let’s use it in CreatePageForm.

function CreatePageForm( { onSaveFinished, onCancel } ) {
    // ...
    const { saveEntityRecord } = useDispatch( coreDataStore );
    const handleSave = async () => {
        const savedRecord = await saveEntityRecord(
            'postType',
            'page',
            { title }
        );
        if ( savedRecord ) {
            onSaveFinished();
        }
    };
    return (
        <PageForm
            { /* ... */ }
            onSave={ handleSave }
            onCancel={ onCancel }
        />
    );
}

There is one more detail to address: our newly created pages are not yet picked up by the PagesList. Accordingly to the REST API documentation, the /wp/v2/pages endpoint creates (POST requests) pages with status=draft by default, but returns (GET requests) pages with status=publish. The solution is to pass the status parameter explicitly:

function CreatePageForm( { onSaveFinished, onCancel } ) {
    // ...
    const { saveEntityRecord } = useDispatch( coreDataStore );
    const handleSave = async () => {
        const savedRecord = await saveEntityRecord(
            'postType',
            'page',
            { title, status: 'publish' }
        );
        if ( savedRecord ) {
            onSaveFinished();
        }
    };
    return (
        <PageForm
            { /* ... */ }
            onSave={ handleSave }
            onCancel={ onCancel }
        />
    );
}

Go ahead and apply that change to your local CreatePageForm component, and let’s tackle the remaining two props.

lastError, isSaving

The EditPageForm retrieved the error and progress information via the getLastEntitySaveError and isSavingEntityRecord selectors. In both cases, it passed the following three arguments: ( 'postType', 'page', pageId ).

In CreatePageForm however, we do not have a pageId. What now? We can skip the pageId argument to retrieve the information about the entity record without any id – this will be the newly created one. The useSelect call is thus very similar to the one from EditPageForm:

function CreatePageForm( { onCancel, onSaveFinished } ) {
    // ...
    const { lastError, isSaving } = useSelect(
        ( select ) => ( {
            // Notice the missing pageId argument:
            lastError: select( coreDataStore )
                .getLastEntitySaveError( 'postType', 'page' ),
            // Notice the missing pageId argument
            isSaving: select( coreDataStore )
                .isSavingEntityRecord( 'postType', 'page' ),
        } ),
        []
    );
    // ...
    return (
        <PageForm
            { /* ... */ }
            lastError={ lastError }
            isSaving={ isSaving }
        />
    );
}

And that’s it! Here’s what our new form looks like in action:


Wiring it all together

Here’s everything we built in this chapter in one place:

function CreatePageForm( { onCancel, onSaveFinished } ) {
    const [title, setTitle] = useState();
    const { lastError, isSaving } = useSelect(
        ( select ) => ( {
            lastError: select( coreDataStore )
                .getLastEntitySaveError( 'postType', 'page' ),
            isSaving: select( coreDataStore )
                .isSavingEntityRecord( 'postType', 'page' ),
        } ),
        []
    );

    const { saveEntityRecord } = useDispatch( coreDataStore );
    const handleSave = async () => {
        const savedRecord = await saveEntityRecord(
            'postType',
            'page',
            { title, status: 'publish' }
        );
        if ( savedRecord ) {
            onSaveFinished();
        }
    };

    return (
        <PageForm
            title={ title }
            onChangeTitle={ setTitle }
            hasEdits={ !!title }
            onSave={ handleSave }
            lastError={ lastError }
            onCancel={ onCancel }
            isSaving={ isSaving }
        />
    );
}

function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
    const { page, lastError, isSaving, hasEdits } = useSelect(
        ( select ) => ( {
            page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
            lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId ),
            isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ),
            hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ),
        } ),
        [pageId]
    );

    const { saveEditedEntityRecord, editEntityRecord } = useDispatch( coreDataStore );
    const handleSave = async () => {
        const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
        if ( savedRecord ) {
            onSaveFinished();
        }
    };
    const handleChange = ( title ) => editEntityRecord( 'postType', 'page', page.id, { title } );

    return (
        <PageForm
            title={ page.title }
            onChangeTitle={ handleChange }
            hasEdits={ hasEdits }
            lastError={ lastError }
            isSaving={ isSaving }
            onCancel={ onCancel }
            onSave={ handleSave }
        />
    );
}

function PageForm( { title, onChangeTitle, hasEdits, lastError, isSaving, onCancel, onSave } ) {
    return (
        <div className="my-gutenberg-form">
            <TextControl
                __nextHasNoMarginBottom
                __next40pxDefaultSize
                label="Page title:"
                value={ title }
                onChange={ onChangeTitle }
            />
            { lastError ? (
                <div className="form-error">Error: { lastError.message }</div>
            ) : (
                false
            ) }
            <div className="form-buttons">
                <Button
                    onClick={ onSave }
                    variant="primary"
                    disabled={ !hasEdits || isSaving }
                >
                    { isSaving ? (
                        <>
                            <Spinner/>
                            Saving
                        </>
                    ) : 'Save' }
                </Button>
                <Button
                    onClick={ onCancel }
                    variant="tertiary"
                    disabled={ isSaving }
                >
                    Cancel
                </Button>
            </div>
        </div>
    );
}

All that’s left is to refresh the page and enjoy the form:

What’s next?