A simple Gutenberg block plugin

We are really close to the public release of WordPress 5.0 with the new Gutenberg editor concept.

For all of you that needs to add functionalities to the new editor, I want to share a basic implementation of a Gutenberg block you can use as a template for your own development.

The Gutenberg editor is based on content blocks concept; each block can contain different types of content. text, images, title, photo gallery, videos, etc. It comes with many different types of block that you can use, but for anyone who needs to create its own content, this can be a starting point to understand how you can add your own block with its functionalities.

A Gutenberg block element needs a PHP file and a Javascript file.


The PHP code:

The first step is to create our PHP plugin file that will register the new block.

function mb_block_init()
{
  if (!function_exists('register_block_type'))
    return;

  wp_register_script(
      'mb-simple-block',
      plugins_url( 'mb-block.js', __FILE__ ),
      array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-editor' )
  );

  register_block_type( 'mbideas/mb-simple-block', array(
    /** Define the attributes used in your block */
      'attributes'  => array(
          'mb_title' => array(
              'type' => 'string'
          ), 
          'mb_text' => array(
              'type' => 'string'
          ),
          'mb_url' => array(
              'type' => 'string'
          )
      ),
      'category' => 'mb.ideas',
      'editor_script'   => 'mb-simple-block',
      'render_callback' => 'mb_block_render',
  ) );
}
add_action( 'init', 'mb_block_init' );


function mb_block_render( $attributes )
{

  $is_in_edit_mode = strrpos($_SSERVER ['REQUEST_URI'], 'context=edit');

  $UID = rand(0, 10000);

  if ($is_in_edit_mode) {

    if(!empty($attributes['mb_text'])){
      $content = '<div class="mb-editor-content" id="mb-editor-content_' . $UID . '">';
      $content .= '<h2 class="mb-editor-title"> ' . $attributes['mb_title'] . '</h2>';
      $content .= '<p class="mb-editor-text"> ' . $attributes['mb_text'] . '</p>';
      $content .= '<a class="mb-editor-url" href="' . $attributes['mb_url'] . '"> ' . $attributes['mb_url'] . '</a>';
      $content .= '</div>';
    } else {
      $content = '<div class="mb-editor-content" id="mb-editor-content_' . $UID . '">';
      $content .= '<h2 class="mb-editor-title"> ' . $attributes['mb_title'] . '</h2>';
      $content .= '</div>';
    }

  } else {
    $content = '<div class="mb-editor-content" id="mb-editor-content_' . $UID . '" style="background:#f3f3f3; padding:20px">';
    $content .= '<h2 class="mb-editor-title"> ' . $attributes['mb_title'] . '</h2>';
    $content .= '<p class="mb-editor-text"> ' . $attributes['mb_text'] . '</p>';
    $content .= '<a class="mb-editor-url" href="' . $attributes['mb_url'] . '"> ' . $attributes['mb_url'] . '</a>';
    $content .= '</div>';
  }
  return $content;
}


add_filter( 'block_categories', 'mb_block_categories', 10, 2 );
function mb_block_categories( $categories, $post )
{
  return $categories;
  /** If mb.ideas is already in categories return categories */
  if($categories['slug'] == 'mb.ideas')
    return $categories;
  return array_merge(
      $categories,
      array(
          array(
              'slug' => 'mb.ideas',
              'title' => __( 'mb.ideas Blocks', 'mbBlock' ),
          ),
      )
  );
}

The init function:

function mb_block_init()
{
  if (!function_exists('register_block_type'))
    return;

  wp_register_script(
      'mb-simple-block',
      plugins_url( 'mb-block.js', __FILE__ ),
      array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-editor' )
  );

  register_block_type( 'mbideas/mb-simple-block', array(
    /** Define the attributes used in your block */
      'attributes'  => array(
          'mb_title' => array(
              'type' => 'string'
          ), 
          'mb_text' => array(
              'type' => 'string'
          ),
          'mb_url' => array(
              'type' => 'string'
          )
      ),
      'category' => 'mb.ideas',
      'editor_script'   => 'mb-simple-block',
      'render_callback' => 'mb_block_render',
  ) );
}
add_action( 'init', 'mb_block_init' );

Check if the Gutenberg editor is active; if not just return:

/**
* Check if Gutemberg is active
*/
if (!function_exists('register_block_type'))
&nbsp; return;

Add the block editor javascript file:

/**
* Register our block editor script
*/
wp_register_script(
    'mb-simple-block',
     plugins_url( 'mb-block.js', __FILE__ ),
     array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-editor' )
);

Register the new block:

  register_block_type( 'mbideas/mb-simple-block', array(
      'attributes'  => array(
          'mb_title' => array(
              'type' => 'string'
          ), 
          'mb_text' => array(
              'type' => 'string'
          ),
          'mb_url' => array(
              'type' => 'string'
          )
      ),

      'category' => 'mb.ideas',

      'editor_script'   => 'mb-simple-block',

      'render_callback' => 'mb_block_render',
  ) );

Use the register_block_type function to register a new block.
The first parameter ‘mbideas/mb-simple-block’ is the unique block name with a namespace that identify your blocks.
The second is an arrey containing the properties of the block we are going to realize.

 ‘attributes’

Those are the dynamic element of your block; in this case I’m going to define a title, a text and an URL as dynamic element for my block.

‘category’

The category let you set the section where your block will be listed on the main block menu of the editor. You can set one of the built in category or create one your own as explained below.

‘editor_script’

We specify the reference to the javascript file of the block.

‘render_callback’

We specify the PHP callback used by the block to render the content on the front-end and in the back-end.

The mb_block_render function

This function is called each time the Edit javascript method is invoked and a parameter is changed.

function mb_block_render( $attributes )
{

  $is_in_edit_mode = strrpos($_SSERVER ['REQUEST_URI'], 'context=edit');

  $UID = rand(0, 10000);

  if ($is_in_edit_mode) {

    if(!empty($attributes['mb_text'])){
      $content = '<div class="mb-editor-content" id="mb-editor-content_' . $UID . '">';
      $content .= '<h2 class="mb-editor-title"> ' . $attributes['mb_title'] . '</h2>';
      $content .= '<p class="mb-editor-text"> ' . $attributes['mb_text'] . '</p>';
      $content .= '<a class="mb-editor-url" href="' . $attributes['mb_url'] . '"> ' . $attributes['mb_url'] . '</a>';
      $content .= '</div>';
    } else {
      $content = '<div class="mb-editor-content" id="mb-editor-content_' . $UID . '">';
      $content .= '<h2 class="mb-editor-title"> ' . $attributes['mb_title'] . '</h2>';
      $content .= '</div>';
    }

  } else {
    $content = '<div class="mb-editor-content" id="mb-editor-content_' . $UID . '" style="background:#f3f3f3; padding:20px">';
    $content .= '<h2 class="mb-editor-title"> ' . $attributes['mb_title'] . '</h2>';
    $content .= '<p class="mb-editor-text"> ' . $attributes['mb_text'] . '</p>';
    $content .= '<a class="mb-editor-url" href="' . $attributes['mb_url'] . '"> ' . $attributes['mb_url'] . '</a>';
    $content .= '</div>';
  }
  return $content;
}

Check if it is rendering on the back end of in the front end:

  $is_in_edit_mode = strrpos($_SSERVER['REQUEST_URI'], 'context=edit');
  ...
  if ($is_in_edit_mode) {
     ...
  }

First we check if this function is called from the back end or from the front end.
The block content changes according with the context of its call.

Check if properties are already defined:

  if(!empty($attributes['mb_text'])){
     .....
  }

We also change the content depending if it’s a new content or if it has been already edited by checking if a property is already defined.

Add a new block category:

If you want to add your own block category here is the filter you should use:

add_filter( 'block_categories', 'mb_block_categories', 10, 2 );
function mb_block_categories( $categories, $post )
{

  /** If mb.ideas is already in categories return categories */
  if($categories['slug'] == 'mb.ideas')
    return $categories;

  return array_merge(
      $categories,
      array(
          array(
              'slug' => 'mb.ideas',
              'title' => __( 'mb.ideas Blocks', 'mbBlock' ),
          ),
      )
  );
}

The javascript code:

/**
 * mb Gutemberg block
 *  Copyright (c) 2001-2018. Matteo Bicocchi (Pupunzi)
 */
var el = wp.element.createElement,
    registerBlockType = wp.blocks.registerBlockType,
    ServerSideRender = wp.components.ServerSideRender,
    TextControl = wp.components.TextControl,
    TextareaControl = wp.components.TextareaControl,
    InspectorControls = wp.editor.InspectorControls;

/** Set the icon for your block */
var mb_icon = el ("img", {
  src: "/wp-content/plugins/mb.gutemberg-block/images/mb_block.svg",
  width: "50px",
  height: "50px"
});

/** Register the block */ 
registerBlockType( 'mbideas/mb-simple-block', {
  title: 'mb Simple Block',
  icon: mb_icon,
  category: 'mb.ideas',
  
  attributes: {
    
    'mb_title': {
      type: 'string',
      default: "mb Editor content block"
    },
    'mb_text': {
      type: 'string',
      default: "Write here some text"
    },
    
    'mb_url': {
      type: 'string',
      default: "https://pupunzi.com"
    },
  },

  edit: (props) => {
    
    if(props.isSelected){
      // do something...
      //console.debug(props.attributes);
    };
    
    return [
      /**
       * Server side render
       */
      el("div", {
            className: "mb-editor-container",
            style: {textAlign: "center"}
          },
          el( ServerSideRender, {
            block: 'mbideas/mb-simple-block',
            attributes: props.attributes
          } )
      ),

      /**
       * Inspector
       */
      el( InspectorControls,
          {}, [
            el( "hr", {
              style: {marginTop:20}
            }),
      
            el( TextControl, {
              label: 'Title',
              value: props.attributes.mb_title,
              onChange: ( value ) => {
                props.setAttributes( { mb_title: value } );
              }
            } ),
            
            el( TextareaControl, {
              style: {height:250},
              label: 'Content',
              value: props.attributes.mb_text,
              onChange: ( value ) => {
                props.setAttributes( { mb_text: value } );
                console.debug(props.attributes)
              }
            }, props.attributes.mb_text ),
      
            el( TextControl, {
              label: 'Url',
              value: props.attributes.mb_url,
              onChange: ( value ) => {
                props.setAttributes( { mb_url: value } );
              }
            } ),
          ]
      )
    ]
  },
  
  save: () => {
    /** this is resolved server side */
    return null
  }
} );

The Gutenberg blocks are rendered in the editor via javascript generally using React.
In this example we use a server side function to render the HTML block content (b_block_render) and simple ES6 for the javascript without the need to install the React framework to compile the script.

register the block via javascript:

The registerBlockType function define the new block and all its properties.

registerBlockType( 'mbideas/mb-simple-block', {
  title: 'mb Simple Block',
  icon: mb_icon,
  category: 'mb.ideas',

As in the PHP register_block_type function it accept two parameters: the unique block namespace and an object containing all the properties of the block including the “attributes”, the “edit” and the “save” method for the back end and the front end renderer.

attributes

  
  attributes: {
    
    'mb_title': {
      type: 'string',
      default: "mb Editor content block"
    },
    'mb_text': {
      type: 'string',
      default: "Write here some text"
    },
    'mb_url': {
      type: 'string',
      default: "https://pupunzi.com"
    },
  },

  ...

The attributes object define all the dynamic elements of your block.

edit

  edit: (props) => {
    
    if(props.isSelected){
      // do something...
      //console.debug(props.attributes);
    };
    
    return [
      /**
       * Server side render
       */
      el("div", {
            className: "mb-editor-container",
            style: {textAlign: "center"}
          },
          el( ServerSideRender, {
            block: 'mbideas/mb-simple-block',
            attributes: props.attributes
          } )
      ),

      /**
       * Inspector
       */
      el( InspectorControls,
          {}, [
            el( "hr", {
              style: {marginTop:20}
            }),
      
            el( TextControl, {
              label: 'Title',
              value: props.attributes.mb_title,
              onChange: ( value ) => {
                props.setAttributes( { mb_title: value } );
              }
            } ),
            
            el( TextareaControl, {
              style: {height:250},
              label: 'Content',
              value: props.attributes.mb_text,
              onChange: ( value ) => {
                props.setAttributes( { mb_text: value } );
                console.debug(props.attributes)
              }
            }, props.attributes.mb_text ),
      
            el( TextControl, {
              label: 'Url',
              value: props.attributes.mb_url,
              onChange: ( value ) => {
                props.setAttributes( { mb_url: value } );
              }
            } ),
          ]
      )
    ]
  }

In the edit method we specify the content for the block and the content for the inspector creating the HTML elements. In this example the block content element is created using the server side renderer function (wp.components.ServerSideRender) that we defined on the PHP file:

      el("div", {
            className: "mb-editor-container",
            style: {textAlign: "center"}
          },
          el( ServerSideRender, {
            block: 'mbideas/mb-simple-block',
            attributes: props.attributes
          } )
      ),

The inspector content contains the field for the dynamic elements:

      el( InspectorControls,
          {}, [
            el( "hr", {
              style: {marginTop:20}
            }),
      
            el( TextControl, {
              label: 'Title',
              value: props.attributes.mb_title,
              onChange: ( value ) => {
                props.setAttributes( { mb_title: value } );
              }
            } ),

            ...

Changing the value of the inspector element the block properties are updated changing the block content.

You can download this example here