whatwg/html

Standardize <template> variables and event handlers

Mevrael opened this issue Β· 87 comments

Proposal by Apple - https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md
Proposal by Google - https://github.com/domenic/template-parts


This is a proposal on standardizing <template> parser which would allow developers:

  1. put variables,
  2. use simple statements
  3. and attach event handlers when template Node generated/inserted into DOM

Current stage: Open Discussion about the HTML syntax and JS API

Why?

Templates finally standardized the way of making re-usable parts of HTML, templates for widgets/components, however, they are still weak and one of the most general use cases in templates is to put data into them which, currently, is not possible without using a 3rd party library (template engine) or a custom implementation.

This brings:

  1. too much diversity across the Web (everyone is doing the same but in a different way)
  2. code works slower because it is not a native code which is implemented by a browser.
  3. bundle size increases and in case of huge production apps with many 3rd party components it is often required to import also different template engines or frameworks.

What is needed to be discussed and standardized?

  1. How variables should be included inside a <template>.
  2. How event handlers should be attached to elements inside a <template>.
  3. Which statements (for example "if") are allowed and how they should be used inside a <template>

1. Variables inside a <template>

I would +1 the most popular solution used across many template engines in many languages - variable name should be put inside a {{ }}. To make life even easier and faster it could be implemented as a simple string.replace. We may force developers to write a variable name without a spaces, i.e. {{ var }} will be invalid and {{var}} will work.

2. Attaching event handlers to a <template> elements

I would propose here new handler="method" attribute which would receive an Node when called.

Example HTML

<template id="card">
  <card>
    <h2>{{title}}</h2>
    <div>{{description}}</div>
    <a href="/card/{{id}}">Read more</a>
    <button handler="onEdit">Edit</button>
  </card>
</temlate>

Example JavaScript

In JS I would suggest just adding new function

Node parseTemplate(String id, Object data = {}, Object handlers = {})

because current syntax and manual clone/importNode is ridiculous. We already have functions parseInt, parseFloat, etc.

document.body.appendChild(parseTemplate('card', {
  title: 'Card title',
  description: 'Hello, World',
  id: 42
}, {
 onEdit(btn) {
   btn.addEventListener('click', () => {
      // ...
   });
  }
});

Currently I have no opinion on statements, may be, we could allow only simple inline statements like in ES6 template strings.

  1. Too much diversity across the Web (everyone is doing the same but in a different way).
    Is this a problem? It's not like they have to interact with each other, so I don't see why this matters.
  2. Code works slower because it is not a native code which is implemented by a browser.
    Again, is this actually a problem? Are you generating so much HTML that it has a noticeable negative impact on the user experience?
  3. Bundle size increases, and in case of huge production apps with many 3rd party components it is often required to import many different template engines and frameworks.
    Surely each component isn't doing the same thing, so they would still need their own individual code, would they not (not to mention needing to stay backwards compatible with older browsers)?

As it is, I can easily create 'Card's with a simple factory function in JavaScript:

function makeCard(title, description, id, method) {
    var card = document.createElement('div');
    card.classList.add('card');

    card.innerHTML = '<h2>' + title + '</h2>' +
                     '<div>' + description + '</div>' + 
                     '<a href="/card/' + id + '">Read more</a>' + 
                     '<button>Edit</button>';

    card.querySelector('button').addEventListener('click', method);

    return card;
}

This proposal breaks all sites that use angular default template. They will be forced to change {{ }} brackets to something else, I think.

@Yay295

  1. How your example is re-usable and scalable?
  2. How I can change the CSS of this card, add classes at least? (without rebuilding your JS of course)
  3. What is your example on more complicated components, like the row inside a datatable and you from JS can't know what columns, data UI engineer will need?
  4. How you can edit a template/layout/HTML from the back-end?

@rianby64

It doesn't breaks anything. Angular is not forced to use parseTemplate(), moreover, Angular in the future also will be able to benefit from the specification by keeping almost the same syntax.

@Mevrael Sounds good to add a new special case for element.textContent in order to process combinations like {{myVariable}} and compile it as Mustache, Angular and others do. But, what about if try something like this:

window.parseTemplate = (template, data) => {
  var clone = document.createElement('template');
  clone.innerHTML = Object.keys(data)
    .reduce((acc, variable) => 
      acc.replace('{{' + variable + '}}', data[variable]), template.innerHTML);
  return clone.content;
};

And instead of passing an id you could pass a template. The handlers param looks like should be in other place. parseTemplate shouldn't take a lot of responsibilities.

In this way, there were no need to add an special case for {{variable}}... I think.

@rianby64

This is how I and many other engineers do now with a custom function. The problem and fact is still with a "custom" implementation. Just having such simple parseTemplate() in a Web Standards would solve many small problems and people would know what to use in that case by default and stop writing another framework.

However, I may agree on that 1st argument should be a Node and not a String as all current native API works.

Don't have strong opinion on handlers. Yes, it could be done later.

There is still a question about how to handle at least simple if statements inside a template or what else we might need there by default. Since it is part of JS and we already have ES6 template strings implemented I believe it won't be too hard to implement this, something like:

<template>
  <alert class="alert {{ className ? className : 'alert-info' }}">
     ...
  </alert>
</template>

However I prefer real if constructions with may be custom syntax:

<alert class="alert 
@if(className) 
  {{className}} 
@else
  alert-info
@endif
">

I'm not going to respond at all to the templating language proposed. There are problems there, but they are secondary to the bigger issues.

Here are a few quick, before breakfast, thoughts:

  • One of the things that tends to differentiate frameworks is their templating language. So, none of the big players are going to agree with something like this. This isn't a technical critique but it's a matter of politics, which play a bigger part in web standards than anyone admits.
  • While this seems like a simple idea, this is actually a huge proposal. So, you aren't just trying to get consensus on a small contentious item. You are trying to get consensus on a bunch of large, complicated, contentious items.
  • Are you going to be the person to drive this? The above issues aside, something like this is going to take a lot of time and work. It wasn't too long ago that Object.observe (something that would be part of a proposal like this) was dropped because the person working on it just lost interest.
  • Templating languages/engines are still a pretty big area of research, experimentation and rapid change. It seems premature to tie the web to something like this now. Whoever did drive this is probably going to favor their own favorite framework, which is going to make everyone else mad...there's just a ton of baggage to deal with.
  • I don't think this is in line with the spirit of web "bedrock". There are things that could be done to improve the lower level capabilities of the template element which would be fundamental enablers to today's frameworks. These items should be done first and then, once those have trickled out, we can think of higher level abstractions.

Here are a few lower-level improvements that could be made to the template element:

  • Make sure that the template's content (DocumentFragment) has the same set of query selector methods as the Document. Right now, it's missing a bunch of methods and so less-performant querying is required by templating engines that rely on the template element. We should fix this.
  • Improve template cloning by enabling the consumer to pass a "reviver" function. Similar to how the reviver works when parsing JSON, the reviver used when cloning a template would pass each cloned node to the reviver function, enabling it to process the node. This would enable templating engines built on the template element to work more efficiently. As it stands today, they have to first clone and then query...and per the issue above, they have to use less performant query operations as well.

I'm sure there are others who have additional thoughts on improving template. I would prefer to stick to lower-level improvements at this point in time.

@EisenbergEffect

Make sure that the template's content (DocumentFragment) has the same set of query selector methods as the Document. Right now, it's missing a bunch of methods and so less-performant querying is required by templating engines that rely on the template element. We should fix this.

Yes, DocumentFragment has only getElementById(), querySelector() and querySelectorAll().

However, why do you even need to search the template.content (DocumentFragment itself)? You should NOT change it because by that you will also modify the template itself while it should be static. Template should be cloned first and then you will get a fresh instance of Node which you will be able to iterate/search/do whatever you want.

function cloneTemplate(id) {
	return document.getElementById(id).content.firstElementChild.cloneNode(true);
}

About your second point (reviver function). I agree that parseTemplate() at the end may receive some kind of callback (but definitely with a different name and not a "reviver"). Could you elaborate more, what do you mean by each node? Should it be each node in DocumentFragment only which usually has only one or should it be each Node with all children recursively, should it contain only element nodes and ignore text nodes?

In example above I used firstElementChild and in all templates I've seen they all had only one root node - component itself, row of the table, etc. Do we even need to return all the nodes or just first element child?

At the and if we will have an official template parser, wouldn't be it parsed simply as an template.innerHTML string without any nodes or you suggest to parse each node's textContent, arguments and children? There will be nothing to pass to a callback/reviver, well the parsed string could be and a custom template engine on the top of that could use it.

And about the

was dropped because the person working on it just lost interest

It is not up to a one person to change the world, it is the responsibility of all of us.

As far as I understand, this proposal has an small meaning because you can template/compile by using the current set of JavaScript and HTML features. The proof of this - there are already different template solutions in the market with different flavors. And this means, there's no need to bring template/compile/interpolate process to be part of the standard.

rniwa commented

I think there is a value in coming up with a standardized syntax for HTML templates just like template literals in ES6.

We can let each framework & library define how each string inside {{ and }} are interpreted but having this capability in the browser would avoid having to implement (often incompletely) a HTML parser in JS frameworks & libraries.

I do like tagged template string model. They’re simple enough for anyone to understand but can be powerful depending on what the tagged function does.

@rianby64 I don't think this would necessarily break Angular. This would only apply within <template>, and presumably be a new API. template.content should return the same nodes as it does now, but something like template.likeTaggedTemplateLiteral(callback) could provide new functionality.

rniwa commented

Yeah, this will most likely be an opt-in feature. We can add a new attribute to enable this. e.g.

<template processor=β€œmyLibrary.templateFunction”>
   ~
</template>

Then we may have, in scripts:

myLibrary = {~};
myLibrary.templateFunction = (template, parts) => {
    return parts.forEach((part) => { eval(part); })
}

where parts is an array of strings wrapped in {{ and }}. I think this model is a bit problematic in that most of template library would like to know the context in which these strings appear so more realistic model introduces a new interface that wraps each part so that you can query things like: part.element, part.attribute, etc...

There are a range of needs arounds templates, and many different ways they might be used. It'd be useful to list the needs to see how well a proposal might address them.

Some of the problems I'm aware of that that frameworks and template libraries encounter:

  1. Loading, finding and/or associating templates with components
  2. Finding expressions within attribute and text nodes
  3. Parsing expressions
  4. Evaluating expressions
  5. Stamping templates into nodes
  6. Re-evaluating templates to incrementally update previously stamped nodes.
  7. Implementing control flow constructs like if and repeat
  8. Implementing template composition or inheritance

In my experience, finding expressions within a template is the easiest problem of the above to tackle. Actual expression handling and incremental updates are the hard parts.

It would be great if platform supported template syntax could scale from a case where I just want to provide some data and get back nodes with expressions evaluated (all with workable defaults) to a case where I want to provide my own expression parser/evaluator, and want to get back a data structure I can use to bridge to a vdom or incremental-dom library.

The first simple use case would require some kind of "safe" or "controlled" eval that didn't have access to surrounding scopes. Something like evalScope(expression, scope). Then you could do:

<template id="foo"><div>{{ x }}</div></template>

Simple template stamping:

fooTemplate.eval({x: 42}); // <div>42</div>

Template interpreting with incremental updates:

const container = document.createElement('div');
container.appendChild(fooTemplate.eval(data));
const parsedTemplate = fooTemplate.parse(); // like template.content but expressions get their own nodes?

// later on data change, some made up APIs in here...
const walker = document.createTreeWalker(parsedTemplate, ...);
idom.patch(container, () => {
  while (walker.nextNode()) {
    const node = walker.currentNode;
    if (node.nodeType = NodeType.ELEMENT_NODE) {
      for (const attr of getAttributes(node)) {
        const attrs = new Map();
        attrs.set(attr.name, attr.hasExpression() ? evalScope(attr.expression, data) : attr.value;
      }
      idom.elementOpen(node.nodeName, attrs);
    } else if (node.nodeType = NodeType.TEXT_NODE) {
      if (node.hasExpression()) {
        idom.text(evalScope(node.expression, data));
      } else {
        idom.text(node.textContent);
      }
    } else { ... }
  }
});

I'm trying to show there that adding expression finding, parsing and safe eval could enable more advanced template rendering, maybe compatible with existing template systems, with much less code than now.

rniwa commented

I agree that adding parsing & custom processing/eval function is the key to make it work for many frameworks & libraries.

I think dynamic updating is nice-to-have but not must-have for v1 of this API. For example, if that custom processing function could return an arbitrary list of node, or could get hung of an node/attribute getting instantiated, then such a mechanism could be implemented in JS.

@justinfagnani

It would be great if platform supported template syntax could scale from a case where I just want to provide some data and get back nodes with expressions evaluated (all with workable defaults) to a case where I want to provide my own expression parser/evaluator, and want to get back a data structure I can use to bridge to a vdom or incremental-dom library.

This is exactly the final goal of this proposal.

However, implementing complicated parser at the beginning might be too much work to start from and, probably, for now we should just focus on simpler stuff which would allow just to put variables (including objects like author.name) into a parseTemplate() or similar function which would return a Node or a DocumentFragment.

So I would suggest for now discussing only those topics:

1. Should there be a global function parseTemlate() (or other name ideas?) If no what Interface/prototype/whatever that function/method should be part of?

2. Should the function above return a
a: Node always (only first template.content.childNode[0])
b: DocumentFragment always;
c: Node if template has only 1 node and fragment if there are > 1
d: other options?

3. Repeating single template many times if data is an Array of Objects and not an Object. Function should return in that case a DocumentFragment of nodes where each node is a same template but with a different data - for example to generate all table rows at once.

@rniwa for the incremental update case I'm talking about exposing the nodes so that it can be implemented in JS - not trying to bake in any incremental updating functionality into the platform (yet). In my snippet above I'm just iterating over the template nodes with expressions parsed out and calling into a library like incremental-dom to handle the updates.

To me parsing and evaluating expressions is key to this area because simply finding double-brace delimited expressions is trivial - it's by far the easiest task among everything that a template system has to take care of.

I implemented a template system on top <template> with a simplifying restriction that expressions have to occupy an entire text node or attributes. Checking for an expression is as easy as text.startsWith('{{') && text.endsWith('}}'): https://github.com/justinfagnani/stampino/blob/master/src/stampino.ts#L20

Even allowing arbitrary positions for expressions requires only a tiny more logic, and this work is often done once - making the performance benefits less than something that speeds up every render.

rniwa commented

@justinfagnani : Sure, by β€œparsing”, I mean that it needs to provide some kind of context to interact with the interleaved parts.

Okay, let's consider a concrete example.

<template id="foo"><div class="foo {{ y }}">{{ x }} world</div></template>

Here, x and y are variables we want to expose.

I think we want some interface that can represent a position in DOM like Range's boundary point and let the script set/get the text out of it. Let us call this object part for now. Then we want to be able to set string values:

y.value = 'bar'; // Equivalent to div.setAttribute('foo bar').
x.value = 'hello'; // Equivalent to div.textContent = 'hello world';  We might want to keep 'hello' as a separate text node for simplicity.

We might want to replace the content with a list of elements:

y.replace([document.createElement('span'), 'hello']); // Throws.
x.replace([document.createElement('span'), 'hello']); // Inserts span and a text node whose value is "hello" as children of div before ' world'.
x.replaceHTML('<b>hello</b>'); Inserts the result of parsing the HTML before ' world'.

We also want to figure out where these things exist in the template:

y.expression; // Returns y, or whatever expression that appeared with in {{ and }}.
y.attributeOwner; // Returns div.
y.attributeName; // Returns "class".
y.attributeParts; // Returns ['foo ', y].  If class="foo {{y}} {{z}}" then we'd have ['foo', y, ' ', z] instead.

x.expression; // Returns x.
x.parentNode; // Returns div.
x.nextSibling; // Returns the text node of ' world'.
x.previousSibling // Returns null.

As for how to get these parts objects, we can either have a callback for each part as the engine parses it or can have a single callback for the entire thing. I'd imagine having a single callback is better for performance because going back & forth between the engine code & JS is slow. So how about something like this:

foo.processor = (clonedContent, parts, params) => {
  [y, x] = parts;
  y.value = params.y;
  x.value = params.x;
  return clonedContent;
}
foo.createInstance({y: 'foo', x: 'hello'});

Supporting looping or conditional constructs that appear in many templating languages with this model requires a bit of thinking, however. Since the engine doesn't know that these constructs exist, it would probably create a single part object for each. Then how does library & framework clone these part objects so that it'll continue to function after such logical statements are evaluated?

To see why. Let's say we have:

<template id="list">
  <ul>
    {{foreach items}}
        <li class={{class}} data-value={{value}}>{{label}}</li>
    {{/foreach}}
  </ul>
</template>
list.processor = (clonedContent, parts, params) => {
  for (let part of parts) {
    ...
    if (command == 'foreach') {
      for (let item of params.items) {
          // BUT there are only one part for each: class, value, and label. 
      }
    }
    ...
  }
  return clonedContent;
}
list.createInstance({items: [{class: 'baz', value: 'baz', label: 'hello world'}]});

When this came up last time (in 2011?), we thought that these logical constructions need to have nested templates as in:

<template id="list">
  <ul>
    <template directive='foreach items'>
        <li class={{class}} data-value={{value}}>{{label}}</li>
    </template>
  </ul>
</template>

Then we can recurse whenever these constructs appear. To avoid having to traverse the cloned DOM, we can include each nested template instance in our part array so that we can do something like:

list.processor = function myProcessor(clonedContent, parts, params) {
  for (let part of parts) {
    ...
    if (part instance of HTMLTemplate) {
        [directive, directiveParam] = part.getAttribute('directive').split(' ');
        ...
        if (directive == 'foreach') {
          part.processor = myProcessor;
          part.replace(params[directiveParam].map((item) => { return part.createInstance(item); }));
        }
    }
    ...
  }
  return clonedContent;
}
list.createInstance({items: [{class: 'baz', value: 'baz', label: 'hello world'}]});

If people are fined with these nested template elements, I think this is a pretty clean approach.

rniwa commented

I guess an alternative approach to the conditionals & loops would be letting developers create part objects manually scripts to a specific place in DOM. That would allow scripts to create these part objects as they parse objects in addition to ones we automatically create initially.

I'm partial to nested templates, since it's been working very well so far in Polymer and my Stampino library. It also looks like it meshes very well with parts.

I really like the Part objects and being able to directly manipulate them, after creating a template instance from a new method.

This addresses one problem I kind of skip over as part 5) of my list above, and that's keeping track of where to insert content. vdom and incremental-dom approaches, as they have re-render-the-world semantics, can do extra work when conditionals and loops are involved - if they don't know that new nodes are being inserted to a location, they may change nodes that are after the insertion point which should instead be preserved. This is usually solved by assigning unique keys to nodes. Currently with <template> directives you can solve this problem by cloning the directives into the stamped output as placeholders, but this increases the overall node count, and has trouble in SVG.

Even though this doesn't address expression evaluation, it increases the ergonomics enough to be very useful, IMO, and I think it can be easily polyfilled :)

I'm almost afraid to bring up my next thought, but bear with me here... :)

I'm unsure about the stateful templateEl.processor setter, and how it relates to nested templates. It seems like the top-level template processor would need to understand the attributes/signals used on nested templates, or have some abstraction for that, when there's possibly an opportunity here to decouple nested templates from their parents a bit, and to rationalize the template processor function via custom elements.

That is, these could simply be customized templates with a special callback:

class FancyTemplate extends HTMLTemplateElement {
  cloneTemplateCallback(clonedContent, parts, params) {
    // ...
  }
}
customElements.register('fancy-template', FancyTemplate);
<template is="fancy-template"><div class="foo {{ y }}">{{ x }} world</div></template>

This allows for a few niceties like lazy-loading template processors and attaching them to all templates of a type with the same machinery we have for custom elements, creating auto-stamping templates that clone themselves on attach, etc. Of course this all can be done with wrapping to.

I think where it might really click with the rest of your API is with nested templates. If they are also just custom elements, they can interact with the parent template via an instance API to receive data:

<template is="fancy-template">
  <ul>
    <template is="for-each" items="{{items}}">
      <li class={{class}} data-value={{value}}>{{label}}</li>
    </template>
  </ul>
</template>
class FancyTemplate extends HTMLTemplateElement {
  cloneTemplateCallback(clonedContent, parts, params) {
    for (let part of parts) {
      if (part instance of HTMLTemplate) {
        // attributes parts, like items="{{items}}" must have been set first for this to work
        // passing params lets the directive access the outer scope
        part.replace(part.createInstance(params)); 
      } else if (part.isAttribute) {
        // in this hypothetical template system we set properties by default
        part.attributeOwner[part.attributeName] = params[part.expression];
      } else if (part.isText) {
        part.replace(params[part.expression]);
      }
    }
    return clonedContent;
  }
}

const list = new FancyTemplate();
list.createInstance({items: [{class: 'baz', value: 'baz', label: 'hello world'}]});

This example doesn't support incremental updates, but FancyTemplate.cloneTemplateCallback could stash parts and offer an update() that took the clonedContent and new data.

I do think overall this is a pretty clean approach.

rniwa commented

That's an interesting approach but if a template element is specialized for a specific purpose at the time of creation, it doesn't address your concern about processor being stateful.

Perhaps it's better for templete.createInstance to take a second argument which is a function that processes it.

Alternatively, template.createInstance could just be a function that returns a cloned tree with a new node type which has parts objects as it's property such that library can manipulate them.

e.g.

function instantiate(template, params) {
  let instance = template.createInstance();
  instance.parts.map((part) => {
    part.value = params[part.expression];
  });
  return instance;

One drawback with this approach is that we almost always need some framework / library code to supplement this feature. But this may not be the end of the world.

rniwa commented

Note that I can see some library may want to dynamically change the "processor" function at runtime based on the semantics or even "directive" specified on a template if it appears inside some other library-specific constructs. It may even need to combine multiple functions to create a processor at runtime (e.g. Pipelining filters).

From that perspective, passing a processor function as an argument to createInstance seems pretty clean to me because then template element doesn't hold onto any state.

We could even make this argument optional and make it possible to specify the default one via attribute so that an user of a library that utiliziss this feature doesn't have to specify it when calling createInstance.

Thanks everyone for interest and replies

To make this discussion organized and moving forward step-by-step please share your opinion by answering those questions:

For now let presume that there is something called a "Template Function (TF)" which needs a use-cases 1st, API 2nd, and an implementation third.

1. What TF should exactly do?

1.1. Only parse template.innerHTML/contents, everyone is ok with {{ }} syntax, what about spaces?

I would stay with {{ }}, spaces doesn't matter, they could be trimmed first.

1.2. Attach event handlers (how handlers should be defined - in the same function or there should be another one, should handlers be attached during TF call or they should be attached manually later?)

There are 2 options.

  1. Both parsing and attaching event handlers happens in the same TF, i.e. parseTemplate(tpl, data, handlers), however that approach won't allow to register a new event handlers outside of the TF.
  2. Have a "Template Events Function (TEF)", for example a new global function registerTemplateEvents(tpl, handlers) or a template.registerHandlers(handlers). This approach is more flexible and there could be a template.handlers property. When a TF is called and a Node/DocumentFragment returned - all the event handlers are already attached

Example syntax with a new handler attribute: <template id="card">...<button handler="delete">...</template>. and TEF(tpl, {delete(btn) { ... }}).

Totally against attaching event handlers manually later. They can be defined later (Option 2) but are still attached when a TF is called.

1.3. Have a built-in lexer and expose all the statements/variables to something like template.expressions?

Not a big fan of that, however, the only use-case I see is allowing developers adding a custom control structures/syntax to a template statements, for example to register {{ doit(user.name) }} and to do so there could be a a new function/method template.registerLexer()

1.4. What TF should parse itself (recognize) by default?

  • variables, including object properties
  • if, endif, inside only variables and ==, ===, !=, !==, >, >=, <, <= allowed
  • since JS don't have foreach - for (indexOrPropertyName in variable) and endfor

2. Should TF be a new global function, or a new method (where?), or a new instance (of what?). TF example names?

There is no need in complicating things and I am totally against any "instances" and class syntactic sugar. A new global function parseTemplate(template, data, ...?) would be enough. We don't have any global Template like JSON anyway. For now we have only a DocuemntFragment available in the template.elements, however, there also could be a template.parse() method. Since templates are always referenced by IDs I still like a short syntax with a parseTemplate(templateId, ...) more.

3. What TF should return? Always a Node and ignore rest child elements, or always a DocumentFragment, or it depends (on what, elaborate)?

First of all in my vision if there are multiple child nodes in DocumentFragment, ignore them, template usually have only one main element child

And about the return type, it depends:

  1. If params is an object, return a Node
  2. If params is an Array of Objects - return DocumentFragment, just parse same template but with different data sets, useful for table rows, list items, etc.

4. What about a nested templates?

May be leave it for now and discuss it later.

@Mevrael hey, just wanted to poke my head in here and lend some hopefully-wisdom to the great thread you started. Which is: don't try to keep too tight a reign on the discussion here :). You've gotten that rarest of things---implementer and framework interest---in a remarkably short time. I'd encourage you to let @rniwa and @justinfagnani continue their free-form exploration instead of trying to redirect their energies with large posts full of headings and to-do lists for them to address.

It's certainly up to you; just some friendly advice.

@Mevrael

since JS don't have foreach

JS does have a foreach for arrays.

rniwa commented
  1. I think we should agree on a single tokenizer / lexer. There is very little value in allowing different syntax. e.g. SGML allowed <, >, etc... to be replaced by other symbols but in practice, nobody did. Also one of the biggest benefit of standardizing templates is to have a common syntax at least for tokenizing things.

    However, I am of the opinion that we shouldn't natively support conditionals and logical statements like for, if, etc... at least in v1. Libraries and frameworks are quite opinionated about how to approach them, and I'd rather have an API which allows framework authors to easily implement those semantics than baking them into the platform at least for now.

    I don't understand what you're referring to by "event handler". If you're talking about regular event handlers like onclick, etc... then they should already work.

  2. It should probably be a method on HTMLTemplateElement's prototype. Adding new things in the global name scope for every new feature is not a scalable approach, and this doesn't need to be in the global scope at all since it's specific to templating.

  3. I don't see why we want to make that change. What are use cases which require this behavior? People have different opinions and preferences for these things, and it's not useful to discuss based on personal preferences and what you typically do. Instead, we need to evaluate each behavior & proposal using concrete use cases and evaluate against the cost of introducing such a feature / behavior.

  4. Nested templates is probably the best way of defining logical and looping so we absolutely have to consider it as a critical part of this feature.

rniwa commented

But most importantly, we need a list of concrete use cases to evaluate each proposal.

rniwa commented

Since I had some time over the MLK weekend, I wrote a polyfill in ES2016 for TemplatePart:
https://bugs.webkit.org/show_bug.cgi?id=167135 (BSD licensed with Apple copyright).

Why so complicated and why Apple copyright.

I've created a repository for making PRs on Use-cases and Implementation

https://github.com/Mevrael/html-template

And here is a first implementation which works currently only with vars and nested vars, i.e. author.name:
https://github.com/Mevrael/html-template/blob/master/template.js

It adds a parse(data) method to a template prototype.

Talking about the custom logic, I suppose many other methods I defined there could be also available in JS and authors, for custom logic could be able to replace any method on template prototype.

Talking about the nested templates, it is NOT part of this proposal, moreover, currently <template> inside a <template> is also not supported. I don't see any use-cases there.

rniwa commented

The most of complexity comes from having to monitor DOM mutations made by other scripts. Apple copyright comes from a simple fact that it is Apple's work (since I'm an Apple employee). Also, please don't move the discussion to another Github repo. For various reasons, many of us won't be able to look at your repository or participate in discussions that happen outside WHATWG/W3C issue trackers and repositories.

A template element inside a template element definitely works. In fact, this was one of the design constraints we had when we were spec'ing template elements. If anything with a template inside another template doesn't work that's a bug (either of a spec or of a browser).

I'm curious about

template inside another template

and the cases it brings up... If recursion is possible, then I see two places:

<template>
  Some HTML code with some {{variables}}
  <template>
    Some nested-HTML code with {{variables}}
  </template>
</template>

And...

<script>
  // for the sake of this example, the following variable 
  // will be bound with the template below...
  var nestedTemplate = `
  Some HTML code with some {{variables}} and a template
    <template>
      Some HTML code with some {{variables}} and a nested template
    </template>`;
</script>
<template>
  {{ nestedTemplate }}
</template>

So, there could be templates that hold templates, and variables that hold templates... Will be there any restriction to these cases?

The most of complexity comes from having to monitor DOM mutations made by other scripts.

Why you even need it? A. Templates are static. B. Dynamically changing template will not change nodes created from that template, that what copy/clone means and anyway changing template in a such way is a bad practice because of A. Templates are static.

Anyway dynamically changing the template contents will only affect nodes created from that template only after that because parse() parses content in that time, however, cache also may be implemented.

Apple copyright comes from a simple fact that it is Apple's work (since I'm an Apple employee).

Well, expected answer but let me explain you from the legal point of view that you actually even can't call anything a company's work if you wasn't given an order to implement that task by the company. It is only your own work and Apple has 0 connection to it.

Also, please don't move the discussion to another Github repo.

I am not moving discussion. Discussion stays here. However, we need a repo for implementation where everyone can send PRs and keep track on the code and use-cases. You put a code in a webkit's issue tracker which is not an option either.

Every discussion should be organized. Everything we are going to sum up and implement as a result of this discussion will be contained in an organized way in a separate repo, I also don't care about the namespace. Nevertheless in the future we will need a polyfill which everyone would be able to download from a GitHub or via npm.

Now back to nested templates

  1. Currently in which browser I can check that <template> inside another works?
  2. Can someone point to the paragraph in the spec describing how such case should be handled?
  3. Any real world examples with templates inside a template? It would add too much confusion, how at least simple parse(data) should work? In case of recursion you are using the same template, that what recursion means. I've been rendering a data in adjacency list format as a tree of comments with sub-comments from a single template, just within a template I had a label where nested template could be placed, something like:
<template id="comment">
  <comment data-id={{ id }}>
    <a href="/users/{{ author_id }}">
      <img src="{{ author.photo_path }}">
    </a>
    <p>{{ message }}</p>
    <answers>
      {{ answers }}
    </answers>
  </comment>
</template>

Only such kind of "nesting" I can approve, but again there would be a question - should we implement a support for adjacency list which would come from an SQL or more like a document store from a NoSQL? People are handling this data structure differently.

How those "tests" are related to my questions?

  1. Link to spec
  2. Use case with real example

Anyone ok with using only first element node in template, parse it and return a valid node? If no, any real world examples where templates are used in a different way? I am not talking about cloning same template multiple times.

rniwa commented

The most of complexity comes from having to monitor DOM mutations made by other scripts.
Why you even need it? A. Templates are static. B. Dynamically changing template will not change nodes created from that template, that what copy/clone means and anyway changing template in a such way is a bad practice because of A. Templates are static.

You need it in order to support an instance of a template content getting mutated by other scripts. It's possible that we just spec so that updating a template instance wouldn't work as soon as you've mutated the instance's subtree but I made our version a lot more granular than that because my experience tells me web developers DO expect "part" objects to continue to work even after DOM is mutated by other scripts.

As for nested templates, you might want to read up on how Polymer uses them: https://www.polymer-project.org/1.0/docs/devguide/templates

my experience tells me web developers DO expect "part" objects to continue to work even after DOM is mutated by other scripts.

If someone expects that a new fresh created/cloned Node should change because something else changed then it is only his/her expectations and problems. You can do whatever you want with a template contents, but .parse() will generate a new Node based on a template contents at exact that time. And as I said above template contents should not be modified never ever by any script. If someone changes native prototypes and breaks a browser this is only his/her problems. Templates are static, that what template means, it is the same as in C++ to modify a class in a runtime.

And bout the nested templates:

Well, what I see in Polymer is not a nested templates but a their weird if/endif/for/forech/endfor/endforeach syntax. What else to expect from a Google with 0 design thinking.

Templates should be simple.

Each template should define only one component. Sub-components are also components and should be in a separate template.

If you want to render a list in a template you may use foreach

<template>
  <article>
    ...
    {{ foreach comments as comment }}
       <comment>
          {{ comment.message }}
       </comment>
     {{ endforeach }}
  </article>
</template>

However by that you loose flexibility, what if I need to create just a comment, but there are no template for comment? You just define 2 templates and, maybe, referencing one template from another, something like:

<template>
  <article>
    ...
    {{ foreach comments in 'comment' }}
  </article>
</template>

<template id="comment">
   <comment>
       {{ message }}
    </comment>
</template>

Having worked with Google and interacted with multiple teams there, though I disagree with many of their choices, I would never attribute to them zero design thinking. If you ever want to see something like this become a standard, you should refrain from insulting other framework teams, especially when the framework authors work for the browser vendors you are ultimately going to have to convince to implement this thing.

... template contents should not be modified never ever by any script.

While I agree that a template should be static, this would prevent someone from creating a template from JavaScript. Every template you might want to use would have to be already defined in the HTML.

The reason why nested <template>s for control flow like if and repeat, makes a lot of sense to us is that they semantically are template-like: they are chunks of dom that may or may not be stamped out one or more times. I'm not sure what the fuss is about though, since nested templates already work just fine. Their interaction with @rniwa's ideas here would likely just be whether <template> is automatically a part.

Added: while I think nested templates are an elegant solution, it would be nice if it were possible to do handlebar-type control flow as binding expressions. That {{ foreach ... }} and {{ endforeach }} would be parts might be enough. The template processor would be responsible for collecting the DOM and parts contained by the foreach, dealing with nesting, and stamping.

rniwa commented

While writing my polyfill, I realized that we don't even have to make an inner template a part because you can just querySelectorAll('template') unless you needed to know the ordering of it with respect to other parts. Is that important in Polymer's use case? (Remember that parts inside inner template won't appear until you instantiate the inner template).

@rniwa I don't think the ordering matters so much in Polymer, but I could see it being convenient for some template language that maybe has side-effects.

What is really, really useful about <template> being a Part is the ability to replace it with content. Tracking where a template ends up in content is one reason why Polymer puts template instances in the output when ideally it wouldn't need to. If part.value or part.replace() can just take care of putting content in the right place that take care of a lot of complexity.

rniwa commented

Oh, interesting. So you want to create NodeTemplatePart object (in my polyfill) at where template element appeared. I've started to think maybe the solution here is to let author manually construct a TemplatePart? The simplicity of just having to worry about an inner template is nice though.

Sorry for the length comment here I wasn't aware of this discussion until now.

I think there is some value in this still even though web components have vastly solved most of the use case.

Some comments on my main two previous directions:

  • Making an extensible parser for templates
    • This doesn't start a syntax war
    • This also doesn't prevent a future "native" format
  • Adding elements to be exit points for variables
    • This would permit observing of data which is a use case people want (rebuilding a template is expensive each time)
    • Could reuse slots?

Another idea is to use template string format as this is already established, potentially marking it as JavaScript.

<template id="test" type="application/javascript">
  <div>
    <h2 class="heading">${heading}</h2>
    <div class="content">${content}</div>
  </div>
</template>

With usage like:

<script>
  let statements = [
    {heading: 'Shouty statement', content: 'Going somewhere'},
    {heading: 'Appeasing hook', content: 'Disappointing ending'}
  ];
  let template = document.getElementById('test');
  statements.forEach(function (statement) {
    document.body.appendChild(template.content.emit(statement));
  });
</script>

All in all though I think @EisenbergEffect's comment here makes the most sense


To @Mevrael
Fragmented discussion
This discussion has been fragmented at least five times from its original place:
https://discourse.wicg.io/t/extension-of-template/447

Of which I would argue the message board is still a better place for discussion where bugs are for work however I know this is isn't the case here sometimes. Either way I wish I was made aware of the discussion.

Be polite

Insulting design decisions of frameworks/authors isn't going to get you any love either. Syntax is actually the least interesting part of the discussion here. Sure it is great to explain your view however please refrain from suggesting things are "ridiculous", "crazy", "0 design thinking".

WICG discussion responses

Thank you for reply. Actually main discussion is now on GitHub #2254 since it is easier for developers to contribute and many people are not aware of this platform.

It's not easy for developers to follow 4 threads, if you want more exposure point everyone to the same discussion. I wasn't aware until now.

I still totally against "web components" and that weird syntax of "classes"

Well if there is an issue, it should be fixed also. I would however recognise that this won't be implemented before components are done.

You already pointed out that Google with Polymer moves it into different angle and doesn't cares about standardization.

I didn't say that, yes they add features but they do care about standards for web components. Their API has shaped the standard.

There is difference between chat and summary.

I agree, but please loop existing people into the discussions next time.

I've been talking about this idea with more people recently, and one thing we realized should be clarified is that we would also need to be able to do some processing on the apparently static parts of the templates, like the attribute names.

This is particularly important for template languages that allow setting of properties, or adding event handlers, via HTML syntax, ie:

<template>
  <my-element prop="{{value}}" attr$="{{thing}}" on-click="{{onClick}}"></my-element>
</template>

Here, at least in our syntax, setting the value for {{value}} or {{onClick}} should not set the attributes prop or onClick, and {{thing}} should set the attribute attr (not attr$).

I think this should be fine with either createInstance or cloneTemplateCallback or other similar ideas, but I wanted to point it out since I missed it in my list of template system concerns above.

Also from the conversations, I think that even without any expression parsing or evaluation (which would really help still, IMO), it's clear that just tracking parts/dynamic holes, would add a lot of value to <template>.

(not specifically related to the above comment)

Why does this proposal involve templates? Given that this syntax is entirely embeddable within HTML - i.e. the information about these 'parts' would survive cloneNode because they're valid attribute values and text node content - it seems like the process of extracting embedded 'parts' could apply to any node.

Consider this HTML as if it were somewhere in a document outside of a template:

<div id="theDiv" some-attr="${someAttrPart}">
  ${firstTextPart}
  <span>middle text</span>
  ${secondTextPart}
</div>

It seems like I should easily be able to do this:

const theDiv = document.getElementById('theDiv');
const theDivParts = theDiv.extractTheParts(defaults);
theDivParts.someAttrPart.set("some-attr value");
theDivParts.firstTextPart.set("Here's some text.");
theDivParts.secondTextPart.set("Here's some more text.");

Also, I think that this proposal desperately needs clarification of the interaction between the proposal and the DOM API when used on the the nodes which have had 'parts' extracted. Particularly, what happens if I start manipulating attributes that have 'parts'? What happens if I start adding or removing child nodes of a parent node which had (has?) 'parts' in its child text nodes? If I push data to a part after doing either of these things, what happens? Are the 'parts' now inert? Here's an example of this problem:

<div id="theDiv">Hello, ${name}!</span>
const theDiv = document.getElementById('theDiv');

const theDivParts = theDiv.extractTheParts({name: "Alice"});
// I assume the text content of #theDiv is "Hello, Alice!".

theDiv.appendChild(new Text(" How are you?"));
// I assume the text content of #theDiv is "Hello, Alice! How are you?".

theDivParts.name.set("Bob");
// Is the text content of #theDiv now "Hello, Bob!" or are we attempting to
// make this produce "Hello, Bob! How are you?"? If so, how do we track the
// location of the part?

One possible way handle the "'parts' are in child text nodes and the children are changed" situation might be to have the 'part extraction' process record the child nodes generated after the parts are extracted and have "setting data to a part" cause that same set of children be restored, if changed, when a part is set. In the above example, the text after the last set would be "Hello, Bob!". That way, there's no need to worry about where a part is in the tree after something has caused the element's children to change. Regardless of the preferred behavior, these interactions need to be strictly defined.

(moving to a different topic)

Also, I don't think that the embedded DSL use case is particularly compelling because I think we should be encouraging people to write their logic in JavaScript rather than making it even more compelling for every framework to invent their own ad-hoc flavor of PHP. Although I'm not a fan of logic in templates in general, I think Polymer actually found a decent way to rationalize this concept that doesn't require some entity (read: a 'template engine') being given complete control over some region of the DOM. You can create a custom element that expects attributes or properties as inputs to the logic it encapsulates and a set of templates as children to use as output options. The element waits for changes to the input attributes / properties and then does whatever logic it needs to do to decide which / how many templates it needs to clone, where and in what order to put them, and what data should be passed to them - possibly through 'parts' you've set, but not extracted, in the templates you provide it.

rniwa commented

Why does this proposal involve templates? Given that this syntax is entirely embeddable within HTML - i.e. the information about these 'parts' would survive cloneNode because they're valid attribute values and text node content - it seems like the process of extracting embedded 'parts' could apply to any node.

The problem that a regular HTML markup isn't "inert". Any img, script, and link element would immediately start to load and execute. Yet you rarely want that behavior when you're defining a re-usable template.

Also, I think that this proposal desperately needs clarification of the interaction between the proposal and the DOM API when used on the the nodes which have had 'parts' extracted. Particularly, what happens if I start manipulating attributes that have 'parts'? What happens if I start adding or removing child nodes of a parent node which had (has?) 'parts' in its child text nodes? If I push data to a part after doing either of these things, what happens? Are the 'parts' now inert? Here's an example of this problem

Quite right. In my early prototype of my proposal, you see that I'm mostly modeling after how Range reacts to DOM mutations. Given Range is a well established object in DOM and how it reacts to DOM changes is quite well understood, we should probably follow the same model if we were to make it robust against DOM mutations.

I agree with your overall point that a lot of details need to be hashed out. This is why we're adding this as a topic discuss at this year's TPAC.

@EisenbergEffect @domenic @niwa @justinfagnani @Mevrael @jonathanKingston I've been watching this thread for a while now. Created a low level library in response to needing some solid use cases. Weighs about 1kb and a testament of not needing too much more than what the platform provides us already. I feel we've solved many of the use case experiments in this entire thread over the past 6 months. At least from the use cases being requested. Would like to provide some conventions discovered from someone who feels we should be using the platform more than trying to create a rounder wheel. Rare to see anything that is remotely CSS or HTML or Javascript these days. Usually a hybrid of all three. Which i think diverges away from the hard work the standards bodies are putting in over the recent years. All that is needed is lightweight syntactic sugar DSLs around existing DOM APIs. Which is the responsibility of the dev not the platform. Again do keep in mind these are not suggestions but actual working cases we use in production today. Working code based on current status of implementation, not so much (only) theory which too much of tends to stifle progress.

https://github.com/devpunks/snuggsi

I've separated all the concerns into respective Web APIs:

GlobalEventHandlers - ("automagically" binds functions of same name as on* events)
EventTarget - (implements EventTarget.on ('click', this.handler) interface also binding this to upgraded exotic custom element)
HTMLTemplateElement - (implements HTMLTemplateElement.bind (object) API interface)
Element - (custom element factory)
HTMLLinkElement (for imports)
TokenList - (for token replacement) -

GlobalEventHandlers.on(*) Event Registration

The TreeWalker which has been around since DOM Level 2 is used to walk the DOM tree and introspect/reflect on* event registrations defined on our custom elements (outside the scope of this issue).

<foo-bar>
  <button onclick=onbaz>Baz Fired by {organization}</button>
</foo-bar>

<script src=//unpkg.com/snuggsi></script>
<script>

Element `foo-bar`

(class extends HTMLElement {

  get organization () { return 'WHATWG' }

  onclick (event) { // this gets automatically registered and bound to `this` foo-bar
    event.preventDefault () // stops default re-rendering of {tokens}

    console.log ('this is "automagically" bound to element from `GlobalEventHandlers.on*`')

    this.render () // re-render templates & tokens on next rAF `requestAnimationFrame`
    // no need to call `.render ()` if `event` wasn't prevented previously.
  }

  onbaz (event) // event.target is the `<button onclick=onbaz>`
    { console.log (this, 'is bound to the custom element foo-bar') }
})

</script>

HTMLTemplateElement

We have a <template> in the DOM and need to:

  1. Bind a context (or Javascript object) to the template
  2. Append rendered template to the document.
  • If context is an object bind a single <template>.
  • If context is a collection (i.e. Array) bind a tandem collection of <template>s.
  1. or use default <template> from within an HTML Import.
    Templates work like this: JSFiddle Example

<template> With Object Context

<section id=lead></section>

<template name=developer>
  <!-- `{name}` will bind to `context` property `name` -->
  <h1>{name}</h1>
</template>

<script nomodule src=//unpkg.com/snuggsi></script>
<script nomodule>

const
  template = Template `developer`
, context  = { name: 'That Beast' }

template
  .bind (context)

document
  .getElementById `lead`   // select element to append bound template
  .append (template.content) // .content returns an appendable HTMLDocumentFragment
  // see https://html.spec.whatwg.org/multipage/scripting.html#dom-template-content

/*
   <section id='lead'>
     <h1>That Beast</h1>
   </section>
*/

</script>

<template> With Array Context

<ul>
  <template name=item>
    <li>Hello {name}!</li>
  </template>
</ul>

<script nomodule src=//unpkg.com/snuggsi></script>
<script nomodule>

// when context is a collection
const
  template = Template `item`
, context  = [ {name: 'DevPunk'}, {name: 'Snuggsi'} ]

template
   // internal template render for each item in context
  .bind (context)

document
  .querySelector `ul`
  .append (template.content)

/*
<ul>
  <li>Hello DevPunk!</li>
  <li>Hello Snuggsi!</li>
</ul>
*/

</script>

Default <template> (HTML Custom Element Import)

foo-bar.html

<template onclick=onfoo>
  <h1>foo-bar custom element</h1>

  <slot name=content>Some Default Content</slot>

  <ul>

  <template name=bat>
    <li>Item {#} - Value {self}
  </template>

  </ul>

</template>

<script>

Element `foo-bar`

(class extends HTMLElement {

  onfoo (event) { alert `Registered on foo-bar` }

  get bat ()
    { return ['football', 'soccer', 'baseball']}
})

</script>

index.html

<script src=//unpkg.com/snuggsi></script>

<link rel=import href=foo-bar.html>

<foo-bar>
  <p slot=content>The quick brown fox jumped over the lazy dog
</foo-bar>

<!-- After import

<foo-bar onclick=onfoo>
  <h1>foo-bar custom element</h1>

  <p slot=content>The quick brown fox jumped over the lazy dog

  <ul>
    <li>Item 0 - Value football
    <li>Item 1 - Value soccer
    <li>Item 2 - Value baseball
  </ul>
</foo-bar>
-->

@Mevrael The following uses a <template> inside of a <template>. devpunks/snuggsi#42

The use case is to have a <nav-view> element. Since this element is an HTML Import a convention we use is to have a <template> that is the default innerHTML of the custom element. The functionality of this element is to create <a>nchor links based off found <main view>s within the document.body.

The custom element is also responsible for toggling the visibility of each Page Fragment based on Fragment Identifier found relative to it's respective anchor.

Working code sample

Live Demonstration

gif

Also here is a full working calendar with nested templating as well for date dropdown and days. The convention we use is to map tokens within the <template> to computed properties of the exotic element registered with CustomElementRegistry

Calendar Demo with <template>

Lastly there seems to be little to no jank while TreeWalking template content. Library loads in 1ms and renders events in ~1ms.
capture d ecran 2017-06-17 a 10 29 29
capture d ecran 2017-06-17 a 10 28 30

The library is merely conventions around most of what is discussed here. Is in no way a suggestion. Just real world use cases as was requested. Down to template rendering. Library loads in microseconds and weighs 1.5kilobytes.

Please let me know where I can fit in to this conversation as i've dedicated a large portion of the past 6 months of my life on this topic and would love to give feedback wherever I can. But pretty much we've got a working model that is a super thin layer of the existing api available today.

<template> has been a Godsend! Thanks for this!

Please advise. And thanks in advance!

@rniwa

The problem that a regular HTML markup isn't "inert". Any img, script, and link element would immediately start to load and execute. Yet you rarely want that behavior when you're defining a re-usable template.

I don't think it's necessary to make the "initialize parts" process in this proposal only available on templates to avoid this scenario. Specifically, if "initialize parts" was defined as (e.g.) some method of Node or Element then you could continue to use a template for the purpose of defining inert structure with parts; however, instead of asking the template for a clone with initialized parts, you would clone the template first and then immediately "initialize parts" of the clone. This way, the benefit of the template content's inertness isn't lost yet the template isn't involved in "part initialization". I agree that this feature seems most likely to be used with a template than without one but restricting it to templates feels artificial.

Given Range is a well established object in DOM and how it reacts to DOM changes is quite well understood, we should probably follow the same model if we were to make it robust against DOM mutations.

That sounds pretty reasonable to me.

rniwa commented

instead of asking the template for a clone with initialized parts, you would clone the template first and then immediately "initialize parts" of the clone. This way, the benefit of the template content's inertness isn't lost yet the template isn't involved in "part initialization". I agree that this feature seems most likely to be used with a template than without one but restricting it to templates feels artificial.

But what's the use case for allowing this on non-template elements? Polluting Element's attribute space has a very high cost (both in terms of backwards & forwards compatibility) so we shouldn't do it unless there is a very compelling use case for it.

Polluting Element's attribute space has a very high cost (both in terms of backwards & forwards compatibility) so we shouldn't do it unless there is a very compelling use case for it.

If this feature, which could apply to all elements, is not compelling enough to be applied to all elements then maybe this should not be a property of the HTMLTemplateElement (or Element) prototype?

For example, given this template:

<template id="template">
  <span>Hello, ${name}!</span>
<template>

the code to grab the parts changes from something like this:

const {clone, parts} = template.cloneAndExtractTheParts(defaults);

to something like this:

const partExtractor = new PartExtractorThing(template);
const {clone, parts} = partExtractor.cloneAndExtractTheParts(defaults);

?

Or rather, since the my last example still used a template:

const clone = template.content.cloneNode(true);
const partExtractor = new PartExtractorThing(clone);
const parts = partExtractor.extractTheParts();

or maybe

const clone = template.content.cloneNode(true);
const partExtractor = new PartExtractorThing(clone);
partExtractor.extract();
// partExtractor.parts is now a map of parts by name?

In the Example JavaScript, the third parameter is either a SyntaxError or a new feature where object literals have function calls, as the call to onEdit.

document.body.appendChild(parseTemplate('card', {
  title: 'Card title',
  description: 'Hello, World',
  id: 42
}, {
 ***onEdit***(btn) {
   btn.addEventListener('click', () => {
      // ...
   });
  }
});

I'm very very expect of this feature, which will make HTML5's template easily to use likes other more javascript template engine.

caub commented

I think this template standard is interesting, with the variables addition, but what's most interesting are event handlers, and I think it's best solved with a template tagged function (https://github.com/caub/dom-tagged-template an attempt), but the validation is not as strong as <template> or jsx, since it's dynamic

caub commented

@rniwa I see, but sorry, it looks not practical to me, just like Angular, .. it tries to copy. A more React-like approach would be awesome, I'll try to make a proposal if I can

@rniwa FWIW createInstance => create.
Also, why not just evolve from Mustache? IMHO <template>{foo}</template>. Perhaps reserving {{escaped}} for brace escaping. A superior dev ergonomics experience than <template>{{foo}}</template>. I do realize old habits die hard but may be the perfect timing to make conventions more succinct if we have the opportunity. /cc @brandondees

@snuggs the expression delimiter can be changed to anything reasonable - the proposal doe not hang on it being {{}}. I would strongly argue against {} though because it interferes with CSS and JavaScript. Look at how ugly it is to embed CSS in JSX.

@caub it's important to keep in mind the new capability this proposal includes: the ability to create and update large trees of DOM without having to do any tree walks or create wrapper objects for static nodes. That's the really important thing here for the platform and all kinds of frameworks and template libraries.

It's easy to look at the specific syntax and think that it looks like Angular or Handlebars, but in many cases users might not even be writing these template by hand - they'll be generated at compile time or runtime from some other template representation, even JSX.

lit-html, a HTML-template-in-JS library (that works and looks similar JSX) I've been working to on generates HTML templates from JS template literals. It would easily be able to take advantage of this proposal instead of it's own template part management. Someone even wrote a JSX-to-lit-html compiler, which could probably be changed to target this proposed spec directly, giving us a way to speed up JSX directly in the platform.

1. How variables should be included inside a <template>, and 
3. Which statements (for example "if") are allowed and how they should be used inside a <template>

Similar to ${expression} in JS, it may be nice to not just substitute variables, but entire JS expressions in between delimiters: functions, conditionals, iterations. I believe this would answer both questions.

Additionally, I think the { } React-style delimiters are the most familiar for most developers (it is a fact that React is the most popular declarative UI framework, ergo most familiar). To make a stronger case for these delimiters, JS itself uses them, however prepended with the $ symbol in template literals.

How event handlers should be attached to elements inside a <template>.

My suggestion:

<template>

    <!-- Where `const doThing = () => something` is declared somewhere else -->
    <button onClick={() => doThing()}>Do it!</button>

    <!-- Alternatively, -->
    <button onClick={() => { let thing = 1; return thing + window.otherThing }>Add?</button>

</template>

It would be nice to pass the event to the event listener, which of course, contains the Node as well.

I feel most developers would have an easier time adopting this.

@TejasQ the delimiters are really the most changeable aspect of this proposal, and it's too easy to get bogged down in syntax discussions when the features are what matters here. That said :) ...

{} is a poor choice, IMO, because it breaks: <template><style>x-foo {color: blue;}</style></template>. While <style> tags are not so popular in JSX because most VDOM frameworks lack style scoping, they're extremely common in templates meant to stamp to Shadow DOM.

${} is enticing for matching JS, but it makes writing templates w/ interpolation more cumbersome in JS template literals: template.innerHTML = `<div>${foo}</div>`; <- here the ${} is interpreted as a template literal expression. To write this as an HTML expression, you'd have to escape: template.innerHTML = `<div>\${foo}</div>`;. A small annoyance, but I'd rather avoid it.

Over in Chrome land we've been working on very similar things, and @domenic wrote up our ideas here: https://github.com/domenic/template-parts

It's a little smaller in scope, and the document less detailed, but it's mostly the same ideas.

I've read over both specs. I have a lot of issues with them. I'm not feeling well right now, so I'm not going to be exhaustive or dump everything out here. I'd like to start with one basic request though. Please separate out the phases of:

  • template compilation
  • template instantiation
  • data binding

These seem to be all mixed together under the instantiate action. In practice, to make templating engines fast and to enable real-world usage patterns, these steps need to be separated.

I've added Apple and Google proposal links to the 1st post.

I am not happy with them both either. I would say people are trying to add too much complexity at this stage.

We will move faster if we will first prepare a short list of features and will vote on them after that.

  1. Basic var replace {{ }}
  2. ifs
  3. loops
  4. update() feature, i.e. ability to track and update variables in templates after they were compiled/inserted into DOM aka one-way data binding.
  5. Nested <template>'s, i.e. template inside another template (I am not talking about foreach or similar syntax)
  6. event handling
  7. something else?
rniwa commented

@justinfagnani, Thanks for pretty summarizing my thinking process on {{ vs ${ vs {. Those are exactly the reasons I went with {{. It's pretty important that this feature works well with CSS & JS template literals when we're considering to change the delimiters.

@EisenbergEffect our proposal doesn't have any native data binding beyond what the default template processor provides by default. The separation of template compilation & instantiation is an interesting idea. What kind of operations do you want to do during the compilation phase? FWIW, parsing/processing of templates won't be an issue because browsers can optimize it.

The fact that the create API allows passing in a data object for replacement, implies that the binding phase is part of the creation phase, which it should not be.

rniwa commented

@TejasQ : Being able to use any JS expressions is totally possible with a custom template processor (just call eval). I think the only concern we had was that we wanted to prevent XSS as much as possible by design since innerHTML and friends historically had a bunch of bad XSS attack surfaces. How do you feel about making this an optional or a separate syntax. e.g. ${~} for any JS expression to match template literal but {{~}} for the template processor to do data binding.

rniwa commented

@EisenbergEffect : Why? Telling something shouldn't be is very unproductive in standardization processes. We need objective empirically observable use cases or some other evidence as to what is a problem.

Note that createInstance certainly takes a JS object but it's really up to the template processor to handle it. There is no automatic storing of the JS object to a template instance. The default template processor, for example, completely forgets about the JS object passed to createInstance, and you'd have to call update with the same object next time you update it.

If you wanted to create just a template instance and not do any data bindings, you simply omit the JS object to createInstance. The fact it takes a JS object is sort of a syntax sugar over template.createInstance().update(state) to improve the developer ergonomics.

There seems to be a lot of JavaScript in this HTML. If someone disables JavaScript, are templates still supposed to work?

Templates already don't work without JavaScript.

@EisenbergEffect

The fact that the create API allows passing in a data object for replacement, implies that the binding phase is part of the creation phase, which it should not be.

Interpolating the data during clone is actually a very important part of this proposal.

lit-html, which is based on a lot of the ideas here, currently has two-phase template instantiation:

  1. Clone the template with no values
  2. Call TemplateInstance#update() to interpolate the values.

This is done only because there's currently no way to customize cloning in Document#importNode(). The problem is that between the clone and the update() call, Custom Element reactions like construction, connectedCallback and attributeChangedCallback run, while the DOM is in an incomplete, intermediate state. Then the values are populated, causing possibly another round of attributeChangedCallback, but also more construction and connectedCallbacks if some of the values are themselves TemplateInstances.

It'll be both faster and more correct to create the initial DOM with values filled in and run all the reactions at the end of createInstance() before returning to user script.

@snuggs I was referring to JSX's choice of {} making it difficult to write style tags, not whether templates are in JavaScript or HTML. If we used {} in <template>the same problem would apply.

Anyway, this really is the least interesting part of this discussion. The delimiter can be changed without effecting the rest of the proposal at all.

@justinfagnani Hence why i deleted my comment in retrospect as I see your point for sure. But must have empathy for what may be the least interesting to you doesn't speak for rest of community. As long as it's been noted (multiple times) it's something people care about there is interest. πŸ™

@rniwa If it's just syntax over create/update, then that's fine, assuming there's no negative effect to not passing a data object. It's important that a program be able to instantiate a template in one place, and then pass the instance to some other part of the program to actually supply the data. We made the mistake of mixing the create and bind responsibilities early on during the alpha phase of Aurelia and it caused us a lot of problems in the context of the larger component model. So, we had to make a breaking change to split them apart.

@EisenbergEffect I remember that... πŸ’œ

rniwa commented

@EisenbergEffect yeah, it seems like this is an important feature for libraries & frameworks. The polymer team had a proposal which fixes this problem. My only concern is that the API exposed to the end user should be simple but I'm sure we can come up with an API that satisfies both concerns.

...I'm sure we can come up with an API that satisfies both concerns. @rniwa

Apologize in advance for losing track of context. However this conversation has gotten quite detailed. Can you please refresh me with a comment link perhaps to the two "concerns"?

Thanks in advance πŸ™

rniwa commented

Two concerns are:

  1. Framework & library authors need a way to "compile" a template before being instantiated.
  2. Users of a template shouldn't have to first compile then instantiate a template. (i.e. there should be a single step API for end-users of a template).

@snuggs I was referring to JSX's choice of {} making it difficult to write style tags, not whether templates are in JavaScript or HTML. If we used {} in <template>the same problem would apply.

Anyway, this really is the least interesting part of this discussion. The delimiter can be changed without effecting the rest of the proposal at all.

Opened a bikeshed ticket (#4219) to avoid polluting this thread any further πŸ˜„.

jurca commented

After reading the whole discussion, I would like to propose a different approach - one that dodges the issues with delimiter choice / preferences, does not suffer from HTML parser issues (as much), does not have to deal with what kind of expressions should be allowed within the dynamic parts, nor requires us to decide whether to include extra syntax for features such as conditions, loops and template composition.

The example below roughly demonstrates what I have in mind, demonstating data binding, looping, event listeners, element references, composing templates and updates:

const appTemplate = document.createDynamicTemplate(`
  <div class="comments">
    <ul>
      `, `
    </ul>
  </div>
  <form `, `>
    <textarea name="comment" `, `></textarea>
    <button type="submit">submit comment</button>
  </form>
`)

const commentTemplate = document.createDynamicTemplate(`
  <li class="`, `">
    <p>Author: `, `</p>
    <p>`, `</p>
  </li>
`)

const appInstance = appTemplate.instantiate(
  (instance, comments) => {
    instance.parts[0].replaceWith(...comments.map(
      comment => commentTemplate.instantiate(
        commentTemplateProcessor,
        comment,
      ),
    ))
    instance.parts[1].element.addEventListener('submit', (event) => {
      event.preventDefault()
      const text = instance.parts[2].element.value
      // both instance and appInstance can be used here
      appInstance.parts[0].nodes.appendNode(commentTemplate.instantiate(
        commentTemplateProcessor,
        {
          text,
          author: 'you',
        },
      ))
    })
  },
  await apiClient.fetchComments(),
)

function commentTemplateProcessor(instance, comment) {
  if (comment.author === 'you') {
    instance.parts[0].value = '--authored-by-current-user'
  }
  instance.parts[1].replaceWith(comment.author)
  instance.parts[2].replaceWith(comment.text)
}

document.body.appendChild(appInstance)

In this proposal I am deliberately limiting the number of parts controlling a single attribute or comment value to 1, just to keep things simpler. I am afraid that allowing multiple parts in an attribute value would lead to confusing behavior for consumers of the parts API (part.value = 'foo' vs part.attribute.value = 'foo', and I'm not really sure what the latter should do if multiple parts are present, especially if mixed with "static" parts). A wrapper library may implement support for multiple dynamic areas in attribute's value.

My proposal, however, does allow multiple Node parts mixed with "static" Nodes on the same DOM hierarchy level, including Node parts at the root level of the template.

Note that the instantiated template keeps track of its parts and child nodes and exposes them to allow easier updates. The instantiated template extends the DocumentFragment, so that multiple root-level nodes can be supported, which is something we use at work and is expectable in both web components' shadow DOM or React world (React.Fragment).

The proposed API is indeed more low-level, but that would IMHO enable most (if not all) current template engines/libraries to utilize it in their implementations, including React, Vue.js, Polymer or HyperHTML. This could reduce the complexity of their own code and (hopefully) improve their performance.

The obvious down side is worse developer ergonomics, however, since everyone has different preferences when it comes to what the ergonomic API should look like, and these preferences even change with time, I suggest leaving that to the libraries that would be built upon this.

A more detailed, more formal description of the proposed API, a partial polyfill and a few demos are available here: https://github.com/jurca/dynamic-template.

This can be alternated with <slot>

@jurca Your suggestion can be easily implemented in JavaScript using template tag functions. I'm guessing that's why people downvoted it.

Many libraries already do this, f.e. with syntax like

const name = "Nambaru"

const div  = html`
  <div>
    <span>Hello ${name}!</span>
  </div>
`

document.body.append(div)

and the html function will receive exactly what you proposed: an array of string parts. It will also receive an array of the interpolated values.

To this effect, there's is not much new in your suggestion, except proposing what any of the existing libraries could make their API be like.

The proposal being discussed is about adding the feature to directly to HTML DOM itself. In theory perhaps this can be expanded so that even a user can use templates (importing data with <link> or something) without any JavaScript at all. Imagine you can write a multi-page application, with data from a database, without any JavaScript whatsoever.

At the moment, there are plenty of libraries with very fast templating that a built-in solution would need to outperform. See the well-known js-framework-benchmark by @krausest:

https://krausest.github.io/js-framework-benchmark/current.html

Will a built-in solution be able to bring better performance to even the fastest of those libraries?

My gut feeling is that, for the libraries that use "fine-grained updated" (just like the above template parts proposal does), the answer may be yes, if we ignore the eval feature (I think that will be performance loss).

The current fastest way to stamp templates is to make a <template> element, get the content, then from that moment forward use cloneNode on the content in order to make new copies for any time a component that uses it is instantiated. The "template parts" are tracked by the library.

Based on this, a library will want to be able to stamp templates out multiple time over and over, and if they have to create a new <template> every time, this may be slower.

Will cloneNode also copy template parts, and thus that will still be the fastest approach?

Basically if the new approach is not faster than current, I think lib authors will be hesitant to adopt it. End users won't care much, they just keep writing what they already write.

I disagree with the placeholder. I'd use the same as string templates ${var} and I'd use that processor to process the template texts.

I disagree with the handler. I believe that should be done by the javascript itself. A few querySelector[All]() will do all the requirements for that. If, later on, handler appears as an attribute in a tag, that can become a problem of both clashing, so whatever name is chosen will become a class of names.
Alternatively, it could be possible to specify callbacks (working close to what you show in that quote) but, instead of a "@handler" in the tag, a CSS selector is provided in the function that parses the template and a callback for it. Not really a fan of this option but I thought it was worth it to give a counter-argument

jurca commented

@trusktr You are correct. I assumed - wrongly :) - that this might be the easier way for everyone to agree on something minimalistic, that can be used by the current and possible future solutions. With hindsight, I can see that I approached the problem from the wrong angle. Also, I admin that the web probably doesn't need another hard-to-use needs-to-be-wrapped-in-a-library API.

My apologies to everyone for that needless post, I was just trying to help.

@carnoxen <slot> can be meaningfully used only in combination with shadow DOM, which restricts CSS styling from the parent context. Furthermore, text nodes cannot be slotted without being wrapped in a container element first.

@brunoais While it may seem convenient to use the same templating syntax that we already use for strings, I'm afraid that this will lead to confusion for end-users writing their HTML templates in ES6 template string literals (which are the only string literals that support multi-line strings without escaping). Choosing a different syntax will enable users to template "semi-static" parts in the string literal and dynamic parts using the template API, and won't lead to the aforementioned confusion.

However, I agree that dropping the handler (at least for now) from the discussion might be better. It might be better to not include it for now, and re-evaluate whether and in what form it could be useful based on the actual usage of this API.

I think we have to resolve two main problems.

  • Choose one consensual template engine to be provided natively in HTML to avoid knowledge fragmentation (the "too much diversity across the Web" problem pointed by the initial post of this issue)
  • Don't break what it already works (how is actualy treated the template element by the parser, already existing template engines provided by frameworks should be able to be integrated in the template element)

There is actually a problem with how is parsed the template element content. (see last part of https://github.com/domenic/template-parts#parser-problems)
We can propose that the presence of the type attribute (like type="text/mustache" or type="text/x-handlebars-template") should be interpreted by the parser as a strict text node container.
It is easy to transform text to html. But the constraint to fisrtly have valid html content in the template element break the desired flexibility of this element if we want to use it with different template engine than html itself.

Why type attribute ?
"processor" attribute is ambiguious because it can be a lot of things (a framework name, or a mime type)
So type attribute seems to be more relevant, because we want to qualify what is contented by templates elements without presume what kind technology have to interpret the content. Ideally, javascript itself should not be presumed as a used technology because we can imagine future declarative and dynamic propositions without javascript requirements (the old unobstrusive javascript recommandation should never be forgot)

With it, each framework is free to continue to use their own template engines. So the second point is respected ("Don't break what it already works"). But for the first point, we may choose what kind of template engine should be natively available in html, in order to provide a standardization, a default prefered template, without impose it and break existing solutions.
So we should list what criteria a template engine should respect to be a good candidate.

  • Simple enough
  • But complete enough too (For example, if we choose a so basic template engine, we can regrets its lack of features)
  • Mature enough
  • Language agnostic (= ported in many languages)
  • Fully documented
  • Performant
  • Other points?

I've already posted a proposition (in a duplicate ticket, oops, sorry) for the handlebars template engine, whish seems (to me) complete enough, simple enough, and consensual enough to be choosen (see #9004).

Note that since handlebars is based on mustach and is compatible with it, choose handlebars is like choosing mustach more few basics features like helpers in addition.

From the javascript side, we should have a standardize way to feed variables assignements for the template and to render it. I'm not sure it will be required to provide more normalisation constraints here, regarding helpers adding for example, because each template engine have their own logic.
But, for example, if we choose handlebars, we should recommand user agents to just implement the existing javascript library as the job is already done, API is already provided (https://handlebarsjs.com/guide/), so html does not have to endorse all handlebars specific problematics, but html can just regularly assume what version or branch should be considere as part of the standard scope. (separation of concerns)

Note about user agents implementations : handlebars is already ported in a lot of various languages (JS, RUST, C, PHP, PYTHON, JAVA, RUBY, OBJECTIVE C, SCALA, .NET, and probably others). Which makes it a good candidate (language agnostic).

When I said

implement the existing javascript library as the job is already done, API is already provided

This does not mean that User Agent have to embed the acutal library in JS format. Browsers can be free to use the C compiled implementation for the lower level, but following how API functions are already provided by the existing JS version for the upper level, just to not reinvent the wheel.

The last mentionned issue (#9014) aims to provide a way to indirectly resolve the parser problem from the server side.

If this new feature is adopted, browsers that do not implement it will encounter the parser problem described at the end of this document : https://github.com/domenic/template-parts#parser-problems
Moreover, because of conditionnal statements, these browsers are likely to interpret unclosed tag as an error when it's not because template content aims to be, for now, valid html, and no text content (like script and textarea tags does)
Since HTML syntax does not provide a way to encapsule CDATA, the only way to be sure to provide a fallback is to use the script tag instead. But template tag is more relevant for templates than the script tag.
So we may prefer to use the template tag every time it is possible.
The only way I see to resolve it is : The browser should send the information to the server that UA is able to interpret the new template element content as text.
I hope someone will find an elegant solution to this problem.
Please keep in mind that it is possible to encounter this kind of problem with others future features too.
So the way to go is probably to find a general - non specific to template tag - solution to cover possible repetitions of this problem.