2

I have a custom mapping plugin that (among other things) saves/updates coordinates to a custom field when the user drags a map marker within a custom block. Everything works as expected except for the fact that the custom field is only stored during the initial interaction. For some reason, it is not updating the post meta during various events.

The question is how to update post meta in a custom field in response to a user interaction in the editor?

I've tried both setAttributes and wp.data.dispatch but must be missing something.

// register custom field
add_action('init', 'register_block_attributes');
function register_block_attributes()
{
    register_meta('post', 'api_coordinates_pp', array(
        'object_subtype' => 'locations',
        'show_in_rest' => true,
        'single' => true,
        'type' => 'string',
        'auth_callback' => function() {
            return current_user_can( 'edit_posts' );
        }
    ));
}
// in registerBlockType...
attributes: {
    api_coordinates_pp: {
        type: "string",
        source: "meta", // this is a custom field!
        meta: "api_coordinates_pp",
    },
    lat: {
        type: "string",
        selector: "div.map-pp",
        source: "attribute",
        attribute: "data-lat",
    },
    lon: {
        type: "string",
        selector: "div.map-pp",
        source: "attribute",
        attribute: "data-lon",
    },
// an event where I need to update the post meta
marker.on("dragend", function (e) {
    const ll = e.target.getLatLng();
    // these work as expected
    props.setAttributes({ lat: ll.lat });
    props.setAttributes({ lon: ll.lng });

    // this doesn't update the value in the custom field
    props.setAttributes({
        api_coordinates_pp: ll.lat + "," + ll.lng,
    });
    
    // this also does not work, the value never gets updated
    wp.data
        .dispatch("core/editor")
        .editPost({ meta: { api_coordinates_pp: ll.lat + "," + ll.lng } });

    map.setView([ll.lat, ll.lng], ll.zoom, { animation: true });
});

UPDATE: I also tried this, calling updateMetaCoordinates where appropriate, but same issue, the changes are not saved.

const postType = useSelect(
    (select) => select("core/editor").getCurrentPostType(),
    []
);
const [meta, setMeta] = useEntityProp("postType", postType, "meta");
const updateMetaCoordinates = (newValue) => {
    console.log(newValue);
    setMeta({ ...meta, api_coordinates_pp: newValue });
};

ANOTHER UPDATE: I tried useDispatch and the same thing. It works the first time but subsequent edits are never saved.

const { editPost } = useDispatch("core/editor");
const updateMetaCoordinates = (newValue) => {
    editPost({ meta: { api_coordinates_pp: newValue } });
};

Basically, it seems like all of these work the very first time, but nothing I do allows me to actually change the meta values, not even before the initial post save. The only way to actuate a change is to delete the meta field for api_coordinates_pp and then perform an action that causes a change.

9
  • Can we see the full code, e.g. on GitHub? Have you tried removing the api_coordinates_pp from the attributes list, and then use useEntityProp to get/set the meta?
    – Sally CJ
    Jul 28, 2022 at 12:12
  • Hi @SallyCJ, thanks for the reply! I tried removing api_coordinates_pp from the attributes list but nothing changed (it still worked in the exact same way, so I guess that wasn't necessary anyway). Here is the full code for this block.
    – ebell
    Jul 28, 2022 at 13:45
  • Hi @ebell. So you've tried changing the props.setAttributes({ api_coordinates_pp: ll.lat + "," + ll.lng, }) to updateMetaCoordinates( ll.lat + "," + ll.lng ), and it didn't work? How did you check/confirm that the meta wasn't being updated?
    – Sally CJ
    Jul 28, 2022 at 15:54
  • 1
    @SallyCJ, I haven't committed any changes to the repo since I haven't found the solution, but I removed all instances of props.setAttributes({ api_coordinates_pp: ...}) to use updateMetaCoordinates instead. I'm verifying that changes aren't being applied in two ways: visually in the editor "custom fields" UI (after a page refresh) and by checking the API output at ?feed=placepress_locations_public.
    – ebell
    Jul 28, 2022 at 19:32
  • I've also edited the updateMetaCoordinates function in various different ways – using useEntityProp, useDispatch, various syntax changes – and nothing seems to make a difference. The documentation for this is not great but I've found several code examples to work from. Unfortunately, none seem to work more than one time (i.e. no editing after the meta field has been populated the first time). If I could think of a better way to add this data to the custom API, I would probably just do that, but custom post meta seems like the most logical choice.
    – ebell
    Jul 28, 2022 at 19:37

2 Answers 2

3

how to update post meta in a custom field in response to a user interaction in the editor?

editPost() can be used for that, but https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#meta-source-deprecated stated that:

Although attributes may be obtained from a post’s meta, meta attribute sources are considered deprecated; EntityProvider and related hook APIs should be used instead, as shown in the Create Meta Block how-to.

So that means (in a function component), we should use useEntityProp to update a post meta via the block editor, which changes the post/editor state and then activates the submit button so that one can click on it to actually save the new meta value.

But then, here's why it didn't work

You commented that:

I'm verifying that changes aren't being applied in two ways: visually in the editor "custom fields" UI (after a page refresh) and by checking the API output at ?feed=placepress_locations_public

Which means the "Custom Fields" meta box was enabled on the post editing screen (for your locations CPT), right?

Because if so, then you should know that the editor will make 2 AJAX requests:

  1. Request 1 will save the post title, content, meta, etc. via the REST API.

  2. Request 2 will save the active custom fields in the above meta box via the wp-admin/post.php route, where the request URL might look like so:

    https://example.com/wp-admin/post.php?post=149&action=edit&meta-box-loader=1&meta-box-loader-nonce=7be53e3b5b&_locale=user

Preview image

So that means, your api_coordinates_pp meta will be saved twice and thus I believe all your attempts/code for updating the meta actually worked, but then the value got overwritten via the second request above and because the block editor does not automatically update the meta in that meta box, then that's why the value remained the same as when the meta box was initially loaded. I.e. The meta value remained the same as the one that was last saved.

How to easily fix/avoid the issue

  1. Change your meta to a protected meta, i.e. prefix the meta key with _ (an underscore).

  2. Or add your meta to the list of protected meta using the is_protected_meta filter:

    // Turn the api_coordinates_pp meta to a *protected* meta without having to change
    // the meta key (to _api_coordinates_pp).
    add_filter( 'is_protected_meta', 'wpse_408053_filter_is_protected_meta', 10, 3 );
    function wpse_408053_filter_is_protected_meta( $protected, $meta_key, $meta_type ) {
        return ( 'post' === $meta_type && 'api_coordinates_pp' === $meta_key ) ?
            true : $protected;
    }
    

So whether you used that filter or that you actually changed the meta key (to _api_coordinates_pp), the meta would now no longer be available in the "Custom Fields" meta box which then avoids the meta value from being overwritten.

PS: You could also update the meta in that meta box using JS, e.g. after calling updateMetaCoordinates(), but why bother with the extra code when the above options are easier :)

Additional Notes

  1. As I commented, you should use useEffect instead of the onload hack. You can see a full example here which uses Leaflet v1.8.0, and @wordpress/dom-ready for the view/front-end-only script, but the main parts are basically:

    function initMap( { clientId, attributes, setAttributes, updateMetaCoordinates } ) {
        const mapId  = 'map-' + clientId;
        const latLng = [ attributes.lat, attributes.lon ];
        const map    = L.map( mapId ).setView( latLng, 13 );
    
        ...
    
        const marker = L.marker( latLng, { draggable: true } ).addTo( map );
        const popup  = L.popup();
    
        ...
    
        const onDragend = e => {
            const latLng    = e.latlng || e.target.getLatLng();
            const latLngStr = latLng.lat + ',' + latLng.lng;
    
            openPopup( latLng, 'Current latitude & longitude: ' + latLngStr );
    
            setAttributes( {
                lat: latLng.lat,
                lon: latLng.lng,
            } );
    
            updateMetaCoordinates( latLngStr );
        };
    
        marker.on( 'dragend', onDragend );
        ...
    }
    
    function edit( props ) {
        const postType = useSelect(
            select => select( 'core/editor' ).getCurrentPostType(),
            []
        );
    
        const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
        const updateMetaCoordinates = value => {
            setMeta( { ...meta, api_coordinates_pp: value } );
            console.log( 'meta api_coordinates_pp set to ' + value );
        };
    
        const mapId = 'map-' + props.clientId;
    
        // Create the Leaflet map once this block has been attacted to the DOM.
        useEffect( () => initMap( { ...props, updateMetaCoordinates } ), [ mapId ] );
    
        return (
            <div { ...useBlockProps() }>
                <p>
                    Current latitude: { props.attributes.lat }<br />
                    Current longitude: { props.attributes.lon }
                </p>
                <div id={ mapId }
                    className="map-pp"
                    style={ { height: '180px' } }
                >
                    Loading map..
                </div>
            </div>
        );
    }
    

    And if you want, you can quickly try my block by downloading this plugin: wpse-408053.zip :)

  2. Leaflet has a plugin for React, so you might want to try/check it out: https://github.com/PaulLeCam/react-leaflet

3
  • 1
    Thanks, this is a great explanation! I'll follow up on this in a few days and expect I'll be marking this as the answer.
    – ebell
    Aug 1, 2022 at 13:57
  • 1
    Thanks again, I went with option 2, using the is_protected_meta filter. That immediately fixed the core issue.
    – ebell
    Aug 4, 2022 at 16:44
  • I'm glad my answer helped, and thanks for accepting it - I was actually about to ask you if I was correct regarding the meta being saved twice :) And I thought I should just remove option 3, hence I revised my answer. (I've also updated my GitHub repo)
    – Sally CJ
    Aug 5, 2022 at 1:49
0

To use custom post meta in the Gutenberg block editor, you'll have to enable the REST API accessibility like the example below:

/**
 * Registers the custom_post_meta field.
 *
 * @return void
 */
function register_custom_post_meta_field() {
    register_post_meta(
        'post',
        'custom_post_meta',
        [
            'type'         => 'string',
            'show_in_rest' => true,
            'single'       => true,
        ]
    );
}
add_action( 'init', 'register_custom_post_meta_field' );

Then, you have to use @wordpress/data module to select and update the post meta value. Follow the approach below on your JS component:

import { __ } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
import { PanelBody, PanelRow, TextControl } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';

/**
 * My Custom Block
 */
const MyCustomBlock = () => {
    const META_KEY = 'custom_post_meta'; // This is your custom meta name
    const { editPost } = useDispatch('core/editor');
    const meta = useSelect((select) =>
        select('core/editor').getEditedPostAttribute('meta')
    );
    const { [META_KEY]: customMeta } = meta;

    const onCustomMetaChange = (newValue) => {
        editPost({
            meta: { ...meta, [META_KEY]: newValue },
        });
    };

    return (
        <div>
            <InspectorControls>
                <PanelBody title={__('Settings', 'textdomain')}>
                    <PanelRow>
                        <TextControl
                            label={__('Custom Meta', 'textdomain')}
                            value={customMeta}
                            onChange={onCustomMetaChange}
                        />
                    </PanelRow>
                </PanelBody>
            </InspectorControls>
        </div>
    );
};

If you are looking for a complete guide, check this tutorial on how to create and save post meta in the Gutenberg block editor.

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.