最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to embed a DOM node inside a template literal? - Stack Overflow

programmeradmin2浏览0评论

Is there any way to embed a DOM element inside a template string?

const btn = document.createElement('button');
btn.addEventListener('click', () => {
  alert("hello");
});

document.body.innerHTML = `
  <div>${btn}</div>
`;

Is there any way to embed a DOM element inside a template string?

const btn = document.createElement('button');
btn.addEventListener('click', () => {
  alert("hello");
});

document.body.innerHTML = `
  <div>${btn}</div>
`;

The above just calls HTMLElement's toString function and renders “[object HTMLButtonElement]” instead of the actual button.

Share Improve this question edited Nov 28, 2024 at 12:18 dumbass 27.3k4 gold badges38 silver badges74 bronze badges asked Feb 19, 2019 at 14:05 Slev7nSlev7n 3713 silver badges14 bronze badges 0
Add a ment  | 

4 Answers 4

Reset to default 2

You can use Element.outerHTML to get element as string. Note it will not append the real btn to div it will copy the element's html and eventListener attached to it will not work.

If you want the copy elements with its function you can use cloneNode() and appendChild() to insert it to parent.

let somediv = document.querySelector('#somediv');
const btn= document.createElement('button');
btn.innerHTML = 'Click me';
btn.addEventListener('click',(e) => {
  somediv.innerHTML = `<div>${btn.outerHTML}</div>`
  console.log(somediv.innerHTML);
})
document.body.appendChild(btn);
#somediv{
  position:absolute;
  top:200px;
  left:200px;
  padding:10px;
  background:blue;
}
<div id="somediv"></div>

No, the element itself can not be inserted that way. You could serialize it to HTML, but you'll lose any updates you made to the element, such as the event handler.

Instead you could create the entire structure using HTML, then select the button to add the listener.

someDiv.innerHTML = `
<div><button>click me</button></div>
`;

someDiv.querySelector("button")
  .addEventListener('click', handler);

var pre = document.querySelector("pre");

function handler(e) {
  pre.textContent += "foo ";
}
<div id=someDiv>
</div>
<pre></pre>

Such an API, if it existed, would not be able to return a string. A string consists of characters, not of DOM nodes with their own identities. And since it wouldn’t be a string, you wouldn’t be able to use it with .innerHTML (which can be a pretty poor API for interacting with the DOM for reasons I will not delve into here). That said, if there were a tag function taking HTML templates, there is something it could construct instead – a DocumentFragment. While you can’t assign one to .innerHTML, it’s pretty convenient to use with node-based DOM APIs like .appendChild, .insertBefore or .replaceChildren. But as of 2024, there is no such tag function built in – you will have to create your own.

The biggest obstacle is parsing HTML piecemeal. DOM APIs do not offer an interface that allows feeding inplete pieces of HTML code into the parser and modify the DOM in the middle of parsing – most parsing APIs are pletely black-box, and document.write is pretty inadequate too. Parsing the HTML pieces between interpolation points yourself is also a non-starter – unlike XML which is merely annoying to parse, HTML is fiendishly plex to parse. So the best you can do is to first insert placeholders at interpolation points, parse the resulting string, then look for the placeholders in the constructed DOM tree and replace them with actual values.

Below is a very crude proof of concept of how the latter might be done:

const html = (pieces, ...values) => {
  const frag = document.createRange().createContextualFragment(
    String.raw(
      { raw: pieces.map(piece => piece.replaceAll('\x91', '\x91\x92')) },
      ...values.map((_, i) => `\x91${i}\x92`)));
  const walker = document.createTreeWalker(frag, NodeFilter.SHOW_TEXT);

  while (walker.nextNode()) {
    let node = walker.currentNode;
    const parent = node.parentNode;
    let m;
    
    while (m = /\x91(\d*)\x92/u.exec(node.data)) {
      const ipoint = node.splitText(m.index);
      node = ipoint.splitText(m[0].length);
      let val = m[1] !== '' ? values[m[1]] : '\x91';
      if (typeof val === 'string')
        val = document.createTextNode(val);
      parent.replaceChild(val, ipoint);
    }
  }

  return frag;
};

// example usage

const button = document.createElement('button')
button.appendChild(html`<em>Please</em> click me, ${"Bobby <table>s"}!`);
button.onclick = (ev) => {
  alert("hello!");
};

document.body.appendChild(html`
  <p> Press this button: ${button} – or else.
`);

// example with a non-element node

const clockNode = document.createTextNode("");
const updateClock = () => {
  const d = new Date();
  clockNode.data =
    `${d.getHours()}` + ":" +
    `${d.getMinutes()}`.padStart(2, '0') + ":" +
    `${d.getSeconds()}`.padStart(2, '0');
};
setInterval(updateClock, 1000);
updateClock();

document.body.appendChild(html`
  <p> The current time is ${clockNode}. Try selecting this paragraph!
`);

The above is pretty inplete – putting an interpolation point in places other than within element bodies will yield incorrect results without so much as an error thrown:

// this leaves un-substituted placeholders in place
document.body.appendChild(html`
  <p> Visit <a href="${'https://example./'}">our website</a>.

  <p> Visit <a ${Object.assign(
    document.createAttribute('href'),
    { value: 'https://example./' })}>our website</a>.

  <!-- ${"will this work?"} -->
`);

This may be acceptable if you don’t ever need to interpolate data into those places, but it could have been addressed as well, even with the kludgy placeholder technique used here, by scanning DOM nodes other than text nodes. For the sake of simplicity, the above demonstrative implementation does not bother.

At least you don’t need to worry about spurious placeholder markers – and in fact the characters U+0091 and U+0092 are very good delimiter choices for that reason. That is because the only way they can appear within the parsed DOM is because they appeared literally in the source code, as HTML5 does not allow escaping them. Per the specification, most numeric character references in the C1 range do not resolve to their corresponding Unicode code points and have to be instead interpreted as referring to windows-1252 code points (HTML LS 2024-11-28 §13.2.5.80 “Numeric character reference end state”). This would normally be a misfeature, but in this case it works to our advantage: it means that escaping the delimiters within ambient code fragments (i.e. within pieces) is just a matter of dumb substring replacement and does not require implementing a full HTML parser – you don’t also have to worry about things like html` ${"foo"} &#x91;0&#x92;`.

in template literals the replacement values (${...}) by default are converted to their string representation ([object HTMLButtonElement] in your case).

It may be an idea to create a tag function to convert HTML element replacements [in a template literal] with their outerHTML. This idea is worked out in the next snippet.

Btw: innerHTML is considered unsafe and slow. There are several alternatives to inject html into an existing element. The snippet uses insertAdjacentHTML bined with a tag function and event delegation to handle a button click:

document.addEventListener(`click`, handle);

// ↓ a tag function to convert html *nodes* within
//   a template string to HTML *strings*
function insertElements(strings, ...insertions) {
  return insertions.reduce( 
    (result, value, i) => 
      result.concat( strings[i], 
      value instanceof HTMLElement ? value.outerHTML : value ), 
    "" );
}

// ↓ create a span
const span = document.createElement(`span`);
span.append(`Give me a button please! `);

// ↓ create a button
const bttn = document.createElement(`button`);
bttn.classList.add(`doclickme`);
bttn.append(`Ok, I Am Button`);

// ↓ append the button to the span
span.append(bttn);

// ↓ append the span to document.body using 
//   the tag function icw insertAdjacentHTML
document.body.insertAdjacentHTML(
  `afterend`, 
  insertElements`<div>${span}}</div>`
);

// ↓ document wide handling
function handle(evt) {
  if ( evt.target.closest(`.doclickme`) ) {
    console.clear();
    return console.log(`Hi. I am indeed added dynamically`);
  }
}

See also ...

发布评论

评论列表(0)

  1. 暂无评论