1

I have a problem with Gutenberg block. I created block which pulls recent custom post type posts and display them as carousel.

I struggled with saving that block but I found info that I should use ServerSideRender in js file.

Now I have a problem in backend editor. When I add my custom block I'm unable to select it or delete.

Below the code of js:

const { serverSideRender: ServerSideRender } = wp;

wp.blocks.registerBlockType('wp-portfolio-manager/wppm-block', {
    title: 'Portfolio Block', 
    description: 'Block containing portfolio projects',
    category: 'common',
    icon: 'screenoptions',
    keywords: [
         'portfolio',
         'project', 
         'wppm'
    ],
    supports: {
        html: true, 
        align: true
    },
    attributes: {
        projects_count: {
            type: 'integer' 
        }
    },
    edit: ( props ) => {
        return <ServerSideRender block="wp-portfolio-manager/wppm-block"></ServerSideRender>;
    },
    save: (props) => {
        return null;
    }
});
export class Portfolio extends React.Component{
    constructor(props){
        super(props);
        this.props = props;
        this.state = {
            error: null,
            postsFetched: false,
            mediaFetched: false,
            posts: []
        };
    }

    componentDidMount(){
        let postes = [];
        fetch("/foobar/wp-json/wp/v2/foobar_post?per_page=5&_embed")
         .then(res => res.json())
         .then(
             (result) => {
                 result.forEach(function(item){
                    postes.push({ 
                        id: item['id'],
                        title: item.title['rendered'],
                        featured_image: item['_embedded']['wp:featuredmedia'][0]['source_url'],
                        url: item.link,
                    });
                });
                this.setState({
                    postsFetched: true,
                    mediaFetched:  true,
                    posts: postes
                    }
                );
             },
             (error) => {
                 this.setState({
                    postsFetched: true,
                    mediaFetched: true,
                    error: error
                 });
             }
         );
    }

    render(){
        const {postsFetched, mediaFetched, posts, error} = this.state;

        if(postsFetched && mediaFetched){
            let items = [];
            for(let i = 0; i < this.state.posts.length; i++){
                items.push(
                    <Project post={posts[i]}></Project>
                );
            }
            return <div className="wppm-portfolio">
                {items}
            </div>;
            
        }
        else if(error){
            return <div className="wppm-portfolio error">{error.message}</div>;
        }
        else{
            return <div className="wppm-portfolio loading dashicons dashicons-update"></div>;
        }
    }

}

class Project extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            media: []
        };
    }

    componentDidMount(){
        let medias = [];
        fetch("/foobar/wp-json/wp/v2/media?media_type=image&parent=" + this.props.post.id)
        .then(res => res.json())
        .then(
            (result) => {
                result.forEach(function(item){
                    medias.push(
                        {
                            url: item.source_url,
                            title: item.title['rendered'],
                            alt: item.alt_text
                        }
                        
                    );
                });
                this.setState(
                    { media: medias }
                );
            }
        );
    }

    render(){
        const { post } = this.props;
        const { media } = this.state;
        if(media === null || media == 'undefined' || media.length < 1)
            return <div className="portfolio-project loading">Loading</div>;

        let gallery = [];
        for(let i = 0; i < media.length; i++){
            gallery.push(
                <ProjectImage url={media[i].url} title={media[i].title} alt={media[i].alt}></ProjectImage>
            );
        }
        return <div className="portfolio-project">
            <div className="project-container">
                <div className="project-actions">
                    <span className="dashicons dashicons-search"></span>
                    <span className="dashicons dashicons-format-gallery"></span>                
                </div>
                <img src={post.featured_image} className="img-fluid"/>
                <div className="project-title">{post.title}</div>
            </div>
            <div className="project-gallery hidden">
                {gallery}
            </div>
        </div>;

    }
}

class ProjectImage extends React.Component{
    constructor(props){
        super(props);
    }

    render(){
        const {url, alt, title} = this.props;
        return <div className="image-container">
            <img src={url} alt={alt} title={title}/>
        </div>;
    }
}

and here is the code of render_callback function in php:


function wppm_portfolio_block_render_callback($block_attributes, $content){
    if ( !defined( 'REST_REQUEST' ) || !REST_REQUEST ) {
        $projects = wp_get_recent_posts( array(
            'numberposts' => 5, //ToDo: Ilość z bloku (w bloku obsłużyć attributes)
            'post_status' => 'publish',
            'post_type'   => 'wppm_portfolio'
        ) );
        if ( count( $projects ) === 0 ) {
            return 'No posts';
        }
        $elements = '';
        foreach($projects as $post){
            $featured_image = get_the_post_thumbnail( $post['ID'] ) == '' ? '<img src="' . __WPPM_IMG__ . '/placeholder.jpg' . '">' : get_the_post_thumbnail( $post['ID'] );
            $post_media = get_attached_media( 'image', $post['ID'] );
            $gallery = '';
            if(isset($post_media) && is_array($post_media) && count($post_media) > 1){
                $images = '';
                foreach($post_media as $image){
                    $image_tag = wp_get_attachment_image($image->ID, 'full');
                    $image_element = sprintf('
                        <div class="image-container">
                            %1$s
                            <div class="image-title">
                                %2$s
                            </div>
                        </div>', 
                        $image_tag, 
                        $image->title
                    );
                    $images .= $image_element;
                }
                $gallery = sprintf(
                    '<div class="project-gallery hidden">
                        %s
                    </div>',
                    $images
                );
            }
            $element = sprintf('<div class="portfolio-project">
                            <div class="project-container">
                                <div class="project-actions">
                                    <span class="dashicons dashicons-search"></span>
                                    <span class="dashicons dashicons-format-gallery"></span>
                                </div>
                                %1$s
                                <div class="project-title">%2$s</div>
                            </div>
                            %3$s
                        </div>', $featured_image, $post['post_title'], $gallery);
            $elements .= $element;
        }
        $result = sprintf('<div class="wppm-portfolio">%s</div>', $elements);
        return $result;
}
function wppm_portfolio_block(){
    wp_register_script( 'wppm_block', __WPPM_ADMIN_JS__ . '/block.js', array('wp-block-editor', 'wp-blocks', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-server-side-render'), null);
    register_block_type('wp-portfolio-manager/wppm-block', array(
        'api_version' => 2,
        'editor_script' => 'wppm_block',
        'render_callback' => 'wppm_portfolio_block_render_callback'
    ));
}
add_action( 'init', 'wppm_portfolio_block' );

What should I do to make it selectable and removable on the backend editor?

1 Answer 1

1

You've gone with a class based component and the normal solution for this problem is that you forgot to use useBlockProps.

Your edit component and save components do not use useBlockProps:

    edit: ( props ) => {
        return <ServerSideRender block="wp-portfolio-manager/wppm-block"></ServerSideRender>;
    },
    save: (props) => {
        return null;
    }

Wrap your server side render component in a div and apply useBlockProps to that div and the problem should go away.

Something similar to this:

<div { ...useBlockProps() }>
    ... etc
</div>

useBlockProps gives the opportunity for the block editor to inject event handlers and extra CSS classes. This is how it styles the selected block and handles the selection process, as well as things like animating block movers etc.

Also, be wary of class based components. WP hook APIs such as useBlockProps don't work with class based components. Everything you are doing here can be done with useState and useEffect, though it looks like none of your class based components are actually in use.

17
  • Do I need to switch entire code or only the base class (the top most which contains rest of objects)?
    – bgolemba
    Commented Dec 1, 2021 at 12:34
  • the component used for save/edit, your block can be selected, there's just nothing for the editor to attach event handles and HTML classes to for styling, that's the primary use for useBlockProps ( though not the only purpose )
    – Tom J Nowell
    Commented Dec 1, 2021 at 12:44
  • for your API fetch, a useEffect should work, you can pass dependencies as a parameter so that it only fetches when those variables change. I would also be wary of polling the REST API that way, you should use the built in entities API, that way WP itself will handle state for you as well as fetching the data. At that point there's no need for any lifecycle or useEffect hooks in your code
    – Tom J Nowell
    Commented Dec 1, 2021 at 12:47
  • Also const { value, setValue } = useState( initialValue );
    – Tom J Nowell
    Commented Dec 1, 2021 at 12:47
  • To summarize in my case I need to switch only code of Portfolio class. The rest could stay as is. Am I right?
    – bgolemba
    Commented Dec 1, 2021 at 13:12

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.