1

Let's say I have:

<body>
  <div class="root">
    <div class="text1"></div>
    <div class="text2"></div>
  </div>
</body>

And want to insert some HTML after text2.

var targetHandle = document.querySelector('.root')
targetHandle.insertAdjacentHTML( 'beforeEnd', `
  <div class="text3"></div>
`);

We can get that handle two ways:

var targetHandle = document.querySelector('.root')
var newNode1 = targetHandle.lastElementChild
var newNode2 = document.querySelector('.root > .text3')

So far so good. But if the contents of insertAdjacentHTML is very large and contained many nodes:

var targetHandle = document.querySelector('.root')
targetHandle.insertAdjacentHTML( 'beforeEnd', `
  <div class="text3"></div>
  <div class="text4"></div>
  <div class="text5"></div>
`);

Using document.querySelector becomes a real burden to find every top level node, and a lastElementChild selector would only point to the last element being injected.

You might say, that's so silly, just wrap these DOM nodes with a parent so you can query and traverse that:

var targetHandle = document.querySelector('.root')
targetHandle.insertAdjacentHTML( 'beforeEnd', `
  <div class="parentToTheRescue">
    <div class="text3"></div>
    <div class="text4"></div>
    <div class="text5"></div>
  </div>
`);

And while this is a valid way to find these Nodes, we've made a compromise to our solution by creating HTML as a way to get them.

What I really want is a way to get handles of each top level Node being injected with insertAdjacentHTML but without the compromise of needing a parent wrapper:

var nodeList = [ ... ]

Couple ideas I have, which I'm not crazy about so far because I fear performance issues:

  • Use the parent wrapper anyway. After inserting get handles to child nodes. Copy/paste the children to the parents level, delete the parent.

  • Take a snapshot of the existing Nodes in the tree I'm about to paste into. After inserting take another snapshot and diff to find the new Nodes.

Looking for other ideas! Thanks

Dan Kanze
  • 18,485
  • 28
  • 81
  • 134
  • 1
    Why are you inserting them as HTML in the first place, rather than creating all the nodes and inserting them? – Barmar Aug 20 '19 at 21:09
  • Building a templating engine and state manager. The state manager needs the node list to clean up when changing states, the template engine needs the flexibility to enter large arbitrary blocks of html. – Dan Kanze Aug 20 '19 at 21:16
  • Sounds like the templating engine needs to parse the HTML itself. – Barmar Aug 20 '19 at 21:21

1 Answers1

2

const targetHandle = document.querySelector('.root');
const markup = `
  <div class="text3"></div>
  <div class="text4"></div>
  <div class="text5"></div>
`;
const elems = getInsertedElementsFromMarkup( targetHandle, markup, 'beforeend' );
elems.forEach( (elem) => {
  elem.textContent = 'I just got inserted in the doc';
});

function getInsertedElementsFromMarkup( target, markup, position = "beforeend" ) {
  if( !(target instanceof Node) ) {
    throw new TypeError( 'Argument 1 is not a Node' );
  }
  if( typeof markup !== "string" ) {
    throw new TypeError( 'Argument 2 is not a DOMString' );
  }
  
  // Convert our markup to a DocumentFragment
  const frag = target.ownerDocument.createRange()
    .createContextualFragment( markup );

  // Convert to Array,
  // DocumentFragments don't support `:scope` and HTMLCollection is live...
  const elems = [... frag.children];

  switch ( position ) {
    case 'beforebegin':
      target.parentNode.insertBefore( frag, target );
      break;
    case 'afterbegin':
      target.insertBefore( frag, target.firstChild );
      break;
    case 'afterend':
      target.parentNode.insertBefore( frag, target.nextSibling );
      break;
    default: // "beforeend"
      target.appendChild( frag );
  }

  // return our queried nodes, once they are in the doc
  return elems;
}
.root>div {
  border: 1px solid;
  height: 18px;
  margin: 6px 0;
}
<div class="root">
  <div class="text1"></div>
  <div class="text2"></div>
</div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285