Step-by-Step Guide to building a custom dynamic lead collector block for Gutenberg using Vanilla JS Web Components
Project Setup and Environment Configuration
This guide details the construction of a custom Gutenberg block for WordPress, specifically a dynamic lead collector. We will leverage Vanilla JavaScript and Web Components for a modern, framework-agnostic approach. The focus is on a production-ready implementation, eschewing build tools like Webpack for simplicity and direct control, suitable for environments where build pipelines are managed separately or not desired for simpler plugins.
Our project structure will be straightforward. We’ll create a minimal WordPress plugin that registers our custom block. All JavaScript and PHP will reside within this plugin’s directory.
Plugin Structure and Registration
Begin by creating a new directory within your WordPress installation’s wp-content/plugins/ folder. Let’s name it custom-lead-collector.
Inside this directory, create the main plugin file, custom-lead-collector.php.
custom-lead-collector.php
<?php
/**
* Plugin Name: Custom Lead Collector Block
* Description: A custom Gutenberg block for collecting leads using Vanilla JS Web Components.
* Version: 1.0.0
* Author: Antigravity
* License: GPL-2.0-or-later
* Text Domain: custom-lead-collector
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Registers the custom block.
*/
function custom_lead_collector_register_block() {
// Automatically load dependencies and version.
$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');
wp_register_script(
'custom-lead-collector-editor-script',
plugins_url( 'build/index.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_register_script(
'custom-lead-collector-frontend-script',
plugins_url( 'build/frontend.js', __FILE__ ),
array(), // No external dependencies for frontend script
'1.0.0'
);
register_block_type( 'custom-lead-collector/lead-form', array(
'editor_script' => 'custom-lead-collector-editor-script',
'render_callback' => 'custom_lead_collector_render_frontend',
) );
}
add_action( 'init', 'custom_lead_collector_register_block' );
/**
* Renders the frontend view of the block.
*
* @param array $attributes Block attributes.
* @return string HTML output.
*/
function custom_lead_collector_render_frontend( $attributes ) {
// Enqueue frontend script only when the block is rendered.
wp_enqueue_script('custom-lead-collector-frontend-script');
// Basic attributes for demonstration. In a real scenario, you'd fetch these.
$form_title = isset($attributes['formTitle']) ? esc_html($attributes['formTitle']) : __('Subscribe to Our Newsletter', 'custom-lead-collector');
$submit_button_text = isset($attributes['submitButtonText']) ? esc_html($attributes['submitButtonText']) : __('Submit', 'custom-lead-collector');
// The Web Component will be initialized by frontend.js.
// We pass data via data attributes.
return sprintf(
'<div class="wp-block-custom-lead-collector-container" data-form-title="%s" data-submit-button-text="%s"></div>',
esc_attr($form_title),
esc_attr($submit_button_text)
);
}
/**
* Enqueues block assets for the editor.
*/
function custom_lead_collector_editor_assets() {
wp_enqueue_script(
'custom-lead-collector-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.asset.php' )
);
wp_enqueue_style(
'custom-lead-collector-editor-style',
plugins_url( 'build/index.css', __FILE__ ),
array( 'wp-edit-blocks' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
);
}
add_action( 'enqueue_block_editor_assets', 'custom_lead_collector_editor_assets' );
/**
* Enqueues block assets for the frontend.
*/
function custom_lead_collector_frontend_assets() {
wp_enqueue_script(
'custom-lead-collector-frontend-script',
plugins_url( 'build/frontend.js', __FILE__ ),
array(),
'1.0.0'
);
wp_enqueue_style(
'custom-lead-collector-frontend-style',
plugins_url( 'build/style.css', __FILE__ ),
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'build/style.css' )
);
}
add_action( 'wp_enqueue_scripts', 'custom_lead_collector_frontend_assets' );
This PHP file:
- Defines the plugin header.
- Registers the block type using
register_block_type. - Specifies the editor script (
index.js) and the render callback for the frontend (custom_lead_collector_render_frontend). - The render callback enqueues the frontend script and outputs a container div with data attributes that will be used by our Web Component.
- Enqueues editor-specific assets (
index.jsandindex.css) usingenqueue_block_editor_assets. - Enqueues frontend-specific assets (
frontend.jsandstyle.css) usingwp_enqueue_scripts.
Gutenberg Block Registration and Editor Interface
We’ll use the WordPress Script Dependencies API to manage our JavaScript. For this, we need an index.asset.php file generated by a build process. Since we’re avoiding complex build tools for this example, we’ll manually create this file. In a real-world, more complex plugin, you’d use `@wordpress/scripts` or a similar tool to generate this automatically.
build/index.asset.php (Manual Creation)
<?php
// This file is typically generated by @wordpress/scripts.
// For this manual example, we define the dependencies and version.
return array(
'dependencies' => array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
'version' => '1.0.0', // Use a version that matches your plugin or is dynamic
);
Next, we define the block’s JavaScript entry point for the editor. Create a src directory and inside it, a file named index.js.
src/index.js (Editor Script)
const { registerBlockType } = wp.blocks;
const { InspectorControls, RichText } = wp.editor;
const { PanelBody } = wp.components;
const { __ } = wp.i18n;
import './style.scss'; // Import editor styles
registerBlockType( 'custom-lead-collector/lead-form', {
title: __( 'Custom Lead Collector', 'custom-lead-collector' ),
icon: 'email-alt',
category: 'widgets',
attributes: {
formTitle: {
type: 'string',
default: __( 'Subscribe to Our Newsletter', 'custom-lead-collector' ),
},
submitButtonText: {
type: 'string',
default: __( 'Submit', 'custom-lead-collector' ),
},
},
edit: ( { attributes, setAttributes } ) => {
const { formTitle, submitButtonText } = attributes;
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Form Settings', 'custom-lead-collector' ) } initialOpen={ true }>
<RichText
tagName="p"
value={ formTitle }
onChange={ ( newTitle ) => setAttributes( { formTitle: newTitle } ) }
placeholder={ __( 'Enter form title...', 'custom-lead-collector' ) }
allowedFormats={ [ 'core/bold', 'core/italic' ] }
/>
<RichText
tagName="p"
value={ submitButtonText }
onChange={ ( newText ) => setAttributes( { submitButtonText: newText } ) }
placeholder={ __( 'Enter submit button text...', 'custom-lead-collector' ) }
allowedFormats={ [ 'core/bold', 'core/italic' ] }
/>
</PanelBody>
</InspectorControls>
<div className="custom-lead-collector-editor">
<h3>{ formTitle }</h3>
<p>{ submitButtonText }</p>
<!-- This is a placeholder for the actual form fields -->
<div className="form-fields-placeholder">
<input type="email" placeholder="Email Address" disabled />
<button disabled>{ submitButtonText }</button>
</div>
<p>{ __( 'This is a dynamic block. The actual form will render on the frontend.', 'custom-lead-collector' ) }</p>
</div>
</>
);
},
save: () => {
// The save function should return null for dynamic blocks.
// The rendering is handled by the PHP render_callback.
return null;
},
} );
In this index.js:
- We import necessary components from WordPress packages.
registerBlockTypeis used to define our block with a unique name (custom-lead-collector/lead-form).title,icon, andcategoryare standard block registration properties.attributesdefine the data our block will store (formTitleandsubmitButtonText).- The
editfunction defines the block’s appearance and controls in the Gutenberg editor. We useInspectorControlsto add settings to the block sidebar, specifically aPanelBodycontaining twoRichTextcomponents for editable titles. - The
savefunction returnsnull, indicating this is a dynamic block whose frontend rendering is handled server-side by PHP.
Styling the Block
Create a style.scss file in the src directory for editor-specific styles and a style.css for frontend styles. For this example, we’ll use SCSS for both and compile them.
src/style.scss (Editor & Frontend Styles)
.wp-block-custom-lead-collector-container {
border: 1px solid #ccc;
padding: 20px;
margin-bottom: 20px;
background-color: #f9f9f9;
border-radius: 5px;
font-family: sans-serif;
}
.wp-block-custom-lead-collector-container h3 {
margin-top: 0;
color: #333;
}
.wp-block-custom-lead-collector-container input[type="email"],
.wp-block-custom-lead-collector-container input[type="text"] {
width: calc(100% - 22px); /* Account for padding and border */
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
.wp-block-custom-lead-collector-container button {
background-color: #0073aa;
color: white;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 1em;
}
.wp-block-custom-lead-collector-container button:hover {
background-color: #005177;
}
/* Editor specific styles */
.custom-lead-collector-editor {
border: 1px dashed #ccc;
padding: 15px;
background-color: #fff;
text-align: center;
}
.custom-lead-collector-editor h3 {
color: #555;
}
.form-fields-placeholder {
margin-top: 15px;
opacity: 0.6;
}
To compile SCSS to CSS, you’ll need a tool like Node-Sass or Dart Sass. For this manual setup, you can use a command-line approach. Assuming you have Node.js and npm installed:
Compiling SCSS
1. Install Sass:
npm install -g sass
2. Create a build directory in your plugin’s root.
3. Compile the SCSS files:
sass src/style.scss build/index.css sass src/style.scss build/style.css
This creates build/index.css for the editor and build/style.css for the frontend. The PHP file enqueues these accordingly.
Frontend Implementation with Vanilla JS Web Components
Now, let’s build the dynamic part: the Web Component that will render the actual lead collection form on the frontend. Create src/frontend.js.
src/frontend.js (Web Component Logic)
class LeadCollectorForm extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }); // Use Shadow DOM for encapsulation
this.formTitle = this.getAttribute('data-form-title') || 'Subscribe';
this.submitButtonText = this.getAttribute('data-submit-button-text') || 'Submit';
this.email = '';
}
connectedCallback() {
this.render();
this.addEventListeners();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: sans-serif;
border: 1px solid #ccc;
padding: 20px;
margin-bottom: 20px;
background-color: #f9f9f9;
border-radius: 5px;
}
h3 {
margin-top: 0;
color: #333;
}
input[type="email"],
input[type="text"] {
width: calc(100% - 22px);
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
button {
background-color: #0073aa;
color: white;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 1em;
}
button:hover {
background-color: #005177;
}
.message {
margin-top: 10px;
font-size: 0.9em;
color: green;
}
.error {
color: red;
}
</style>
<div class="lead-collector-wrapper">
<h3>${this.formTitle}</h3>
<input type="email" id="email" placeholder="Enter your email" required>
<button id="submit-btn">${this.submitButtonText}</button>
<div id="message-area"></div>
</div>
`;
}
addEventListeners() {
const emailInput = this.shadowRoot.getElementById('email');
const submitButton = this.shadowRoot.getElementById('submit-btn');
const messageArea = this.shadowRoot.getElementById('message-area');
emailInput.addEventListener('input', (e) => {
this.email = e.target.value;
messageArea.textContent = ''; // Clear messages on input
messageArea.className = '';
});
submitButton.addEventListener('click', async () => {
if (!this.email || !this.isValidEmail(this.email)) {
this.displayMessage('Please enter a valid email address.', 'error');
return;
}
submitButton.disabled = true;
submitButton.textContent = 'Submitting...';
try {
// In a real application, this would be an AJAX request to a WordPress REST API endpoint
// or a custom AJAX handler. For demonstration, we simulate a successful submission.
const response = await this.submitLead(this.email);
if (response.success) {
this.displayMessage(response.message, 'success');
emailInput.value = ''; // Clear input on success
this.email = '';
} else {
this.displayMessage(response.message, 'error');
}
} catch (error) {
console.error('Lead submission failed:', error);
this.displayMessage('An unexpected error occurred. Please try again later.', 'error');
} finally {
submitButton.disabled = false;
submitButton.textContent = this.submitButtonText;
}
});
}
isValidEmail(email) {
// Basic email validation regex
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
displayMessage(message, type = 'success') {
const messageArea = this.shadowRoot.getElementById('message-area');
messageArea.textContent = message;
messageArea.className = type; // 'success' or 'error'
}
async submitLead(email) {
// Simulate an AJAX call to a WordPress endpoint.
// Replace with actual AJAX call to your custom endpoint.
return new Promise((resolve) => {
setTimeout(() => {
// Simulate success or failure
const success = Math.random() > 0.2; // 80% success rate
if (success) {
resolve({ success: true, message: 'Thank you for subscribing!' });
} else {
resolve({ success: false, message: 'Something went wrong. Please try again.' });
}
}, 1500); // Simulate network latency
});
}
}
// Define the custom element
if (!customElements.get('lead-collector-form')) {
customElements.define('lead-collector-form', LeadCollectorForm);
}
// Find all container divs and replace them with the Web Component
document.addEventListener('DOMContentLoaded', () => {
const containers = document.querySelectorAll('.wp-block-custom-lead-collector-container');
containers.forEach(container => {
const formTitle = container.dataset.formTitle;
const submitButtonText = container.dataset.submitButtonText;
const webComponent = document.createElement('lead-collector-form');
webComponent.dataset.formTitle = formTitle;
webComponent.dataset.submitButtonText = submitButtonText;
container.parentNode.replaceChild(webComponent, container);
});
});
This frontend.js:
- Defines a class
LeadCollectorFormthat extendsHTMLElement, creating a custom element. - Uses Shadow DOM for style and DOM encapsulation.
connectedCallbackis invoked when the element is added to the DOM. It callsrenderandaddEventListeners.rendergenerates the HTML structure for the form, including input, button, and message area, and injects scoped styles. It uses thedata-form-titleanddata-submit-button-textattributes passed from the PHP render callback.addEventListenerssets up event listeners for the email input and submit button.- The submit button handler performs basic email validation, simulates an AJAX submission (you’ll replace this with a real AJAX call to a WordPress endpoint), and displays success or error messages.
isValidEmailis a helper for basic email format validation.displayMessageupdates the message area within the Shadow DOM.- Finally,
customElements.defineregisters the new element with the tag namelead-collector-form. The DOMContentLoaded listener then finds the placeholder divs rendered by PHP and replaces them with instances of our custom element, passing the necessary data attributes.
Compiling Frontend JavaScript
We need to compile our src/frontend.js. Since it doesn’t have WordPress dependencies, we can use a simple bundler like Rollup or even just copy it. For consistency with the manual approach, we’ll assume a simple copy or a basic Babel compilation if needed for older browser support. For this example, we’ll just copy it.
cp src/frontend.js build/frontend.js
If you need ES6+ features compiled for older browsers, you would integrate a tool like Babel:
# Install Babel CLI and preset-env npm install --save-dev @babel/core @babel/cli @babel/preset-env # Add a script to your package.json (if you had one) or run directly: # npx babel src/frontend.js --out-file build/frontend.js --presets=@babel/preset-env
Backend Integration: AJAX Handler for Lead Submission
The submitLead function in frontend.js currently simulates a submission. In a production environment, you need a robust backend handler. We’ll add this to our main plugin file, custom-lead-collector.php.
Adding AJAX Handler in custom-lead-collector.php
__( 'Email is required.', 'custom-lead-collector' ) ) );
}
$email = sanitize_email( $_POST['email'] );
if ( ! is_email( $email ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid email format.', 'custom-lead-collector' ) ) );
}
// --- Lead Storage Logic ---
// In a real application, you would:
// 1. Store the email in the database (e.g., custom table, wp_users if applicable, or a plugin-specific table).
// 2. Send an email notification to the site administrator.
// 3. Potentially add the subscriber to a mailing list service via API.
// For demonstration, we'll just log it and return success.
error_log( 'New lead collected: ' . $email );
// Example: Storing in wp_options (not recommended for large scale)
// $leads = get_option('custom_lead_collector_leads', []);
// $leads[] = ['email' => $email, 'timestamp' => current_time('mysql')];
// update_option('custom_lead_collector_leads', $leads);
// Example: Using WP_User_Meta (if treating leads as users)
// $user_id = username_exists('lead_' . md5($email));
// if (!$user_id) {
// $password = wp_generate_password(12, false);
// $user_id = wp_create_user('lead_' . md5($email), $password, $email);
// if (!is_wp_error($user_id)) {
// update_user_meta($user_id, 'lead_source', 'custom-lead-collector-block');
// // Optionally send a welcome email to the lead
// }
// }
wp_send_json_success( array( 'message' => __( 'Thank you for subscribing!', 'custom-lead-collector' ) ) );
}
add_action( 'wp_ajax_custom_lead_collector_submit', 'custom_lead_collector_handle_ajax_submission' );
add_action( 'wp_ajax_nopriv_custom_lead_collector_submit', 'custom_lead_collector_handle_ajax_submission' ); // For logged-out users
/**
* Enqueues scripts and localizes data for AJAX.
*/
function custom_lead_collector_enqueue_scripts() {
// Enqueue the frontend script if it's not already handled by the render_callback
// This is a fallback or alternative way to ensure it's loaded.
// wp_enqueue_script('custom-lead-collector-frontend-script');
wp_localize_script( 'custom-lead-collector-frontend-script', 'customLeadCollectorAjax', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'custom_lead_collector_nonce' ),
) );
}
add_action( 'wp_enqueue_scripts', 'custom_lead_collector_enqueue_scripts' );
?>
And update the src/frontend.js to use this AJAX endpoint:
Updated src/frontend.js (AJAX Call)
// ... (class LeadCollectorForm definition remains the same) ...
async submitLead(email) {
const data = new URLSearchParams();
data.append('action', 'custom_lead_collector_submit'); // WordPress AJAX action hook
data.append('nonce', customLeadCollectorAjax.nonce); // Security nonce
data.append('email', email);
try {
const response = await fetch(customLeadCollectorAjax.ajax_url, {
method: 'POST',
body: data,
});
if (!response.ok) {
// Handle HTTP errors
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
return { success: true, message: result.data.message };
} else {
return { success: false, message: result.data.message };
}
} catch (error) {
console.error('Error submitting lead:', error);
throw error; // Re-throw to be caught by the caller
}
}
// ... (rest of the script, including customElements.define and DOMContentLoaded listener) ...
Key changes in the PHP and JS:
- PHP:
custom_lead_collector_handle_ajax_submission: This function is hooked into WordPress AJAX actions (wp_ajax_andwp_ajax_nopriv_). It verifies the nonce, sanitizes the email, and performs basic validation. Crucially, it includes comments outlining where you’d implement actual lead storage logic. It useswp_send_json_successandwp_send_json_errorfor standardized JSON responses.custom_lead_collector_enqueue_scripts: This function enqueues the frontend script and useswp_localize_scriptto pass AJAX URL and security nonce to the JavaScript. This is essential for secure AJAX requests in WordPress.
- JavaScript:
- The
submitLeadmethod now constructs aFormDataobject (orURLSearchParamsas shown) with the necessary parameters for the WordPress AJAX handler (action,nonce,email). - It uses the `fetch` API to send a POST request to `customLeadCollectorAjax.ajax_url`.
- It parses the JSON response from WordPress and returns an object indicating success or failure along with a message.
- The
Final Steps and Activation
1. **Organize Files:** Ensure your plugin directory looks like this:
custom-lead-collector/ ├── build/ │ ├── index.js │ ├── index.css │ ├── style.css │ └── frontend.js ├── src/ │ ├── index.js │ ├── style.scss │ └── frontend.js ├── custom-lead-collector.php └── index.asset.php
2. **Compile Assets:** Run the Sass compilation and JavaScript copy/transpilation commands as detailed above.
3. **Activate Plugin:** Upload the custom-lead-collector folder to your wp-content/plugins/ directory and activate the “Custom Lead Collector Block” plugin from your WordPress admin panel.
4. **Use the Block:** Go to the WordPress editor, add the “Custom Lead Collector” block, configure the title and button text in the sidebar, and publish your post or page. View the page on the frontend to see your dynamic Web Component in action.
This setup provides a robust, modern, and maintainable way to create dynamic Gutenberg blocks using Vanilla JS and Web Components, offering excellent encapsulation and performance without relying on heavy JavaScript frameworks.