1

With HTML Templates it makes it easy to stamp out snippets of html.

What is a sane way of populating the stamped out templates? In the MDN link above they render the template as follows:

    td = clone2.querySelectorAll("td");
    td[0].textContent = "0384928528";
    td[1].textContent = "Acme Kidney Beans 2";

This obviously only works if all the elements are the same tag, and ordered in the same way, which is very brittle.

What if I had a template like this:

<template>
  <div>
    <h2>__heading__</h2>
    <label>__label1__</label><input type="text" value="__value1__">
    <label>__label2__</label><input type="text" value="__value2__">
    <div>__instruction__</div>
    <label>__label3__</label><input type="text" value="__value3__">
  </div>
</template>

And say one had this data to render it with:

{
    __heading__: 'Lots of things',
    __label1__: 'label 1',
    __value1__: 'value 1',
    __label2__: 'label 2',
    __value2__: 'value 2',
    __instruction__: 'Do the thing',
    __label3__: 'label 3',
    __value3__: 'value 3',
}

Then the rendered result would be:

<div>
    <h2>Lots of things</h2>
    <label>label 1</label><input type="text" value="value 1">
    <label>label 2</label><input type="text" value="value 2">
    <div>Do the thing</div>
    <label>label 3</label><input type="text" value="value 3">
</div>

How would one render the template? PS if this is a XY question, you can use some other means to instead of the dunder fields.

I can only think of adding classes or attributes to each element which has a field to populate, and then perform lots of clonedNode.querySelector() ...seems very unelegant.

1
  • 1
    It is not a standard, but common to use the double-moustache notation: {{heading}}; you still have to do the parsing. Commented Sep 9, 2022 at 12:33

3 Answers 3

2

Note 9/9/2022: There is a proposal for DOM Parts on the table: https://github.com/WICG/webcomponents/blob/gh-pages/proposals/DOM-Parts.md


You could replace content with Template Literal String notation

string:

  <h1>Run ${name}! Run!</h1>
  Ain't <b>${tooling}</b> great!!

property Object:

{ 
   name: this.getAttribute("name"),
   tooling: "Web Components",
}
  • The parse(str,v) function
  • creates a new Function
  • with a String literal (note the back-ticks)
  • then executes that Function
  • passing all v Object values
  • and Returns the parsed String literal

<template id="MY-ELEMENT">
  <h1 style="background:${color}"> Run ${name}! Run!</h1>
  Ain't <b>${tooling}</b> great!!
</template>

<my-element name="Forrest"></my-element>

<script>
  function parse(str, v = {}) {
    try {
      return new Function("v", 
             "return((" + Object.keys(v).join(",") + ")=>`" + 
              str + 
             "`)(...Object.values(v))")(v) || "";
    } catch (e) {
      console.error(e);
    }
  };
  customElements.define("my-element", class extends HTMLElement {
    color = "gold";
    constructor() {
      super().attachShadow({mode: 'open'}).innerHTML = parse(
        document.getElementById(this.nodeName).innerHTML, // String
        { // properties
          name: this.getAttribute("name"),
          tooling: "Web Components",
          ...this, // to show you can add anything you want to the property bag
        });
    }
  })
</script>

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks Danny thats a great answer, I always learn something when I read one of your answers. My only concern is that I thought the advantage of templates is that its really fast because the browser "pre interprets" them, instead of having to parse it each time its stamped. This method I suspect negates the advantages of templates because of the string literal. The template might as well be a <div hidden>Some literal {{foo}}</div>
The only purpose of the <template> here is to create inert content, could also have used a JS String. Any code that creates new HTML (from a Template.innerHTML or an ordinary String) is using CPU cycles. Only re-used STATIC Templates have the benefit of having already parsed HTML. It all depends on your use-case; String parse gives you a lot of flexibility (also in DX). DOM parsing (your first example) gives you more speed (because the majority of the template was already parsed) (although I doubt you will notice the performance gain)
Before you dig yourself in a deep custom code hole; there is a standards proposal on "DOM Parts": github.com/WICG/webcomponents/blob/gh-pages/proposals/…
That looks great, DOM parts cant come soon enough! But I guessing we are years away from being able to use it.
1

One of the possible solution is manipulation of the HTML string itself. There is not much stuff you can do with content inside DOM tree without some special markup (like you mentioned in your question).

So, you clone the template and modify inner HTML of every child inside the template:

// Gather the template from DOM and clone it
const template = document.querySelector('#target_template');
const clonedDocumentFragment = template.content.cloneNode(true);

// Looks like there is no better way to modify HTML of the whole
// DocumentFragment, so we modify HTML of each child node:
Array
    .from(clonedDocumentFragment.children)
    .forEach(childElement => renderInnerVariables(
        childElement,
        {
            // Pass your values here
        }
    ));

// And here example of the method replacing those values
function renderInnerVariables(targetElement, variables = {}) {
    // Reminder: it can be really unsafe to put user data here
    targetElement.innerHTML = targetElement.innerHTML.replace(
        // Instead of looping through variables, we can use regexp
        // to get all the variables in content
        /__([\w_]+)__/,
        (original, variableName) => {
            // Check if variables passed and target variable exists
            return variables && variables.hasOwnProperty(variableName)
                // Pass the variable value
                ? variables[variableName]
                // Or pass the original string
                : original;
        }
    );
}

But since it's just a string content modification, you can pretty much just use string templates instead.

1 Comment

Does this solution no negate the high speed benefit of using templates. Look like a middle ground between one string literal and templates, i.e. you are using a string literal replacement for each direct descendant of the template.
0

If I understood correctly you want to create a template based on object.. if yes you can use this as a start..

What I did was to create a function getTagName(key) which will return a specific string based on the object key (it can be made with a switch most probably but eh.. :( switch on regex ) For example __value2__ will return input

Then I created a function createHtmlElementwith with some conditions in order to set the text/value to my element then I iterate through all the properties and append the newly created element

const data = {
  __heading__: 'Lots of things',
  __label1__: 'label 1',
  __value1__: 'value 1',
  __label2__: 'label 2',
  __value2__: 'value 2',
  __instruction__: 'Do the thing',
  __label3__: 'label 3',
  __value3__: 'value 3',
}
let container = document.getElementById('container');

// this can be done with switch i think, but for demo i did if statements :(
function getTagName(key) {
  key = key.replace('__', ''); //remove __ from front

  if (/^heading/.test(key)) return "h2";
  if (/^label/.test(key)) return "label";
  if (/^value/.test(key)) return "input";
  if (/^instruction/.test(key)) return "div";
}

function createHtmlElement(name, key, value) {
  let element = document.createElement(name);
  if (name === 'label' || name === "h2" || name === "div") {
    element.innerHTML = value;
  }
  if (name === "input") {
    element.type = "text";
    //element.value = key; // this is not working for i don't know what reason :(
    element.setAttribute('value', key);
  }

  // console.log(element) // decomment if you want to see the element as plain html
  return element;
}

for (const [key, value] of Object.entries(data)) {
  let tagName = getTagName(key);
  let element = createHtmlElement(tagName, key, value);

  let brTag = document.createElement("br");

  container.appendChild(element);
  container.appendChild(brTag);

}
<div id="container">
</div>

<!--
    <h2>__heading__</h2>
    <label>__label1__</label>
    <input type="text" value="__value1__">
    <label>__label2__</label>
    <input type="text" value="__value2__">
    <div>__instruction__</div>
    <label>__label3__</label>
    <input type="text" value="__value3__">   
 -->

1 Comment

Thanks for the answer, sorry I want to render a template with the given context, not create a template. I updated the question to make it a little more explicit.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.