Step-by-Step Guide to building a custom dynamic lead collector block for Gutenberg using Alpine.js lightweight states
Gutenberg Block Structure and Registration
We’ll start by defining the core structure of our Gutenberg block. This involves creating a JavaScript file that registers the block with WordPress. For this example, we’ll create a simple dynamic block that renders a lead collection form. Dynamic blocks are ideal here because they allow server-side rendering, ensuring that form submissions are processed securely and efficiently.
First, let’s set up the necessary files. We’ll need a main plugin file (e.g., custom-lead-collector.php) and a JavaScript file for the block editor (e.g., src/index.js). We’ll also need a PHP file to handle the server-side rendering of the block (e.g., templates/lead-form-template.php).
Plugin Activation and Enqueueing Scripts
The main plugin file will handle the registration of our Gutenberg block and enqueueing of necessary scripts. We’ll use the register_block_type function for this.
<?php
/**
* Plugin Name: Custom Lead Collector Block
* Description: A custom Gutenberg block for collecting leads with Alpine.js.
* 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 block using the metadata loaded from the `block.json` file.
* Behind the scenes, it registers also all assets so they can be enqueued
* through the block editor in the corresponding context.
*
* @see https://developer.wordpress.org/reference/functions/register_block_type/
*/
function custom_lead_collector_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_lead_collector_block_init' );
?>
For a dynamic block, we also need to specify the render_callback in the block.json file. This callback will point to a PHP function that renders the block’s front-end output.
{
"apiVersion": 2,
"name": "custom-lead-collector/lead-form",
"version": "0.1.0",
"title": "Lead Collector Form",
"category": "widgets",
"icon": "email-alt",
"description": "A custom block to collect leads using Alpine.js.",
"attributes": {
"formTitle": {
"type": "string",
"default": "Get in Touch"
},
"submitButtonText": {
"type": "string",
"default": "Submit"
}
},
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style.css",
"render_callback": "render_lead_form_block"
}
Client-Side Block Editor Implementation with Alpine.js
The src/index.js file will define how the block appears and behaves within the Gutenberg editor. We’ll use React for the editor interface and Alpine.js for managing the lightweight state of our form elements.
First, ensure you have Node.js and npm/yarn installed. You’ll need to set up a build process (e.g., using `@wordpress/scripts`) to compile your JavaScript and CSS.
Install the necessary development dependencies:
npm install --save-dev @wordpress/scripts # or yarn add --dev @wordpress/scripts
Add a build script to your package.json:
{
"name": "custom-lead-collector",
"version": "1.0.0",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"devDependencies": {
"@wordpress/scripts": "^26.7.0"
}
}
Now, let’s write the JavaScript for the block editor. We’ll use @wordpress/blocks and @wordpress/element to define the block’s structure and attributes. We’ll also import Alpine.js.
// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
import Alpine from 'alpinejs';
// Import the block's CSS
import './style.scss';
import './editor.scss';
// Import the template for the front-end rendering (used by the server)
// This is not directly used in the editor, but good practice to have it accessible.
// import template from '../templates/lead-form-template.php'; // Not directly importable in JS
registerBlockType( 'custom-lead-collector/lead-form', {
apiVersion: 2,
title: 'Lead Collector Form',
icon: 'email-alt',
category: 'widgets',
attributes: {
formTitle: {
type: 'string',
default: 'Get in Touch',
},
submitButtonText: {
type: 'string',
default: 'Submit',
},
},
edit: ( { attributes, setAttributes } ) => {
const blockProps = useBlockProps();
const { formTitle, submitButtonText } = attributes;
// Alpine.js state management for the form fields within the editor preview
// This is for demonstration; actual form submission logic will be server-side.
const alpineState = {
name: '',
email: '',
message: '',
isSubmitting: false,
submissionStatus: null, // 'success' or 'error'
init() {
// Initialize Alpine state if needed, e.g., for form validation previews
},
submitForm() {
this.isSubmitting = true;
this.submissionStatus = null;
// In a real editor preview, you might simulate submission or use a placeholder API.
// For this example, we'll just simulate a delay and success.
setTimeout(() => {
console.log('Simulated submission:', { name: this.name, email: this.email, message: this.message });
this.submissionStatus = 'success';
this.name = '';
this.email = '';
this.message = '';
this.isSubmitting = false;
setTimeout(() => this.submissionStatus = null, 3000); // Clear status after 3 seconds
}, 1500);
}
};
return (
<>
<InspectorControls>
<PanelBody title={ 'Form Settings' }>
<TextControl
label={ 'Form Title' }
value={ formTitle }
onChange={ ( value ) => setAttributes( { formTitle: value } ) }
/>
<TextControl
label={ 'Submit Button Text' }
value={ submitButtonText }
onChange={ ( value ) => setAttributes( { submitButtonText: value } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<div x-data="{ ...alpineState }">
<h3>{ formTitle }</h3>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" x-model="name" required />
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" x-model="email" required />
</div>
<div class="form-group">
<label for="message">Message:</label>
<textarea id="message" x-model="message" required></textarea>
</div>
<button type="submit" x-text="isSubmitting ? 'Submitting...' : submitButtonText" :disabled="isSubmitting"></button>
<!-- Submission Feedback -->
<template x-if="submissionStatus === 'success'">
<p style="color: green;">Thank you! Your message has been sent.</p>
</template>
<template x-if="submissionStatus === 'error'">
<p style="color: red;">There was an error. Please try again.</p>
</template>
</form>
</div>
</div>
</>
);
},
save: () => {
// For dynamic blocks, the save function should return null.
// The front-end rendering is handled by the PHP render_callback.
return null;
},
} );
// Initialize Alpine.js globally if needed for front-end,
// but for the editor, we are scoping it within the block's x-data.
// Alpine.start(); // Not typically needed here for editor preview.
In this editor component:
- We use
useBlockPropsto get the necessary wrapper props for the block. InspectorControlsprovides a sidebar panel for editing block attributes like the form title and submit button text.useState(from@wordpress/element) is used for managing React state within the editor, though for the form’s interactive elements, we delegate to Alpine.js.- The
x-datadirective in the returned JSX initializes an Alpine.js component. We define properties likename,email,message,isSubmitting, andsubmissionStatus. - The
submitFormmethod withinx-datasimulates form submission. In a real-world scenario, this would likely involve an AJAX request to a WordPress REST API endpoint or a custom AJAX handler. x-modelbinds the input fields to the Alpine.js state.x-textdynamically updates the submit button’s text.x-ifis used for conditional rendering of success/error messages.
Server-Side Rendering and Form Submission Handling
The render_lead_form_block function, specified in block.json, is responsible for rendering the block’s output on the front end. This is where we’ll include the Alpine.js logic for the actual form submission.
Create a file named templates/lead-form-template.php:
<?php
/**
* Server-side rendering for the Lead Collector Form block.
*
* @param array $attributes The block attributes.
* @return string The HTML output for the block.
*/
function render_lead_form_block( $attributes ) {
$form_title = isset( $attributes['formTitle'] ) ? sanitize_text_field( $attributes['formTitle'] ) : 'Get in Touch';
$submit_button_text = isset( $attributes['submitButtonText'] ) ? sanitize_text_field( $attributes['submitButtonText'] ) : 'Submit';
// Ensure Alpine.js is enqueued on the front-end if not already.
// You might want to enqueue it globally in your plugin's main file or theme's functions.php.
// For this example, we assume it's available.
// wp_enqueue_script( 'alpinejs', 'https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js', array(), '3.x.x', true );
ob_start();
?>
<div class="wp-block-custom-lead-collector-lead-form">
<div x-data="{
name: '',
email: '',
message: '',
isSubmitting: false,
submissionStatus: null, // 'success' or 'error'
init() {
// Initialize Alpine state if needed, e.g., load from cookies or session
},
async submitForm() {
this.isSubmitting = true;
this.submissionStatus = null;
const response = await fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'submit_lead_form', // AJAX action hook
nonce: '',
name: this.name,
email: this.email,
message: this.message,
}),
});
if (response.ok) {
const result = await response.json();
if (result.success) {
this.submissionStatus = 'success';
this.name = '';
this.email = '';
this.message = '';
setTimeout(() => this.submissionStatus = null, 5000); // Clear status after 5 seconds
} else {
this.submissionStatus = 'error';
console.error('Form submission failed:', result.data.message);
setTimeout(() => this.submissionStatus = null, 5000);
}
} else {
this.submissionStatus = 'error';
console.error('Network response was not ok');
setTimeout(() => this.submissionStatus = null, 5000);
}
this.isSubmitting = false;
}
}">
<h3><?php echo esc_html( $form_title ); ?></h3>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="lead-name">Name:</label>
<input type="text" id="lead-name" x-model="name" required />
</div>
<div class="form-group">
<label for="lead-email">Email:</label>
<input type="email" id="lead-email" x-model="email" required />
</div>
<div class="form-group">
<label for="lead-message">Message:</label>
<textarea id="lead-message" x-model="message" required></textarea>
</div>
<button type="submit" x-text="isSubmitting ? 'Submitting...' : '<?php echo esc_js( $submit_button_text ); ?>'" :disabled="isSubmitting"></button>
<!-- Submission Feedback -->
<template x-if="submissionStatus === 'success'">
<p style="color: green;">Thank you! Your message has been sent.</p>
</template>
<template x-if="submissionStatus === 'error'">
<p style="color: red;">There was an error. Please try again.</p>
</template>
</form>
</div>
</div>
<?php
return ob_get_clean();
}
?>
In the render_lead_form_block function:
- We retrieve and sanitize the block attributes (
formTitle,submitButtonText). - We use
ob_start()andob_get_clean()to capture the HTML output. - The core Alpine.js logic is embedded directly within the
x-dataattribute of a<div>. This keeps the Alpine.js state local to the block instance. - The
submitFormmethod now uses the Fetch API to send a POST request toadmin-ajax.php, WordPress’s endpoint for handling AJAX requests. - We pass the
action(submit_lead_form), a nonce for security, and the form data (name, email, message). - The response from the AJAX handler is parsed, and
submissionStatusis updated accordingly to display feedback to the user. wp_create_nonce()is crucial for security, ensuring the request originates from a legitimate WordPress session.
AJAX Handler for Lead Submission
We need to register an AJAX handler in our main plugin file to process the form submission.
// Add this to your custom-lead-collector.php file
add_action( 'wp_ajax_submit_lead_form', 'handle_submit_lead_form' );
add_action( 'wp_ajax_nopriv_submit_lead_form', 'handle_submit_lead_form' ); // For logged-out users
function handle_submit_lead_form() {
// Verify nonce for security
check_ajax_referer( 'submit_lead_form_nonce', 'nonce' );
// Sanitize and validate input data
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';
$email = isset( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : '';
$message = isset( $_POST['message'] ) ? sanitize_textarea_field( $_POST['message'] ) : '';
// Basic validation
if ( empty( $name ) || ! is_email( $email ) || empty( $message ) ) {
wp_send_json_error( array( 'message' => 'Invalid input data.' ), 400 );
}
// --- Process the lead data ---
// This is where you would typically:
// 1. Save the lead to a custom database table.
// 2. Send an email notification to the site administrator.
// 3. Integrate with a CRM or email marketing service.
// Example: Saving to a custom table (requires custom table creation)
// global $wpdb;
// $table_name = $wpdb->prefix . 'leads';
// $wpdb->insert( $table_name, array(
// 'name' => $name,
// 'email' => $email,
// 'message' => $message,
// 'submission_time' => current_time( 'mysql' ),
// ) );
// Example: Sending an email
$to = get_option( 'admin_email' );
$subject = 'New Lead Collected: ' . $name;
$body = "You have received a new lead:\n\n";
$body .= "Name: " . $name . "\n";
$body .= "Email: " . $email . "\n";
$body .= "Message: " . $message . "\n";
$headers = array( 'Content-Type: text/plain; charset=UTF-8' );
$mail_sent = wp_mail( $to, $subject, $body, $headers );
if ( $mail_sent ) {
wp_send_json_success( array( 'message' => 'Lead submitted successfully.' ) );
} else {
wp_send_json_error( array( 'message' => 'Failed to send email notification.' ), 500 );
}
}
In the AJAX handler:
check_ajax_referer()verifies that the request is legitimate and not a cross-site request forgery.- Input data is retrieved from
$_POSTand thoroughly sanitized using WordPress functions likesanitize_text_field(),sanitize_email(), andsanitize_textarea_field(). - Basic validation checks ensure that required fields are present and the email format is valid.
- The code includes commented-out examples for saving leads to a custom database table and sending email notifications. You would implement these based on your specific requirements.
wp_send_json_success()andwp_send_json_error()are used to send JSON responses back to the client-side JavaScript, indicating the outcome of the submission.
Styling the Block
You’ll need CSS for both the editor and the front end. Create src/style.scss for front-end styles and src/editor.scss for editor-specific styles.
/* src/style.scss */
.wp-block-custom-lead-collector-lead-form {
border: 1px solid #eee;
padding: 20px;
margin-bottom: 20px;
background-color: #f9f9f9;
border-radius: 5px;
h3 {
margin-top: 0;
color: #333;
}
.form-group {
margin-bottom: 15px;
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input[type="text"],
input[type="email"],
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box; /* Important for consistent sizing */
}
textarea {
min-height: 100px;
resize: vertical;
}
}
button {
background-color: #0073aa;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
&:hover:not(:disabled) {
background-color: #005177;
}
&:disabled {
background-color: #ccc;
cursor: not-allowed;
}
}
}
/* src/editor.scss */
.wp-block-custom-lead-collector-lead-form {
/* Styles specific to the editor preview */
/* Often, you might want to make the editor preview look similar to the front-end */
border: 1px dashed #ccc; /* Differentiate in editor */
background-color: #fff;
padding: 15px;
h3 {
color: #555;
}
.form-group label {
color: #777;
}
input, textarea {
border-color: #ddd;
}
}
After adding these SCSS files, run your build command (npm run build or yarn build) to compile them into CSS files in the build directory.
Final Steps and Considerations
To finalize the setup:
- Place the
custom-lead-collector.phpfile and thebuilddirectory (containing compiled JS and CSS) into yourwp-content/plugins/directory. - Activate the “Custom Lead Collector Block” plugin from your WordPress admin panel.
- You can now add the “Lead Collector Form” block to any post or page using the Gutenberg editor.
Security Best Practices
Always prioritize security:
- Nonce Verification: Crucial for AJAX requests to prevent CSRF attacks.
- Input Sanitization: Use WordPress’s built-in sanitization functions for all user-provided data before processing or storing it.
- Data Validation: Ensure data conforms to expected formats (e.g., valid email addresses).
- Escaping Output: Use WordPress escaping functions (e.g.,
esc_html(),esc_js()) when outputting data to prevent XSS vulnerabilities. - HTTPS: Ensure your site uses HTTPS, especially when handling sensitive data.
- Error Handling: Provide user-friendly error messages without revealing sensitive system details.
Further Enhancements
Consider these potential improvements:
- Custom Database Table: For robust lead management, create a dedicated database table to store leads.
- Email Notifications: Configure more sophisticated email templates for admin notifications.
- CRM/Marketing Integration: Connect to services like HubSpot, Mailchimp, or Salesforce via their APIs.
- Advanced Validation: Implement more complex client-side and server-side validation rules (e.g., password strength, custom regex).
- File Uploads: If needed, add functionality for users to upload files, ensuring secure handling and storage.
- Internationalization: Use WordPress internationalization functions (
__,_e) for translatable strings. - Accessibility: Ensure form elements have proper ARIA attributes and keyboard navigation support.
By leveraging Gutenberg’s block API, Alpine.js for lightweight interactivity, and secure AJAX handling in PHP, you can build powerful and dynamic custom Gutenberg blocks tailored to your specific needs, such as this lead collection form.