elementor-forms
Elementor Forms Extension Reference
Elementor Pro only. All form APIs require Elementor Pro active.
1. Form Actions
Actions execute after form submission. Extend \ElementorPro\Modules\Forms\Classes\Action_Base.
Registration
add_action( 'elementor_pro/forms/actions/register', function ( $form_actions_registrar ) {
require_once __DIR__ . '/form-actions/my-action.php';
$form_actions_registrar->register( new \My_Custom_Action() );
});
Required Methods
| Method | Returns | Purpose |
|---|---|---|
get_name() |
string |
Unique action ID used in code |
get_label() |
string |
Display label in editor |
run( $record, $ajax_handler ) |
void |
Execute on form submission |
register_settings_section( $widget ) |
void |
Optional: add action controls |
on_export( $element ) |
array |
Optional: strip sensitive data on export |
Action Controls
Always wrap in a section with submit_actions condition:
public function register_settings_section( $widget ): void {
$widget->start_controls_section( 'section_my_action', [
'label' => esc_html__( 'My Action', 'textdomain' ),
'condition' => [ 'submit_actions' => $this->get_name() ],
]);
$widget->add_control( 'my_api_key', [
'label' => esc_html__( 'API Key', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
]);
$widget->end_controls_section();
}
Record Data ($record) and AJAX Handler
public function run( $record, $ajax_handler ): void {
$settings = $record->get( 'form_settings' ); // Editor control values
$raw_fields = $record->get( 'fields' ); // All submitted fields
// Normalize: $fields[ $id ] = $field['value']
$fields = [];
foreach ( $raw_fields as $id => $field ) {
$fields[ $id ] = $field['value'];
}
// AJAX handler methods:
$ajax_handler->add_error( $field_id, 'Error message' );
$ajax_handler->add_success_message( 'Success!' );
}
On Export -- strip sensitive settings
public function on_export( $element ): array {
unset( $element['my_api_key'], $element['my_secret'] );
return $element;
}
Simple Example: Webhook Ping Action
class Ping_Action_After_Submit extends \ElementorPro\Modules\Forms\Classes\Action_Base {
public function get_name(): string { return 'ping'; }
public function get_label(): string { return esc_html__( 'Ping', 'textdomain' ); }
public function run( $record, $ajax_handler ): void {
wp_remote_post( 'https://api.example.com/', [
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode([
'site' => get_home_url(),
'action' => 'Form submitted',
]),
'timeout' => 60,
]);
}
public function register_settings_section( $widget ): void {}
public function on_export( $element ): array { return $element; }
}
Advanced Example: Sendy Subscriber Action
class Sendy_Action_After_Submit extends \ElementorPro\Modules\Forms\Classes\Action_Base {
public function get_name(): string { return 'sendy'; }
public function get_label(): string { return esc_html__( 'Sendy', 'textdomain' ); }
public function register_settings_section( $widget ): void {
$widget->start_controls_section( 'section_sendy', [
'label' => esc_html__( 'Sendy', 'textdomain' ),
'condition' => [ 'submit_actions' => $this->get_name() ],
]);
$widget->add_control( 'sendy_url', [
'label' => esc_html__( 'Sendy URL', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
'placeholder' => 'https://your_sendy_installation/',
]);
$widget->add_control( 'sendy_list', [
'label' => esc_html__( 'Sendy List ID', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
]);
$widget->add_control( 'sendy_email_field', [
'label' => esc_html__( 'Email Field ID', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
]);
$widget->add_control( 'sendy_name_field', [
'label' => esc_html__( 'Name Field ID', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
]);
$widget->end_controls_section();
}
public function run( $record, $ajax_handler ): void {
$settings = $record->get( 'form_settings' );
if ( empty( $settings['sendy_url'] ) || empty( $settings['sendy_list'] ) || empty( $settings['sendy_email_field'] ) ) {
return;
}
$raw_fields = $record->get( 'fields' );
$fields = [];
foreach ( $raw_fields as $id => $field ) { $fields[ $id ] = $field['value']; }
if ( empty( $fields[ $settings['sendy_email_field'] ] ) ) { return; }
$sendy_data = [
'email' => $fields[ $settings['sendy_email_field'] ],
'list' => $settings['sendy_list'],
'ipaddress' => \ElementorPro\Core\Utils::get_client_ip(),
'referrer' => isset( $_POST['referrer'] ) ? $_POST['referrer'] : '',
];
if ( ! empty( $fields[ $settings['sendy_name_field'] ] ) ) {
$sendy_data['name'] = $fields[ $settings['sendy_name_field'] ];
}
wp_remote_post( $settings['sendy_url'] . 'subscribe', [ 'body' => $sendy_data ] );
}
public function on_export( $element ): array {
unset( $element['sendy_url'], $element['sendy_list'], $element['sendy_email_field'], $element['sendy_name_field'] );
return $element;
}
}
2. Form Fields
Custom field types. Extend \ElementorPro\Modules\Forms\Fields\Field_Base.
Registration
add_action( 'elementor_pro/forms/fields/register', function ( $form_fields_registrar ) {
require_once __DIR__ . '/form-fields/my-field.php';
$form_fields_registrar->register( new \My_Custom_Field() );
});
Required Methods
| Method | Returns | Purpose |
|---|---|---|
get_type() |
string |
Unique field type ID |
get_name() |
string |
Display label in editor dropdown |
render( $item, $item_index, $form ) |
void |
Output field HTML on frontend |
validation( $field, $record, $ajax_handler ) |
void |
Optional: validate submitted value |
update_controls( $widget ) |
void |
Optional: add field-specific controls |
get_script_depends() |
array |
Optional: JS dependency handles |
get_style_depends() |
array |
Optional: CSS dependency handles |
Render -- use add_render_attribute
public function render( $item, $item_index, $form ): void {
$form->add_render_attribute( 'input' . $item_index, [
'type' => 'text',
'class' => 'elementor-field-textual',
'placeholder' => esc_html__( 'Placeholder', 'textdomain' ),
]);
echo '<input ' . $form->get_render_attribute_string( 'input' . $item_index ) . '>';
}
Access field control values from $item: $item['my-control-name'].
Field Validation
public function validation( $field, $record, $ajax_handler ): void {
if ( empty( $field['value'] ) ) { return; }
if ( ! preg_match( '/^[0-9]+$/', $field['value'] ) ) {
$ajax_handler->add_error( $field['id'], esc_html__( 'Only numbers.', 'textdomain' ) );
}
}
Field Controls (update_controls)
Inject into the form field repeater. Requires condition, tab, inner_tab, tabs_wrapper:
public function update_controls( $widget ): void {
$elementor = \ElementorPro\Plugin::elementor();
$control_data = $elementor->controls_manager->get_control_from_stack( $widget->get_unique_name(), 'form_fields' );
if ( is_wp_error( $control_data ) ) { return; }
$field_controls = [
'my-placeholder' => [
'name' => 'my-placeholder',
'label' => esc_html__( 'Placeholder', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
'condition' => [ 'field_type' => $this->get_type() ],
'tab' => 'content',
'inner_tab' => 'form_fields_content_tab',
'tabs_wrapper' => 'form_fields_tabs',
],
];
$control_data['fields'] = $this->inject_field_controls( $control_data['fields'], $field_controls );
$widget->update_control( 'form_fields', $control_data );
}
Content Template (JS Editor Preview)
Workaround for live preview. Do NOT name your method content_template() (reserved for future use):
public function __construct() {
parent::__construct();
add_action( 'elementor/preview/init', [ $this, 'editor_preview_footer' ] );
}
public function editor_preview_footer(): void {
add_action( 'wp_footer', [ $this, 'content_template_script' ] );
}
public function content_template_script(): void {
?>
<script>
jQuery( document ).ready( () => {
elementor.hooks.addFilter(
'elementor_pro/forms/content_template/field/<?php echo $this->get_type(); ?>',
function ( inputField, item, i ) {
const fieldId = `form_field_${i}`;
const fieldClass = `elementor-field-textual elementor-field ${item.css_classes}`;
return `<input id="${fieldId}" class="${fieldClass}" type="text">`;
}, 10, 3
);
});
</script>
<?php
}
Field Dependencies
// Register in plugin main file
add_action( 'wp_enqueue_scripts', function () {
wp_register_script( 'my-field-js', plugins_url( 'assets/js/field.js', __FILE__ ) );
wp_register_style( 'my-field-css', plugins_url( 'assets/css/field.css', __FILE__ ) );
});
// Declare in field class
public function get_script_depends(): array { return [ 'my-field-js' ]; }
public function get_style_depends(): array { return [ 'my-field-css' ]; }
// Backward compat (Elementor < 3.28): also set public properties
public $depended_scripts = [ 'my-field-js' ];
public $depended_styles = [ 'my-field-css' ];
Simple Example: Local Tel Field with Pattern
class Elementor_Local_Tel_Field extends \ElementorPro\Modules\Forms\Fields\Field_Base {
public function get_type(): string { return 'local-tel'; }
public function get_name(): string { return esc_html__( 'Local Tel', 'textdomain' ); }
public function render( $item, $item_index, $form ): void {
$form->add_render_attribute( 'input' . $item_index, [
'size' => '1', 'class' => 'elementor-field-textual',
'pattern' => '[0-9]{3}-[0-9]{3}-[0-9]{4}',
'title' => esc_html__( 'Format: 123-456-7890', 'textdomain' ),
]);
echo '<input ' . $form->get_render_attribute_string( 'input' . $item_index ) . '>';
}
public function validation( $field, $record, $ajax_handler ): void {
if ( empty( $field['value'] ) ) { return; }
if ( preg_match( '/^[0-9]{3}-[0-9]{3}-[0-9]{4}$/', $field['value'] ) !== 1 ) {
$ajax_handler->add_error( $field['id'],
esc_html__( 'Phone must be "123-456-7890" format.', 'textdomain' ) );
}
}
public function __construct() {
parent::__construct();
add_action( 'elementor/preview/init', [ $this, 'editor_preview_footer' ] );
}
public function editor_preview_footer(): void { add_action( 'wp_footer', [ $this, 'content_template_script' ] ); }
public function content_template_script(): void { ?>
<script>
jQuery( document ).ready( () => {
elementor.hooks.addFilter( 'elementor_pro/forms/content_template/field/<?php echo $this->get_type(); ?>',
function ( inputField, item, i ) {
return `<input id="form_field_${i}" class="elementor-field-textual elementor-field ${item.css_classes}" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}">`;
}, 10, 3 );
});
</script>
<?php }
}
Advanced Example: Credit Card Field with Controls and Validation
class Elementor_Credit_Card_Number_Field extends \ElementorPro\Modules\Forms\Fields\Field_Base {
public function get_type(): string { return 'credit-card-number'; }
public function get_name(): string { return esc_html__( 'Credit Card Number', 'textdomain' ); }
public function render( $item, $item_index, $form ): void {
$form->add_render_attribute( 'input' . $item_index, [
'class' => 'elementor-field-textual', 'type' => 'tel',
'inputmode' => 'numeric', 'maxlength' => '19',
'pattern' => '[0-9]{4}\s[0-9]{4}\s[0-9]{4}\s[0-9]{4}',
'placeholder' => $item['credit-card-placeholder'],
'autocomplete' => 'cc-number',
]);
echo '<input ' . $form->get_render_attribute_string( 'input' . $item_index ) . '>';
}
public function validation( $field, $record, $ajax_handler ): void {
if ( empty( $field['value'] ) ) { return; }
if ( preg_match( '/^[0-9]{4}\s[0-9]{4}\s[0-9]{4}\s[0-9]{4}$/', $field['value'] ) !== 1 ) {
$ajax_handler->add_error( $field['id'],
esc_html__( 'Card number must be "XXXX XXXX XXXX XXXX".', 'textdomain' ) );
}
}
public function update_controls( $widget ): void {
$elementor = \ElementorPro\Plugin::elementor();
$control_data = $elementor->controls_manager->get_control_from_stack( $widget->get_unique_name(), 'form_fields' );
if ( is_wp_error( $control_data ) ) { return; }
$field_controls = [
'credit-card-placeholder' => [
'name' => 'credit-card-placeholder',
'label' => esc_html__( 'Card Placeholder', 'textdomain' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => 'xxxx xxxx xxxx xxxx',
'dynamic' => [ 'active' => true ],
'condition' => [ 'field_type' => $this->get_type() ],
'tab' => 'content', 'inner_tab' => 'form_fields_content_tab', 'tabs_wrapper' => 'form_fields_tabs',
],
];
$control_data['fields'] = $this->inject_field_controls( $control_data['fields'], $field_controls );
$widget->update_control( 'form_fields', $control_data );
}
public function __construct() {
parent::__construct();
add_action( 'elementor/preview/init', [ $this, 'editor_preview_footer' ] );
}
public function editor_preview_footer(): void { add_action( 'wp_footer', [ $this, 'content_template_script' ] ); }
public function content_template_script(): void { ?>
<script>
jQuery( document ).ready( () => {
elementor.hooks.addFilter( 'elementor_pro/forms/content_template/field/<?php echo $this->get_type(); ?>',
function ( inputField, item, i ) {
return `<input type="tel" id="form_field_${i}" class="elementor-field-textual elementor-field ${item.css_classes}" inputmode="numeric" maxlength="19" placeholder="${item['credit-card-placeholder']}" autocomplete="cc-number">`;
}, 10, 3 );
});
</script>
<?php }
}
Removing Built-in Fields
add_filter( 'elementor_pro/forms/field_types', function ( $fields ) {
unset( $fields['upload'] ); // Remove file upload field
return $fields;
});
3. Form Validation
Global validation hook fires before form processing:
add_action( 'elementor_pro/forms/validation', function ( $record, $ajax_handler ) {
$fields = $record->get( 'fields' );
// Single field validation
if ( ! empty( $fields['my_field']['value'] ) && strlen( $fields['my_field']['value'] ) < 5 ) {
$ajax_handler->add_error( 'my_field', esc_html__( 'Min 5 characters.', 'textdomain' ) );
}
// Cross-field validation
if ( ! empty( $fields['password']['value'] ) && ! empty( $fields['confirm']['value'] ) ) {
if ( $fields['password']['value'] !== $fields['confirm']['value'] ) {
$ajax_handler->add_error( 'confirm', esc_html__( 'Passwords do not match.', 'textdomain' ) );
}
}
}, 10, 2 );
Any add_error() call halts submission and returns errors to the client.
4. Form Processing Hooks
| Hook | Params | When |
|---|---|---|
elementor_pro/forms/validation |
$record, $ajax_handler |
Before processing -- validate fields |
elementor_pro/forms/process |
$record, $ajax_handler |
During form processing |
elementor_pro/forms/new_record |
$record, $ajax_handler |
After successful submission |
elementor_pro/forms/mail_sent |
$settings, $record |
After email action sends |
Email Filters
add_filter( 'elementor_pro/forms/wp_mail_headers', function ( $headers ) {
return $headers . "Cc: copy@example.com\r\n";
});
add_filter( 'elementor_pro/forms/wp_mail_message', function ( $message ) {
return $message . "\n\n-- Sent via My Site";
});
Webhook Filter
add_filter( 'elementor_pro/forms/webhooks/response', function ( $response, $record ) {
if ( is_wp_error( $response ) ) {
error_log( 'Webhook failed: ' . $response->get_error_message() );
}
return $response;
}, 10, 2 );
5. Common Mistakes
| Mistake | Fix |
|---|---|
Missing condition on action controls section |
Set 'condition' => [ 'submit_actions' => $this->get_name() ] |
Hardcoding HTML attributes in render() |
Use $form->add_render_attribute() / get_render_attribute_string() |
Not checking empty( $field['value'] ) in validation |
Always return early if empty (required check is separate) |
Naming a method content_template() on field class |
Reserved for future use -- use content_template_script() workaround |
| Exporting sensitive control data | Implement on_export() with unset() for all sensitive keys |
| Not escaping labels and attributes | Use esc_html__() for labels, esc_attr() for attributes |
Missing is_wp_error() check in update_controls() |
Always guard get_control_from_stack() result |
Missing tab/inner_tab/tabs_wrapper on field controls |
Required for controls to appear in the correct repeater tab |
| Wrong registration hook | Actions: elementor_pro/forms/actions/register. Fields: elementor_pro/forms/fields/register |
Not calling parent::__construct() in field constructor |
Required when overriding __construct() for editor preview |
More from peixotorms/odinlayer-skills
elementor-development
Use when building Elementor addons, creating custom widgets, or managing Elementor components. Covers Widget_Base class (get_name, get_title, get_icon, register_controls, render, content_template), widget registration via elementor/widgets/register hook, addon structure and plugin header, wp_enqueue_script for widget assets, get_script_depends, get_style_depends, inline editing toolbars, custom widget categories, manager registration (register/unregister), selector tokens ({{WRAPPER}}), deprecation handling, and Elementor CLI commands.
65elementor-hooks
Use when hooking into Elementor lifecycle events, injecting controls, filtering widget output, or using the JS APIs. Covers elementor/init, elementor/element/before_section_end, elementor/element/after_section_end, elementor/widget/render_content filter, elementor/frontend/after_enqueue_styles, frontend JS hooks (elementorFrontend.hooks, frontend/element_ready), editor JS hooks (elementor.hooks), $e.commands API ($e.run, $e.commands.register), $e.routes, $e.hooks (registerUIBefore, registerUIAfter), control injection patterns, CSS file hooks, forms hooks (Pro), and query filters.
26elementor-themes
Use when building Elementor-compatible themes, registering theme locations, creating dynamic tags, or extending the Finder. Covers register_location, theme_builder locations, elementor_theme_do_location, Theme_Document and theme conditions, Tag_Base for dynamic tags (register_tag, get_value, render), Finder extension (Category_Base, register via elementor/finder/register), Context_Menu customization (elements/context-menu/groups filter), Hello Elementor theme (elementor-hello-theme, hello_elementor_* filters), and hosting page cache integration hooks.
25elementor-controls
Use when adding controls to Elementor widgets, creating custom controls, or referencing control type parameters. Covers add_control with types (TEXT, SELECT, SLIDER, COLOR, MEDIA, REPEATER, CHOOSE, NUMBER, SWITCHER, URL, ICONS), TYPOGRAPHY and BACKGROUND group controls, BORDER, BOX_SHADOW group controls, add_responsive_control, add_group_control, CSS selectors ({{WRAPPER}}, {{VALUE}}), condition and conditions for conditional display, dynamic content tags, POPOVER_TOGGLE, and global styles integration.
16flyonui
Use when building with FlyonUI — Tailwind CSS component library with CSS classes and optional JS plugins. Covers CSS component classes, JS plugin system (accordion, carousel, collapse, combobox, datatable, dropdown, select, tabs, tooltip, etc.), theming, installation, class reference, and plugin initialization via MCP tools.
9wp-javascript
Use when working with JavaScript in WordPress plugins or themes. Covers wp_enqueue_script, wp_localize_script, wp_add_inline_script, jQuery in WordPress (noConflict mode, $.ajax), AJAX handlers (wp_ajax_, admin-ajax.php, wp_create_nonce, check_ajax_referer), wp.ajax, wp.apiFetch (wp-api-fetch), wp-util and wp.template (Underscore templates), Heartbeat API, script dependencies, defer/async loading strategies (WordPress 6.3+), wp_set_script_translations, and frontend-backend communication patterns.
7