elementor-dynamic-tag-fields
Build the body of an Elementor Dynamic Tag — choose Tag
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/elementor/elementor-dynamic-tag-fields
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Elementor: Dynamic Tag types, fields & fallback
The body of a dynamic tag has three decisions: which base class (Tag vs Data_Tag), which categories it produces (so the right controls accept it), and which settings fields the editor shows. Plus the easily-missed fallback asymmetry between the two base classes. This skill assumes the tag is already being registered — see elementor-dynamic-tag-register for that.
Decision 1 — Tag vs Data_Tag
Tag |
Data_Tag |
|
|---|---|---|
| Extend | \Elementor\Core\DynamicTags\Tag |
\Elementor\Core\DynamicTags\Data_Tag |
| You implement | render() — echo the output |
get_value( array $options = [] ) — return the value |
get_content_type() |
'ui' (final) |
'plain' (final) |
| Use for | rendered text/HTML fragments (price, reading time, a badge) | structured values consumed by a control — an image array [ 'id' => …, 'url' => … ], a URL string, a color |
Verified: Tag::get_content() does ob_start(); $this->render(); $value = ob_get_clean(); (tag.php:30-37) and get_content_type() returns 'ui' (tag.php:64). Data_Tag declares abstract protected function get_value() (data-tag.php:25), returns it directly from get_content() (data-tag.php:43-45), and get_content_type() returns 'plain' (data-tag.php:31).
Rule of thumb: if the value feeds a MEDIA / IMAGE / URL / COLOR control (the control needs a structured value, not printed markup), use Data_Tag. If it feeds a TEXT context (it's printed inline), use Tag.
// Tag — echoes
class Reading_Time extends Tag {
public function render(): void {
echo esc_html( $this->compute() . ' min' ); // print, don't return
}
}
// Data_Tag — returns
class Featured_Image_Fallback extends Data_Tag {
protected function get_value( array $options = [] ) {
$id = get_post_thumbnail_id();
if ( $id ) {
return [ 'id' => $id, 'url' => wp_get_attachment_image_src( $id, 'full' )[0] ];
}
return $this->get_settings( 'fallback' ); // see "Fallback" below
}
}
Decision 2 — categories (what value the tag produces)
get_categories() returns one or more category constants from Elementor\Modules\DynamicTags\Module. Categories declare the kind of value the tag emits; a control accepts a tag only when their categories overlap, so the editor shows your tag only under compatible controls.
Verified constants (modules/dynamic-tags/module.php:31-76):
| Constant | Value | Typical use |
|---|---|---|
TEXT_CATEGORY |
'text' |
printed strings (most Tags) |
URL_CATEGORY |
'url' |
link fields |
IMAGE_CATEGORY |
'image' |
image controls |
MEDIA_CATEGORY |
'media' |
media (image/video) controls |
POST_META_CATEGORY |
'post_meta' |
meta-field contexts |
GALLERY_CATEGORY |
'gallery' |
gallery controls |
NUMBER_CATEGORY |
'number' |
number controls |
COLOR_CATEGORY |
'color' |
color controls |
DATETIME_CATEGORY |
'datetime' |
date/time controls |
SVG_CATEGORY |
'svg' |
inline-SVG / icon controls |
A tag may declare several — Pro's Post_Custom_Field returns [ TEXT, URL, POST_META, COLOR, DATETIME, MEDIA ] because a meta value can drive any of those controls. Reference the constant, never the bare string, so a renamed value can't break you.
use Elementor\Modules\DynamicTags\Module as TagsModule;
public function get_categories(): array {
return [ TagsModule::TEXT_CATEGORY ];
}
Decision 3 — settings fields via register_controls()
Add the tag's configuration fields in register_controls(). Elementor has already opened a "Settings" controls section around your call (base-tag.php:175-187) — add controls directly; do not wrap them in your own start_controls_section().
use Elementor\Controls_Manager;
protected function register_controls(): void {
$this->add_control( 'format', [
'label' => esc_html__( 'Format', 'myplugin' ),
'type' => Controls_Manager::SELECT,
'default' => 'minutes',
'options' => [
'minutes' => esc_html__( 'Minutes', 'myplugin' ),
'words' => esc_html__( 'Word count', 'myplugin' ),
],
] );
$this->add_control( 'wpm', [
'label' => esc_html__( 'Words / minute', 'myplugin' ),
'type' => Controls_Manager::NUMBER,
'default' => 200,
'min' => 50,
] );
}
Common control types (from Elementor\Controls_Manager, all seen in verified tags/widgets): TEXT, TEXTAREA, NUMBER, SELECT, SELECT2 (add 'multiple' => true for multi), SWITCHER, CHOOSE, COLOR, MEDIA, ICONS, ALERT, REPEATER. Read settings back with $this->get_settings( 'key' ) (raw) or $this->get_settings_for_display() (parsed). For a large-dataset picker (products/posts) use the AJAX query control — see elementor-dynamic-tag-ajax-select; a plain SELECT2 preloaded with thousands of options freezes the editor.
Name your method register_controls(), not _register_controls(). The underscore form is deprecated since 3.1.0 — init_controls() calls it but emits a _doing_it_wrong notice (base-tag.php:179-182). (The reference plugin used the underscore form until its 2026-06-17 migration; older copies and tutorials still show it, so recognise it but don't copy it.)
Two optional panel hints on Base_Tag:
is_settings_required()— returntrueif the tag is useless until configured (Pro'sPost_Custom_Fielddoes). Defaultfalse(base-tag.php:83).get_panel_template_setting_key()— return a control key to surface in the tag's panel label (e.g.'key'). Default''(base-tag.php:75).
The fallback system (the asymmetry that bites)
A Tag gets Before / After / Fallback controls for free and applies the fallback automatically when render() produces empty output. A Data_Tag gets none of this and must do it by hand.
Tag — automatic
Tag::register_advanced_section() adds an "Advanced" section with before, after, and fallback controls (tag.php:84-123), and get_content() applies them: if the rendered value is non-empty it prepends before / appends after; else if a fallback is set it uses wp_kses_post_deep( $settings['fallback'] ) (tag.php:39-55). You write render() and get fallback behaviour automatically — just make sure render() outputs nothing when there's no value (don't echo '0', '—', or an empty wrapper, or the fallback never triggers).
public function render(): void {
$value = $this->compute();
if ( '' === $value ) {
return; // emit nothing → Elementor's Fallback control takes over
}
echo esc_html( $value );
}
Data_Tag — manual
Data_Tag inherits the empty Base_Tag::register_advanced_section() (base-tag.php:166) — no Before/After/Fallback section is added, and get_content() does no fallback logic. To support a fallback you register the control yourself and consult it in get_value(). Pro's Post_Featured_Image is the canonical pattern:
protected function register_controls(): void {
$this->add_control( 'fallback', [
'label' => esc_html__( 'Fallback', 'myplugin' ),
'type' => Controls_Manager::MEDIA, // match the control type your value feeds
] );
}
protected function get_value( array $options = [] ) {
$id = get_post_thumbnail_id();
if ( $id ) {
return [ 'id' => $id, 'url' => wp_get_attachment_image_src( $id, 'full' )[0] ];
}
return $this->get_settings( 'fallback' ); // the manual fallback
}
Verified at post-featured-image.php:33-56 — get_value() returns the image array or $this->get_settings( 'fallback' ), and register_controls() adds a single MEDIA fallback control.
Critical rules
Tagechoes (render());Data_Tagreturns (get_value()). Mixing them up (returning fromrender(), or echoing fromget_value()) silently produces empty/garbage output.get_categories()returnsModule::*_CATEGORYconstants, never bare strings, and matches the control kinds your value feeds. Wrong categories → the tag never appears under the intended control.- Add controls directly in
register_controls()— Elementor already opened the "Settings" section. A self-opened section nests incorrectly. - Method is
register_controls(), not_register_controls()(deprecated 3.1.0). Tagfallback is automatic but only triggers on empty output —render()must emit nothing when there's no value.Data_Taghas no automatic fallback/before/after — register afallbackcontrol and read it inget_value()yourself, with atypematching the consuming control.- Escape on output in
Tag::render()(esc_html/wp_kses_post) — it's echoed into the page like any front-end output.
Common mistakes
// WRONG — Data_Tag that echoes (output is captured nowhere; control gets null)
class My_Image extends Data_Tag {
protected function get_value( array $options = [] ) {
echo wp_get_attachment_url( $id ); // <-- should RETURN
}
}
// WRONG — Tag that returns (nothing is printed; tag renders empty)
class My_Text extends Tag {
public function render() {
return get_the_title(); // <-- should ECHO
}
}
// WRONG — render() prints a placeholder, so Elementor's Fallback never fires
public function render() {
$v = $this->compute();
echo esc_html( $v ?: '—' ); // <-- non-empty → fallback control is dead
}
// RIGHT — emit nothing on empty
public function render() {
$v = $this->compute();
if ( '' === $v ) { return; }
echo esc_html( $v );
}
// WRONG — Data_Tag expecting an automatic Fallback control (there is none)
class My_Url extends Data_Tag {
protected function get_value( array $o = [] ) {
return $this->get_settings( 'fallback' ); // <-- 'fallback' was never registered
}
}
// RIGHT — register it first
protected function register_controls() {
$this->add_control( 'fallback', [ 'type' => Controls_Manager::URL ] );
}
// WRONG — wrapping controls in your own section
protected function register_controls() {
$this->start_controls_section( 'sec', [ 'label' => 'X' ] ); // <-- already inside "Settings"
$this->add_control( /* … */ );
$this->end_controls_section();
}
Cross-references
- Run
elementor-dynamic-tag-registerfor registering the tag, groups, hooks, and bootstrap timing. - Run
elementor-dynamic-tag-ajax-selectwhen a settings field must pick from a large dataset without a preloadedSELECT2. - See
reference.mdin this skill folder for the full control-type catalog and a complete workedData_Tag.
What this skill does NOT cover
- Registering the tag / groups / hooks —
elementor-dynamic-tag-register. - AJAX/search option fields —
elementor-dynamic-tag-ajax-select. - Group-control families (Typography, Box Shadow, etc.) — those are for widgets, not tag settings.
- The full
Controls_Managercontrol reference — see Elementor's controls docs; this skill cites only the types proven in dynamic-tag source.
References
Tag: wp-content/plugins/elementor/core/dynamic-tags/tag.php —get_content()ob/render/fallback (30-58),'ui'type (64), advanced section with before/after/fallback (84-123).Data_Tag: wp-content/plugins/elementor/core/dynamic-tags/data-tag.php — abstractget_value()(25),'plain'type (31),get_content()returns value (43-45).Base_Tag: wp-content/plugins/elementor/core/dynamic-tags/base-tag.php —init_controls()opens Settings +_register_controlsdeprecation (172-187), emptyregister_advanced_section()(166),is_settings_required()(83),get_panel_template_setting_key()(75).- Categories: wp-content/plugins/elementor/modules/dynamic-tags/module.php:31-76.
- Manual-fallback Data_Tag: wp-content/plugins/elementor-pro/modules/dynamic-tags/tags/post-featured-image.php:33-56.
- Tag with SELECT + TEXT controls +
is_settings_required(): wp-content/plugins/elementor-pro/modules/dynamic-tags/tags/post-custom-field.php:46-105. - Official documentation: https://developers.elementor.com/docs/dynamic-tags/
- Official documentation: https://developers.elementor.com/docs/dynamic-tags/dynamic-tag-data/
Files (wp-agent-skills)
-
reference.md 7 KB
# elementor-dynamic-tag-fields — reference Supporting detail for the `elementor-dynamic-tag-fields` skill: the control-type catalog and a complete end-to-end `Data_Tag`. ## Control type catalog (`Elementor\Controls_Manager`) Verified constants in [wp-content/plugins/elementor/includes/managers/controls.php](controls.php). These are the types commonly useful inside a dynamic tag's `register_controls()`: | Constant | Value | Notes | |---|---|---| | `Controls_Manager::TEXT` | `'text'` | single-line string | | `Controls_Manager::TEXTAREA` | `'textarea'` | multi-line string | | `Controls_Manager::NUMBER` | `'number'` | supports `min` / `max` / `step` | | `Controls_Manager::SELECT` | `'select'` | single choice; `options` is `[ value => label ]` | | `Controls_Manager::SELECT2` | `'select2'` | searchable; add `'multiple' => true` for multi. **Preload only small sets** — see `elementor-dynamic-tag-ajax-select` | | `Controls_Manager::SWITCHER` | `'switcher'` | on/off; set `return_value` + `label_on`/`label_off` | | `Controls_Manager::CHOOSE` | `'choose'` | icon button group | | `Controls_Manager::COLOR` | `'color'` | pairs with `COLOR_CATEGORY` | | `Controls_Manager::MEDIA` | `'media'` | image/file; pairs with `IMAGE`/`MEDIA` category | | `Controls_Manager::URL` | `'url'` | link; pairs with `URL_CATEGORY` | | `Controls_Manager::DATE_TIME` | `'date_time'` | pairs with `DATETIME_CATEGORY` | | `Controls_Manager::WYSIWYG` | `'wysiwyg'` | rich-text editor | | `Controls_Manager::CODE` | `'code'` | code editor | | `Controls_Manager::ICONS` | `'icons'` | icon picker (v2 icon library) | | `Controls_Manager::GALLERY` | `'gallery'` | multiple images; pairs with `GALLERY_CATEGORY` | | `Controls_Manager::REPEATER` | `'repeater'` | rows of sub-controls (build with `\Elementor\Repeater`) | | `Controls_Manager::ALERT` | `'alert'` | static notice in the panel (no value) | | `Controls_Manager::HEADING` | `'heading'` | section label (no value) | | `Controls_Manager::RAW_HTML` | `'raw_html'` | static markup in the panel (no value) | | `Controls_Manager::HIDDEN` | `'hidden'` | stored, not shown | Line refs: TEXT 54, NUMBER 59, TEXTAREA 64, SELECT 69, SWITCHER 74, HIDDEN 84, HEADING 89, RAW_HTML 94, ALERT 109, COLOR 139, MEDIA 144, CHOOSE 159, WYSIWYG 169, CODE 174, URL 194, REPEATER 199, ICON 204, ICONS 209, GALLERY 214, SELECT2 224, DATE_TIME 229. ### Reading values back - `$this->get_settings( 'key' )` — raw stored value. - `$this->get_settings_for_display()` — full settings, parsed (use `['key']`); resolves nested dynamic values where applicable. - `SWITCHER` returns the `return_value` (e.g. `'yes'`) when on, `''` when off. - `SELECT2` multiple returns an array. - `MEDIA` returns `[ 'id' => int, 'url' => string ]`; `URL` returns `[ 'url' => string, 'is_external' => '', 'nofollow' => '' ]`. ## Complete worked `Data_Tag` — author Twitter URL with fallback A `Data_Tag` feeding a `URL` control, with a manually-wired fallback (Data_Tags get no automatic Before/After/Fallback — see the skill body). ```php namespace MyPlugin\Tags; use Elementor\Core\DynamicTags\Data_Tag; use Elementor\Controls_Manager; use Elementor\Modules\DynamicTags\Module as TagsModule; class Author_Twitter_Url extends Data_Tag { public function get_name(): string { return 'myplugin-author-twitter-url'; } public function get_title(): string { return esc_html__( 'Author Twitter URL', 'myplugin' ); } public function get_group(): string { return 'myplugin'; // registered via register_group() — see elementor-dynamic-tag-register } public function get_categories(): array { return [ TagsModule::URL_CATEGORY ]; // feeds link controls } protected function register_controls(): void { // No automatic fallback on Data_Tag — register one whose type // matches the consuming control (URL here). $this->add_control( 'fallback', [ 'label' => esc_html__( 'Fallback URL', 'myplugin' ), 'type' => Controls_Manager::URL, ] ); } protected function get_value( array $options = [] ) { $author_id = get_the_author_meta( 'ID' ); $handle = $author_id ? get_user_meta( $author_id, 'twitter', true ) : ''; if ( $handle ) { return [ 'url' => 'https://twitter.com/' . ltrim( $handle, '@' ), 'is_external' => 'on', 'nofollow' => '', ]; } // Manual fallback — the URL control value, or empty. return $this->get_settings( 'fallback' ); } } ``` ## Complete worked `Tag` — reading time with automatic fallback A `Tag` (echoes) automatically gets Before / After / Fallback in an "Advanced" section, and applies the fallback when `render()` outputs empty. The only contract: emit **nothing** when there's no value. ```php namespace MyPlugin\Tags; use Elementor\Core\DynamicTags\Tag; use Elementor\Controls_Manager; use Elementor\Modules\DynamicTags\Module as TagsModule; class Reading_Time extends Tag { public function get_name(): string { return 'myplugin-reading-time'; } public function get_title(): string { return esc_html__( 'Reading Time', 'myplugin' ); } public function get_group(): string { return 'myplugin'; } public function get_categories(): array { return [ TagsModule::TEXT_CATEGORY ]; } protected function register_controls(): void { $this->add_control( 'wpm', [ 'label' => esc_html__( 'Words / minute', 'myplugin' ), 'type' => Controls_Manager::NUMBER, 'default' => 200, 'min' => 50, ] ); $this->add_control( 'suffix', [ 'label' => esc_html__( 'Suffix', 'myplugin' ), 'type' => Controls_Manager::TEXT, 'default' => esc_html__( 'min read', 'myplugin' ), ] ); } public function render(): void { $content = get_the_content(); if ( '' === trim( $content ) ) { return; // emit nothing → Elementor's Fallback control takes over } $wpm = max( 50, (int) $this->get_settings( 'wpm' ) ); $words = str_word_count( wp_strip_all_tags( $content ) ); $minutes = max( 1, (int) ceil( $words / $wpm ) ); echo esc_html( $minutes . ' ' . $this->get_settings( 'suffix' ) ); } } ``` ## Conditional controls Use `'condition'` to show a control only for certain settings (same engine as widgets): ```php $this->add_control( 'type', [ 'type' => Controls_Manager::SELECT, 'options' => [ 'billing' => 'Billing', 'shipping' => 'Shipping' ], 'default' => 'billing', ] ); $this->add_control( 'billing_field', [ 'type' => Controls_Manager::SELECT, 'options' => [ /* … */ ], 'condition' => [ 'type' => 'billing' ], // hidden unless type === billing ] ); ``` Verified shape in `CustomerDetails` ([wp-content/plugins/dynamic-elementor-extension-main/dynamic-tags/woo-tags/CustomerDetails.php](CustomerDetails.php)) and `Internal_URL` (`condition` per query control). -
SKILL.md 13.8 KB
--- name: elementor-dynamic-tag-fields description: Build the body of an Elementor Dynamic Tag — choose Tag (echoes via render(), content_type 'ui') vs Data_Tag (returns a value via get_value(), content_type 'plain'); declare what kind of value it produces via get_categories() using the Module constants (TEXT_CATEGORY, URL_CATEGORY, IMAGE_CATEGORY, MEDIA_CATEGORY, POST_META_CATEGORY, GALLERY_CATEGORY, NUMBER_CATEGORY, COLOR_CATEGORY, DATETIME_CATEGORY, SVG_CATEGORY); add settings fields in register_controls() (NOT the deprecated _register_controls()) with Controls_Manager types (TEXT, NUMBER, SELECT, SELECT2, SWITCHER, MEDIA, …); and wire the fallback system. Important — a Tag gets Before / After / Fallback controls automatically and applies the fallback when render() outputs empty, but a Data_Tag gets NONE of that and must register its own fallback control and consult it in get_value(). Use when implementing or reviewing a dynamic tag's render/get_value, controls, categories, or fallback behaviour. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "elementor" wp-skills-plugin-version-tested: "4.0.7 (free) / 4.0.4 (pro)" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-06-17" --- # Elementor: Dynamic Tag types, fields & fallback The body of a dynamic tag has three decisions: **which base class** (`Tag` vs `Data_Tag`), **which categories** it produces (so the right controls accept it), and **which settings fields** the editor shows. Plus the easily-missed **fallback** asymmetry between the two base classes. This skill assumes the tag is already being registered — see `elementor-dynamic-tag-register` for that. ## Decision 1 — `Tag` vs `Data_Tag` | | `Tag` | `Data_Tag` | |---|---|---| | Extend | `\Elementor\Core\DynamicTags\Tag` | `\Elementor\Core\DynamicTags\Data_Tag` | | You implement | `render()` — **echo** the output | `get_value( array $options = [] )` — **return** the value | | `get_content_type()` | `'ui'` (final) | `'plain'` (final) | | Use for | rendered text/HTML fragments (price, reading time, a badge) | structured values consumed by a control — an image array `[ 'id' => …, 'url' => … ]`, a URL string, a color | Verified: `Tag::get_content()` does `ob_start(); $this->render(); $value = ob_get_clean();` ([tag.php:30-37](tag.php)) and `get_content_type()` returns `'ui'` ([tag.php:64](tag.php)). `Data_Tag` declares `abstract protected function get_value()` ([data-tag.php:25](data-tag.php)), returns it directly from `get_content()` ([data-tag.php:43-45](data-tag.php)), and `get_content_type()` returns `'plain'` ([data-tag.php:31](data-tag.php)). Rule of thumb: if the value feeds a **MEDIA / IMAGE / URL / COLOR** control (the control needs a structured value, not printed markup), use `Data_Tag`. If it feeds a **TEXT** context (it's printed inline), use `Tag`. ```php // Tag — echoes class Reading_Time extends Tag { public function render(): void { echo esc_html( $this->compute() . ' min' ); // print, don't return } } // Data_Tag — returns class Featured_Image_Fallback extends Data_Tag { protected function get_value( array $options = [] ) { $id = get_post_thumbnail_id(); if ( $id ) { return [ 'id' => $id, 'url' => wp_get_attachment_image_src( $id, 'full' )[0] ]; } return $this->get_settings( 'fallback' ); // see "Fallback" below } } ``` ## Decision 2 — categories (what value the tag produces) `get_categories()` returns one or more **category constants** from `Elementor\Modules\DynamicTags\Module`. Categories declare the *kind* of value the tag emits; a control accepts a tag only when their categories overlap, so the editor shows your tag only under compatible controls. Verified constants ([modules/dynamic-tags/module.php:31-76](module.php)): | Constant | Value | Typical use | |---|---|---| | `TEXT_CATEGORY` | `'text'` | printed strings (most `Tag`s) | | `URL_CATEGORY` | `'url'` | link fields | | `IMAGE_CATEGORY` | `'image'` | image controls | | `MEDIA_CATEGORY` | `'media'` | media (image/video) controls | | `POST_META_CATEGORY` | `'post_meta'` | meta-field contexts | | `GALLERY_CATEGORY` | `'gallery'` | gallery controls | | `NUMBER_CATEGORY` | `'number'` | number controls | | `COLOR_CATEGORY` | `'color'` | color controls | | `DATETIME_CATEGORY` | `'datetime'` | date/time controls | | `SVG_CATEGORY` | `'svg'` | inline-SVG / icon controls | A tag may declare several — Pro's `Post_Custom_Field` returns `[ TEXT, URL, POST_META, COLOR, DATETIME, MEDIA ]` because a meta value can drive any of those controls. Reference the constant, never the bare string, so a renamed value can't break you. ```php use Elementor\Modules\DynamicTags\Module as TagsModule; public function get_categories(): array { return [ TagsModule::TEXT_CATEGORY ]; } ``` ## Decision 3 — settings fields via `register_controls()` Add the tag's configuration fields in `register_controls()`. Elementor has **already opened a "Settings" controls section** around your call ([base-tag.php:175-187](base-tag.php)) — add controls directly; do **not** wrap them in your own `start_controls_section()`. ```php use Elementor\Controls_Manager; protected function register_controls(): void { $this->add_control( 'format', [ 'label' => esc_html__( 'Format', 'myplugin' ), 'type' => Controls_Manager::SELECT, 'default' => 'minutes', 'options' => [ 'minutes' => esc_html__( 'Minutes', 'myplugin' ), 'words' => esc_html__( 'Word count', 'myplugin' ), ], ] ); $this->add_control( 'wpm', [ 'label' => esc_html__( 'Words / minute', 'myplugin' ), 'type' => Controls_Manager::NUMBER, 'default' => 200, 'min' => 50, ] ); } ``` Common control `type`s (from `Elementor\Controls_Manager`, all seen in verified tags/widgets): `TEXT`, `TEXTAREA`, `NUMBER`, `SELECT`, `SELECT2` (add `'multiple' => true` for multi), `SWITCHER`, `CHOOSE`, `COLOR`, `MEDIA`, `ICONS`, `ALERT`, `REPEATER`. Read settings back with `$this->get_settings( 'key' )` (raw) or `$this->get_settings_for_display()` (parsed). For a large-dataset picker (products/posts) use the AJAX query control — see `elementor-dynamic-tag-ajax-select`; a plain `SELECT2` preloaded with thousands of options freezes the editor. **Name your method `register_controls()`, not `_register_controls()`.** The underscore form is deprecated since 3.1.0 — `init_controls()` calls it but emits a `_doing_it_wrong` notice ([base-tag.php:179-182](base-tag.php)). (The reference plugin used the underscore form until its 2026-06-17 migration; older copies and tutorials still show it, so recognise it but don't copy it.) Two optional panel hints on `Base_Tag`: - `is_settings_required()` — return `true` if the tag is useless until configured (Pro's `Post_Custom_Field` does). Default `false` ([base-tag.php:83](base-tag.php)). - `get_panel_template_setting_key()` — return a control key to surface in the tag's panel label (e.g. `'key'`). Default `''` ([base-tag.php:75](base-tag.php)). ## The fallback system (the asymmetry that bites) A `Tag` gets **Before / After / Fallback** controls **for free** and applies the fallback automatically when `render()` produces empty output. A `Data_Tag` gets **none of this** and must do it by hand. ### `Tag` — automatic `Tag::register_advanced_section()` adds an "Advanced" section with `before`, `after`, and `fallback` controls ([tag.php:84-123](tag.php)), and `get_content()` applies them: if the rendered value is non-empty it prepends `before` / appends `after`; **else if** a `fallback` is set it uses `wp_kses_post_deep( $settings['fallback'] )` ([tag.php:39-55](tag.php)). You write `render()` and get fallback behaviour automatically — just make sure `render()` outputs **nothing** when there's no value (don't echo `'0'`, `'—'`, or an empty wrapper, or the fallback never triggers). ```php public function render(): void { $value = $this->compute(); if ( '' === $value ) { return; // emit nothing → Elementor's Fallback control takes over } echo esc_html( $value ); } ``` ### `Data_Tag` — manual `Data_Tag` inherits the **empty** `Base_Tag::register_advanced_section()` ([base-tag.php:166](base-tag.php)) — no Before/After/Fallback section is added, and `get_content()` does **no** fallback logic. To support a fallback you register the control yourself and consult it in `get_value()`. Pro's `Post_Featured_Image` is the canonical pattern: ```php protected function register_controls(): void { $this->add_control( 'fallback', [ 'label' => esc_html__( 'Fallback', 'myplugin' ), 'type' => Controls_Manager::MEDIA, // match the control type your value feeds ] ); } protected function get_value( array $options = [] ) { $id = get_post_thumbnail_id(); if ( $id ) { return [ 'id' => $id, 'url' => wp_get_attachment_image_src( $id, 'full' )[0] ]; } return $this->get_settings( 'fallback' ); // the manual fallback } ``` Verified at [post-featured-image.php:33-56](post-featured-image.php) — `get_value()` returns the image array or `$this->get_settings( 'fallback' )`, and `register_controls()` adds a single `MEDIA` fallback control. ## Critical rules - **`Tag` echoes (`render()`); `Data_Tag` returns (`get_value()`).** Mixing them up (returning from `render()`, or echoing from `get_value()`) silently produces empty/garbage output. - **`get_categories()` returns `Module::*_CATEGORY` constants**, never bare strings, and matches the control kinds your value feeds. Wrong categories → the tag never appears under the intended control. - **Add controls directly in `register_controls()`** — Elementor already opened the "Settings" section. A self-opened section nests incorrectly. - **Method is `register_controls()`**, not `_register_controls()` (deprecated 3.1.0). - **`Tag` fallback is automatic** but only triggers on **empty** output — `render()` must emit nothing when there's no value. - **`Data_Tag` has no automatic fallback/before/after** — register a `fallback` control and read it in `get_value()` yourself, with a `type` matching the consuming control. - **Escape on output in `Tag::render()`** (`esc_html` / `wp_kses_post`) — it's echoed into the page like any front-end output. ## Common mistakes ```php // WRONG — Data_Tag that echoes (output is captured nowhere; control gets null) class My_Image extends Data_Tag { protected function get_value( array $options = [] ) { echo wp_get_attachment_url( $id ); // <-- should RETURN } } // WRONG — Tag that returns (nothing is printed; tag renders empty) class My_Text extends Tag { public function render() { return get_the_title(); // <-- should ECHO } } // WRONG — render() prints a placeholder, so Elementor's Fallback never fires public function render() { $v = $this->compute(); echo esc_html( $v ?: '—' ); // <-- non-empty → fallback control is dead } // RIGHT — emit nothing on empty public function render() { $v = $this->compute(); if ( '' === $v ) { return; } echo esc_html( $v ); } // WRONG — Data_Tag expecting an automatic Fallback control (there is none) class My_Url extends Data_Tag { protected function get_value( array $o = [] ) { return $this->get_settings( 'fallback' ); // <-- 'fallback' was never registered } } // RIGHT — register it first protected function register_controls() { $this->add_control( 'fallback', [ 'type' => Controls_Manager::URL ] ); } // WRONG — wrapping controls in your own section protected function register_controls() { $this->start_controls_section( 'sec', [ 'label' => 'X' ] ); // <-- already inside "Settings" $this->add_control( /* … */ ); $this->end_controls_section(); } ``` ## Cross-references - Run **`elementor-dynamic-tag-register`** for registering the tag, groups, hooks, and bootstrap timing. - Run **`elementor-dynamic-tag-ajax-select`** when a settings field must pick from a large dataset without a preloaded `SELECT2`. - See `reference.md` in this skill folder for the full control-type catalog and a complete worked `Data_Tag`. ## What this skill does NOT cover - **Registering the tag / groups / hooks** — `elementor-dynamic-tag-register`. - **AJAX/search option fields** — `elementor-dynamic-tag-ajax-select`. - **Group-control families** (Typography, Box Shadow, etc.) — those are for widgets, not tag settings. - **The full `Controls_Manager` control reference** — see Elementor's controls docs; this skill cites only the types proven in dynamic-tag source. ## References - `Tag`: [wp-content/plugins/elementor/core/dynamic-tags/tag.php](tag.php) — `get_content()` ob/render/fallback (30-58), `'ui'` type (64), advanced section with before/after/fallback (84-123). - `Data_Tag`: [wp-content/plugins/elementor/core/dynamic-tags/data-tag.php](data-tag.php) — abstract `get_value()` (25), `'plain'` type (31), `get_content()` returns value (43-45). - `Base_Tag`: [wp-content/plugins/elementor/core/dynamic-tags/base-tag.php](base-tag.php) — `init_controls()` opens Settings + `_register_controls` deprecation (172-187), empty `register_advanced_section()` (166), `is_settings_required()` (83), `get_panel_template_setting_key()` (75). - Categories: [wp-content/plugins/elementor/modules/dynamic-tags/module.php:31-76](module.php). - Manual-fallback Data_Tag: [wp-content/plugins/elementor-pro/modules/dynamic-tags/tags/post-featured-image.php:33-56](post-featured-image.php). - Tag with SELECT + TEXT controls + `is_settings_required()`: [wp-content/plugins/elementor-pro/modules/dynamic-tags/tags/post-custom-field.php:46-105](post-custom-field.php). - Official documentation: <https://developers.elementor.com/docs/dynamic-tags/> - Official documentation: <https://developers.elementor.com/docs/dynamic-tags/dynamic-tag-data/>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.