Step-by-Step Guide to building a custom automated asset optimization manager block for Gutenberg using Vanilla JS Web Components
Leveraging Web Components for Dynamic Asset Management in Gutenberg
WordPress’s Gutenberg editor offers immense flexibility, but managing assets (scripts and styles) on a per-block basis can become complex. This guide details building a custom Gutenberg block that acts as an automated asset optimization manager, utilizing Vanilla JS Web Components for encapsulated, reusable functionality. This approach decouples asset loading logic from the block’s core rendering, promoting cleaner code and better performance.
Project Setup and Web Component Fundamentals
We’ll start by defining the basic structure for our Web Component. This component will be responsible for registering and deregistering specific assets based on its presence or configuration within a post. For this example, we’ll assume we have a hypothetical script my-custom-script.js and stylesheet my-custom-style.css that should only load when our custom block is active.
First, create a JavaScript file for your Web Component, e.g., asset-manager-component.js. We’ll use the Custom Elements API to define a new HTML tag, <asset-manager>.
Defining the Asset Manager Web Component
This component will expose attributes to specify the scripts and styles it needs to manage. We’ll use the connectedCallback lifecycle method to register assets when the component is added to the DOM and disconnectedCallback to deregister them.
asset-manager-component.js
class AssetManager extends HTMLElement {
constructor() {
super();
this.scriptsToManage = [];
this.stylesToManage = [];
}
static get observedAttributes() {
return ['scripts', 'styles'];
}
connectedCallback() {
console.log('AssetManager connected.');
this.parseAttributes();
this.registerAssets();
}
disconnectedCallback() {
console.log('AssetManager disconnected.');
this.deregisterAssets();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.parseAttributes();
// Re-register if attributes change after initial connection
if (this.isConnected) {
this.deregisterAssets(); // Deregister old ones first
this.registerAssets();
}
}
}
parseAttributes() {
const scriptsAttr = this.getAttribute('scripts');
if (scriptsAttr) {
try {
this.scriptsToManage = JSON.parse(scriptsAttr);
} catch (e) {
console.error('Failed to parse scripts attribute:', e);
this.scriptsToManage = [];
}
}
const stylesAttr = this.getAttribute('styles');
if (stylesAttr) {
try {
this.stylesToManage = JSON.parse(stylesAttr);
} catch (e) {
console.error('Failed to parse styles attribute:', e);
this.stylesToManage = [];
}
}
}
registerAssets() {
console.log('Registering assets:', this.scriptsToManage, this.stylesToManage);
this.scriptsToManage.forEach(scriptHandle => {
// In a real WordPress context, this would interact with wp_enqueue_script
// For demonstration, we'll simulate registration.
console.log(`Simulating registration for script: ${scriptHandle}`);
// Example: wp.hooks.doAction('enqueue_script', scriptHandle);
});
this.stylesToManage.forEach(styleHandle => {
// In a real WordPress context, this would interact with wp_enqueue_style
// For demonstration, we'll simulate registration.
console.log(`Simulating registration for style: ${styleHandle}`);
// Example: wp.hooks.doAction('enqueue_style', styleHandle);
});
}
deregisterAssets() {
console.log('Deregistering assets:', this.scriptsToManage, this.stylesToManage);
this.scriptsToManage.forEach(scriptHandle => {
// In a real WordPress context, this would interact with wp_dequeue_script
console.log(`Simulating deregistration for script: ${scriptHandle}`);
// Example: wp.hooks.doAction('dequeue_script', scriptHandle);
});
this.stylesToManage.forEach(styleHandle => {
// In a real WordPress context, this would interact with wp_dequeue_style
console.log(`Simulating deregistration for style: ${styleHandle}`);
// Example: wp.hooks.doAction('dequeue_style', styleHandle);
});
}
}
// Define the custom element
if (!customElements.get('asset-manager')) {
customElements.define('asset-manager', AssetManager);
}
Integrating with Gutenberg Block Registration
Now, we need to register a Gutenberg block that utilizes this Web Component. The block’s edit function will render the <asset-manager> element, passing the asset handles as JSON-encoded strings to the scripts and styles attributes. The save function will also render this element, ensuring assets are managed correctly when the post is saved and rendered on the front-end.
Gutenberg Block Registration (JavaScript)
This JavaScript code would typically reside in your theme’s or plugin’s main JavaScript file for Gutenberg blocks, enqueued appropriately.
my-asset-block.js
import { registerBlockType } from '@wordpress/blocks';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, ToggleControl } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
// Import the Web Component definition
import './asset-manager-component.js'; // Ensure this path is correct
const blockName = 'my-plugin/asset-manager-block';
registerBlockType(blockName, {
title: 'Asset Manager Block',
icon: 'hammer',
category: 'widgets',
attributes: {
managedScripts: {
type: 'string', // Store as JSON string
default: '[]',
},
managedStyles: {
type: 'string', // Store as JSON string
default: '[]',
},
enableFeatureX: {
type: 'boolean',
default: false,
},
},
edit: ({ attributes, setAttributes }) => {
const { managedScripts, managedStyles, enableFeatureX } = attributes;
// State to manage script/style inputs locally before saving
const [localScripts, setLocalScripts] = useState(JSON.parse(managedScripts));
const [localStyles, setLocalStyles] = useState(JSON.parse(managedStyles));
const [newScriptHandle, setNewScriptHandle] = useState('');
const [newStyleHandle, setNewStyleHandle] = useState('');
// Effect to update attributes when local state changes
useEffect(() => {
setAttributes({ managedScripts: JSON.stringify(localScripts) });
}, [localScripts]);
useEffect(() => {
setAttributes({ managedStyles: JSON.stringify(localStyles) });
}, [localStyles]);
// Effect to update local state when attributes change (e.g., loading from saved post)
useEffect(() => {
setLocalScripts(JSON.parse(managedScripts));
setLocalStyles(JSON.parse(managedStyles));
}, [managedScripts, managedStyles]);
const addScript = () => {
if (newScriptHandle && !localScripts.includes(newScriptHandle)) {
setLocalScripts([...localScripts, newScriptHandle]);
setNewScriptHandle('');
}
};
const removeScript = (handleToRemove) => {
setLocalScripts(localScripts.filter(handle => handle !== handleToRemove));
};
const addStyle = () => {
if (newStyleHandle && !localStyles.includes(newStyleHandle)) {
setLocalStyles([...localStyles, newStyleHandle]);
setNewStyleHandle('');
}
};
const removeStyle = (handleToRemove) => {
setLocalStyles(localStyles.filter(handle => handle !== handleToRemove));
};
// The actual Web Component rendered in the editor
const AssetManagerElement = () => {
// We need to ensure the Web Component is defined before rendering
// In a real scenario, you might need a more robust check or ensure
// the script is loaded and defined before this component renders.
// For simplicity, we assume it's available.
return (
<asset-manager
scripts={managedScripts}
styles={managedStyles}
></asset-manager>
);
};
return (
<>
<InspectorControls>
<PanelBody title="Asset Management" initialOpen={true}>
<div>
<h4>Managed Scripts</h4>
{localScripts.map(handle => (
<div key={handle} style={{ display: 'flex', alignItems: 'center', marginBottom: '5px' }}>
<span style={{ flexGrow: 1 }}>{handle}</span>
<button onClick={() => removeScript(handle)}>Remove</button>
</div>
))}<
<div style={{ display: 'flex', marginTop: '10px' }}>
<TextControl
label="New Script Handle"
value={newScriptHandle}
onChange={setNewScriptHandle}
onBlur={addScript} // Add on blur for convenience
onKeyPress={(e) => { if (e.key === 'Enter') addScript(); }}
/>
<button onClick={addScript} style={{ marginLeft: '5px' }}>Add</button>
</div>
</div>
<hr />
<div>
<h4>Managed Styles</h4>
{localStyles.map(handle => (
<div key={handle} style={{ display: 'flex', alignItems: 'center', marginBottom: '5px' }}>
<span style={{ flexGrow: 1 }}>{handle}</span>
<button onClick={() => removeStyle(handle)}>Remove</button>
</div>
))}<
<div style={{ display: 'flex', marginTop: '10px' }}>
<TextControl
label="New Style Handle"
value={newStyleHandle}
onChange={setNewStyleHandle}
onBlur={addStyle}
onKeyPress={(e) => { if (e.key === 'Enter') addStyle(); }}
/>
<button onClick={addStyle} style={{ marginLeft: '5px' }}>Add</button>
</div>
</div>
<hr />
<ToggleControl
label="Enable Feature X"
checked={enableFeatureX}
onChange={(value) => setAttributes({ enableFeatureX: value })}
/>
</PanelBody>
</InspectorControls>
<div className="asset-manager-block-editor">
<p>Asset Manager Block (Editor View)</p>
<p>Scripts managed: {localScripts.join(', ')}</p>
<p>Styles managed: {localStyles.join(', ')}</p>
<p>Feature X enabled: {enableFeatureX ? 'Yes' : 'No'}</p>
{/* Render the Web Component for its side effects (asset management) */}
<AssetManagerElement />
</div>
</>
);
},
save: ({ attributes }) => {
const { managedScripts, managedStyles } = attributes;
// The save function MUST return what should be rendered in the DOM.
// We render the Web Component here so it's present on the front-end
// and can manage assets based on its attributes.
return (
<asset-manager
scripts={managedScripts}
styles={managedStyles}
></asset-manager>
);
},
});
Backend Asset Registration (PHP)
The JavaScript Web Component, when rendered, will trigger its connectedCallback. However, WordPress’s PHP-based asset enqueuing system needs to be aware of these assets. We’ll use WordPress hooks to intercept the asset management calls initiated by our Web Component.
This requires a bridge between the client-side JavaScript and the server-side PHP. A common pattern is to use WordPress’s REST API or AJAX to communicate asset needs, or more directly, to leverage the wp.hooks API (if available and configured) or custom JavaScript events that PHP can listen to via wp_localize_script and subsequent AJAX handlers.
A more robust approach for this specific use case is to have the Web Component *not* directly call enqueue/dequeue functions, but rather to emit custom events. These events can be caught by a main JavaScript handler that then makes an AJAX request to a WordPress REST API endpoint or a custom AJAX action. The PHP backend then processes these requests to enqueue/dequeue assets.
PHP Implementation for Asset Handling
Let’s assume our Web Component emits custom events like asset-manager:register-script and asset-manager:deregister-style. We’ll need a PHP script to handle these.
my-plugin.php (or your theme’s functions.php)
admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'my_asset_manager_nonce' ),
) );
}
add_action( 'enqueue_block_editor_assets', 'my_asset_manager_block_scripts' );
// Also enqueue for front-end if the Web Component needs to run there independently
// add_action( 'wp_enqueue_scripts', 'my_asset_manager_block_scripts' );
// Register custom REST API endpoint for asset management
add_action( 'rest_api_init', function () {
register_rest_route( 'my-asset-manager/v1', '/manage-assets', array(
'methods' => 'POST',
'callback' => 'handle_asset_management_request',
'permission_callback' => function () {
// Ensure user has capability to edit posts or manage assets
return current_user_can( 'edit_posts' );
},
) );
} );
// Callback function for the REST API endpoint
function handle_asset_management_request( WP_REST_Request $request ) {
if ( ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'my_asset_manager_nonce' ) ) {
return new WP_Error( 'rest_nonce_invalid', 'Nonce is invalid.', array( 'status' => 403 ) );
}
$params = $request->get_json_params();
$action = $params['action'] ?? ''; // e.g., 'register', 'deregister'
$type = $params['type'] ?? ''; // e.g., 'script', 'style'
$handle = $params['handle'] ?? '';
if ( empty( $action ) || empty( $type ) || empty( $handle ) ) {
return new WP_Error( 'rest_invalid_param', 'Missing required parameters.', array( 'status' => 400 ) );
}
// Define your actual script and style handles here.
// This is a simplified example. In a real scenario, you'd have a
// defined list of available assets and their dependencies.
$available_scripts = array( 'my-custom-script', 'another-script' );
$available_styles = array( 'my-custom-style', 'another-style' );
if ( $type === 'script' ) {
if ( ! in_array( $handle, $available_scripts, true ) ) {
return new WP_Error( 'rest_invalid_handle', 'Invalid script handle.', array( 'status' => 400 ) );
}
if ( $action === 'register' ) {
// Check if already enqueued to avoid redundant calls
if ( ! wp_script_is( $handle, 'enqueued' ) ) {
wp_enqueue_script( $handle );
// You might need to specify dependencies, version, etc. here
// wp_enqueue_script( $handle, null, array('jquery'), '1.0.0' );
}
return new WP_REST_Response( array( 'message' => "Script '{$handle}' registered." ), 200 );
} elseif ( $action === 'deregister' ) {
// Check if it's actually enqueued before attempting to dequeue
if ( wp_script_is( $handle, 'enqueued' ) || wp_script_is( $handle, 'registered' ) ) {
wp_dequeue_script( $handle );
}
return new WP_REST_Response( array( 'message' => "Script '{$handle}' deregistered." ), 200 );
}
} elseif ( $type === 'style' ) {
if ( ! in_array( $handle, $available_styles, true ) ) {
return new WP_Error( 'rest_invalid_handle', 'Invalid style handle.', array( 'status' => 400 ) );
}
if ( $action === 'register' ) {
if ( ! wp_style_is( $handle, 'enqueued' ) ) {
wp_enqueue_style( $handle );
// wp_enqueue_style( $handle, null, array(), '1.0.0' );
}
return new WP_REST_Response( array( 'message' => "Style '{$handle}' registered." ), 200 );
} elseif ( $action === 'deregister' ) {
if ( wp_style_is( $handle, 'enqueued' ) || wp_style_is( $handle, 'registered' ) ) {
wp_dequeue_style( $handle );
}
return new WP_REST_Response( array( 'message' => "Style '{$handle}' deregistered." ), 200 );
}
}
return new WP_Error( 'rest_invalid_action', 'Invalid action or type.', array( 'status' => 400 ) );
}
// --- AJAX Handler Alternative (if not using REST API) ---
/*
add_action( 'wp_ajax_my_asset_manager_handle', 'handle_asset_manager_ajax' );
add_action( 'wp_ajax_nopriv_my_asset_manager_handle', 'handle_asset_manager_ajax' ); // If needed for non-logged-in users
function handle_asset_manager_ajax() {
check_ajax_referer( 'my_asset_manager_nonce', 'nonce' );
$action = $_POST['action_type'] ?? ''; // e.g., 'register', 'deregister'
$type = $_POST['asset_type'] ?? ''; // e.g., 'script', 'style'
$handle = $_POST['asset_handle'] ?? '';
// ... (similar logic as handle_asset_management_request using $_POST variables) ...
// Example:
if ( $type === 'script' && $action === 'register' ) {
wp_enqueue_script( $handle );
wp_send_json_success( array( 'message' => "Script '{$handle}' registered via AJAX." ) );
}
wp_send_json_error( array( 'message' => 'Invalid request.' ) );
wp_die();
}
*/
// --- Helper function to register actual assets ---
// You would call these in your theme's functions.php or plugin's main file
// to make the handles known to WordPress.
function my_plugin_register_all_assets() {
// Example: Registering a script and its dependencies
wp_register_script( 'my-custom-script', plugin_dir_url( __FILE__ ) . 'assets/js/my-custom-script.js', array('jquery'), '1.0.0', true );
wp_localize_script( 'my-custom-script', 'myCustomScriptData', array( 'ajax_url' => admin_url( 'admin-ajax.php' ) ) );
// Example: Registering a stylesheet
wp_register_style( 'my-custom-style', plugin_dir_url( __FILE__ ) . 'assets/css/my-custom-style.css', array(), '1.0.0' );
// Register other assets as needed...
wp_register_script( 'another-script', plugin_dir_url( __FILE__ ) . 'assets/js/another-script.js', array(), '1.1.0', true );
wp_register_style( 'another-style', plugin_dir_url( __FILE__ ) . 'assets/css/another-style.css', array(), '1.2.0' );
}
add_action( 'init', 'my_plugin_register_all_assets' );
// --- Modify Web Component JS to use REST API ---
// The asset-manager-component.js needs to be updated to send AJAX requests.
// Replace the console.log statements in registerAssets/deregisterAssets
// with actual fetch calls to the REST API.
Bridging Web Component Events to PHP Backend
The current asset-manager-component.js simulates asset registration. To make it functional with the PHP backend, we need to modify the registerAssets and deregisterAssets methods to send requests to the WordPress REST API endpoint we defined.
Updating asset-manager-component.js for AJAX
// ... (previous class definition) ...
async sendAssetRequest(action, type, handle) {
const nonce = MyAssetManagerConfig.nonce; // Assuming wp_localize_script is used
const ajaxUrl = MyAssetManagerConfig.ajax_url; // Assuming wp_localize_script is used
try {
const response = await fetch(`${ajaxUrl}?rest_route=/my-asset-manager/v1/manage-assets`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce,
},
body: JSON.stringify({
action: action, // 'register' or 'deregister'
type: type, // 'script' or 'style'
handle: handle,
}),
});
if (!response.ok) {
const errorData = await response.json();
console.error(`Error ${action}ing ${type} '${handle}':`, response.status, errorData);
return false;
}
const result = await response.json();
console.log(`Successfully ${action}ed ${type} '${handle}':`, result.message);
return true;
} catch (error) {
console.error(`Network or fetch error ${action}ing ${type} '${handle}':`, error);
return false;
}
}
registerAssets() {
console.log('Attempting to register assets:', this.scriptsToManage, this.stylesToManage);
this.scriptsToManage.forEach(scriptHandle => {
this.sendAssetRequest('register', 'script', scriptHandle);
});
this.stylesToManage.forEach(styleHandle => {
this.sendAssetRequest('register', 'style', styleHandle);
});
}
deregisterAssets() {
console.log('Attempting to deregister assets:', this.scriptsToManage, this.stylesToManage);
this.scriptsToManage.forEach(scriptHandle => {
this.sendAssetRequest('deregister', 'script', scriptHandle);
});
this.stylesToManage.forEach(styleHandle => {
this.sendAssetRequest('deregister', 'style', styleHandle);
});
}
// ... (rest of the class definition) ...
// Ensure MyAssetManagerConfig is available globally or passed differently
// If using webpack/build process, ensure wp_localize_script makes it available.
// Example: window.MyAssetManagerConfig = { ajax_url: '...', nonce: '...' };
Build Process and Deployment
For modern WordPress development, you’ll typically use a build tool like Webpack or Gulp to compile your JavaScript (including JSX for React components if used elsewhere) and CSS. Ensure your build process:
- Transpiles modern JavaScript to a compatible version (e.g., ES5).
- Bundles your scripts (
asset-manager-component.jsandmy-asset-block.js) into single files. - Places the compiled files in a
builddirectory (or similar) referenced in your PHP enqueue functions.
A typical webpack.config.js might look like this:
Example webpack.config.js
const path = require('path');
module.exports = {
entry: {
'asset-manager-component': './src/asset-manager-component.js',
'my-asset-block': './src/my-asset-block.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'build'), // Output to a 'build' directory
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader', // Requires @babel/core, @babel/preset-env, babel-loader
options: {
presets: ['@babel/preset-env'],
},
},
},
// Add rules for CSS, images, etc. if needed
],
},
// Add plugins like MiniCssExtractPlugin if handling CSS
// Add devtool for source maps if needed during development
};
After setting up your build tools, run the build command (e.g., npm run build) to generate the necessary JavaScript files in the build directory. Then, ensure your PHP correctly enqueues these files.
Advanced Considerations and Best Practices
Conditional Loading: The current setup loads assets whenever the block is present. For more granular control, you could add logic within the Web Component or PHP to check specific conditions (e.g., user roles, post meta) before registering assets.
Asset Dependencies: When enqueuing scripts and styles in PHP, always define dependencies correctly (e.g., array('jquery') for scripts) to prevent conflicts and ensure proper loading order.
Performance: While Web Components offer encapsulation, ensure your build process optimizes asset delivery (minification, concatenation). The core benefit here is *conditional* loading, preventing unnecessary assets from being loaded on pages where the block isn’t used.
Error Handling: Implement robust error handling in both the JavaScript (fetch requests, JSON parsing) and PHP (REST API validation, nonce checks) to gracefully manage unexpected situations.
Security: Always validate and sanitize any data received via AJAX or REST API requests. Use nonces to verify the origin of requests.
SSR (Server-Side Rendering): If your block relies heavily on server-rendered output, ensure the Web Component’s asset management logic is also considered during SSR, or that the PHP enqueuing happens reliably regardless of client-side rendering.
By combining the declarative nature of Web Components with WordPress’s robust asset management system via PHP and the REST API, you can build highly dynamic and performant Gutenberg blocks that precisely control their own dependencies.