Skip to content
Advertisement

How to uniquely identify DOM elements while using little size for the identifier

I am working on a project where two browsers should synchronize their DOM elements. Whenever a DOM element changes on one browser only the changes and the position of the changed element get sent to the other browser by a websocket.

In order for this to work I need a way to give the changed element a uniquely identifier that uses a little amount of size while the other browser should be able to locate the position of the changed element.

What would be a good approach for this?

Advertisement

Answer

To replicate one-way DOM-1 to DOM-2 you can use, in DOM-1, a document MutationObserver, which will notify you about:

  • attributes changes per each node
  • nodes that have been removed
  • nodes that have been added
  • nodes that have been moved, by combining the sequence of the previous notifications

If both DOM-1 and DOM-2 are identical since the beginning, an easy way to to uniquely map a node in DOM-1, and address it in DOM-2, is the following one:

const {indexOf} = [];
const nodePath = node => {
  const path = [];
  let parentElement;
  // crawl up to the document.documentElement
  while (parentElement = node.parentElement) {
    path.push(indexOf.call(parentElement.childNodes, node));
    node = parentElement;
  }
  return path;
};

const findNode = path => path.reduceRight(
  // crawl down from the document.documentElement
  (parentElement, i) => parentElement.childNodes[i],
  document.documentElement
);

In DOM-1 you would const path = nodePath(anyNode) and you’d find it in DOM-2 via findNode(path).

However, the thing the mutation observer won’t tell you, whenever its records populates removedNodes list, is where these nodes have been removed from.

To circumvent this limitation you need to store, in DOM-1, and likely through a WeakMap, all nodes that are appended in the document, so that you can always propagate the change to DOM-2.

// the most top script on the page (at least in DOM-1)
const paths = new WeakMap;
new MutationObserver(records => {
  for (const record of records) {
    for (const node of record.addedNodes)
      paths.set(node, nodePath(node));
  }
}).observe(document, {childList: true, subtree: true});

Now, whenever the other generic MutationObserver, in charge of notify, via Web sockets or anything else, DOM-2 for changes, you need to signal the operation type, which can be attribute, inserted, or removed, and for the removed case, you can send right away the path to crawl, in this case paths.get(node).

I hope these details are useful, as what you are trying to do is complex, but not impossible, and I won’t write the whole software for you, as that’s not what this site is about 👋

P.S. to have literally all nodes paths, you might also want to recursively set these paths in record.addedNodes, so that per each added node, you need to crawl all its childNodes and do so until all childNodes are mapped. This won’t be fast, but it’ll give you the ability to notify every single generic node on the page.

Advertisement