Building a Vue-Powered Custom Post Type Editor in Gutenberg: A Deep Dive

Gutenberg, WordPress’s block editor, offers incredible flexibility through its block API. However, sometimes you need more than just standard blocks. Complex custom post types often demand a richer, more interactive editing experience. This is where integrating a Vue.js frontend framework into your Gutenberg editor shines. This blog post will guide you through the process of building a custom post type editor with a Vue.js frontend powered by the Gutenberg block editor.

We’ll create a custom post type for "Products," which will have fields for title, description, price, and an image. We will build a Vue component to handle the editing of these fields within a Gutenberg block.

1. Setting up the Environment:

First, you need a WordPress installation with the necessary plugins and tools. Ensure you have Node.js and npm (or yarn) installed on your system. We’ll use npm for this example.

# Install necessary packages (adjust if you use yarn)
npm install @wordpress/scripts @wordpress/element wp-cli

Create a new directory for your plugin, for example, vue-gutenberg-product-editor. Inside, create the following files:

  • gutenberg-vue-product-block.php: The main plugin file.
  • src/index.js: The entry point for our Vue component.
  • src/components/ProductEditor.vue: Our Vue component.
  • src/App.vue: A top level component if you’re adding further features (optional).
  • webpack.config.js: The Webpack configuration file.

2. The Plugin File (gutenberg-vue-product-block.php):

This file registers our plugin and enqueues necessary scripts and styles.

<?php
/**
 * Plugin Name:       Vue Gutenberg Product Editor
 * Plugin URI:        https://yourwebsite.com/
 * Description:       A custom post type editor using Vue.js in Gutenberg.
 * Version:           1.0.0
 * Author:            Your Name
 * Author URI:        https://yourwebsite.com/
 * License:           GPL2
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       vue-gutenberg-product-editor
 */

// Register the custom post type
function register_product_post_type() {
    register_post_type( 'product', [
        'labels' => [
            'name' => 'Products',
            'singular_name' => 'Product',
        ],
        'public' => true,
        'supports' => [ 'title', 'editor' ], // We'll handle other fields via our Vue component
        'show_in_rest' => true, // Enable REST API access
    ] );
}
add_action( 'init', 'register_product_post_type' );

// Enqueue scripts and styles
function enqueue_vue_gutenberg_scripts() {
    wp_enqueue_script(
        'vue-gutenberg-product-editor',
        plugins_url( 'src/index.js', __FILE__ ),
        [ 'wp-blocks', 'wp-element', 'wp-i18n', 'wp-components', 'wp-data' ], //Dependencies
        filemtime( plugin_dir_path( __FILE__ ) . 'src/index.js' ), // Versioning
        true
    );
    wp_enqueue_style(
        'vue-gutenberg-product-editor',
        plugins_url( 'src/style.css', __FILE__ ),
        [],
        filemtime( plugin_dir_path( __FILE__ ) . 'src/style.css' ),
        'all'
    );
}
add_action( 'enqueue_block_editor_assets', 'enqueue_vue_gutenberg_scripts' );

3. The Vue Component (src/components/ProductEditor.vue):

This is the heart of our plugin. It uses Vue.js to create a dynamic form for editing product details.

<template>
  <div>
    <div>
      <label for="title">Title:</label>
      <input type="text" v-model="title" id="title">
    </div>
    <div>
      <label for="description">Description:</label>
      <textarea v-model="description" id="description"></textarea>
    </div>
    <div>
      <label for="price">Price:</label>
      <input type="number" v-model="price" id="price">
    </div>
    <div>
      <label for="image">Image:</label>
      <input type="file" @change="handleImageChange" id="image">
      <img v-if="imageUrl" :src="imageUrl" alt="Product Image">
    </div>
  </div>
</template>

<script>
import { useState } from '@wordpress/element';
export default {
  name: 'ProductEditor',
  setup() {
    const [title, setTitle] = useState('');
    const [description, setDescription] = useState('');
    const [price, setPrice] = useState(0);
    const [image, setImage] = useState(null);
    const [imageUrl, setImageUrl] = useState('');

    const handleImageChange = (event) => {
      const file = event.target.files[0];
      const reader = new FileReader();
      reader.onload = (e) => {
        setImageUrl(e.target.result);
        setImage(file);
      };
      reader.readAsDataURL(file);
    };

    return {
      title,
      setTitle,
      description,
      setDescription,
      price,
      setPrice,
      image,
      imageUrl,
      handleImageChange,
    };
  },
};
</script>

4. The Entry Point (src/index.js):

This file registers our Vue component as a Gutenberg block.

import { registerBlockType } from '@wordpress/blocks';
import ProductEditor from './components/ProductEditor.vue';
import { createApp } from 'vue';

const { __ } = wp.i18n; // Import i18n

registerBlockType('my-plugin/product-block', {
    title: __('Product Block', 'my-plugin'),
    icon: 'media-document',
    category: 'common',
    attributes: {
        title: { type: 'string' },
        description: { type: 'string' },
        price: { type: 'number' },
        image: { type: 'string' } //store the image URL
    },
    edit: (props) => {
      const app = createApp(ProductEditor, {
        props
      }).mount(document.createElement('div'))
      return app.$el;
  },
    save: (props) => {
        // This is where you could save the product data to the post's meta.
        return null; //Render nothing on frontend.
    },
});

5. Webpack Configuration (webpack.config.js):

This configures Webpack to bundle our Vue components.

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'index.js',
    },
    module: {
        rules: [
            {
                test: /.vue$/,
                loader: 'vue-loader',
            },
            {
                test: /.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                },
            },
            {
                test: /.css$/,
                use: ['style-loader', 'css-loader'],
            },
        ],
    },
    resolve: {
        alias: {
            vue$: 'vue/dist/vue.esm.js', // for webpack
        },
        extensions: ['.js', '.vue'],
    },
};

6. Saving Data:

The save function in index.js currently renders nothing on the frontend, as the data is handled directly through Gutenberg’s block editor system and it’s recommended to store data in custom fields instead of directly within the post content. In order to save data you need to utilize the WordPress REST API. You will modify src/index.js to include an update function that communicates with the REST API and handles the saving.

// ...other imports
import { dispatch } from '@wordpress/data'
// ...

    edit: (props) => {
        const app = createApp(ProductEditor, { props }).mount(document.createElement('div'))

        const saveData = () => {
          const dataToSave = {
            title: app.component.proxy.$data.title,
            description: app.component.proxy.$data.description,
            price: app.component.proxy.$data.price,
            image: app.component.proxy.$data.imageUrl,
          };

          // Save data via REST API
          fetch( `/wp-json/wp/v2/product/${props.attributes.id}`, { //Replace with correct product ID if necessary
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify(dataToSave)
          })
            .then(response => response.json())
            .then(data => {
              // Handle success
              console.log('Data saved successfully:', data);
            })
            .catch(error => {
              // Handle error
              console.error('Error saving data:', error);
            });
        };
        //Add the update function to a button or similar UI in your vue component.
        return app.$el;
    },

Remember to adjust the REST API endpoint (/wp-json/wp/v2/product/${props.attributes.id}) if your custom post type’s REST endpoint is different. You will also likely need to add error handling and success messages to the frontend for a better user experience.

7. Building and Activation:

Navigate to your plugin directory and run:

npm run build

This will compile your Vue components and output them to the dist directory. Activate your plugin in WordPress. Now, when you create a new "Product" post, you should see your custom Vue-powered editor block.

This comprehensive guide provides a solid foundation for building complex custom post type editors within Gutenberg. Remember to handle potential errors, improve user interface aspects, and thoroughly test your plugin before deploying it to a production environment. Expanding this example to include more advanced features like media handling, validation, and conditional rendering is a natural next step. You can further enhance your workflow by using a state management solution like Vuex for larger and more complex applications. Remember always to secure your REST API endpoints appropriately.

Leave a Reply

Your email address will not be published. Required fields are marked *

Trending