docs/modules/nested-tabs/index.md
true\Elementor\Modules\NestedElements\ModuleThe first files to be loaded in the editor, tells the editor to load the module.Load the widget and register it to editor elementsManager., wait for NestedElements module to be loaded first!.Register the widget in the editor.React component that renders the add section area, rendered via empty.jsReact component, that will be rendered when the widget is empty, prints select-preset or add-section-area.React component that Render the preset for children container.The widget view, actually used to manipulate clicks on the widget (view), register the model, view. emptyView for the widget.Frontend handler(s), custom handlers for interacting with the widget.All CSS will be used in frontend, how visually the widget will looksThe module, enable the experiment to work on/off, register editor scriptsBackend, The widget that will be nested, insert new widget into the system.onInit( ...args ) {
// TODO: Find better solution, Manually adding 'e-collapse' for each container.
if ( elementorFrontend.isEditMode() ) {
const $widget = this.$element,
$removed = this.findElement( '.e-collapse' ).remove();
let index = 1;
this.findElement( '.e-container' ).each( function() {
const $current = jQuery( this ),
$desktopTabTitle = $widget.find( `.e-n-tabs-heading > *:nth-child(${ index })` ),
mobileTitleHTML = `<div class="e-n-tab-title e-collapse" data-tab="${ index }" role="tab">${ $desktopTabTitle.html() }</div>`;
$current.before( mobileTitleHTML );
++index;
} );
// On refresh since indexes are rearranged, do not call `activateDefaultTab` let editor control handle it.
if ( $removed.length ) {
return elementorModules.ViewModule.prototype.onInit.apply( this, args );
}
}
super.onInit( ...args );
}
Since NestedTabs should look like old Tabs widget, there is manual handling of the situation for mobile devices.
--n-tabs-title-color: var(--e-global-color-primary);
--n-tabs-title-active-color: var(--e-global-color-accent);
--n-tabs-title-typography-font-family: var(--e-global-typography-primary-font-family);
--n-tabs-title-typography-font-size: initial;
--n-tabs-title-typography-font-weight: var(--e-global-typography-primary-font-weight);
The module NestedTabs will be used as a live example of the guide.
What are the difference between NestedTabs and Nested Elements modules?
Nested Elements is a base module for all nested elements, it includes the infrastructure for creating nested elements.
NestedTabs is a module that allows you to create nested tabs.
NestedTabs module includes:
modules/nested-tabs/assets/js/editor/views/view.js - The actual view of the widget.modules/nested-tabs/assets/js/editor/views/empty.js - The view that will be rendered when the widget is empty.modules/nested-tabs/assets/js/editor/views/select-preset.js - will be rendered when select preset selected.modules/nested-tabs/assets/js/editor/views/add-section-area.js - The default that will be rendered on the Empty View.modules/nested-tabs/assets/js/frontend/handlers/nested-tabs.jsmodules/nested-tabs/assets/scss/frontend.scssmodules/nested-tabs/widgets/nested-tabs.phpThe views are extra, and they are not required.
The flow: 3 main flows, editor, frontend and backend.
modules/nested-tabs/module.php -> Register the widget in the backend -> widgets/nested-tabs.phpmodules/nested-tabs/widgets/nested-tabs.php -> Register the widget in the backend according to the file code.modules/nested-tabs/module.php -> Tells the editor to enqueue the scripts and styles.assets/js/editor/index.js -> Load editor module -> assets/js/editor/module.js.assets/js/editor/module.js -> Load widget and register it into elementor.elementsManager.registerElementType.assets/js/editor/module.js -> Register the widget assets/js/editor/widgets/nested-tabs.jsassets/js/editor/widgets/nested-tabs.js -> Register the widget with custom views: assets/js/editor/views/view.js, assets/js/editor/views/empty.js, assets/js/editor/views/select-preset.js, assets/js/editor/views/add-section-area.jsassets/js/frontend/handlers/nested-tabs.jsassets/scss/frontend.scssThe following guide will help you to understand how the module works, step by step.
Start by registering the module:
- Module.php - How to register a module.\Elementor\Core\Base\ModuleHow to register a module?
Since NestedTabs (nested tabs) depends on NestedElements module,
get_experimental_data method used to notify the module dependency upon NestedElementsModule. Please see 'dependencies' key.use Elementor\Modules\NestedElements\Module as NestedElementsModule;
public static function get_experimental_data() {
return [
'name' => 'nested-tabs',
'title' => esc_html__( 'Nested Tab', 'elementor' ),
'description' => esc_html__( 'Nested Tabs', 'elementor' ),
'release_status' => Experiments_Manager::RELEASE_STATUS_ALPHA,
'default' => Experiments_Manager::STATE_INACTIVE,
'dependencies' => [ NestedElementsModule::class ],
];
}
Loading the editor scripts.
public function __construct() {
add_action( 'elementor/editor/before_enqueue_scripts', function () {
// The script you load for the editor goes here.
wp_enqueue_script( 'nested-tabs', $this->get_js_assets_url( 'nested-tabs' ), [
'elementor-common',
], ELEMENTOR_VERSION, true );
} );
}
Model to allow child to be another widget.To register the widget, extend the get_widgets method in the Module and return the widget name, eg:
protected function get_widgets() {
return [ 'NestedTabs' ]; // Located at widgets/nested-tabs.php (the file will be loaded automatically).
}
The complete module
<?php
namespace Elementor\Modules\NestedTabs;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\NestedElements\Module as NestedElementsModule;
class Module extends \Elementor\Core\Base\Module {
public static function get_experimental_data() {
return [
'name' => 'nested-tabs',
'title' => esc_html__( 'Nested Tab', 'elementor' ),
'description' => esc_html__( 'Nested Tabs', 'elementor' ),
'release_status' => Experiments_Manager::RELEASE_STATUS_ALPHA,
'default' => Experiments_Manager::STATE_INACTIVE,
'dependencies' => [ NestedElementsModule::class ],
];
}
public function get_name() {
return 'nested-tabs';
}
protected function get_widgets() {
return [ 'NestedTabs' ];
}
public function __construct() {
parent::__construct();
add_action( 'elementor/editor/before_enqueue_scripts', function () {
wp_enqueue_script( $this->get_name(), $this->get_js_assets_url( $this->get_name() ), [
'nested-elements',
], ELEMENTOR_VERSION, true );
} );
}
}
assets/js/frontend/handlers/nested-tabs.js - Custom frontend handler.export default class YourCustomHandler extends elementorModules.frontend.handlers.BaseNestedTabs {
// Create your custom handler.
}
widgets/nested-tabs.php - How to register a widget.Link to the actual file - nested-tabs.php
Description - The widgets/nested-tabs.php is the main backend configuration file for widget with nested capabilities.
Extends - \Elementor\Modules\NestedElements\Base\Widget_Nested_Base
Requirements:
Is it requirement?
The class should extend Widget_Nested_Base class, there are few important methods to note:
get_default_children_elements - The inner children/elements that will be created when the widget created.get_default_repeater_title_setting_key - The setting key that will be used by $e.run( 'document/elements/settings' ) in the frontend for the children title.get_default_children_title - The tab title including %d for the index.get_default_children_placeholder_selector - Custom selector to place the children, in NestedTabs is used inside the tabs content. Return null if the element should be added in the end of the element.<?php
namespace Elementor\Modules\NestedTabs\Widgets;
use Elementor\Modules\NestedElements\Base\Widget_Nested_Base;
use Elementor\Plugin;
class NestedTabs extends Widget_Nested_Base {
protected function get_default_children_elements() {
return [
[
'elType' => 'container',
'settings' => [
'_title' => __( 'Tab #1', 'elementor' ),
],
],
[
'elType' => 'container',
'settings' => [
'_title' => __( 'Tab #2', 'elementor' ),
],
],
];
}
protected function get_default_repeater_title_setting_key() {
return 'tab_title';
}
protected function get_defaults_children_title() {
return esc_html__( 'Tab #%d', 'elementor' );
}
protected function get_default_children_placeholder_selector() {
return '.e-n-tabs-content';
}
protected function get_html_wrapper_class() {
return 'elementor-widget-n-tabs';
}
}
assets/js/editor/index.js - Load the module.Link to the actual file - index.js
This is first loaded file in the editor.
Nested Elements module to be loaded (requirement since the NestedTabs depends on the Nested Elements module).// On editor init components.
elementorCommon.elements.$window.on( 'elementor/init-components', async () => {
// The module should be loaded only when `nestedElements` is available.
await elementor.modules.nestedElements;
// Create the NestedTabs module.
new ( await import( '../editor/module' ) ).default();
} );
assets/js/editor/module.js - The module register the widget.Link to the actual file - module.js
What the modules do? Register the widget only.
What are the advantages of registering the element in the elmeentor.elementsManager?
View, EmptyView, or Model import NestedTabs from './nested-tabs'; // Import the widget.
export default class Module {
constructor() {
// Register new NestedTabs widget.
elementor.elementsManager.registerElementType( new NestedTabs() );
}
}
assets/js/editor/nested-tabs.js - Register the widget.export class YourWidgetName extends elementor.modules.elements.Widget {
getModel() {
// Includes the nested model with support of nested elements.
return $e.components.get( 'nested-elements/nested-repeater' ).exports.NestedModelBase;
}
}
At this point, the widget is ready to be used and those are the minimum requirements, the next examples are extras.
import View from './views/view'; // Custom view for handling the clicks.
import EmptyView from './views/empty'; // Custom empty view for handling empty in the widget.
export class NestedTabs extends elementor.modules.elements.types.Base {
getType() {
return 'nested-tabs'; // Widget type from the backend registration.
}
getView() {
// Custom-View for the element should extend `$e.components.get( 'nested-elements/nested-repeater' ).exports.NestedViewBase`.
return View;
}
getEmptyView() {
// Custom empty-view for the widget should be `React` component.
return EmptyView;
}
getModel() {
// Should extend `$e.components.get( 'nested-elements/nested-repeater' ).exports.NestedRepeaterModel`.
// In this scenario, custom model is not required so default is returned.
return $e.components.get( 'nested-elements/nested-repeater' ).exports.NestedModelBase;
}
}
export default NestedTabs;
assets/js/editor/views/view.js - Custom view for the widget.$e.components.get( 'nested-elements/nested-repeater' ).exports.NestedViewBase, let use NestedTabs view as example:
/**
* @extends {NestedViewBase}
*/
export class View extends $e.components.get( 'nested-elements/nested-repeater' ).exports.NestedViewBase {
events() {
const events = super.events();
events.click = ( e ) => {
const closest = e.target.closest( '.elementor-element' );
let model = this.options.model,
view = this;
// For clicks on container.
if ( 'container' === closest?.dataset.element_type ) { // eslint-disable-line camelcase
// In case the container empty, click should be handled by the EmptyView.
const container = elementor.getContainer( closest.dataset.id );
if ( container.view.isEmpty() ) {
return true;
}
// If not empty, open it.
model = container.model;
view = container.view;
}
e.stopPropagation();
$e.run( 'panel/editor/open', {
model,
view,
} );
};
return events;
}
}
export default View;
$e.components.get( 'nested-elements/nested-repeater' ).exports.NestedViewBase.assets/js/editor/views/add-section-area.js - Custom AddSectionArea for nested tabs.import { useEffect, useRef } from 'react';
export default function AddSectionArea( props ) {
const addAreaElementRef = useRef(),
containerHelper = elementor.helpers.container,
args = {
importOptions: {
target: props.container,
},
};
// Make droppable area.
useEffect( () => {
if ( props.container.view.isDisconnected() ) {
return;
}
const $addAreaElementRef = jQuery( addAreaElementRef.current ),
defaultDroppableOptions = props.container.view.getDroppableOptions();
// Make some adjustments to behave like 'AddSectionArea', use default droppable options from container element.
defaultDroppableOptions.placeholder = false;
defaultDroppableOptions.items = '> .elementor-add-section-inner';
defaultDroppableOptions.hasDraggingOnChildClass = 'elementor-dragging-on-child';
// Make element drop-able.
$addAreaElementRef.html5Droppable( defaultDroppableOptions );
// Cleanup.
return () => {
$addAreaElementRef.html5Droppable( 'destroy' );
};
}, [] );
return (
<div
className="elementor-add-section"
onClick={() => containerHelper.openEditMode( props.container )}
ref={addAreaElementRef}
>
<div className="elementor-add-section-inner">
<div className="e-view elementor-add-new-section">
<button
type="button"
className="elementor-add-section-area-button elementor-add-section-button"
aria-label={__( 'Add new container', 'elementor' )}
onClick={() => props.setIsRenderPresets( true )}
>
<i className="eicon-plus" aria-hidden="true" />
</button>
<div className="elementor-add-section-drag-title">
{__( 'Drag widgets here.', 'elementor' )}
</div>
</div>
</div>
</div>
);
}
AddSectionArea.propTypes = {
container: PropTypes.object.isRequired,
setIsRenderPresets: PropTypes.func.isRequired,
};
assets/js/editor/views/empty.js - Custom empty-view for the widget.React component, it will be the empty view for the widget children, in this case, the tabs.
import { useState } from 'react';
import AddSectionArea from './add-section-area';
import SelectPreset from './select-preset';
export default function Empty( props ) {
const [ isRenderPresets, setIsRenderPresets ] = useState( false );
props = {
...props,
setIsRenderPresets,
};
return isRenderPresets ? <SelectPreset {...props} /> : <AddSectionArea {...props} />;
}
Empty.propTypes = {
container: PropTypes.object.isRequired,
};
SelectPreset or AddSectionArea.assets/js/editor/views/select-preset.js - Custom react component to print the presets available for children containers.export default function SelectPreset( props ) {
const containerHelper = elementor.helpers.container,
onPresetSelected = ( preset, container ) => {
const options = {
createWrapper: false,
};
// Create new one by selected preset.
containerHelper.createContainerFromPreset( preset, container, options );
};
return (
<>
<button
type="button"
className="elementor-add-section-close"
aria-label={ __( 'Close', 'elementor' ) }
onClick={() => props.setIsRenderPresets( false )}
>
<i className="eicon-close" aria-hidden="true"/>
</button>
<div className="e-view e-con-select-preset">
<div className="e-con-select-preset__title">{__( 'Select your Structure', 'elementor' )}</div>
<div className="e-con-select-preset__list">
{
elementor.presetsFactory.getContainerPresets().map( ( preset ) => (
<button
type="button"
className="e-con-preset"
data-preset={preset}
key={preset}
onClick={() => onPresetSelected( preset, props.container )}
dangerouslySetInnerHTML={{ __html: elementor.presetsFactory.generateContainerPreset( preset ) }}
/>
) )
}
</div>
</div>
</>
);
}
SelectPreset.propTypes = {
container: PropTypes.object.isRequired,
setIsRenderPresets: PropTypes.func.isRequired,
};
tab-title for example, and the whole nested-tabs children hierarchy affected, it gets re-render which create huge performance impact <element
'data-binding-type': 'repeater-item', // Type of binding (to know how to behave).
'data-binding-repeater-name': 'tabs', // Repeater setting key that effect the binding.
'data-binding-setting': 'tab_title', // The key in the repeater that effect the binding.
'data-binding-index': tabCount, // Index is required for repeater items.
>
</element>
<element
'data-binding-type': 'content', // Type of binding (to know how to behave).
'data-binding-setting': 'testimonial_content', // Setting change to capture, the value will replace the data-binding.
</element>
Use it in the
_content_template()method.