• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom custom charts dashboard block for Gutenberg using Vue micro-frontends

Step-by-Step Guide to building a custom custom charts dashboard block for Gutenberg using Vue micro-frontends

Project Setup: WordPress Plugin and Vue Micro-Frontend

This guide details building a custom Gutenberg block for WordPress that leverages a Vue.js micro-frontend to render dynamic charts. We’ll start by establishing the foundational WordPress plugin structure and then integrate the Vue application.

First, create a new WordPress plugin directory. For this example, we’ll name it gutenberg-vue-charts.

WordPress Plugin Structure

Inside wp-content/plugins/, create the following directory and file:

  • gutenberg-vue-charts/
  • gutenberg-vue-charts/gutenberg-vue-charts.php

Populate gutenberg-vue-charts.php with the standard plugin header and basic enqueueing for our JavaScript and CSS assets.

gutenberg-vue-charts.php

<?php
/**
 * Plugin Name: Gutenberg Vue Charts
 * Description: A custom Gutenberg block for displaying charts using Vue.js micro-frontends.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain: gutenberg-vue-charts
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Enqueue Gutenberg block assets for both the editor and the frontend.
 */
function gvc_enqueue_block_assets() {
	// Enqueue Vue.js and Chart.js (adjust paths as needed)
	wp_enqueue_script(
		'gvc-vue',
		plugins_url( 'assets/js/vue.min.js', __FILE__ ),
		array(),
		'3.2.0',
		true
	);
	wp_enqueue_script(
		'gvc-chartjs',
		plugins_url( 'assets/js/chart.min.js', __FILE__ ),
		array(),
		'3.7.0',
		true
	);

	// Enqueue our main block script
	wp_enqueue_script(
		'gutenberg-vue-charts-block',
		plugins_url( 'build/index.js', __FILE__ ),
		array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'gvc-vue', 'gvc-chartjs' ),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
	);

	// Enqueue our main block styles
	wp_enqueue_style(
		'gutenberg-vue-charts-block-style',
		plugins_url( 'build/style-index.css', __FILE__ ),
		array( 'wp-edit-blocks' ),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
	);

	// Enqueue editor-only styles
	if ( is_admin() ) {
		wp_enqueue_style(
			'gutenberg-vue-charts-editor-style',
			plugins_url( 'build/index.css', __FILE__ ),
			array( 'wp-edit-blocks' ),
			filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
		);
	}
}
add_action( 'enqueue_block_assets', 'gvc_enqueue_block_assets' );

/**
 * Register the custom block.
 */
function gvc_register_block() {
	register_block_type( 'gutenberg-vue-charts/charts-block', array(
		'editor_script' => 'gutenberg-vue-charts-block',
		'editor_style'  => 'gutenberg-vue-charts-editor-style',
		'style'         => 'gutenberg-vue-charts-block-style',
		'render_callback' => 'gvc_render_charts_block',
		'attributes' => array(
			'chartData' => array(
				'type' => 'string',
				'default' => '{"labels": ["January", "February", "March"], "datasets": [{"label": "Sales", "data": [10, 20, 15]}]}',
			),
			'chartType' => array(
				'type' => 'string',
				'default' => 'bar',
			),
			'chartTitle' => array(
				'type' => 'string',
				'default' => 'Monthly Sales',
			),
		),
	) );
}
add_action( 'init', 'gvc_register_block' );

/**
 * Server-side rendering callback for the charts block.
 *
 * @param array $attributes Block attributes.
 * @return string HTML output.
 */
function gvc_render_charts_block( $attributes ) {
	$chart_data = isset( $attributes['chartData'] ) ? $attributes['chartData'] : '{}';
	$chart_type = isset( $attributes['chartType'] ) ? $attributes['chartType'] : 'bar';
	$chart_title = isset( $attributes['chartTitle'] ) ? $attributes['chartTitle'] : '';

	// We'll use a data attribute to pass data to the Vue app.
	// The Vue app will be initialized to find this element and render the chart.
	$output = sprintf(
		'<div class="gvc-charts-container" data-chart-data="%s" data-chart-type="%s" data-chart-title="%s"></div>',
		esc_attr( $chart_data ),
		esc_attr( $chart_type ),
		esc_attr( $chart_title )
	);

	return $output;
}

You’ll need to download vue.min.js and chart.min.js and place them in an assets/js/ directory within your plugin. For production, consider using a build tool like Webpack or Rollup to manage these dependencies.

Gutenberg Block Development with JavaScript

We’ll use the WordPress Script Package Manager (wp-scripts) to build our JavaScript. Initialize your project with:

Initialize Build Tools

cd wp-content/plugins/gutenberg-vue-charts
npm init -y
npm install @wordpress/scripts --save-dev

Add the following scripts to your package.json:

package.json Scripts

{
  "name": "gutenberg-vue-charts",
  "version": "1.0.0",
  "description": "",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@wordpress/scripts": "^24.0.0"
  }
}

Now, create the source file for your Gutenberg block at src/index.js.

src/index.js: Block Registration and Editor Component

This file will register the block and define its behavior within the Gutenberg editor. We’ll use a Vue component for the editor interface.

const { registerBlockType } = wp.blocks;
const { InspectorControls, RichText } = wp.editor;
const { PanelBody, TextControl, SelectControl } = wp.components;

// Import Vue and the Vue component for the editor
import Vue from 'vue';
import EditorChartComponent from './editor-chart-component.vue'; // We'll create this next

// Register the block
registerBlockType( 'gutenberg-vue-charts/charts-block', {
	title: 'Vue Charts Block',
	icon: 'chart-bar', // WordPress Dashicon
	category: 'widgets', // Or any other category

	edit: function( props ) {
		const { attributes, setAttributes } = props;
		const { chartData, chartType, chartTitle } = attributes;

		// Parse chartData for the editor component
		let parsedChartData = {};
		try {
			parsedChartData = JSON.parse(chartData);
		} catch (e) {
			console.error("Error parsing chartData:", e);
		}

		// Handler for updating chart data from RichText or TextControl
		const updateChartData = (newData) => {
			setAttributes({ chartData: JSON.stringify(newData) });
		};

		// Handler for updating chart type
		const updateChartType = (newType) => {
			setAttributes({ chartType: newType });
		};

		// Handler for updating chart title
		const updateChartTitle = (newTitle) => {
			setAttributes({ chartTitle: newTitle });
		};

		// Mount the Vue editor component
		// We need a temporary div to mount Vue onto, as Gutenberg's edit function
		// expects to return JSX/React elements.
		const mountVueEditor = (el) => {
			if (el) {
				// Ensure Vue is not already initialized on this element
				if (!el.__vue__) {
					new Vue({
						el: el,
						data() {
							return {
								localChartData: parsedChartData,
								localChartType: chartType,
								localChartTitle: chartTitle,
								availableChartTypes: [
									{ label: 'Bar', value: 'bar' },
									{ label: 'Line', value: 'line' },
									{ label: 'Pie', value: 'pie' },
									{ label: 'Doughnut', value: 'doughnut' },
								],
							};
						},
						watch: {
							// Watch for changes in attributes and update local state
							chartData(newValue) {
								try {
									this.localChartData = JSON.parse(newValue);
								} catch (e) {
									console.error("Error parsing chartData in watcher:", e);
								}
							},
							chartType(newValue) {
								this.localChartType = newValue;
							},
							chartTitle(newValue) {
								this.localChartTitle = newValue;
							},
						},
						methods: {
							// Methods to update parent Gutenberg attributes
							onChartDataChange(newData) {
								updateChartData(newData);
							},
							onChartTypeChange(newType) {
								updateChartType(newType);
							},
							onChartTitleChange(newTitle) {
								updateChartTitle(newTitle);
							},
						},
						template: `
							<div>
								<EditorChartComponent
									:chart-data="localChartData"
									:chart-type="localChartType"
									:chart-title="localChartTitle"
									:is-editor="true"
									@update:chartData="onChartDataChange"
									@update:chartType="onChartTypeChange"
									@update:chartTitle="onChartTitleChange"
								/>
							</div>
						`,
						components: {
							EditorChartComponent,
						},
					});
				} else {
					// If Vue instance already exists, update its data
					el.__vue__.$data.localChartData = parsedChartData;
					el.__vue__.$data.localChartType = chartType;
					el.__vue__.$data.localChartTitle = chartTitle;
				}
			}
		};

		return [
			!!props.isSelected && (
				<InspectorControls>
					<PanelBody title={ wp.i18n.__( 'Chart Settings', 'gutenberg-vue-charts' ) } initialOpen={ true }>
						<TextControl
							label={ wp.i18n.__( 'Chart Title', 'gutenberg-vue-charts' ) }
							value={ chartTitle }
							onChange={ updateChartTitle }
						/>
						<SelectControl
							label={ wp.i18n.__( 'Chart Type', 'gutenberg-vue-charts' ) }
							value={ chartType }
							options={ [
								{ label: 'Bar', value: 'bar' },
								{ label: 'Line', value: 'line' },
								{ label: 'Pie', value: 'pie' },
								{ label: 'Doughnut', value: 'doughnut' },
							] }
							onChange={ updateChartType }
						/>
						<!-- A more robust data input would be needed for complex datasets -->
						<RichText
							tagName="div"
							placeholder={ wp.i18n.__( 'Enter chart data (JSON format)', 'gutenberg-vue-charts' ) }
							value={ chartData }
							onChange={ updateChartData }
							style={ { marginTop: '10px', border: '1px solid #ccc', padding: '10px' } }
						/>
					</PanelBody>
				</InspectorControls>
			),
			// This div will be the mount point for our Vue editor component
			<div ref={ mountVueEditor }></div>
		];
	},

	save: function( { attributes } ) {
		// The save function should return the markup that will be saved to the database.
		// Our PHP render_callback will handle the actual chart rendering on the frontend.
		// We just need to ensure the necessary data attributes are present.
		const { chartData, chartType, chartTitle } = attributes;
		return (
			<div
				className="gvc-charts-container"
				data-chart-data={ chartData }
				data-chart-type={ chartType }
				data-chart-title={ chartTitle }
			></div>
		);
	},
} );

Next, create the Vue component for the editor interface at src/editor-chart-component.vue.

src/editor-chart-component.vue: Vue Editor Component

<template>
	<div class="gvc-editor-chart-wrapper">
		<h3>{{ chartTitle || 'Chart Preview' }}</h3>
		<canvas ref="chartCanvas"></canvas>
		<!-- Input for chart data (simplified for example) -->
		<div style="margin-top: 15px;">
			<label>Chart Data (JSON):</label>
			<textarea
				:value="JSON.stringify(chartData, null, 2)"
				@input="handleDataInput"
				rows="5"
				style="width: 100%; border: 1px solid #ccc; padding: 5px;"
			/>
		</div>
	</div>
</template>

<script>
import Chart from 'chart.js/auto'; // Import Chart.js

export default {
	name: 'EditorChartComponent',
	props: {
		chartData: {
			type: Object,
			required: true,
		},
		chartType: {
			type: String,
			required: true,
		},
		chartTitle: {
			type: String,
			default: '',
		},
		isEditor: {
			type: Boolean,
			default: false,
		},
	},
	data() {
		return {
			chartInstance: null,
			localChartData: JSON.parse(JSON.stringify(this.chartData)), // Deep copy
		};
	},
	watch: {
		// Watch for external changes to chartData, chartType, chartTitle
		chartData: {
			deep: true,
			handler(newVal) {
				this.localChartData = JSON.parse(JSON.stringify(newVal)); // Deep copy
				this.updateChart();
			}
		},
		chartType() {
			this.updateChart();
		},
		chartTitle() {
			// Title is handled by template, no chart update needed
		},
	},
	mounted() {
		this.renderChart();
	},
	beforeDestroy() {
		if (this.chartInstance) {
			this.chartInstance.destroy();
		}
	},
	methods: {
		renderChart() {
			const ctx = this.$refs.chartCanvas.getContext('2d');
			if (this.chartInstance) {
				this.chartInstance.destroy(); // Destroy previous instance if exists
			}
			try {
				this.chartInstance = new Chart(ctx, {
					type: this.chartType,
					data: this.localChartData,
					options: {
						responsive: true,
						plugins: {
							title: {
								display: !!this.chartTitle,
								text: this.chartTitle,
							},
						},
					},
				});
			} catch (error) {
				console.error("Chart rendering error:", error);
				// Optionally display an error message to the user
			}
		},
		updateChart() {
			if (this.chartInstance) {
				this.chartInstance.data = this.localChartData;
				this.chartInstance.options.type = this.chartType; // Chart.js v3+ doesn't directly support changing type this way. Re-rendering is better.
				this.chartInstance.options.plugins.title.text = this.chartTitle;
				this.chartInstance.options.plugins.title.display = !!this.chartTitle;

				// For changing chart type, it's often best to destroy and re-render
				this.renderChart();
			} else {
				this.renderChart();
			}
		},
		handleDataInput(event) {
			try {
				const newData = JSON.parse(event.target.value);
				this.localChartData = newData; // Update local state immediately
				this.$emit('update:chartData', newData); // Emit to parent for attribute update
			} catch (e) {
				console.error("Invalid JSON input:", e);
				// Optionally provide user feedback for invalid JSON
			}
		},
	},
};
</script>

<style>
.gvc-editor-chart-wrapper {
	padding: 15px;
	border: 1px dashed #ccc;
	background-color: #f9f9f9;
	text-align: center;
}
.gvc-editor-chart-wrapper h3 {
	margin-top: 0;
}
</style>

After creating these files, run the build command:

Build the Block Assets

npm run build

This will compile your src/index.js and src/editor-chart-component.vue into the build/ directory, creating index.js, index.css, and style-index.css.

Frontend Rendering with Vue Micro-Frontend

The server-side rendering callback in gutenberg-vue-charts.php outputs a <div class="gvc-charts-container"> with data attributes. We need a separate JavaScript file to initialize Vue and render the chart on the frontend.

Frontend Vue Initialization

Create a new file, e.g., assets/js/frontend.js.

// assets/js/frontend.js
import Vue from 'vue';
import Chart from 'chart.js/auto';

// A simple Vue component for the frontend chart
const FrontendChartComponent = {
	props: ['chartData', 'chartType', 'chartTitle'],
	data() {
		return {
			chartInstance: null,
		};
	},
	mounted() {
		this.renderChart();
	},
	updated() {
		this.updateChart();
	},
	beforeDestroy() {
		if (this.chartInstance) {
			this.chartInstance.destroy();
		}
	},
	methods: {
		renderChart() {
			const ctx = this.$el.querySelector('canvas').getContext('2d');
			if (this.chartInstance) {
				this.chartInstance.destroy();
			}
			try {
				this.chartInstance = new Chart(ctx, {
					type: this.chartType,
					data: this.chartData,
					options: {
						responsive: true,
						plugins: {
							title: {
								display: !!this.chartTitle,
								text: this.chartTitle,
							},
						},
					},
				});
			} catch (error) {
				console.error("Frontend Chart rendering error:", error);
				this.$el.innerHTML = '<p>Error rendering chart.</p>'; // Display error in the div
			}
		},
		updateChart() {
			if (this.chartInstance) {
				this.chartInstance.data = this.chartData;
				this.chartInstance.options.plugins.title.text = this.chartTitle;
				this.chartInstance.options.plugins.title.display = !!this.chartTitle;
				// Again, for type changes, re-rendering is safer.
				this.renderChart();
			} else {
				this.renderChart();
			}
		},
	},
	template: `
		<div>
			<h3>{{ chartTitle }}</h3>
			<canvas></canvas>
		</div>
	`,
};

document.addEventListener('DOMContentLoaded', () => {
	const chartContainers = document.querySelectorAll('.gvc-charts-container');

	chartContainers.forEach(container => {
		const chartData = container.dataset.chartData;
		const chartType = container.dataset.chartType;
		const chartTitle = container.dataset.chartTitle;

		if (!chartData) return;

		let parsedChartData = {};
		try {
			parsedChartData = JSON.parse(chartData);
		} catch (e) {
			console.error("Error parsing chart data for frontend:", e);
			container.innerHTML = '<p>Invalid chart data.</p>';
			return;
		}

		// Create a new div for the Vue app instance
		const vueAppContainer = document.createElement('div');
		container.appendChild(vueAppContainer);

		// Mount the Vue component
		new Vue({
			el: vueAppContainer,
			data() {
				return {
					chartData: parsedChartData,
					chartType: chartType || 'bar',
					chartTitle: chartTitle || '',
				};
			},
			components: {
				FrontendChartComponent,
			},
			template: `
				<FrontendChartComponent
					:chart-data="chartData"
					:chart-type="chartType"
					:chart-title="chartTitle"
				/>
			`,
		});
	});
});

You need to enqueue this script. Since it’s for the frontend, we’ll modify the gvc_enqueue_block_assets function in gutenberg-vue-charts.php to enqueue it conditionally on the frontend.

Enqueueing Frontend Script

// ... inside gvc_enqueue_block_assets function ...

	// Enqueue frontend-only script
	wp_enqueue_script(
		'gutenberg-vue-charts-frontend',
		plugins_url( 'assets/js/frontend.js', __FILE__ ),
		array( 'gvc-vue', 'gvc-chartjs' ), // Dependencies: Vue and Chart.js
		filemtime( plugin_dir_path( __FILE__ ) . 'assets/js/frontend.js' ),
		true // Load in footer
	);

// ... rest of the function ...

Remember to add assets/js/frontend.js to your package.json build process. You can achieve this by modifying your wp-scripts configuration or by manually compiling it. For simplicity in this example, we’re assuming it’s placed directly in assets/js/ and enqueued. A more robust setup would involve Webpack configuration to bundle this into build/frontend.js.

Advanced Considerations and Next Steps

This setup provides a basic framework. For production environments, consider:

  • Data Input: The current JSON input for chart data is rudimentary. Implement a more user-friendly interface in the editor for defining labels, datasets, and options.
  • Build Process: Integrate frontend.js into the Webpack build process managed by @wordpress/scripts. This typically involves configuring webpack.config.js.
  • State Management: For complex interactions, consider a state management solution within the Vue micro-frontend (e.g., Vuex).
  • API Integration: Fetch chart data from a WordPress REST API endpoint instead of relying on static JSON.
  • Styling: Ensure consistent styling between the editor and the frontend. Use CSS variables or a shared styling library.
  • Accessibility: Ensure charts are accessible, potentially by providing tabular data alternatives or ARIA attributes.
  • Error Handling: Implement more robust error handling for JSON parsing and chart rendering on both editor and frontend.
  • Vue Version Compatibility: Ensure the Vue version used in the editor and frontend are compatible or managed appropriately. Using Vue 3 Composition API might offer better integration patterns.

By combining Gutenberg’s block API with Vue.js micro-frontends, you can create highly interactive and dynamic custom components within WordPress, offering a powerful way to build custom dashboards and data visualizations.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (726)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala