Skip to content

Add the RenderAdapter interface for every output operation of the VM - #21711

Draft
NullVoxPopuli-ai-agent wants to merge 4 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:nvp/dom-renderer-interface
Draft

NullVoxPopuli-ai-agent wants to merge 4 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:nvp/dom-renderer-interface

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

RenderAdapter is a new interface that has every operation the VM does on the output tree. All code in @glimmer/runtime and @glimmer/node now calls it, and does not read or write a member of a node.

Start with packages/@glimmer/interfaces/lib/render-adapter.d.ts. It is the full interface in about 140 lines.

Names

A renderer and a render adapter are two different things:

  • A renderer owns the roots and the render loop: BaseRenderer, the classic Renderer, and the one that renderComponent makes.
  • A render adapter does the output operations for a renderer. RenderAdapter is the interface. DOMRenderAdapter implements it for the DOM. A terminal output would be a different adapter, with the same renderers.

Why

A different output, for example a terminal, needs one interface to implement. Before this PR, the VM called the DOM in two ways:

  • Some calls went through DOMTreeConstruction and DOMChanges.
  • About 100 other calls were direct, for example node.nextSibling, element.setAttribute(), and element[name] = value.

What the interface has

Group Methods
Create createElement, createElementNS, createTextNode, createComment
Change the tree insertBefore, insertHTMLBefore, removeChild, clearChildren
Read the tree parentNode, firstChild, lastChild, nextSibling, previousSibling, querySelector
Read a node isNode, isFragment, isElement, isText, isComment, tagName, namespaceURI, nodeValue, setNodeValue
Attributes and properties attributeNames, getAttribute, setAttribute, removeAttribute, hasProperty, getProperty, setProperty
Events addEventListener, removeEventListener

The interface does not have getProp, toBool, toIterator, lookups, or managers. Those are the same for each output.

What uses it

  • DOMRenderAdapter in @glimmer/runtime/lib/dom/render-adapter.ts is the adapter for a browser document and for a SimpleDOM document. It replaces DOMOperations, DOMTreeConstruction, and DOMChanges. NodeDOMRenderAdapter in @glimmer/node extends it.
  • Environment#adapter holds the adapter. EnvironmentImpl and runtimeOptions take a RenderAdapter as argument 1.
  • The client builder, the rehydration builder, the serialize builder, the updating VM, the dynamic attributes, the bounds helpers, and the on modifier all call env.adapter.
  • BaseRenderer, BaseRenderer.strict, and the classic Renderer take a RenderAdapter.
  • renderComponent accepts env.adapter. This is the Ember entry point for a different adapter.

Proof that the VM uses only the interface

packages/@glimmer-workspace/integration-tests/test/render-adapter-test.ts has a RenderAdapter on plain objects with no DOM member. A direct DOM access by the VM fails on these objects. The test calls Ember's renderComponent with env: { adapter }, and the run loop does the updates. It renders and updates:

  • elements, static and dynamic attributes, and text
  • {{#if}} and {{#each}}, with add, move, and remove
  • the on modifier

Changes to private API

  • Environment#adapter replaces getDOM() and getAppendOperations().
  • EnvironmentImpl and runtimeOptions take a RenderAdapter in place of the EnvironmentOptions object. EnvironmentOptions (document, appendOperations, updateOperations) is removed.
  • DOMRenderAdapter replaces DOMTreeConstruction and DOMChanges. NodeDOMRenderAdapter replaces NodeDOMTreeConstruction. The exports of @ember/-internals/glimmer have the new names.
  • InternalModifierManager#create gets the environment as argument 5. The on modifier needs it.
  • normalizeProperty, dynamicAttribute, clear, and move take the adapter as argument 1.
  • sanitizeAttributeValue takes the tag name, not the element.
  • BaseRenderer, BaseRenderer.strict, and the classic Renderer take a RenderAdapter in place of a document. Only the private env.document option of renderComponent still takes a document, and it becomes a DOMRenderAdapter before the renderer is made.

Not in this PR

  • The classic Renderer (applications, {{outlet}}) makes its adapter from the -document service. Remove the service:-document registration #21772 removes that service.
  • The node types are still SimpleNode and SimpleElement. A different adapter must cast its nodes.
  • The rules for attribute against property, and the URL sanitization by tag name, stay in the VM. They are DOM rules.
  • Classic Component (appendTo, readDOMAttr, this.element) and the event dispatcher call the DOM directly.
  • isSerializationFirstNode and the debug checks in @glimmer/debug read nodeType and nodeValue directly.

Checks

  • pnpm type-check and pnpm lint:eslint and pnpm lint:format pass.
  • pnpm test passes: 9221 tests, 0 failures, 21 skipped.
  • Performance is not measured. Each direct member access is now one method call on one class.

🤖 Generated with Claude Code

const node = (value: SimpleNode) => value as unknown as TreeNode;
const simple = <T extends SimpleNode>(value: TreeNode | null) => value as unknown as T;

class TreeRenderer implements DOMRenderer {

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 a lot 🙈

NullVoxPopuli and others added 2 commits October 27, 2026 16:44
The VM called the DOM in two ways.
Some calls went through `DOMTreeConstruction` and `DOMChanges`.
About 100 other calls read or wrote a member of a node directly,
for example `node.nextSibling` and `element.setAttribute()`.

`DOMRenderer` is one interface with every operation that the VM does
on the output tree. `DOMRendererImpl` is its implementation for a browser
document and for a SimpleDOM document. All code in `@glimmer/runtime` and
`@glimmer/node` now calls the interface, and nodes are only handles there.

A different output, for example a terminal, can implement the same
interface on its own node objects. The new test does that with plain
objects that have no DOM member.

Changes to private API:

- `Environment#dom` replaces `getDOM()` and `getAppendOperations()`.
- `EnvironmentOptions#dom` replaces `appendOperations` and
  `updateOperations`.
- `DOMRendererImpl` replaces `DOMTreeConstruction` and `DOMChanges`.
- `NodeDOMRenderer` replaces `NodeDOMTreeConstruction`.
- `InternalModifierManager#create` gets the environment as argument 5.
  The `on` modifier needs it for the listeners.
- `normalizeProperty`, `dynamicAttribute`, `clear`, and `move` take the
  renderer as argument 1.
- `sanitizeAttributeValue` takes the tag name, not the element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`renderComponent` takes `env.dom`, and `BaseRenderer.strict` takes a
`DOMRenderer` in place of a document. This gives a renderer that is not
the DOM an Ember entry point.

The proof test now renders with Ember's `renderComponent` and the run
loop, not with the Glimmer render functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NullVoxPopuli and others added 2 commits October 27, 2026 18:08
`EnvironmentImpl`, `runtimeOptions`, `BaseRenderer`, and the classic
`Renderer` take a `DOMRenderer`, not a document or an options object.
`EnvironmentOptions` is gone.

The private `env.document` option of `renderComponent` stays. It is
turned into a `DOMRenderer` before the renderer is made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A renderer (`BaseRenderer`, the classic `Renderer`) owns the roots and
the render loop. The object that does the output operations is a render
adapter, so the names say that:

- `DOMRenderer` (interface) → `RenderAdapter`
- `DOMRendererImpl` → `DOMRenderAdapter`
- `NodeDOMRenderer` → `NodeDOMRenderAdapter`
- `env.dom` → `env.adapter`, and the private `renderComponent`
  option `env.dom` → `env.adapter`

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent changed the title Add the DOMRenderer interface for every DOM operation of the VM Add the RenderAdapter interface for every output operation of the VM Oct 28, 2026

This branch has not been deployed

No deployments
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.

2 participants