Skip to content

Conversation

@etrepum
Copy link
Collaborator

@etrepum etrepum commented Sep 29, 2025

Description

Addresses the createDOM/updateDOM/exportDOM part of #7259 by parameterizing the DOM functionality from the reconciler and @lexical/html to use the editor config via DOMRenderExtension. Part of the thinking here is that we may be able to leverage this work later to make the reconciler itself more flexible.

Follow-up work (out of scope)

Does not yet consider the different types of import/export, e.g. export for clipboard vs. export for serialization.

The same sort of approach can probably also be used for other tree based import/export (json, markdown ast, etc.).

$getDOMSlot could possibly be used for nodes other than just ElementNode. The motivating use case there would be able to inject "widget decorators" (in prosemirror terms) that sit before or after specific nodes to facilitate UI that is "outside" of the document. The sorts of things we do now with popovers.

EditorDOMRenderConfig

This data structure moves responsibility for all DOM rendering (create/update) and export to a single configuration in the editor. These $createDOM, $exportDOM, $updateDOM, etc. properties are all functions which are eventually responsible for calling their respective node methods (at least by default).

The trick here is that we can wrap these implementations with middleware style functions that can be composed to override or otherwise run code that happens "around" the default implementations. This is very difficult to do with the existing infrastructure, especially when writing code that needs to target all nodes, all nodes with some specific NodeState, etc. Since they are middleware, they have the ability to call the $next() function to get the result of the next implementation (e.g. "calling super" and then enhancing its result) or not (to completely override).

DOMRenderExtension

This provides a mechanism to compile the EditorDOMRenderConfig using composable configuration, targeting either all nodes with '*' or some subset of nodes by an array of NodeClass or $isNodeClass guards. This first pass is a relatively blunt approach but we can build more specific extensions to consolidate this wrapping in the future for optimization reasons.

Examples:

Enhancing TextNode export to remove the 'white-space' style unless it's necessary to support the encoding of the content:
domOverride([TextNode], {
  $exportDOM(node, $next) {
    const result = $next();
    if (
      $getRenderContextValue(RenderContextRoot) &&
      isHTMLElement(result.element) &&
      result.element.style.getPropertyValue('white-space') ===
        'pre-wrap' &&
      // we know there aren't tabs or newlines but if there are
      // leading, trailing, or adjacent spaces then we need the
      // pre-wrap to preserve the content
      !/^\s|\s$|\s\s/.test(result.element.textContent)
    ) {
      result.element.style.setProperty('white-space', null);
      if (result.element.style.cssText === '') {
        result.element.removeAttribute('style');
      }
    }
    return result;
  },
})
Adding an arbitrary id property to any node (e.g. for supporting deep linking)
domOverride('*', {
  $exportDOM(node, $next) {
    const result = $next();
    const id = $getState(node, idState);
    if (id && isHTMLElement(result.element)) {
      result.element.setAttribute('id', id);
    }
    return result;
  },
})

DOMImportExtension (WIP)

The flaws with importDOM are very apparent.

  • It's not a very optimizable interface. For a given tag, all implementations of the DOMConversionProp must be called first to see if it returns a conversion, and then they are sorted and then called until something returns. The way this should work is that the priorities should be with the initial predicate, so the sorting can be done when the editor is created.
  • It's not possible to do any sort of enhancement. There's no way to get the "next highest priority importDOM for this node". Existing attempts have to rely on static configuration (e.g. calling TextNode.importDOM and transforming it). This hack is not composable, you can only do this once.
  • There's no contextual data flow when you really need it, data from the parent often has relevance to how the children should be parsed (even the white-space style of a parent should control how Text nodes it contains are treated). The only hack right now is the forChild hack which is something but you have to decide what to do with Lexical nodes and not DOM. You also can't really ignore a subtree from a parent.

This implementation aims to solve these problems with a backwards incompatible API and by adding import context. Import context is a key:value store that cascades (each node has the opportunity to override any set of key:value pairs). Context keys can be created with createImportState, fetched with $getImportContextValue, and set with $withImportContext (for calling $next()) or by returning an array of pairs in nextContext and/or childContext to influence the behavior of the next importer for that node or its children.

Test plan

Currently it's primarily some minimal tests that show the legacy support works the same way that the legacy code does

Also ships a dev-node-state-style example that demonstrates one of the use cases (having NodeState apply styles to any node both in create and export).

@meta-cla meta-cla bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 29, 2025
@vercel
Copy link

vercel bot commented Sep 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
lexical Ready Ready Preview Comment Oct 27, 2025 1:13am
lexical-playground Ready Ready Preview Comment Oct 27, 2025 1:13am

@vercel
Copy link

vercel bot commented Oct 2, 2025

@etrepum is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant