0

I've a custom block which outputs a core/image block inside an innerblock. Everything works hunkydory until I attempt to sent a size to it. When I do, the block validation fails on re-edit:

Block validation: Block validation failed for `core/image` ({name: "core/image", icon: {…}, keywords: Array(3), providesContext: {…}, usesContext: Array(0), …}).

Content generated by `save` function:

<figure class="wp-block-image alignright" style="max-width:50%"><a href="https://.../"><img src="https://.../plank-banner.jpg" alt=""/></a></figure>

Content retrieved from post body:

<div class="wp-block-image"><figure class="alignright is-resized"><a href="https://..."><img src="https://.../plank-banner.jpg" alt="" class="wp-image-8687" width="300px"/></a></figure></div>

You can see the content retrieved from the body is quite different. This is the difference between the two innerblocks templates though:

Doesn't work:

const MY_TEMPLATE = [
            [ 'core/heading', { content: heading } ],
            [ 'core/image', { id: mediaID, url: mediaURL, href: exerciseLink, align: "right", width: "300px" } ],
            [ 'core/paragraph', { content: strippedContent } ]
        ];

Works:

const MY_TEMPLATE = [
            [ 'core/heading', { content: heading } ],
            [ 'core/image', { id: mediaID, url: mediaURL, href: exerciseLink, align: "right" } ],
            [ 'core/paragraph', { content: strippedContent } ]
        ];

My save is simple:

save: function( props ) {
    
    const blockProps = useBlockProps.save();
    const { attributes } = props;
    
    return (
        <div { ...blockProps } key="sandcexercise">
            <InnerBlocks.Content />     
        </div>
    );

Sniffing around a bit, I did find this: https://github.com/WordPress/gutenberg/issues/21989 which feels related, but it's way too complicated for me to get my head around!

--

Added more code as requested. Apologies for code quality here, I'm really hacking this together from lots of different tutorials, many of which were written for API v1, and this is my first experience with EXNext, React and Gutenberg!

This block displays a dropdown in the inspector controls which is populated from a custom post type (called exercises). Once selected, it renders the excerpt, featured image and title in the block inside an innerblock along with a couple of other attributes.

It dies when I attempt to size the core/image block via the width attribute.

I'm getting access to the wp.api using

wp_enqueue_script( 'wp-api' );

in my php file.

Two files:

The index.js

/**
 * Registers a new block provided a unique name and an object defining its behavior.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
 */
import { registerBlockType} from '@wordpress/blocks';

import {
    InnerBlocks,
    useBlockProps,
} from '@wordpress/block-editor';

const { __ } = wp.i18n; // Import __() from wp.i18n

/**
 * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
 * All files containing `style` keyword are bundled together. The code used
 * gets applied both to the front of your site and to the editor.
 *
 * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
 */
import './style.scss';

/**
 * Internal dependencies
 */
import Edit from './edit';


/**
 * Every block starts by registering a new block type definition.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
 */
registerBlockType('get-coached/sandc', {
    apiVersion: 2,
    attributes: {
        posts: {
            type: 'array',
            default: [],
        },
        post: {
            type: 'object',
        },
        selectedPost: {
            type: 'number',
            default: 0      
        },
        options: {
            type: 'array',  
            default: [ { value: 0, label: __( 'Select a Post' ) } ],            
        },
        reps: {
          type: 'number',
          source: 'number',
          selector: '.thereps',
          default: 15,
        },
        sets: {
          types: 'number',
          source: 'number',
          selector: '.thesets',
          default: 3,
        },
        notes: {
          types: 'text',
          source: 'text',
          selector: '.thenotes',
          default: '',
        },
        
     },

    edit: Edit,

    save: function( props ) {
        
        const blockProps = useBlockProps.save();
        const { attributes } = props;
        
        return (
            <div { ...blockProps } key="sandcexercise">
                <InnerBlocks.Content /> 
                <div className="repsrow"><div className="repslabel">Reps:</div><div className="thereps">{ attributes.reps }</div></div>
                <div className="setsrow"><div className="setslabel">Sets:</div><div className="thesets">{ attributes.sets }</div></div>     
                <div className="notesrow"><div className="noteslabel">Notes:</div><div className="thenotes">{ attributes.notes }</div></div>        
            </div>
        );  
    },
});

The edit.js

/**
 * Retrieves the translation of text.
 *
 * @see https://developer.wordpress.org/block-editor/packages/packages-i18n/
 */
import { __ } from '@wordpress/i18n';
        
/**
 * React hook that is used to mark the block wrapper element.
 * It provides all the necessary props like the class name.
 *
 * @see https://developer.wordpress.org/block-editor/packages/packages-block-editor/#useBlockProps
 */
import {
    InnerBlocks,
    useBlockProps,
    InspectorControls,
}  from '@wordpress/block-editor';

/**
 * Other things needed
 *
 */
 
const { SelectControl, TextControl } = wp.components;
const { Component } = wp.element;
const { useDispatch, useSelect } = wp.data;

/**
 * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
 * Those files can contain any CSS code that gets applied to the editor.
 *
 * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
 */
import './editor.scss';

function mySelectPosts({clientId, attributes, setAttributes }) {
    
    // Used to reset inner blocks if we've changed the selected exercise
    const { replaceInnerBlocks } = useDispatch("core/block-editor");
    const { inner_blocks } = useSelect(select => ({
            inner_blocks: select("core/block-editor").getBlocks(clientId)
    }));
        
    /** 
    * Called when the inspector controls select box is changed
    * Saves the value of the selected post to the selectedPost attribute 
    * and resets the innerblock
    **/
    
    const onSelectPost = ( post ) => {
        
        // reset the inner
        let inner_blocks_new = [];

        replaceInnerBlocks(clientId, inner_blocks_new, false);
                           
        setAttributes( {
            selectedPost: parseInt(post),
        } );
    };
    
    // define the exercise custom post type model so we can get the list of them
    const Exercise = wp.api.models.Post.extend( {
        urlRoot: wpApiSettings.root + 'wp/v2/exercise',
        defaults: {
            type: 'exercise',
        },
    } );    
    
    // set up the structure for querying for exercises
    const Exercises = wp.api.collections.Posts.extend( {
        url: wpApiSettings.root + 'wp/v2/exercise?_embed',
        model: Exercise,
    } );    
    const someExercises = new Exercises();
    
    // options will contain our select dropdown options.
    let options = attributes.options;

    const loadPosts = ( ) => {
        
        // grab the exercise custom post types from the db
        someExercises.fetch().then( ( posts ) => {  
            
            // clear out the options array just incase something has changed
            // and add the default back in then repopulate.
            options = [ { value: 0, label: __( 'Select an exercise' ) } ];          
            posts.forEach((post) => {
                // build out the select dropdown options from the exercises
                options.push({value:post.id, label:post.title.rendered});
            });
            
            // save both options and posts.
            setAttributes( { options: options } );
            setAttributes( { posts: posts } );
        }); 
    }
    
    /* Set of functions for saving out attributes
    * reps, sets, notes
    */
    
    const onChangeReps = ( reps ) => {
        setAttributes( {
            reps: reps,
        } );
    }
    
    const onChangeSets = ( sets ) => {
        setAttributes( {
            sets: sets,
        } );
    }
        
    const onChangeNotes = ( notes ) => {
        setAttributes( {
            notes: notes,
        } );
    }   
        
    /* If select post isn't, and the posts has nothing in it
    *  Then we need to go and get some posts!
    */
    if (!attributes.selectedPost && attributes.posts.length == 0) {
            loadPosts();
    }
            
    // will contain the text output for the edit.
    let output = "";

    // array of returned exercise posts
    let posts = attributes.posts; 
    
    const blockProps = useBlockProps( {
        className: "sandcexercise",
    } );
    
    // we've selected a post, so grab the bits from that post to put into the html
    if (attributes.selectedPost) {
        
        // grab the post from the posts array using the ID in selectedPosts
        const selectedPost = posts.find( ( item ) => { return item.id == parseInt( attributes.selectedPost ) } );
                
        // grab the things we are putting in the innerblock from the post
        let mediaID = selectedPost.featured_media;
        let mediaURL = selectedPost['_embedded']['wp:featuredmedia'][0]["source_url"];
        let exerciseLink = selectedPost.link; 
        
        // remove any html content from the excerpt as it explodes the inner block :/
        let strippedContent = selectedPost.excerpt.rendered.replace(/(<([^>]+)>)/gi, "");
        
        // create a linked heading
        let heading = '<a href="' + exerciseLink + '">' + selectedPost.title.rendered + '</a>';
                
        // build the innerblocks template
        const MY_TEMPLATE = [
            [ 'core/heading', { content: heading } ],
            [ 'core/image', { id: mediaID, url: mediaURL, href: exerciseLink, align: "right", sizeSlug: "medium", caption: "Click for instructions" } ],
            [ 'core/paragraph', { content: strippedContent } ]
        ];  
        output = <InnerBlocks
                    template={ MY_TEMPLATE }
                    templateLock=""
                />
                    
    } else {
        output = <p>Select an exercise from the right</p>
    }
    
    // return the edit html.
    return [
            <div { ...blockProps } key="sandcexercise">
                <InspectorControls>
                    <div id="sandcexercise-controls">
                        <SelectControl 
                            onChange={ onSelectPost } 
                            value={ attributes.selectedPost } 
                            label={ __( 'Select a Post' ) } 
                            options={ options } 
                        />
                    </div>
                </InspectorControls>
                {output}
                <TextControl
                    { ...blockProps }
                    label="Reps:"
                    type="number"
                    className="thereps"
                    onChange={ onChangeReps }
                    value={ attributes.reps }
                />
                <TextControl
                    { ...blockProps }
                    label="Sets:"
                    type="number"
                    className="thesets"
                    onChange={ onChangeSets }
                    value={ attributes.sets }
                />
                <TextControl
                    { ...blockProps }
                    label="Notes:"
                    type="text"
                    className="thenotes"
                    onChange={ onChangeNotes }
                    value={ attributes.notes }
                />
            </div>      
    ]
    
}

export default mySelectPosts;

Any improvement suggestions (or obvious bugs pointed out) appreciated.

4
  • Can you show your complete code instead of just the save function?
    – Sally CJ
    Aug 27 '21 at 4:52
  • @SallyCJ I've added the two js files above. Thanks for looking!
    – delanthear
    Aug 27 '21 at 10:04
  • Sorry, I haven't really checked your code (although I already noticed some issues), but have you made any significant changes to your code? Or perhaps solved the issue already?
    – Sally CJ
    Aug 28 '21 at 12:33
  • Hi, just modified my code again to add the 'width: "300px"' attribute to the img block and boom, fails again. I'm fully expecting issues with it, as I barely know what I'm doing! :)
    – delanthear
    Aug 28 '21 at 17:50
0

It dies when I attempt to size the core/image block via the width attribute.

Yes, and it's because you used the wrong value whereby you used 300px which is a string and yet, the width attribute in the core/image block actually uses the number type. And in fact, that attribute is used as the value of the width attribute of an <img> tag, hence that's why the attribute type is number because that's just the way it is — see the MDN web docs for further details.

Therefore in MY_TEMPLATE, you should use width: 300 and not width: "300px".

And note that despite "300" contains just numbers, it's actually a string and not actual number. So when you want to specify a number/integer, don't put the value in quotes, which apparently is what you actually did... see line 383 in your build script here. =)

Any improvement suggestions (or obvious bugs pointed out) appreciated.

  1. Your block type is missing a title — it is mandatory and without it, the block type won't be registered — but I guessed that it's probably just missing in the question?

  2. The wp-api script is awesome, but when it comes to the block editor, I'd instead use getEntityRecords() and getEntityRecord() for retrieving posts from the REST API.

  3. posts shouldn't be added as an attribute. And what's worse is, you're actually storing the entire post object and doing so would make the post content be (extremely) huge, which is not good. :(

See my gist on GitHub for an example of how would posts be defined, and there were also other changes I did like changing the reps to a "real" number.

10
  • Ooh, loads of good feedback which I'll work on I don't think the main issue is actually resolved. WITH width: "300" in the img block, I've just created a new post, added a block, selected an exercise. It renders fine. I then save it and view the resulting page. This displays fine. If the THEN click edit, I get a validation error. See next comment:
    – delanthear
    Aug 30 '21 at 17:28
  • Block validation: Block validation failed for "core/image" (Object). Content generated by "save" function: <figure class="wp-block-image alignright" style="max-width:50%"><a href="http://localhost:8888/exercise/squats/"><img src="http://localhost:8888/wp-content/uploads/2021/08/98A4028-scaled.jpg" alt=""/></a><figcaption>Click for instructions</figcaption></figure>
    – delanthear
    Aug 30 '21 at 17:29
  • Content retrieved from post body: <div class="wp-block-image"><figure class="alignright size-medium is-resized"><a href="http://localhost:8888/exercise/squats/"><img src="http://localhost:8888/wp-content/uploads/2021/08/98A4028-scaled.jpg" alt="" class="wp-image-193" width="300"/></a><figcaption>Click for instructions</figcaption></figure></div>
    – delanthear
    Aug 30 '21 at 17:30
  • 1
    I'm not 100% sure what you mean by build script, but here is the package.json which npm is using to build it (I think that's how it works) pastebin.com/wKQT95iC. If you want the resulting built js file, that's here: pastebin.com/QhjktrVe and here is a screencap of me showing you the block error and the weirdness with the select dropdown: drive.google.com/file/d/1iBLjPeTLdL535S5RyZdBYLwZw-O8kGAI/…
    – delanthear
    Aug 31 '21 at 10:51
  • 1
    Thank you! Amazing that it was something so simple causing that.
    – delanthear
    Aug 31 '21 at 20:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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