# Advanced Dynamic Tags

> Registers 5 EA-branded dynamic tag classes into Elementor's dynamic-tag system. Lets any dynamic-enabled control pull from EA-defined data sources — posts loop, CPT loop, taxonomy terms, WooCommerce products, ACF relationship.

**Class file:** `includes/Extensions/Advanced_Dynamic_Tags.php` (33 lines)
**Slug:** `advanced-dynamic-tags` (`config.php` `extensions` key)
**Public docs:** <https://essential-addons.com/elementor/docs/dynamic-tags/>
**Pro-shared:** ✅ Pro-only (Lite has no dynamic-tag registration of its own)

## Overview

Elementor exposes a "dynamic tag" system for any control marked `'dynamic' => ['active' => true]`. Pro adds a tag group (`EA Dynamic Tags`) with 5 tag classes that let a user inject queried content (post list, term list, product list, ACF relationship) into any dynamic-enabled field.

The extension class itself is tiny (33 lines) — its only job is registration. The actual rendering logic lives in the 5 tag classes under `includes/Extensions/DynamicTags/`.

## Features

- **Posts** — query post loop (title / excerpt / link), configurable separator, format string
- **Custom Post Types** — same as Posts but scoped to a CPT
- **Terms** — taxonomy term list
- **Woo Products** — WooCommerce product loop
- **ACF Relationship** — pulls posts linked via ACF relationship field (extends `Posts`)

## Pro vs Lite

| Capability | Lite | Pro |
| --- | --- | --- |
| Class instantiates | ❌ | ✅ |
| `EA Dynamic Tags` group registered | ❌ | ✅ |
| 5 tag classes available in dynamic-enabled controls | ❌ | ✅ |

Lite has no dynamic-tag infrastructure; the entire feature is Pro-only.

## File Map

| File | Role |
| --- | --- |
| `includes/Extensions/Advanced_Dynamic_Tags.php` | Registration class — hooks `elementor/dynamic_tags/register` |
| `includes/Extensions/DynamicTags/Posts.php` | Posts loop tag |
| `includes/Extensions/DynamicTags/Custom_Post_Types.php` | CPT loop tag |
| `includes/Extensions/DynamicTags/Terms.php` | Taxonomy term tag |
| `includes/Extensions/DynamicTags/Woo_Products.php` | WooCommerce product loop tag |
| `includes/Extensions/DynamicTags/Acf_Relationship.php` | ACF relationship-field tag (extends `Posts`) |
| `config.php` (extensions key) | `'advanced-dynamic-tags' => [ 'class' => '\Essential_Addons_Elementor\Pro\Extensions\Advanced_Dynamic_Tags' ]` |

No `dependency` block — the extension has no assets. Dynamic tags render server-side and inject HTML strings into Elementor's resolved output.

## Architecture

`Advanced_Dynamic_Tags::__construct` registers a single Elementor hook:

```php
add_action( 'elementor/dynamic_tags/register', [ $this, 'register_dynamic_widgets' ] );
```

`register_dynamic_widgets( $dynamic_tags_manager )` calls:

```php
$dynamic_tags_manager->register_group( 'eael-advanced-dynamic-tags', [
    'title' => __( 'EA Dynamic Tags', 'essential-addons-elementor' )
] );

$dynamic_tags_manager->register( new Posts() );
$dynamic_tags_manager->register( new Woo_Products() );
$dynamic_tags_manager->register( new Terms() );
$dynamic_tags_manager->register( new Custom_Post_Types() );
```

**Gap:** `Acf_Relationship` is NOT in the explicit register list — verify whether it auto-registers through inheritance (`Posts` parent registration) or is dead code. See [`docs/architecture/dynamic-data.md`](../architecture/dynamic-data.md) § "What's missing" #7.

## Tag class anatomy

Each tag class extends `\Elementor\Core\DynamicTags\Tag`. Required methods:

```php
public function get_name()       { return 'eael-dynamic-tags-{slug}'; }
public function get_title()      { return __( '...', 'essential-addons-elementor' ); }
public function get_group()      { return 'eael-advanced-dynamic-tags'; }
public function get_categories() { return [ Module::BASE_GROUP, Module::TEXT_CATEGORY ]; }

protected function register_controls() { /* tag-specific controls */ }
public function render()                { /* output HTML string */ }
```

`get_categories` typically returns `BASE_GROUP` + `TEXT_CATEGORY` so the tag appears under text-typed dynamic-enabled controls.

## Posts tag — render pattern

```php
public function render() {
    $settings = $this->get_settings_for_display();
    if ( empty( $settings ) ) { return; }

    $settings['post_type'] = ! empty( $settings['post_type'] )
        ? ClassesHelper::validate_post_types( $settings['post_type'] )
        : '';

    $args = Helper::get_query_args( $settings );         // Lite's helper builds WP_Query args
    $extra_args = $this->get_extra_args();
    $args = array_merge( $args, $extra_args );

    $wp_query = new \WP_Query( $args );
    while ( $wp_query->have_posts() ) {
        $wp_query->the_post();
        if ( 'none' === $settings['separator'] ) {
            echo $this->get_post_by_format();
        } else {
            echo '<a href=' . get_the_permalink() . '>' . $this->get_post_by_format() . '</a>';
        }
        // separator
    }
}
```

**Notes:**

- Uses **Lite's** `Classes/Helper::get_query_args()` — Pro reuses Lite's helper, do not duplicate
- Output is **not escaped at the tag level** — `get_post_by_format()` is expected to return pre-escaped HTML. Verify when editing the helper.
- Permalink concatenated into `<a href>` without `esc_url()` — PHPCS-ignored. Real-world risk low (permalinks are server-generated).

## Controls Reference (Posts tag — sample)

| Control id | Purpose |
| --- | --- |
| `post_type` | Select post type (`'post' \| 'page' \| ...CPT slugs`) |
| `post_status` | Multi-select, default `['publish']`. **Caller-controlled — see `nopriv-ajax-hardening` rule** |
| `separator` | Separator between rendered items (`'comma' \| 'newline' \| 'none'`) |
| `format` | Output format string (e.g. `'{title}'`, `'{title} - {date}'`) |

Full control set is built by `ClassesHelper::query_dynamic_tags( $this, $args )` — shared across Posts/CPT/Terms/Woo_Products.

## Behavior Flow

1. **Editor:** user adds an EA widget (or any widget with a dynamic-enabled text control). Clicks the dynamic-tag icon. Sees "EA Dynamic Tags" group. Picks "Posts".
2. **Editor:** Posts tag's `register_controls()` builds the panel (post_type, post_status, separator, format). User configures.
3. **Save:** Elementor persists tag settings as part of the parent control's value.
4. **Render (frontend):** Elementor expands the dynamic tag. Calls `Posts::render()`. Posts runs `WP_Query` server-side, echoes HTML string. Elementor inserts the string into the parent control's resolved value.

## Hooks & Filters

### Elementor hooks consumed

| Hook | Method | Purpose |
| --- | --- | --- |
| `elementor/dynamic_tags/register` | `register_dynamic_widgets` | Registers the EA group + 4 tag classes |

### Pro / EA hooks emitted

None. The extension's only emission is into Elementor's tag registry.

## Behavior Quirks

- **Acf_Relationship implicit registration.** If ACF isn't installed, the tag silently does nothing — by design.
- **`post_status` is multi-select.** A user can choose `private`, `draft`, `pending` from the control. This is OK in the editor (user is logged-in admin) but a problem if a similar tag ever runs in a nopriv context. Today: tags render only inside Elementor's render flow, which is logged-in or already-resolved post content — not nopriv-AJAX-reachable.
- **`Helper::get_query_args` is Lite's.** When Lite refactors it, all 5 Pro tags break together. See [`pro-lite-bridge.md`](../architecture/pro-lite-bridge.md).

## Known Limitations / Gaps

1. `Acf_Relationship` not explicitly registered — verify or flag as dead code
2. `get_post_by_format()` contract (returns pre-escaped HTML) is not asserted anywhere — silent regression risk
3. Permalink not `esc_url()`'d — PHPCS-ignored; tighten when touched
4. No per-tag docs for the 5 tag classes — future follow-up

## Code Pointers

- Registration entry: `includes/Extensions/Advanced_Dynamic_Tags.php:16` (`__construct`) → `:21` (`register_dynamic_widgets`)
- Posts tag render: `includes/Extensions/DynamicTags/Posts.php` (`render()` method)
- Shared control builder: `\Essential_Addons_Elementor\Pro\Classes\Helper::query_dynamic_tags`
- Lite helper for WP_Query args: `\Essential_Addons_Elementor\Classes\Helper::get_query_args`

## Cross-References

- Architecture: [`docs/architecture/dynamic-data.md`](../architecture/dynamic-data.md)
- Architecture: [`docs/architecture/pro-lite-bridge.md`](../architecture/pro-lite-bridge.md) — explains the Lite helper reuse
- Shared patterns: [`_patterns.md`](_patterns.md)
