Adam Gray

Blog
05/02/2024

Text Editing

I’m rebuilding fibers.app from scratch in order to dramatically improve the user experience. The new version will utilize more web standard APIs to achieve better cross-platform support and do away with some of the frustrations of the existing system (like not being able to select text!).

To make these (sorely needed) improvements, I’m building a virtual DOM from scratch. This involves modifying the tree based on input commands from contenteditable, then applying the changes to only the affected DOM nodes. This approach has already led to significant UX improvements and is far more scalable than my previous implementation.

Working at this lower level has provided an excellent playground for learning and implementing tree based algorithms. The manipulations involve insertion and re-ordering of nodes at many different levels of the tree, efficiently searching for nodes, and finding the most efficient path between two nodes. Gounding these algorithms in a real world problem has helped the concepts stick in my mind much better they ever did from reading a textbook.

//  example tree search algorithm from fibers v2

findCommonAncestor(anchor: string, focus: string) {
    const anchorNode = this.findNodeById(anchor, this.root);
    const focusNode = this.findNodeById(focus, this.root);

    const anchorAncestors: FiberNode[] = [];
    let n = anchorNode;

    while (n) {
      anchorAncestors.push(n);
      n = n.parent;
    }

    let commonAncestor = null;
    n = focusNode;

    while (n) {
      if (anchorAncestors.includes(n)) {
        commonAncestor = n;
        break;
      }
      n = n.parent;
    }

    return commonAncestor;
  }

The inspiration for building a virtual DOM, rather than directly manipulating the DOM itself was twofold:

  1. Building an virtual DOM will allow me to abstract away rendering into it’s own class. Providing a clearer path to supporting other platforms (e.g iOS).
  2. As the DOM API is so fully featured, each Node brings with it various functions and variables. These all need to be loaded into memory when operating on the DOM tree which is inefficient for the complexity of operations I’m working with. Building a virtualized DOM with the minimum amount of data to serve my needs ensures that more data can be served from the CPU cache, increasing the efficiency of operations.

There are complexities to this approach, the most obvious being that I’m now managing two trees that need to be kept in sync. For now, I’m making changes to the DOM tree as changes occur in the virtual DOM. This approach may change over time and I may end up implementing some form of tree reconcilliation algorithm. However, for now this more event-driven approach appears to be reliable and efficient.

If you are interested in following along with this rebuild, the code is open source and available on Github.