1

I was wondering what is a good approach to remove a Gutenberg block programatically.

I have a custom content type where I have the option to choose a forum post via a select field that I added via React and the WP API. This post's title and description then gets added to the post content.

Here is the block:

/**
 * BLOCK: my-forum-block
 *
 * Registering a basic block with Gutenberg.
 * Simple block, renders and saves the same content without any interactivity.
 */

//  Import CSS.
import './editor.scss';
import './style.scss';

const { __ } = wp.i18n; // Import __() from wp.i18n
const { registerBlockType } = wp.blocks; // Import registerBlockType() from wp.blocks
const { InspectorControls } = wp.editor;
const { SelectControl } = wp.components;
const { Component } = wp.element;

class mySelectPosts extends Component {
    // Method for setting the initial state.
    static getInitialState( selectedPost ) {
        return {
            posts: [],
            selectedPost: selectedPost,
            post: {},
        };
    }
    // Constructing our component. With super() we are setting everything to 'this'.
    // Now we can access the attributes with this.props.attributes
    constructor() {
        super( ...arguments );
        this.state = this.constructor.getInitialState( this.props.attributes.selectedPost );
        // Bind so we can use 'this' inside the method.
        this.getOptions = this.getOptions.bind(this);
        // Load posts.
        this.getOptions();
        // bind it
        this.onChangeSelectPost = this.onChangeSelectPost.bind(this);
    }

    onChangeSelectPost( value ) {
        // Find the post
        const post = this.state.posts.find( ( item ) => { return item.id == parseInt( value ) } );
        //console.log('this post');
        //console.log(post);
        // Set the state
        this.setState( { selectedPost: parseInt( value ), post } );
        // Set the attributes
        this.props.setAttributes( {
            selectedPost: parseInt( value ),
            title: post.title.rendered,
            content: post.content.rendered,
            link: post.link,
            due_date: post.due_date,
            slug: post.slug,
        });
    }

    /**
     * Loading Posts
     */
    getOptions() {

        const CustomPost = wp.api.models.Post.extend( {
            urlRoot: wpApiSettings.root + 'wp/v2/topic',
            defaults: {
                type: 'topic',
            },
        } );
        const CustomPosts = wp.api.collections.Posts.extend( {
            url: wpApiSettings.root + 'wp/v2/topic',
            model: CustomPost,
        } );
        const someCustomPosts = new CustomPosts();
        /* someCustomPosts.fetch().then( ( posts ) => {
            // do something with the custom posts
        } ); */

        return ( someCustomPosts ).fetch({ data: { 'filter': { 'orderby': 'title', 'order': 'ASC' } } }).then( ( posts ) => {
                if( posts && 0 !== this.state.selectedPost ) {
            // If we have a selected Post, find that post and add it.

            const post = posts.find( ( item ) => { return item.id == this.state.selectedPost } );
            // This is the same as { post: post, posts: posts }
            this.setState( { post, posts } );
            } else {
                this.setState({ posts });
            }
        } );
    }

    render() {
        let options = [ { value: 0, label: __( 'Select a Post' ) } ];
        let output  = __( 'Loading Posts' );
        if( this.state.posts.length > 0 ) {
            const loading = __( 'We have %d posts. Choose one.' );
            output = loading.replace( '%d', this.state.posts.length );
            this.state.posts.forEach((post) => {
                options.push({value:post.id, label:post.title.rendered});
        });
        } else {
            output = __( 'No posts found. Please create some first.' );
        }
        // Checking if we have anything in the object
        if( this.state.post.hasOwnProperty('title') ) {
            output = <div className="post">
                <a href={ this.state.post.link }><h2 dangerouslySetInnerHTML={ { __html: this.state.post.title.rendered } }></h2></a>
            <p dangerouslySetInnerHTML={ { __html: this.state.post.content.rendered } }></p>
            </div>;
            this.props.className += ' has-post';
        } else {
            this.props.className += ' no-post';
        }
        return [
            !! this.props.isSelected && ( <InspectorControls key='inspector'>
                <SelectControl onChange={this.onChangeSelectPost} value={ this.props.attributes.selectedPost } label={ __( 'Select a Post' ) } options={ options } />
            </InspectorControls>
    ),
    <div className={this.props.className}>{output}</div>
    ]
    }
}

/**
 * Register: aa Gutenberg Block.
 *
 * Registers a new block provided a unique name and an object defining its
 * behavior. Once registered, the block is made editor as an option to any
 * editor interface where blocks are implemented.
 *
 * @link https://wordpress.org/gutenberg/handbook/block-api/
 * @param  {string}   name     Block name.
 * @param  {Object}   settings Block settings.
 * @return {?WPBlock}          The block, if it has been successfully
 *                             registered; otherwise `undefined`.
 */
registerBlockType( 'cgb/block-my-forum-block', {
    // Block name. Block names must be string that contains a namespace prefix. Example: my-plugin/my-custom-block.
    title: __( 'Choose LearnDash Forum Topic' ),
    icon: 'shield', 
    category: 'common',
    keywords: [
        __( 'learndash' ),
        __( 'forum' ),
        __( 'my-forum-block' ),
    ],

    attributes: {
        content: {
            type: 'array',
            source: 'children',
            selector: 'p',
        },
        title: {
            type: 'string',
            selector: 'h3'
        },
        link: {
            type: 'string',
            selector: 'a'
        },
        selectedPost: {
            type: 'number',
            default: 0,
        },
        due_date: {
            type: 'string',
        },
        slug: {
            type: 'string',
        },
    },

    /**
     * The edit function describes the structure of your block in the context of the editor.
     * This represents what the editor will render when the block is used.
     *
     * The "edit" property must be a valid function.
     *
     * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
     *
     * @param {Object} props Props.
     * @returns {Mixed} JSX Component.
     */
    edit: mySelectPosts,

    /**
     * The save function defines the way in which the different attributes should be combined
     * into the final markup, which is then serialized by Gutenberg into post_content.
     *
     * The "save" property must be specified and must be a valid function.
     *
     * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
     *
     * @param {Object} props Props.
     * @returns {Mixed} JSX Frontend HTML.
     */
    save: function( props ) {
        return (
                <div id = {`${props.attributes.slug}`} className="contenttype-wrapper sometopictype-discussion" data-id = {`${props.attributes.selectedPost}`}>
                    <div className = "heading d-flex flex-row">
                        <i className="contenticon fas fa-users fa-7x"></i>
                        <div className = "col">
                            <div className = "row required-discussion-text"><span className="content-name">Required Discussion&nbsp;&nbsp;&nbsp; <i class="far fa-calendar-alt"></i>&nbsp;&nbsp;&nbsp;Due: {props.attributes.due_date}</span></div>
                            <div className = "row"><a href={ props.attributes.link }><h3 dangerouslySetInnerHTML={ { __html: props.attributes.title } }></h3></a></div>
                        </div>
                    </div>
                    <div className = "row discussion-overview" dangerouslySetInnerHTML={ { __html: props.attributes.content } } ></div>
                </div>
        );
    },
} );

But if I delete the forum post, then the block should also be removed. I figure I could read the content of the custom post and use PHP xpath to parse the block then remove it, but was wondering if there is other way, especially since PHP's DOM related functions can break down when they see unexpected tags.

I read about the WP Rest API and how blocks are endpoints themselves. That confused me a bit. It got me thinking, if blocks have endpoints, does that mean I can remove a block via an API call? Or are these endpoints just for the block type (like Layout-My Layout Block) and not for a rendered block?

Thanks, Brian

Edit:

Added block code

  • Can you share the code for your block? This would make it much easier to answer. :) I would build this type of block with a serverSideRender component and let the PHP handle it. That way if forum post title or content ever changes, you're getting the latest based on the post ID. The PHP itself can include a conditional - if the post is found and published, display it, but if not, render nothing at all - that way you don't have to try to delete blocks, they just won't display anything if there's nothing to show. – WebElaine Dec 4 '19 at 16:04
  • Sure, just pasted. Like the idea about serverSideRender, but maybe it really wouldn't be too much trouble to just remove a block from the post content in a delete_post hook. And for edits, I could certainly replace current block content with new content via xpath operations. – Brian Dec 4 '19 at 18:08
1

For the original approach - when you delete a forum post, search the database and remove any blocks that refer to that specific post - it looks like the only way to accomplish this would be with a regular expression, which could break in future updates.

You could hook into the delete_post hook, verify that the deleted post is a "forum" post, then get all posts that could contain the forum block, loop through them and parse_blocks() so you could check whether the current post contains a forum block with the forum post ID as an attribute.

But, where this process breaks down is, we don't currently have a serialize_blocks() function to convert the blocks back into HTML comments. (They're discussing adding one in Trac but it's not ready yet.) So, even though you could identify all of the posts that contain the block you're looking for, you would then have to create a regular expression to update the actual post_content of those posts.

For what it's worth, here is the code as far as it goes:

<?php
add_action('delete_post', 'wpse_delete_forum_blocks', 10, 1);
// The forum post ID is the only argument
function wpse_delete_forum_blocks($post_id) {
    $post_type = get_post_type($post_id);
    // Only run this code if the deleted post was a "forum" CPT
    if($post_type == 'forum') {
        // Get all posts that may contain the forum block
        // Arguments are up to you - perhaps only a specific post type can contain it
        $check_posts = get_posts(
            array(
                // Get all posts
                'numberpost' => -1
                // Get "post" post type / adjust as needed
                'post_type' => 'post'
            )
        );
        // Loop through all the posts
        foreach($check_posts as $post) {
            // If the post has blocks
            if(has_blocks($post->post_content)) {
                // Parse the blocks
                $blocks = parse_blocks($post->post_content);
                // Loop through the blocks
                foreach($blocks as $block) {
                    // If this is a "forum" block, and its "post_id" attribute matches the id we're looking for
                    // (make sure to change this to your applicable namespace and block name)
                    if($block['blockName'] === 'stmu/icon-heading' && $block['attrs']['post_id'] === $post_id) {
                        // This is where you need to create a regular expression to remove just this block.
                    }
                }
            }
        }
    }
}
?>

In the meantime, server-side rendering would allow you to show either the post if found, or nothing if it's gone.

|improve this answer|||||
  • Thanks for showing this! I think the server side rendering sounds easier and future proof. Will look into how to do that. – Brian Dec 5 '19 at 17:41

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.