Creating the p5.js Gutenberg Block

As I dedicate more time toward Gutenberg Phase 2, I realize, what better way to understand some of the core concepts of Gutenberg than by building my own block. Sure I’ve helped build some Jetpack blocks recently, but that was really more teamwork than me soloing through the endeavor on my own. I learned a lot about basic Gutenberg design, but not until now did I learn the nitty-gritty behind the development process.

I’ve been itching to build a block that accepts p5.js code and renders it on the front-end. With winter break upon me, there was no better time. How hard could it be?

I’ve got a completed prototype here:

https://github.com/mapk/p5js-block

Example of block rendered on front-end:

What the block required

  1. p5.js library and other assets.
  2. Ability to accept JS and preview the rendered JS in the editor.
  3. Ability to display the rendered JS on the front-end.

Step 1

As this is my first block from scratch, let’s not have to build it from scratch. Thanks to Ahmad Awais for the Create Guten Block Toolkit. With this “very impressive” (Matt Mullenweg, State of the Word, 2018) toolkit, I had the structure for a block up in a few minutes.

In the block.js file, under registerBlockType, I updated these settings:

title: __( 'p5js' ),
description: __( 'Add custom p5.js code and preview it as you edit.' ),
icon: <SVG viewBox="0 0 125 114" xmlns="http://www.w3.org/2000/svg"><Path d="M75.9,40.4l38.8-11.7l7.6,23.4L83.6,65.3l24,34L87.4,114L62.2,80.6l-24.6,32.5l-19.6-15l24-32.8L3,51.3l7.6-23.5l39.1,12.6V0h26.2L75.9,40.4L75.9,40.4z" fill="#ED225D" /></SVG>,
category: 'formatting',
keywords: [
	__( 'p5js' ),
	__( 'Processing' ),
	__( 'generative art' ),
],

This ultimately gave me the polish in the editor that I needed.

Step 2

I knew I had to enqueue the p5.js library. Without this, ain’t nothing gonna happen. After some quick reading, and scanning the Create Guten Block code, I found it in the init.php file. Under the p5js_cgb_block_assets() function, I added the necessary enqueue.

wp_enqueue_script(
  'p5-js',
  plugins_url( '/assets/js/p5.min.js', dirname( __FILE__ ) ),
    array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor' ), 
    'assets/js/p5.min.js' ),
    true
);

Step 3

I began some investigative work into existing blocks that kind of do what I was hoping to accomplish. The HTML block in core Gutenberg was a great start. It had an input field where one can enter markup, it had a “Preview” mode within the Editor, and rendered the preview mode on the front-end.

As I’m not an avid JavaScript developer, I copy and pasted several lines of code over to my own block. I had taken a few Gutenberg block building tutorials before, so I had some super simple knowledge of how things worked together.

One thing I noticed is that there’s much to be desired in the documentation. Every time I came across something new, I’d try to go read up on it. I wanted to make sure the code I was using was indeed the best code for the job. However, there were a lot of components that didn’t provide enough documentation to really understand what it did (ie. Sandbox).

In addition to the HTML core block, I also reviewed the code for the Jetpack Markdown block. This block also provided a way to preview from within Gutenberg and saved out a rendered version of the block. Still, the process for building these was a bit fuzzy.

Step 4

Once I had all the necessary pieces pasted into my block, I was ready to edit it for what I needed. This was fraught with struggle. Because I didn’t have a super clear understanding of all the parts, and documentation was lacking, I really spent the majority of the time in trial and error. I’d try something out, examine it in the browser, and iterate again.

There were just too many unknowns. One of them was returning what I wanted in the save function. With a little trickery, I figured I could just wrap the { attributes, content } with a <script> tag. This worked a bit, but I couldn’t get the code that I entered into the block to persist upon page refresh.

I hit a wall, so pinged some JavaScript developers. Turns out the SandBox component was a good place to fix. I had to import the p5.js library there as well to get it to render in the Preview mode.

( isPreview || isDisabled ) ? (
  <SandBox html={
    '<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/p5.min.js"></script>' + 
    '<script>' + attributes.content + '</script>'
  } />
)

I was a lot more closer, but just one more bit to tweak in the save function.

save( { attributes } ) {
  return <script dangerouslySetInnerHTML={ { __html: attributes.content } } />;
},

That dangerouslySetInnerHTML was just what the doctor ordered. Of course it’s dangerous, but that’s cool. This block was working… well in Gutenberg. It’s important to note that it still was not working on the front-end at this point. WordPress doesn’t like people rendering JavaScript on the front-end, and for good reason.

Step 5

Couldn’t get it working on the front-end, so it’s time to rewrite what I just wrote. It was recommended to render the code in an iframe on the front-end. So that’s what I did. Had to edit the init.php a bit.

function p5js_cgb__render_block( $attributes, $content ) {
  $scriptsAndStyles = [
    '<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/p5.js"></script>',
    '<script>' . $attributes['content'] . '</script>',
    '<style>body{margin: 0; padding: 0;}</style>'
  ];
  return sprintf(
    '<div class="%s"><iframe src="data:text/html;charset=utf-8,%s"></iframe></div>',
    'wp-block-cgb-block-p5js',
    htmlspecialchars( implode( $scriptsAndStyles, '' ), ENT_QUOTES ) );
}

register_block_type( 'cgb/block-p5js', array(
  'render_callback' => 'p5js_cgb__render_block',
) );

And then rewrite the save function.

save( { attributes } ) {
  return null;
},

This was now working on the front-end! Finally all three states were jiving: the editor, the preview, and the front-end.

Step 6

I had to pass the canvas height up through the iframe and store it as the iframe’s height now. This was quite the endeavor. I’m not familiar with cross-origin stuff, so had to rely on some jQuery and developer help. This finally got resolved by using the srcdoc attribute in the iframe.

return sprintf(
  '<div class="%s"><iframe srcdoc="%s"></iframe></div>',
  'wp-block-cgb-block-p5js',
  htmlspecialchars( implode( $scriptsAndStyles, '' ), ENT_QUOTES ) 
);

Now I was able to dynamically grab the canvas height and set it to the height of the iframe! Yay!

My next step will be to get this block in the WordPress.org Plugins Library.