How to build custom Elementor custom widgets extensions utilizing modern WordPress Options API schemas
Leveraging the WordPress Options API for Advanced Elementor Widget Configuration
For enterprise-level WordPress deployments and complex plugin architectures, extending Elementor requires more than just basic widget functionality. A robust configuration system is paramount for maintainability, scalability, and user experience. This post details how to build custom Elementor widget extensions that harness the power and flexibility of the WordPress Options API, specifically focusing on modern schema definitions for structured data management.
Structuring Widget Settings with Options API Schemas
The traditional approach to storing widget settings often involves direct database entries or simple `update_option()` calls. However, for complex widgets with numerous fields, nested structures, or conditional logic, this becomes unwieldy. The WordPress Options API, when combined with a well-defined schema, offers a more organized and programmatic way to manage these settings. We’ll define our schema using PHP arrays, which can then be translated into UI elements within Elementor’s controls.
Defining a Widget Schema
Consider a hypothetical “Advanced Call to Action” widget that requires settings for text, button URL, button text, background color, and an optional image. We can represent this configuration using a structured PHP array that mirrors the expected data types and nesting.
This schema will serve as the blueprint for both the Elementor controls and the data validation/sanitization process.
<?php
/**
* Schema for the Advanced Call to Action widget settings.
*
* @return array Widget settings schema.
*/
function get_advanced_cta_widget_schema() {
return [
'settings' => [
'cta_title' => [
'type' => 'text',
'label' => esc_html__( 'Call to Action Title', 'your-text-domain' ),
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
'cta_description' => [
'type' => 'textarea',
'label' => esc_html__( 'Call to Action Description', 'your-text-domain' ),
'default' => '',
'sanitize_callback' => 'wp_kses_post',
],
'cta_button_text' => [
'type' => 'text',
'label' => esc_html__( 'Button Text', 'your-text-domain' ),
'default' => esc_html__( 'Learn More', 'your-text-domain' ),
'sanitize_callback' => 'sanitize_text_field',
],
'cta_button_url' => [
'type' => 'url',
'label' => esc_html__( 'Button URL', 'your-text-domain' ),
'default' => '#',
'sanitize_callback' => 'esc_url_raw',
],
'cta_background_color' => [
'type' => 'color',
'label' => esc_html__( 'Background Color', 'your-text-domain' ),
'default' => '#f0f0f0',
'sanitize_callback' => 'sanitize_hex_color',
],
'cta_image' => [
'type' => 'media',
'label' => esc_html__( 'Optional Image', 'your-text-domain' ),
'default' => [
'id' => 0,
'url' => '',
],
'sanitize_callback' => [ 'Your_Widget_Class', 'sanitize_media_field' ], // Custom sanitizer
],
],
'options_group' => 'advanced_cta_widget_options', // Unique identifier for the options group
'option_name' => 'advanced_cta_widget_settings', // The actual option name in wp_options
];
}
Integrating Schema with Elementor Controls
Elementor’s widget development framework allows us to dynamically generate controls based on a schema. This is typically done within the `_register_controls()` method of your custom widget class. By iterating through the defined schema, we can instantiate the appropriate Elementor control types.
<?php
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
use Elementor\Core\Schemes\Typography; // Example for typography scheme
class Your_Advanced_CTA_Widget extends Widget_Base {
// ... other widget methods ...
protected function _register_controls() {
$schema = get_advanced_cta_widget_schema();
$option_name = $schema['option_name'];
// Group controls for better organization
$this->start_controls_section(
'section_cta_content',
[
'label' => esc_html__( 'Call to Action Content', 'your-text-domain' ),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
foreach ( $schema['settings'] as $key => $field_config ) {
$control_args = [
'label' => $field_config['label'],
'type' => $this->get_elementor_control_type( $field_config['type'] ), // Helper to map schema types to Elementor types
'default' => $field_config['default'],
'placeholder' => isset( $field_config['placeholder'] ) ? $field_config['placeholder'] : '',
'condition' => isset( $field_config['condition'] ) ? $field_config['condition'] : [],
];
// Handle specific control types that require different arguments
if ( 'media' === $field_config['type'] ) {
$control_args['show_label'] = false; // Often media controls don't need a separate label
$control_args['type'] = Controls_Manager::MEDIA;
} elseif ( 'color' === $field_config['type'] ) {
$control_args['scheme'] = [
'type' => Typography::COLOR,
'value' => isset( $field_config['value'] ) ? $field_config['value'] : '',
];
}
// Add the control
$this->add_control( $key, $control_args );
}
$this->end_controls_section();
// Add a section for styling if needed, potentially also schema-driven
$this->start_controls_section(
'section_cta_style',
[
'label' => esc_html__( 'Styling', 'your-text-domain' ),
'tab' => Controls_Manager::TAB_STYLE,
]
);
$this->add_control(
'cta_text_color',
[
'label' => esc_html__( 'Text Color', 'your-text-domain' ),
'type' => Controls_Manager::COLOR,
'scheme' => [
'type' => Typography::COLOR,
'value' => Typography::COLOR,
],
'selectors' => [
'{{WRAPPER}} .advanced-cta-title' => 'color: {{VALUE}}',
'{{WRAPPER}} .advanced-cta-description' => 'color: {{VALUE}}',
],
]
);
$this->add_control(
'cta_button_color',
[
'label' => esc_html__( 'Button Background Color', 'your-text-domain' ),
'type' => Controls_Manager::COLOR,
'selectors' => [
'{{WRAPPER}} .advanced-cta-button' => 'background-color: {{VALUE}};',
],
]
);
$this->end_controls_section();
}
/**
* Helper to map schema types to Elementor control types.
*
* @param string $schema_type The schema type.
* @return string Elementor control type.
*/
protected function get_elementor_control_type( $schema_type ) {
$mapping = [
'text' => Controls_Manager::TEXT,
'textarea' => Controls_Manager::TEXTAREA,
'url' => Controls_Manager::URL,
'color' => Controls_Manager::COLOR,
'media' => Controls_Manager::MEDIA,
// Add more mappings as needed
];
return $mapping[ $schema_type ] ?? Controls_Manager::TEXT; // Default to TEXT if not found
}
// ... rest of the widget class ...
}
Storing and Retrieving Settings via Options API
The key advantage of using the Options API is centralized storage. Instead of each widget instance storing its settings individually, we can store all settings for a particular widget type in a single option. This is particularly beneficial for widgets that are used repeatedly across a site, or for managing global widget settings.
When the widget is saved, Elementor’s internal mechanisms will typically save the control values. We need to hook into this process to ensure our settings are stored correctly using `update_option()`. Conversely, when rendering the widget, we retrieve these settings using `get_option()`.
<?php
// In your widget class, within the render() method or a helper function
protected function render() {
$schema = get_advanced_cta_widget_schema();
$option_name = $schema['option_name'];
$widget_settings = get_option( $option_name, [] ); // Retrieve settings from the option
// Ensure we have an array, even if the option is not set yet
if ( ! is_array( $widget_settings ) ) {
$widget_settings = [];
}
// Merge default values from schema if not present in stored settings
foreach ( $schema['settings'] as $key => $field_config ) {
if ( ! isset( $widget_settings[ $key ] ) && isset( $field_config['default'] ) ) {
$widget_settings[ $key ] = $field_config['default'];
}
}
// Extract individual settings for easier access
$cta_title = isset( $widget_settings['cta_title'] ) ? $widget_settings['cta_title'] : '';
$cta_description = isset( $widget_settings['cta_description'] ) ? $widget_settings['cta_description'] : '';
$cta_button_text = isset( $widget_settings['cta_button_text'] ) ? $widget_settings['cta_button_text'] : '';
$cta_button_url = isset( $widget_settings['cta_button_url'] ) ? $widget_settings['cta_button_url'] : '#';
$cta_background_color = isset( $widget_settings['cta_background_color'] ) ? $widget_settings['cta_background_color'] : '#f0f0f0';
$cta_image = isset( $widget_settings['cta_image'] ) ? $widget_settings['cta_image'] : [];
// Sanitize and escape output for security
$cta_title = esc_html( $cta_title );
$cta_description = wp_kses_post( $cta_description );
$cta_button_text = esc_html( $cta_button_text );
$cta_button_url = esc_url( $cta_button_url );
$cta_background_color = sanitize_hex_color( $cta_background_color ); // Already sanitized on save, but good practice
// Render the HTML
?>
<div class="advanced-cta-wrapper" style="background-color: <?php echo esc_attr( $cta_background_color ); ?> padding: 20px;">
<h3 class="advanced-cta-title"><?php echo $cta_title; ?></h3>
<div class="advanced-cta-description"><?php echo $cta_description; ?></div>
<a href="<?php echo esc_url( $cta_button_url ); ?>" class="advanced-cta-button"><?php echo esc_html( $cta_button_text ); ?></a>
<?php
if ( ! empty( $cta_image['url'] ) ) {
?>
<img src="<?php echo esc_url( $cta_image['url'] ); ?>" alt="CTA Image" style="max-width: 100px; margin-top: 15px;" />
<?php
}
?>
</div>
<?php
}
// Hook into Elementor's save process to update the option
add_action( 'elementor/element/parse_element', function( $element ) {
if ( $element->get_type() === 'widget' && $element->get_widget_type() === 'your-advanced-cta-widget-slug' ) { // Replace with your widget slug
$schema = get_advanced_cta_widget_schema();
$option_name = $schema['option_name'];
$new_settings = [];
// Collect settings from the element's controls
foreach ( $schema['settings'] as $key => $field_config ) {
if ( $element->has_control( $key ) ) {
$value = $element->get_settings( $key );
// Apply specific sanitization based on schema
if ( isset( $field_config['sanitize_callback'] ) ) {
if ( is_array( $field_config['sanitize_callback'] ) ) {
// For custom static methods like 'Your_Widget_Class', 'sanitize_media_field'
$new_settings[ $key ] = call_user_func( $field_config['sanitize_callback'], $value );
} else {
// For built-in WordPress sanitization functions
$new_settings[ $key ] = $field_config['sanitize_callback']( $value );
}
} else {
// Default sanitization if none specified
$new_settings[ $key ] = sanitize_text_field( $value );
}
}
}
// Update the option in the database
update_option( $option_name, $new_settings );
}
} );
// Custom sanitization example for media fields
class Your_Widget_Class {
public static function sanitize_media_field( $media_data ) {
if ( ! is_array( $media_data ) || empty( $media_data['id'] ) ) {
return [ 'id' => 0, 'url' => '' ];
}
$image_id = absint( $media_data['id'] );
$image_url = esc_url_raw( $media_data['url'] );
return [ 'id' => $image_id, 'url' => $image_url ];
}
}
Handling Global Widget Settings and Overrides
A significant advantage of the Options API approach is the ability to manage global settings for a widget type. For instance, you might want a default background color or button style that applies to all instances of the “Advanced Call to Action” widget unless explicitly overridden. This is achieved by merging the global settings (stored in the option) with instance-specific settings (which Elementor handles by default).
In the `render()` method, we first retrieve the global settings from `get_option()`. Then, Elementor’s `get_settings()` method retrieves instance-specific settings. A strategic merge ensures that global defaults are used where instance settings are not provided.
<?php
// Modified render() method to incorporate global/instance merging
protected function render() {
$schema = get_advanced_cta_widget_schema();
$option_name = $schema['option_name'];
$global_settings = get_option( $option_name, [] ); // Global settings
// Ensure global settings are an array
if ( ! is_array( $global_settings ) ) {
$global_settings = [];
}
// Merge global defaults with instance settings
$merged_settings = [];
foreach ( $schema['settings'] as $key => $field_config ) {
// Prioritize instance settings, then global settings, then schema defaults
$instance_value = $this->get_settings( $key );
$global_value = isset( $global_settings[ $key ] ) ? $global_settings[ $key ] : null;
$default_value = $field_config['default'] ?? null;
if ( ! empty( $instance_value ) ) {
$merged_settings[ $key ] = $instance_value;
} elseif ( $global_value !== null ) {
$merged_settings[ $key ] = $global_value;
} else {
$merged_settings[ $key ] = $default_value;
}
}
// Now use $merged_settings for rendering
$cta_title = isset( $merged_settings['cta_title'] ) ? $merged_settings['cta_title'] : '';
// ... extract and render other fields using $merged_settings ...
// Example rendering with merged settings
?>
<div class="advanced-cta-wrapper" style="background-color: <?php echo esc_attr( $merged_settings['cta_background_color'] ?? '#f0f0f0' ); ?> padding: 20px;">
<h3 class="advanced-cta-title"><?php echo esc_html( $cta_title ); ?></h3>
<!-- ... rest of the rendering ... -->
</div>
<?php
}
Admin Interface for Global Settings
To manage these global settings effectively, a dedicated admin page is recommended. This page would utilize the WordPress Settings API to create forms that directly update the option defined in our schema. This provides a clean, centralized UI for administrators to configure default widget behaviors without needing to edit individual widget instances.
The process involves:
- Registering a menu page under the WordPress admin menu.
- Defining settings using `register_setting()`, specifying the option name and sanitization callbacks.
- Creating form fields using `add_settings_field()`, linking them to the registered setting.
- Rendering the form using `settings_fields()` and `do_settings_sections()`.
<?php
// In your plugin's main file or an admin-specific file
function advanced_cta_register_admin_page() {
add_options_page(
__( 'Advanced CTA Widget Settings', 'your-text-domain' ),
__( 'Advanced CTA Settings', 'your-text-domain' ),
'manage_options',
'advanced-cta-settings',
'advanced_cta_render_settings_page'
);
}
add_action( 'admin_menu', 'advanced_cta_register_admin_page' );
function advanced_cta_register_settings() {
$schema = get_advanced_cta_widget_schema();
$option_name = $schema['option_name'];
$group_name = $schema['options_group'];
register_setting( $group_name, $option_name, [
'type' => 'array',
'sanitize_callback' => 'advanced_cta_sanitize_global_settings', // Custom sanitization for the whole array
] );
// Add settings fields based on the schema
foreach ( $schema['settings'] as $key => $field_config ) {
add_settings_field(
$key, // ID
$field_config['label'], // Title
'advanced_cta_render_field', // Callback function to render the field
'advanced-cta-settings', // Page slug
$group_name, // Section slug (can be same as group name for simplicity)
[ // Arguments passed to the callback
'field_key' => $key,
'field_config' => $field_config,
'option_name' => $option_name,
]
);
}
}
add_action( 'admin_init', 'advanced_cta_register_settings' );
function advanced_cta_render_settings_page() {
$schema = get_advanced_cta_widget_schema();
$group_name = $schema['options_group'];
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( $group_name ); // Output nonce, action, and option_page fields
do_settings_sections( 'advanced-cta-settings' ); // Render all fields for this page
submit_button();
?>
</form>
</div>
<?php
}
function advanced_cta_render_field( $args ) {
$field_key = $args['field_key'];
$field_config = $args['field_config'];
$option_name = $args['option_name'];
$global_settings = get_option( $option_name, [] );
$value = isset( $global_settings[ $field_key ] ) ? $global_settings[ $field_key ] : ( $field_config['default'] ?? '' );
// Basic rendering logic - needs expansion for different field types
switch ( $field_config['type'] ) {
case 'text':
case 'url':
printf(
'<input type="%1$s" id="%2$s" name="%3$s[%2$s]" value="%4$s" class="regular-text" />',
esc_attr( $field_config['type'] ),
esc_attr( $field_key ),
esc_attr( $option_name ),
esc_attr( $value )
);
break;
case 'textarea':
printf(
'<textarea id="%1$s" name="%2$s[%1$s]" rows="5" class="large-text"%3$s>%4$s</textarea>',
esc_attr( $field_key ),
esc_attr( $option_name ),
'', // Placeholder for attributes
esc_textarea( $value )
);
break;
case 'color':
// Requires wp_enqueue_script for wp-color-picker
printf(
'<input type="text" id="%1$s" name="%2$s[%1$s]" value="%3$s" class="regular-text wp-color-picker" />',
esc_attr( $field_key ),
esc_attr( $option_name ),
esc_attr( $value )
);
break;
// Add cases for 'media', 'select', 'checkbox', etc.
default:
printf(
'<input type="text" id="%1$s" name="%2$s[%1$s]" value="%3$s" class="regular-text" />',
esc_attr( $field_key ),
esc_attr( $option_name ),
esc_attr( $value )
);
break;
}
if ( isset( $field_config['description'] ) ) {
printf( '<p class="description">%1$s</p>', esc_html( $field_config['description'] ) );
}
}
// Custom sanitization for the entire options array
function advanced_cta_sanitize_global_settings( $input ) {
$schema = get_advanced_cta_widget_schema();
$sanitized_output = [];
foreach ( $schema['settings'] as $key => $field_config ) {
if ( isset( $input[ $key ] ) ) {
$value = $input[ $key ];
if ( isset( $field_config['sanitize_callback'] ) ) {
if ( is_array( $field_config['sanitize_callback'] ) ) {
$sanitized_output[ $key ] = call_user_func( $field_config['sanitize_callback'], $value );
} else {
$sanitized_output[ $key ] = $field_config['sanitize_callback']( $value );
}
} else {
// Default sanitization
$sanitized_output[ $key ] = sanitize_text_field( $value );
}
}
}
return $sanitized_output;
}
// Enqueue color picker script for the admin page
function advanced_cta_enqueue_admin_scripts( $hook_suffix ) {
if ( 'settings_page_advanced-cta-settings' === $hook_suffix ) {
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
// Add a small JS snippet to initialize the color picker
?>
<script type="text/javascript">
jQuery(document).ready(function($){
$('.wp-color-picker').wpColorPicker();
});
</script>
<?php
}
}
add_action( 'admin_enqueue_scripts', 'advanced_cta_enqueue_admin_scripts' );
Conclusion and Best Practices
By adopting the WordPress Options API with structured schemas for Elementor widget extensions, you gain a powerful, maintainable, and scalable solution for managing complex configurations. This approach is ideal for enterprise environments where consistency, global defaults, and centralized control are critical.
Key takeaways:
- Define a clear, PHP-based schema for widget settings.
- Dynamically generate Elementor controls from this schema.
- Utilize `get_option()` and `update_option()` for centralized global settings.
- Implement a merge strategy in the `render()` method to combine global and instance-specific settings.
- Create a dedicated admin page using the Settings API for managing global configurations.
- Always prioritize security through proper sanitization and escaping.
This methodology elevates custom Elementor widget development from simple UI elements to robust, configurable components that integrate seamlessly into larger WordPress ecosystems.