3

In the saga so far, you can see my

  1. First post/question
  2. Second post/question

This is my third (and hopefully final) question. I've got everything working except for saving the data in the WP database. Incidentally, this tutorial is a goldmine for an understanding of how things work in the new Gutenberg way, and how all the different pieces fit together and interact.

As stated previously (see links above), I'm working on customizing the featured image block in the standard post editor sidebar. Here's a screenshot of what I have:

[FIXME]

When you click the checkbox, the text field appears. And when you uncheck it it goes away. You can type text in the field and the page goes through all the motions of working as it should. When you save, you get a "success" message. However, nothing is being saved to the database, and that's my problem. I have independent SQL access to the DB, and there's nothing in the wp_postmeta table for any test post I create with metadata that should be saved.

I can click the checkbox, enter something in the text field, tab away, and see that the datastore has my information by executing the following in the debug console:

wp.data.select('core/editor').getEditedPostAttribute('meta')

which returns... Object { _featured_image_video_url: "this is a test", _featured_image_is_video: true }

as expected. But there's no joy if you save the page and look in the DB. Nothing there.

Here's my current JavaScript code

const el = wp.element.createElement;
const { setState, withSelect, dispatch, select} = wp.data;
const { CheckboxControl, TextControl } = wp.components;
const { useState } = wp.element;
const { withState } = wp.compose;
const { __ } = wp.i18n;

//this replaces the default with our custom Featured Image code
wp.hooks.addFilter(
    'editor.PostFeaturedImage',
    'dsplugin/featured-image-as-video',
    wrapPostFeaturedImage
);

//create a checkbox that takes properties
const MyCheckboxControl = (props) => {
    const [ isChecked, setChecked ] = useState( false );
    return(
      <CheckboxControl
          label={ __("Image is a video", "dsplugin") }
          checked={ isChecked }
          onChange={ () =>{
              if (isChecked){
                setChecked(false);
                dispatch('core/editor').editPost({meta: {_featured_image_is_video: false}})
              }else{
                setChecked(true);
                dispatch('core/editor').editPost({meta: {_featured_image_is_video: true}})
              }
              props.onChange.call();
          } }
      />
    )
};
// //this the std TextControl from the example in the documentation
// const MyTextControl = withState({ videoURL: '', }) (({ videoURL, setState }) => (
//     <TextControl
//         // label="Video URL to use with featured image"
//         value={ videoURL }
//         placeholder={ __("Enter video URL to play when clicked", "dsplugin") }
//         onChange={ ( videoURL ) => {
//           //update the text field
//           setState( { videoURL } );
//
//           //save the new value to the DB
//           // meta.featured_image_video_url = videoURL;
//           // withDispach( 'core/editor' ).editPost( {meta});
//         } }
//     />
// ) );

class MyTextControl extends wp.element.Component{
  constructor(){
    super()
    this.state = {
      videoURL: ''
    }
  }
  render() {
    const { videoURL, setState} = this.props;
    // const videoURL = select('core/editor').getEditedPostAttribute('meta').featured_image_video_url;
    return(
      <TextControl
          // select('core/editor').getEditedPostAttribute('meta').featured_image_video_url }
          value={ videoURL }
          placeholder={ __("Enter video URL to play when clicked", "dsplugin") }
          onChange={ ( videoURL ) => {
            //save the new value to the DB
            const currentMeta = select( 'core/editor' ).getEditedPostAttribute( 'meta' );
            const newMeta = { ...currentMeta, _featured_image_video_url: videoURL };
            dispatch('core/editor').editPost({meta: newMeta})
          } }
      />
    )
  }
};

//we put it all together in a wrapper component with a custom state to show/hide the TextControl
class MyFeaturedImageControls extends wp.element.Component{
  constructor(){
    super()
    this.state = {
      isHidden: true
    }
  }
  toggleHidden(){
    this.setState({
      isHidden: !this.state.isHidden
    })
  }
  render() {
    return(
      <div>
        <h4>{ __("Image Options", "dsplugin") }</h4>
        <MyCheckboxControl onChange={this.toggleHidden.bind(this)}/>
        { !this.state.isHidden && <MyTextControl /> }
      </div>
    )
  }
};

//here's the function that wraps the original Featured Image content and adds
//our custom controls below
function wrapPostFeaturedImage( OriginalComponent ) {
    // Get meta field information from the DB.
    let meta = select( 'core/editor' ).getCurrentPostAttribute( 'meta' );
    console.log ("metadata follows:");
    console.log(meta);

    return function( props ) {
        return (
            el(
                wp.element.Fragment,
                {},
                // 'Prepend above',
                el(
                    OriginalComponent,
                    props
                ),
                <MyFeaturedImageControls />
            )
        );
    }
}

And the PHP code that supports it:

//add metadata fields for use with featured image metabox
function register_resource_item_featured_image_metadata() {
  register_meta(
    'post',
    '_featured_image_video_url',
    array(
      'object_subtype' => 'ds_resource_item',
      'show_in_rest' => true, #must be true to work in Guttenberg
      'type' => 'string',
      'single' => true,
      'sanitize_callback' => 'sanitize_text_field',
      'auth_callback' => function() {
          return current_user_can('edit_posts');
      }
    )
  );
  register_meta(
    'post',
    '_featured_image_is_video',
    array(
      'object_subtype' => 'ds_resource_item',
      'show_in_rest' => true, #must be true to work in Guttenberg
      'single' => true,
      // 'sanitize_callback' => 'rest_sanitize_boolean',
      'type' => 'boolean',
      // 'auth_callback' => function() {
      //     return current_user_can('edit_posts');
      // }
    )
  );
}

add_action( 'init', 'register_resource_item_featured_image_metadata' );

Again... being so new to all of this, I think I'm missing some small detail. I realize my code is incomplete, and you'll see a few things commented out for debugging purposes. But as it stands, I think I should at least be saving a newly entered value to the DB based on what's there now. I also realize I still have to put code in place to fetch an initial value from the db and populate the text field. But first things first.

Thanks for the help.

Update: Here's the php code that defines the custom post type I'm using with this code:

    $args = array(
        "label" => __( "Resource Items", "dstheme" ),
        "labels" => $labels,
        "description" => "DS Resource Items are the foundational content type for resources.",
        "public" => true,
        "publicly_queryable" => true,
        "show_ui" => true,
        "delete_with_user" => false,
        "show_in_rest" => true,
        "rest_base" => "dsr_item",
        "rest_controller_class" => "WP_REST_Posts_Controller",
        "has_archive" => false,
        "show_in_menu" => true,
        "show_in_nav_menus" => true,
        "exclude_from_search" => false,
        "capability_type" => "post",
        "map_meta_cap" => true,
        "hierarchical" => false,
    //modify the slug below to change what shows in the URL when an DSResourceItem is accessed
        "rewrite" => array( "slug" => "resource-item", "with_front" => true ),
        "query_var" => true,
        "menu_position" => 5,
        "menu_icon" => "dashicons-images-alt2",
        "supports" => array( "title", "editor", "thumbnail", "custom-fields" ),
        "taxonomies" => array( "post_tag" ),
    );

    register_post_type( "ds_resource_item", $args );

2 Answers 2

3

Your JavaScript code works for me. The metadata do get saved.

But perhaps you'd like to try my code which is pretty much completed, i.e. on page load, the checkbox is auto-checked/unchecked (and the text box is also shown/hidden) depending on the current database value. Secondly, I did it just as the Gutenberg team did it with the original component for the featured image and it's actually easy.. I mean, I'm just hoping you can learn some good stuff from my code. :)

Nonetheless, regarding the question or getting your code to saving the metadata, one issue I noted before I posted the original answer, is the auth_callback for the _featured_image_is_video meta:

register_meta(
  'post',
  '_featured_image_is_video',
  array(
    ...
    // 'auth_callback' => function() {
    //     return current_user_can('edit_posts');
    // }
  )
);

Why did you comment out the auth_callback? Because if you do that, then it would default to __return_false() for protected meta (where the name begins with _/underscore), which means no one is allowed to edit the meta via the REST API! :p

So try un-commenting it out. But I'm not sure about that because if you actually had it commented in your actual code, then you would've noticed it since WordPress/Gutenberg would display a notice on the post editing screen saying the post could not be updated (because you have no permission to edit the meta or that the auth_callback always returns false).

To other readers:

Make sure your custom post type supports custom fields, because the REST API handbook says:

Note that for meta fields registered on custom post types, the post type must have custom-fields support. Otherwise the meta fields will not appear in the REST API.

And when the meta fields do not appear in the REST API, the meta fields will not be saved/updated via the REST API.

15
  • I was hoping you'd see this :). I ran across the requirement you mentioend and believe I am in compliance. I've updated my question to include the PHP that defines the custom post type I'm using.
    – Andrew
    Commented Apr 2, 2020 at 16:10
  • Well I should've just asked in the comment, eh? :p But I'll see if I can find the actual issue.
    – Sally CJ
    Commented Apr 2, 2020 at 23:06
  • No problem. Let me know if you need any more of my code for testing. Is there a way to connect directly if needed so we don't muddy the waters too much here until we've figured it out?
    – Andrew
    Commented Apr 2, 2020 at 23:55
  • If you can setup a test site, I can check what's wrong because actually, other than you should uncomment (or enable) the auth_callback for the _featured_image_is_video meta, your code is working for me - the metadata do get saved properly - the console does show the correct metadata follows: output.
    – Sally CJ
    Commented Apr 3, 2020 at 0:43
  • 1
    Just letting you know @Sally that I found the problem and posted an answer. Thanks so much for all your help. All the best.
    – Andrew
    Commented Apr 15, 2020 at 17:59
2

So I figured it out. @Sally is correct that the basic code I wrote works. However, there's one small caveat that is worth mentioning here and I'll also document a method of testing database connectivity from any post edit page without any UI code.

Firstly, no javascript is required for wordpress to recognize the presence of a metadata variable on an gutenberg/react-based post edit page. Only the register_meta() call in PHP is needed. It is sufficient to put a piece of code such as the following in your plugin file for the metadata variable to exist and for a post's edit page to know how to save it to the database.

function register_resource_item_featured_image_metadata() {
  register_meta(
    'post',
    '_featured_image_video_url',
    array(
      'object_subtype' => 'optional', #omit unless you're dealing with a custom post type
      'show_in_rest' => true, #must be true to work in Guttenberg
      'type' => 'string',
      'single' => true,
      'sanitize_callback' => 'sanitize_text_field',
      'auth_callback' => function() {
          return current_user_can('edit_posts');
      }
    )
  );
}
add_action( 'init', 'register_resource_item_featured_image_metadata' );

With only the above, you won't have any way of setting/changing this metadata field from the WordPress UI, but you can do the following for testing purposes:

  1. Edit any post (or create a new one).
  2. Open the dev console on that post's edit page (right-click somewhere and inspect element)
  3. In the console type the following and press return wp.data.select('core/editor').getEditedPostAttribute('meta')

This will return something like Object { _featured_image_video_url: ""}, which proves the system has properly registered and recognized your metadata variable, and that the DB has no value for it.

  1. Now execute the following in the console: wp.data.dispatch('core/editor').editPost({meta: {_featured_image_video_url: 'http://some_url'}})
  2. Press the Update button in the edit window, and wait for the page to save.
  3. Re-load the page and in the console again, re-execute the first command wp.data.select('core/editor').getEditedPostAttribute('meta')

This time you should see Object { _featured_image_video_url: "http://some_url"}, which proves you've successfully saved a value to the database for that parameter. (when you re-loaded the page, wordpress fetched the metadata values for that page and stored them in memory along with the rest of the page data).

If you have a SQL server admin tool like MySQL Workbench, you can further verify this by running the following query:

select * from wp_postmeta where post_id = 15727

Substitute the post ID from your page's URL for the number in the query above.

                                             vvvvv
(https://localhost/wp/wp-admin/post.php?post=15727&action=edit)
                                             ^^^^^

My Problem I was doing all of this, however, I have my plugin split into two pieces. The main plugin file has all the initialization code, and at the end of the file there's an include as follows:

//load other plugin code a front-end user won't use if admin user
if( is_admin() ){
  require plugin_dir_path( __FILE__ ) . 'admin-code.php';
}

Without thinking, I put my register_meta() calls and their associated add_action(), in the admin-code.php file, which only gets loaded if is_admin() returns true. When I save a post on my system (click the Update button), the if statement above executes 3 times. The first time, is_admin() returns false, the last two times it returns true. I don't claim to know the intricacies of why that's the case, but obviously, not having the _featured_image_video_url meta variable defined the first pass through the code is a problem, and the cause of my issue with saving the variable. If I move the register_meta() code to a place in the file where it executes every time, everything works beautifully.

Hope this helps someone somewhere. Thank you very much Sally for your help in stripping away the noise so I could discover the root cause. Between the two of us, I think we've plugged a few gaping holes in pulling all the pieces together to enhance existing parts of the Gutenberg editor UI, at least for those that are new to React/js.

3
  • 1
    Still relevant almost three years later. I'd been beating my head against my desk the past couple of days trying to figure out why I couldn't get my metadata to save—then I stumbled across your post. Removing is_admin() as a requirement before register_meta worked for me, as well. Hopefully others who are stuck in the same spot find your post quicker than I did. Commented Feb 1, 2023 at 16:36
  • Glad you found the post useful :)
    – Andrew
    Commented Feb 9, 2023 at 18:57
  • 1
    Bless you. Another guy here, nearly 4 years later, helped by this post. I was about to punch out my screen and put my head through it after getting stuck here for days.
    – John Doe
    Commented Nov 24, 2023 at 15:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.