Step-by-Step Guide to building a custom CSV bulk exporter block for Gutenberg using Vanilla JS Web Components
Setting Up the Plugin Environment
We’ll begin by establishing a basic WordPress plugin structure. This involves creating a main plugin file and a directory for our block assets. For this example, we’ll name our plugin custom-csv-exporter.
Create a new folder named custom-csv-exporter in your WordPress installation’s wp-content/plugins/ directory. Inside this folder, create a PHP file named custom-csv-exporter.php.
Add the standard WordPress plugin header to custom-csv-exporter.php:
<?php
/**
* Plugin Name: Custom CSV Exporter
* Description: A custom Gutenberg block for exporting data to CSV.
* Version: 1.0.0
* Author: Your Name
* Author URI: https://yourwebsite.com
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: custom-csv-exporter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register the block.
*/
function custom_csv_exporter_register_block() {
// Register the script for the editor.
wp_register_script(
'custom-csv-exporter-editor-script',
plugins_url( 'build/index.js', __FILE__ ),
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
// Register the script for the front-end.
wp_register_script(
'custom-csv-exporter-frontend-script',
plugins_url( 'build/frontend.js', __FILE__ ),
array(), // No dependencies for the frontend script itself
filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' )
);
// Register the block.
register_block_type( 'custom-csv-exporter/csv-exporter', array(
'editor_script' => 'custom-csv-exporter-editor-script',
'script' => 'custom-csv-exporter-frontend-script',
'editor_style' => 'custom-csv-exporter-editor-style', // Will be defined later
'style' => 'custom-csv-exporter-style', // Will be defined later
'attributes' => array(
'data' => array(
'type' => 'string',
'default' => '[]', // Store data as JSON string
),
'fileName' => array(
'type' => 'string',
'default' => 'export.csv',
),
),
) );
// Register editor styles.
wp_register_style(
'custom-csv-exporter-editor-style',
plugins_url( 'build/index.css', __FILE__ ),
array( 'wp-edit-blocks' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
);
// Register front-end styles.
wp_register_style(
'custom-csv-exporter-style',
plugins_url( 'build/style.css', __FILE__ ),
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'build/style.css' )
);
}
add_action( 'init', 'custom_csv_exporter_register_block' );
Next, create a build directory within your plugin folder. This directory will house the compiled JavaScript and CSS files. We’ll also need a src directory for our source files.
Your plugin structure should now look like this:
custom-csv-exporter/ ├── build/ ├── src/ └── custom-csv-exporter.php
Frontend JavaScript with Vanilla JS Web Components
For the frontend rendering of our block, we’ll leverage Vanilla JavaScript and Web Components. This approach provides encapsulation and reusability. We’ll define a custom element that handles the CSV export logic.
Create a file named csv-exporter-element.js inside the src/ directory.
// src/csv-exporter-element.js
class CsvExporterElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.data = [];
this.fileName = 'export.csv';
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
static get observedAttributes() {
return ['data', 'fileName'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'data') {
try {
this.data = JSON.parse(newValue);
} catch (e) {
console.error('Failed to parse data attribute:', e);
this.data = [];
}
} else if (name === 'fileName') {
this.fileName = newValue || 'export.csv';
}
this.render(); // Re-render when attributes change
}
render() {
this.shadowRoot.innerHTML = `
`;
}
setupEventListeners() {
const exportButton = this.shadowRoot.getElementById('export-csv-button');
if (exportButton) {
exportButton.addEventListener('click', () => this.exportToCsv());
}
}
exportToCsv() {
if (!this.data || this.data.length === 0) {
alert('No data available to export.');
return;
}
const csvRows = [];
// Assuming the first element's keys are the headers
const headers = Object.keys(this.data[0]);
csvRows.push(headers.join(','));
for (const row of this.data) {
const values = headers.map(header => {
const escaped = String(row[header] || '').replace(/"/g, '""'); // Escape quotes
return `"${escaped}"`; // Enclose in quotes
});
csvRows.push(values.join(','));
}
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) { // Feature detection
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', this.fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url); // Clean up
}
}
}
// Define the custom element
if (!customElements.get('csv-exporter-element')) {
customElements.define('csv-exporter-element', CsvExporterElement);
}
This Web Component:
- Attaches a shadow DOM for encapsulation.
- Observes
dataandfileNameattributes. - Parses the
dataattribute (expected to be a JSON string) into an array. - Renders a simple button within its shadow DOM.
- Attaches a click listener to the button to trigger the CSV export.
- Implements the CSV generation logic, including basic escaping for quoted fields.
- Uses the File API to create a downloadable CSV file.
Gutenberg Block Registration and Editor Logic
Now, we’ll create the JavaScript file that registers our Gutenberg block and handles its behavior within the WordPress editor. This file will use the WordPress Block Editor API and will also be responsible for instantiating our Web Component.
Create a file named index.js inside the src/ directory.
// src/index.js
const { registerBlockType } = wp.blocks;
const { InspectorControls, useBlockProps } = wp.blockEditor;
const { PanelBody, TextControl, Button } = wp.components;
const { useState, useEffect } = wp.element;
const { __ } = wp.i18n;
// Import the Web Component definition
import './csv-exporter-element';
registerBlockType('custom-csv-exporter/csv-exporter', {
title: __('Custom CSV Exporter', 'custom-csv-exporter'),
icon: 'download',
category: 'widgets',
attributes: {
data: {
type: 'string',
default: '[]',
},
fileName: {
type: 'string',
default: 'export.csv',
},
},
edit: ({ attributes, setAttributes }) => {
const { data, fileName } = attributes;
const [jsonData, setJsonData] = useState(JSON.parse(data));
const [tempDataInput, setTempDataInput] = useState(data); // For the textarea input
// Update block attributes when internal state changes
useEffect(() => {
setAttributes({ data: JSON.stringify(jsonData) });
}, [jsonData]);
useEffect(() => {
setAttributes({ fileName: fileName });
}, [fileName]);
const handleDataChange = (event) => {
setTempDataInput(event.target.value);
try {
const parsedData = JSON.parse(event.target.value);
setJsonData(parsedData);
} catch (e) {
console.error('Invalid JSON input:', e);
// Optionally show an error to the user
}
};
const handleFileNameChange = (value) => {
setAttributes({ fileName: value });
};
const blockProps = useBlockProps();
// A simple way to display the data in the editor for demonstration
// In a real-world scenario, you'd likely fetch this data dynamically
const displayData = jsonData.length > 0 ? jsonData : [{ message: 'No data loaded. Paste JSON into the input below.' }];
return (
<>
<InspectorControls>
<PanelBody title={__('Export Settings', 'custom-csv-exporter')} initialOpen={true}>
<TextControl
label={__('File Name', 'custom-csv-exporter')}
value={fileName}
onChange={handleFileNameChange}
help={__('Enter the desired name for the CSV file (e.g., my-data.csv).', 'custom-csv-exporter')}
/>
<TextareaControl
label={__('JSON Data', 'custom-csv-exporter')}
value={tempDataInput}
onChange={handleDataChange}
help={__('Paste your data here as a JSON array of objects.', 'custom-csv-exporter')}
rows={10}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<h4>{__('CSV Exporter Block', 'custom-csv-exporter')}</h4>
<p>{__('Configure data and file name in the block settings sidebar.', 'custom-csv-exporter')}</p>
{/* We don't render the Web Component directly in the editor for simplicity,
but we can simulate its presence or use it if needed for live preview.
For this example, we'll just show a placeholder and rely on the inspector. */}
<div style={{ marginTop: '20px', textAlign: 'center' }}>
<em>{__('Export button will appear on the frontend.', 'custom-csv-exporter')}</em>
</div>
</div>
</>
);
},
save: ({ attributes }) => {
const { data, fileName } = attributes;
// We render the custom element here, passing attributes as properties/data attributes
// The Web Component will pick these up.
// Using data-* attributes is a common way to pass data to Web Components from HTML.
return (
<csv-exporter-element data={data} fileName={fileName}></csv-exporter-element>
);
},
});
Key aspects of the index.js file:
registerBlockType: Registers our custom block with WordPress.title,icon,category: Standard block registration properties.attributes: Defines the data that the block will store. We usedata(as a JSON string) andfileName.editfunction: This is the React component that renders the block in the Gutenberg editor.- It uses
InspectorControlsto add a sidebar panel for configuring the file name and pasting JSON data. useStateanduseEffecthooks manage the internal state of the editor view and synchronize it with block attributes.- A
TextareaControlis used for inputting JSON data, with basic parsing to validate it. - Crucially, the
editfunction does not render the<csv-exporter-element>directly. Instead, it provides controls in the sidebar. The actual export button is handled by the Web Component on the frontend.
- It uses
savefunction: This function defines how the block’s output is saved to the database.- It renders our custom Web Component,
<csv-exporter-element>. - The block’s
attributes(dataandfileName) are passed as attributes to the custom element. The Web Component’sattributeChangedCallbackwill pick these up.
- It renders our custom Web Component,
Frontend JavaScript for Block Rendering
The save function in our block registration renders the <csv-exporter-element>. However, for this element to be correctly instantiated and function on the frontend when the page loads, we need a separate JavaScript file that ensures the Web Component is registered and ready. This file will be enqueued as the block’s script.
Create a file named frontend.js inside the src/ directory.
// src/frontend.js // Import the Web Component definition. // This ensures that customElements.define() is called when the script loads. import './csv-exporter-element'; // No further action is strictly needed here if the Web Component // definition itself handles its rendering and event binding. // The 'save' function in index.js already outputs thetag. // When the browser parses the HTML, it will find this tag. // If the custom element has been defined (which it has, via the import above), // the browser will automatically instantiate it. // The attributes passed in the save function (data, fileName) will be // available to the Web Component instance. console.log('Custom CSV Exporter frontend script loaded.');
This frontend.js file is minimal. Its primary purpose is to import the Web Component definition. When WordPress enqueues this script for the block, the import statement ensures that customElements.define('csv-exporter-element', CsvExporterElement); is executed. The browser then automatically upgrades any instances of <csv-exporter-element> found in the HTML output generated by the save function.
Styling the Block
We need CSS for both the editor and the frontend. Create two files in the src/ directory: index.css for the editor styles and style.css for the frontend styles.
src/index.css (Editor Styles)
/* src/index.css */
.wp-block-custom-csv-exporter-csv-exporter {
border: 1px dashed #999;
padding: 15px;
background-color: #f0f0f0;
text-align: center;
}
.wp-block-custom-csv-exporter-csv-exporter h4 {
margin-top: 0;
color: #333;
}
src/style.css (Frontend Styles)
/* src/style.css */
/* Styles for the Web Component itself can be defined here if needed,
but the Web Component's shadow DOM has its own internal styles.
This file is for any global styles that might affect the block container
on the frontend, or if you want to style the host element from outside. */
.wp-block-custom-csv-exporter-csv-exporter {
/* Example: Add some margin on the frontend */
margin-bottom: 20px;
}
Build Process with @wordpress/scripts
To compile our JavaScript and CSS from the src/ directory into the build/ directory, we’ll use the @wordpress/scripts package. This package provides a convenient way to handle the build process for WordPress block development.
First, navigate to your plugin’s root directory (custom-csv-exporter/) in your terminal and initialize a Node.js project:
cd wp-content/plugins/custom-csv-exporter npm init -y
Next, install the @wordpress/scripts package as a development dependency:
npm install @wordpress/scripts --save-dev
Now, add the following scripts to your package.json file:
{
"name": "custom-csv-exporter",
"version": "1.0.0",
"description": "",
"main": "custom-csv-exporter.php",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wordpress/scripts": "^26.10.0"
}
}
You can now run the build process:
npm run build
This command will compile:
src/index.jsintobuild/index.js(for the editor).src/index.cssintobuild/index.css(for the editor).src/frontend.jsintobuild/frontend.js(for the frontend).src/style.cssintobuild/style.css(for the frontend).
The npm run start command will watch for changes in your src/ directory and automatically recompile the assets, which is very useful during development.
Activation and Usage
After running npm run build, activate the “Custom CSV Exporter” plugin from your WordPress admin area. Then, navigate to the post or page editor.
Add a new block and search for “Custom CSV Exporter”. Insert the block.
In the block’s settings sidebar (click the block and then the cog icon in the toolbar), you will find the “Export Settings” panel. Here you can:
- Enter the desired File Name for your export.
- Paste your data as a JSON array of objects into the JSON Data textarea. For example:
[ {"id": 1, "name": "Alice", "email": "[email protected]"}, {"id": 2, "name": "Bob", "email": "[email protected]"}, {"id": 3, "name": "Charlie", "email": "[email protected]"} ]
Save the post/page and view it on the frontend. You should see the “Export to CSV” button rendered by the Web Component. Clicking this button will download the CSV file with the data and filename you configured.
Advanced Considerations and Enhancements
This implementation provides a solid foundation. Here are some areas for further development:
- Dynamic Data Fetching: Instead of a textarea, implement a mechanism to fetch data dynamically (e.g., from a custom post type, a specific database table, or an external API) using WordPress REST API or AJAX. This would involve adding more controls in the
editfunction to select data sources. - Data Validation and Error Handling: Implement more robust validation for the JSON input and provide clearer feedback to the user in the editor if the data is malformed or if export fails.
- CSV Formatting Options: Add options for delimiter (comma, semicolon, tab), enclosure characters, and handling of special characters within data fields. The current implementation uses basic double-quote escaping.
- Large Datasets: For very large datasets, consider server-side CSV generation to avoid browser memory limitations and timeouts. This would involve an AJAX request to a WordPress AJAX handler that generates and streams the CSV.
- Internationalization (i18n): Ensure all user-facing strings are translatable using WordPress’s i18n functions (
__,_x, etc.). We’ve included basic internationalization in the block registration. - Accessibility: Ensure the button and any associated controls are accessible (e.g., proper ARIA attributes, keyboard navigation).
- Web Component Styling: For more complex styling, consider passing CSS variables from the block editor to the Web Component’s shadow DOM or using CSS custom properties.