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.jsinto the Webpack build process managed by@wordpress/scripts. This typically involves configuringwebpack.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.