Wordpress's new Gutenberg editor is an extremely hot new topic in the web development world. With it, Wordpress is fully embracing React and modern JavaScript, bringing millions of developers into the modern front-end world, and creating a massive audience for existing Frontend developers.

While React is the primary framework supported for Gutenberg, there have been indications that implementing Gutenberg blocks using other JavaScript frameworks like Vue.js should be possible using libraries like vuera, so I decided to explore how to get this to happen and how well it works.

TL/DR: We can implement basic blocks in Vue without much trouble, but we quickly run into limitations if we try to use Wordpress builtins like BlockControls or InnerContent.

Setting Up

First off, we're going to set up the plugin with create-guten-block scaffold.

Go into the `wp-content/plugins` directory and set up a new plugin:

npm install -g create-guten-block
npx create-guten-block vuetenberg

This creates a scaffold with a very basic initial block that lives in src/block/block.js. Once you activate it in your wordpress admin, you'll be able to see it.

For the purpose of this blog post, I'm not going to change much about the functionality of this block, simply convert it to using Vue and Vue Single File Components (SFCs)

To do this it is helpful to understand the core structure of a Gutenberg block. It consists of a pure JavaScript object that contains a number of fields, including two - edit and save - which are React components.

registerBlockType( 'cgb/block-vuetenberg', {
  // Block name. Block names must be string that contains a namespace prefix. Example: my-plugin/my-custom-block.
  title: __( 'vuetenberg - CGB Block' ), // Block title.
  icon: 'shield', // Block icon from Dashicons → https://developer.wordpress.org/resource/dashicons/.
  category: 'common', // Block category — Group blocks together based on common traits E.g. common, formatting, layout widgets, embed.
  keywords: [
    __( 'vuetenberg — CGB Block' ),
    __( 'CGB Example' ),
    __( 'create-guten-block' ),
  ],

  edit: function( props ) {
    // Creates a <div class='wp-block-cgb-block-vuetenberg'></div>.
    return (
      <div className={ props.className }>
        <p> Hello from the backend.</p>
      </div>
    );
  },

  save: function( props ) {
    return (
      <div>
        <p> Hello from the frontend.</p>
      </div>
    );
  },
} );

In order to use Vue.js components for our core blocks, we'll use a library called vuera that allows us to invoke Vue components within React components.

Then we'll simply replace edit and save with wrappers that pass along props to our Vue components.

Preparing to Customize Configuration

In order to add Vue to our component, we'll need to do some customization of our build environment. To do this with create-guten-app we will need to eject the build scripts - otherwise they are managed internal to the plugin. We do by running the following from within the plugin directory:

npm run eject

This populates our directory with a set of build scripts in the scripts directory, and some webpack configuration files in the config directory.

Setting up Vue and Vuera

Our next step then is to install Vuera using npm, and set up our build configuration to allow us to use it. We will also need to install Vue, and since we want to use Vue SFCs we need vue-loader.

Using vue-loader also requires using css-loader and vue-template-compiler, so our final NPM install looks like:

npm install --save vuera vue vue-loader css-loader vue-template-compiler

To use Vue inside of React, Vuera recommends configuring a babel plugin via .babelrc, but I was not able to get that to work within the Gutenberg environment. Instead, we'll use an alternative method of wrapping Vue components with a VueInReact higher order component.

First, to compile our .vue files, we'll need to configure webpack to add vue-loader. There are two webpack configurations in create-guten-block, config/webpack.config.dev.js and config/webpack.config.prod.js.

The changes we need to make are:

  1. Add a Vue Loader Plugin
  2. Add a vue-loader reference to the rules

This means that to each config file we need to add this to the plugins list:

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
  // ...
  plugins: [ blocksCSSPlugin, editBlocksCSSPlugin, new VueLoaderPlugin() ],
}

And add this block to the rules

module.exports = {
  // ...
  rules: [
    // ...
    {
        test: /.vue$/,
        use: [
          {
            loader: 'vue-loader'
          }
        ]
      }
  ]
}

Bare Minimum Vue in Gutenberg

We're now ready for our bare minimum "Proof of concept" for putting Vue in Gutenberg. To do this I created a very simple Vue edit component that does nothing but say hello from Vue:

<template>
  <p>{{message}}</p>
</template>
<script>
export default {
  data() {
    return {
      message: 'Hello from Vue',
    };
  },
}
</script>

Then, to include this in my block, I need to import it, wrap it with a VueInReact higher order component from vuera, and just put it into my template.

import { VueInReact } from 'vuera'
import EditComponent from './edit.vue';
const Edit = VueInReact(EditComponent);

registerBlockType( 'cgb/block-vuetenberg', {
  // ...
  edit: Edit
}

Note: Once we wrap our component in VueInReact, it behaves as a React component, letting us use it inside of JSX or return it anywhere that expects a component.

Props are passed exactly as you would expect, so our Edit Vue component can reference any Gutenberg specific properties.

Using Gutenberg Builtin Components

Ok, great, so we have our Vue component rendering inside of Gutenberg just fine. But what if we want to use some of Gutenberg's builtin components, like their nice BlockControls?

This should be similarly straightforward to implement using a ReactInVue wrapper similar to how we embedded Vue inside React.

Lets try adding some block controls to customize alignment. First we set up an attribute for alignment in our block:

registerBlockType( 'cgb/block-vuetenberg', {
  //...
  attributes: {
    align: {
      type: 'string',
      default: 'full',
    },
  },
  //...
}

Next, in our component we'll utilize the BlockControls and BlockAlignmentToolbar builtin components from wp.editor.

In our script portion:

import { ReactInVue } from 'vuera';
const {
  BlockControls,
  BlockAlignmentToolbar,
} = wp.editor;


export default {
  props: ['attributes', 'setAttributes'],
  components: {
    'block-controls': ReactInVue(BlockControls),
    'block-alignment-toolbar': ReactInVue(BlockAlignmentToolbar),
  },
  data() {
    return {
      message: 'Hello from Vue',
    };
  },
}

And then in our template:

<template>
  <div>
    <block-controls>
      <block-alignment-toolbar :value="attributes.align"
          :onChange="align => setAttributes( { align } )"
          :controls="[ 'wide', 'full' ]"
       />
    </block-controls>
    <p>{{message}}</p>
  </div>
</template>

Seems straightforward, but here we run into a bit of a challenge and drawback in the current state of Gutenberg and Vuera.

The BlockControls component only is visible when the block is selected - but in our Vue based implementation it never shows up!

After some digging, I traced this to a challenge with the new React Context API.

While we can render React components just fine inside of Vue components with Vuera, many of Gutenberg's builtin components take advantage of React's Context API to change behavior based on whether an element is selected, and Context does not appear to cross the React/Vue border.

In the case of BlockControls, this means that the element never shows up.

Workarounds

This is a severe limitation for building Gutenberg blocks with Vue - the builtin editor components are a huge part of making the interface consistent across all blocks.

For things like the controls - BlockControls or InspectorControls, these are positioned absolutely and don't need to live inside of our core block.

We could work around this limitation by placing them outside of our Vue component using pure React, and continue to have just the meat of our component in Vue:

import { VueInReact } from 'vuera'
import EditComponent from './edit.vue';
const Edit = VueInReact(EditComponent);
const {
  BlockControls,
  BlockAlignmentToolbar,
} = wp.editor;

registerBlockType( 'cgb/block-vuetenberg', {
  // ...
  edit: function(props) {
    return (
      <div>
        <BlockControls>
          <BlockAlignmentToolbar />
        </BlockControls>
        <Edit {...props} />
      </div>
    );
  }
}

However, for things like InnerBlocks, this workaround is insufficient, because by its vary nature it is embedded within the block.

At this time, I must conclude that only Gutenberg blocks that don't depend on builtins and don't nest content can be built using Vue.js

The Path Forward

The React Context API is still relatively new, and it is very possible Vuera will be able to implement a way to pass along contexts. I have opened a github issue for this exact thing, and spent a fair amount of time trying to understand how to implement it, but so far haven't been able to figure it out.

If anyone reading this understands the inner workings of the Context API and could help point me in the right direction, I'd greatly appreciate it!

Another possibility, if it turns out that passing Contexts through Vuera isn't possible, is that Gutenberg could implement an alternative way to pass down the selected state of components to subcomponents.

The main Vue component receives an isSelected prop that is updating properly, and it could pass this down to child components. However, those components right now are not set up to receive this prop, only looking at the Context.

However we get there, I am optimistic that one day we will be able to implement complex Gutenberg Blocks using Vue.js almost as easily as we can in React. We just aren't there quite yet.