Skip to content

Implement (element) helper for dynamic tag names (RFC #0389)#21230

Merged
NullVoxPopuli merged 10 commits intoemberjs:mainfrom
NullVoxPopuli-ai-agent:implement-element-helper
Apr 8, 2026
Merged

Implement (element) helper for dynamic tag names (RFC #0389)#21230
NullVoxPopuli merged 10 commits intoemberjs:mainfrom
NullVoxPopuli-ai-agent:implement-element-helper

Conversation

@NullVoxPopuli-ai-agent
Copy link
Copy Markdown
Contributor

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Mar 23, 2026

Summary

Implements the element helper as specified in RFC #0389, continuing the work started in #21048.

  • (element "h1") — renders an <h1> wrapping the block content, with full support for attributes, modifiers, and ...attributes
  • (element "") — renders block content without a wrapping element
  • (element null) / (element undefined) / non-string values — throws an assertion error in development mode (departure from RFC, which suggested rendering nothing for null/undefined)
  • Tag name changes trigger teardown/recreation of the element

The helper is available in strict mode only (importable from @ember/helper). It is not registered as a loose-mode builtin to avoid conflicts with ember-element-helper.

Implementation approach

Uses the Glimmer VM's WrappedBuilder with a minimal ElementComponentManager (4 capabilities: dynamicLayout, dynamicTag, createInstance, wrapped). The element helper returns a cached component definition per tag name:

  • Non-empty string: ElementComponentDefinition with getTagName() returning the tag name. The VM's default template ({{yield}}) is used via asWrappedLayout().
  • Empty string: Same component but getTagName() returns null, so the WrappedBuilder skips the element and just yields the block.

Files changed

  • packages/@ember/-internals/glimmer/lib/helpers/element.ts — Core implementation
  • packages/@ember/-internals/glimmer/index.ts — Re-export
  • packages/@ember/helper/index.ts — Public export for strict mode
  • packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.js — 14 strict-mode tests
  • smoke-tests/scenarios/basic-test.ts — Smoke test

Test plan

  • Basic rendering with static tag name (strict mode)
  • Empty string renders content without wrapper
  • null and undefined throw assertion errors
  • Non-string types (number, boolean, object) throw assertion errors
  • Element modifiers ({{on "click" ...}}) work on the dynamic element
  • Same element definition can be rendered multiple times
  • Dynamic tag name changes trigger teardown/recreation
  • Works as @tag argument with ...attributes
  • Validation: requires exactly 1 positional argument
  • Validation: rejects named arguments
  • All tests are strict mode only
  • TypeScript type-checking passes
  • ESLint + Prettier pass

🤖 Generated with Claude Code

@NullVoxPopuli NullVoxPopuli marked this pull request as draft March 23, 2026 19:37
@NullVoxPopuli NullVoxPopuli self-assigned this Mar 23, 2026
@NullVoxPopuli NullVoxPopuli marked this pull request as ready for review March 26, 2026 14:08
NullVoxPopuli
NullVoxPopuli previously approved these changes Mar 26, 2026
@NullVoxPopuli
Copy link
Copy Markdown
Contributor

Tested on the big app at work, and aside from ember not owning the [Invoke] symbol in Glint, everything worked great

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

Feedback:

  • don't allow (element) to receive undefined/null / non-string values. This is a departure from the RFC (for which will need a PR to update), but falsey values returned from (element) can't be rendered (RFC suggests returning null ( null errors )).

NullVoxPopuli and others added 5 commits April 8, 2026 10:05
Implements the `element` helper as specified in RFC emberjs#389, allowing
dynamic tag names in Glimmer templates. The helper returns a component
definition that renders the specified HTML element around yielded content.

Behavior:
- `(element "h1")` renders an `<h1>` wrapping the block content
- `(element "")` renders block content without a wrapping element
- `(element null)` / `(element undefined)` renders nothing
- Invalid types (number, boolean, object) throw in development mode
- Tag name changes trigger teardown/recreation of the element

Available in both loose mode (built-in) and strict mode (importable
from `@ember/helper`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Departing from the RFC: instead of rendering nothing for null/undefined,
throw an assertion error for all non-string values. Falsey values returned
from (element) can't actually be rendered, so it's better to fail early.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent force-pushed the implement-element-helper branch 2 times, most recently from ad1aa36 to 97517bf Compare April 8, 2026 14:26
…te/precompileTemplate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

const ELEMENT_COMPONENT_MANAGER = new ElementComponentManager();

class ElementComponentDefinition {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is basically @glimmer/component's Component, but way more constrained

Strip explicit interface annotations, unused imports, and verbose
type parameters. Reduce capabilities to only the 4 truthy flags
(missing properties default to falsy in capabilityFlagsFrom).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per review feedback: don't register element in loose mode (users with
ember-element-helper may have different behavior), and use strict mode
templates exclusively in tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove getTag helper wrapper; access state.htmlTag directly in scope
- Restore "requires at least one argument" test
- Restore "requires no more than one argument" test
- Restore "does not take any named arguments" test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@ef4 ef4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggested edits to reduce the component manager to the capabilities it actually needs.

…icLayout

Per review: dynamicTag does nothing, and dynamicLayout is not needed here.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@NullVoxPopuli NullVoxPopuli requested a review from ef4 April 8, 2026 19:05
@NullVoxPopuli NullVoxPopuli merged commit 1886f8d into emberjs:main Apr 8, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants