elementor-forms

Elementor Forms Extension Reference

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "elementor-forms" with this command: npx skills add peixotorms/odinlayer-skills/peixotorms-odinlayer-skills-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;
}

}

  1. 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 &#x3C;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 '&#x3C;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 { ?>
    &#x3C;script>
    jQuery( document ).ready( () => {
        elementor.hooks.addFilter( 'elementor_pro/forms/content_template/field/&#x3C;?php echo $this->get_type(); ?>',
            function ( inputField, item, i ) {
                return `&#x3C;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 );
    });
    &#x3C;/script>
&#x3C;?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 '&#x3C;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 { ?>
    &#x3C;script>
    jQuery( document ).ready( () => {
        elementor.hooks.addFilter( 'elementor_pro/forms/content_template/field/&#x3C;?php echo $this->get_type(); ?>',
            function ( inputField, item, i ) {
                return `&#x3C;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 );
    });
    &#x3C;/script>
&#x3C;?php }

}

Removing Built-in Fields

add_filter( 'elementor_pro/forms/field_types', function ( $fields ) { unset( $fields['upload'] ); // Remove file upload field return $fields; });

  1. 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'] ) &#x26;&#x26; strlen( $fields['my_field']['value'] ) &#x3C; 5 ) {
    $ajax_handler->add_error( 'my_field', esc_html__( 'Min 5 characters.', 'textdomain' ) );
}

// Cross-field validation
if ( ! empty( $fields['password']['value'] ) &#x26;&#x26; ! 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.

  1. 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 );

  1. 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

elementor-hooks

No summary provided by upstream source.

Repository SourceNeeds Review
General

elementor-themes

No summary provided by upstream source.

Repository SourceNeeds Review
General

elementor-controls

No summary provided by upstream source.

Repository SourceNeeds Review