whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.06k stars 2.65k forks source link

Standardize <template> variables and event handlers #2254

Open Mevrael opened 7 years ago

Mevrael commented 7 years ago

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.

Yay295 commented 7 years ago
  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;
}
rianby64 commented 7 years ago

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

Mevrael commented 7 years ago

@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.

rianby64 commented 7 years ago

@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.

Mevrael commented 7 years ago

@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
">
EisenbergEffect commented 7 years ago

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:

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

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.

Mevrael commented 7 years ago

@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.

rianby64 commented 7 years ago

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 7 years ago

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.

justinfagnani commented 7 years ago

@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 7 years ago

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...

justinfagnani commented 7 years ago

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 7 years ago

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.

Mevrael commented 7 years ago

@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.

justinfagnani commented 7 years ago

@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 7 years ago

@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 7 years ago

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.

justinfagnani commented 7 years ago

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 7 years ago

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 7 years ago

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.

Mevrael commented 7 years ago

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?

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.

domenic commented 7 years ago

@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.

Yay295 commented 7 years ago

@Mevrael

since JS don't have foreach

JS does have a foreach for arrays.

rniwa commented 7 years ago
  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 7 years ago

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

rniwa commented 7 years ago

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).

Mevrael commented 7 years ago

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 7 years ago

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).

rianby64 commented 7 years ago

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?

Mevrael commented 7 years ago

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.

rianby64 commented 7 years ago

https://github.com/w3c/web-platform-tests/blob/master/html/syntax/parsing/template/appending-to-a-template/template-child-nodes.html#L40 ... :confused:

Mevrael commented 7 years ago

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 7 years ago

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

Mevrael commented 7 years ago

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>
EisenbergEffect commented 7 years ago

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.

Yay295 commented 7 years ago

... 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.

justinfagnani commented 7 years ago

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 7 years ago

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).

justinfagnani commented 7 years ago

@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 7 years ago

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.

jonathanKingston commented 7 years ago

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:

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 https://github.com/whatwg/html/issues/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.

justinfagnani commented 7 years ago

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>.

bicknellr commented 7 years ago

(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 7 years ago

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.

snuggs commented 7 years ago

@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.
  3. 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>. https://github.com/devpunks/snuggsi/pull/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!

bicknellr commented 7 years ago

@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 7 years ago

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.

bicknellr commented 7 years ago

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);

?

bicknellr commented 7 years ago

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?
GarrettS commented 7 years ago

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', () => {
      // ...
   });
  }
});