Extension of <template>


#1

The main motive of this article is to add variable interpolation into <template>

I’m not precious of the particular naming conventions used here (engine vs emitter and variables); just keen to see the interest in the idea and if it is worth implementing a mock up.

Use cases for extension:

  • Removal of boilerplate template rendering code
  • Defined interface for client and server side detection of template types
  • Fall back should allow for simple code to make up for lack of support

Here is a sample example template:

<template id="test">
  <div>
    <h2 class="heading"></h2>
    <div class="content"></div>
  </div>
</template>

Here are three alternative implementation proposals.

Registering typed content emitters:

<template id="test" type="application/x-handlebars-template">
  <div>
    <h2 class="heading">{{heading}}</h2>
    <div class="content">{{content}}</div>
  </div>
</template>

<script>
  document.registerTemplateContentEmitter('application/x-handlebars-template', function (documentFragment, context) {
    let template = Handlebars.compile(source);
    return template(context);
  });
</script>

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

Advantages:

  • More flexibility for content producers
  • Emitter types are likely to work cross site reliably due to defined names globally
  • Ability to use logic supplied in template engines
  • Browsers could choose to adopt native type handlers too; much like video encodings (this would likely need a fall back still)

Disadvantages

  • More JavaScript required to bind the type

Add bindings element for data output:

<template id="test">
  <div>
    <h2 class="heading"><binding bind-key="heading" /></h2>
    <div class="content"><binding bind-key="content" /></div>
  </div>
</template>

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

Advantages:

  • Browsers could precompile, cache and optimise for included templates
  • Interpolation of variables is simplified
  • Boilerplate code for use of templates is reduced
  • Native interpolation

Disadvantages:

  • Adding extra complexity to HTML
  • Less flexibility for using existing templating

Inline emitter declaration:

<template id="test" type="application/x-handlebars-template">
  <script>
    document.registerEmitter(function (documentFragment, context) {
    let template = Handlebars.compile(source);
    return template(context);
  });
  </script>
  <div>
    <h2 class="heading">{{heading}}</h2>
    <div class="content">{{content}}</div>
  </div>
</template>

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

Advantages:

  • Most flexibility per template output
  • Precompilation of library code would be possible internally by browsers

Disadvantages:

  • Adding in a new context based JavaScript usage in the DOM which developers will be less familiar with. Being its own document instance this should be possible.
  • More boilerplate code duplicated per template which could be just based upon content type
  • Type attribute would then be possible to be omitted which then means crawlers and other technologies wouldn’t get the benefit of knowing the types of engine used.

I’m interested to hearing peoples interest in this.


#2

This is work that could reasonably build on Polymer’s Node.bind, which depends on Jafar Husain’s effort to standardize Observables.

Personally, I would like to see us standardize observables first, followed by Node.bind, and then proceed to <template>-based syntax.


#3

Agreed, I appreciate you taking the time to respond to this.

I wasn’t particularly expecting the binding variables in terms of updates at all; merely a one way simplified output render.

However it certainly seems to cover even more use cases with this and simplifies/adds native support to the active update features of the likes of Meteor, Derby and other libraries.

So with that I agree completely that it is worth the wait for these features to standardise before considering this; assuming the time frame won’t be 5 years.


#4

Yes, five years would be very very sad.

In the meantime, I would strongly encourage you to write a library adding variable interpolation that assumes the existence of <template>. It could be nice way to explore the ideas while Polymer (and Ember, likely) explore Node.bind in userland as well.


#5

Could you make a custom element that extends template?


#6

Would be this to just to prove the concept so browsers could eventually support it?

For example the result would be:

  <x-template id="test" type="application/x-handlebars-template">
    <div>
      <h2 class="heading">{{heading}}</h2>
      <div class="content">{{content}}</div>
    </div>
  </x-template>

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

#7

As a quick example here is the above working:

Setup

(function () {
    var XTemplateProto = Object.create(HTMLTemplateElement.prototype);
    XTemplateProto.emit = function (context) {
        var source = this.innerHTML;
        if (this.type in this.emitters) {
            return this.emitters[this.type].call(this, context);
        }
        return source;
    };
    
    XTemplateProto.emitElement = function (context) {
        var dummyNode = document.createElement('div');
        var fragment = document.createDocumentFragment();
        dummyNode.innerHTML = this.emit.call(this, context);
        if (dummyNode.children.length) {
            for (var i = 0; i < dummyNode.children.length; i++) {
                fragment.appendChild(document.importNode(dummyNode.children[i], true));
            }
        }
        return fragment;
    };

    Object.defineProperty(XTemplateProto, 'type', { 
        value: 'text/x-handlebars-template',
        writable: true
    });

    Object.defineProperty(XTemplateProto, 'emitters', { 
        value: {},
        writable: false
    });

    XTemplateProto.registerEmitter = function (type, callback) {
        this.emitters[type] = callback;
    };

    window.XTemplateElement = document.registerElement('x-template', {
        prototype: XTemplateProto
    });

}());

Usage

// Define emitter for chosen template type
XTemplateElement.prototype.registerEmitter('text/x-handlebars-template', function (context) {
    var source = this.innerHTML;
    var template = Handlebars.compile(source);
    return template(context);
});

// Create a template node
var tag = new XTemplateElement();
tag.innerHTML = '<div><h2 class="heading">{{heading}}</h2><div class="content">{{content}}</div></div>';
tag.id = "template-test";
tag.type = "text/x-handlebars-template";
document.body.appendChild(tag);

// Use the template
var statements = [
  {heading: 'Shouty statement', content: 'Going somewhere'},
  {heading: 'Appeasing hook', content: 'Disappointing ending'}
];
var template = document.getElementById('template-test');
statements.forEach(function (statement) {
  if (statement) {
       document.body.appendChild(template.emit(statement));
  }
});

#8

I’m thinking that the code above would be modified to then hold the context and also expose methods to modify that state. The emitter could then also be rerun and innerHTML be modified into the document when the context changes.

I will try and get a working example going with HTMLbars to see if that implementation would be possible.


#9

I really don’t see the need for this once Template Strings are standard in all browsers.


#10

@eisaksen unless there is planned support for this in the DOM with HTML bars style auto interpolation of variables on update… I don’t see it helping much for the use case.


#11

Template Strings combined with Observers does serve the same purpose (which are planned specs). Template Strings are already in FF & Observers are in Chrome. Both are high on IE’s list to look at.

I’m not saying HTML Bars or other JS templates are bad at all, I just don’t see why we need it as a standard when we have tools already that solve the same problems. I guess it depends if you are more comfortable using HTML Bars. All of your code examples show cases that can easily be solved with either so it just depends on your poison.


#12

My examples failed to illustrate that different parts would be used by different authors of the system. Having a large number of markup authors in a company is a common setup yet that requirment shifts the more we move things away from being markup.

Sure as a developer I could create a mapping from markup to template string or something but all to easily separation of concerns would likely be forgotten. So as a quick example I guess I could pick out hbs files and package them up into template strings but rather than making cacheable native html template includes; I would be custom rolling something similar to html over json approach.

The idea behind these languages is to get as close to writing html as possible, so why not make it part of the language. I’m not suggesting any one of these flavours has it 100% right however what it does do is separate concerns of templates and scripts.

I’m not suggesting your approach is wrong or flawed just in a commercial setting it seems more error prone and against the normal distribution of labour(not that any of this should define standards I might add).

Secondly when the templates use dom the statements within the template string would have to be reevaluated and likely rebuild the dom the statements wrapped. This leads to many engineering issues surrounding keeping state of dom and memory management that most developers are likely not going to want to solve in their day job (sure programmers love challenges and pet projects but I would rather leave as much as this to shared libraries where possible).


#13

@robin you mentioned on twitter this would have not to suck before it could become native, do you think there is merit in this happening at all?

I liked the approach of making it agnostic however I can see that that would lead to the same support issues that codecs have. I still think the registerEmitter solution is sound to this.

However I would like to see peoples feedback on this and see if there is real interest in making this happen.


#14

I like the approach, I like the idea of making it pluggable. The thing I was referring to for the “not suck” bit is templating languages in general (I really don’t like HBS and friends); the idea of making <template> more powerful to support actual templating is certainly attractive.


#15

So the current code is here: https://github.com/jonathanKingston/extendTheWebForward/blob/master/templateEmit/README.md

This only supports binding an format to the template.

I played around with using Object.observe to rewrite the DOM on the fly which would match similar support to what @Yehuda_Katz was saying.

The things I think we need to make the live DOM updates much easier are:

  • inert placeholder elements
  • an event to know when a fragment has reached the DOM with a NodeRange of the elements inserted

#16

Why this proposal is so complicated and why do you even want to give developers that weird syntax and ask him to register a 3rd party JavaScript template engine?

This has no differences. You can do it now - clone template and pass it into any 3rd party template engine.

Standards are standards. I want to see a variables and if statements parsed by a native code and syntax should be always the same, and no 3rd party library should be included. Imagine a huge production app with a lot of 3rd party components and if each of them will use custom syntax and will require to import different template engines, code will become a mess very fast, not talking about the bundle size.

I believe there is no need in discussing that such feature should be allowed in tag. So the only topic to discuss is - the syntax.

I would +1 the most popular solution used across many template engines in many languages - {{ }}, 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.

2nd point we should standardize is - attaching event handlers when new template was inserted into DOM.

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

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

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

Please provide your thoughts and let revive this old discussion.

I would suggest using Slack, there is popular public frontend room - http://frontenddevelopers.org/ (PM me any time there @mevrael) I know about IRC and not using it. Slack is the key of a successful discussion and actually making things really done. Would anyone be interested in making a Web Standards public Slack group?


#17

TL;DR I think web components are probably moving in a different direction.

Firstly I don’t see anyone wanting to adopt this, which I think is a shame as React/Polymer could adopt something like this for JSX but they aren’t looking to standardise etc.

Secondly I’m not convinced we can standardise something in HTML that isn’t HTML even if just scoped within a template.

Why this proposal is so complicated and why do you even want to give developers that weird syntax and ask him to register a 3rd party JavaScript template engine?

I proposed three syntaxes initially all of which I still see as more feasible than the level of complexity of your proposal. The weird syntax you are referring to is to extend templates how they currently behave without breaking existing behaviour. I’m not sure based on other comments if you were referring to the shim code with the importNode and clone etc.

The code I was suggesting was:

<template id="card">
  <card>
    <h2>{{title}}</h2>
    <div>{{description}}</div>
    <a href="/card/{{id}}">Read more</a>
    <button>Edit</button>
  </card>
</template>
document.body.appendChild(document.getElementById("card").emit({
  title: 'Card title',
  description: 'Hello, World',
  id: 42
}));

This removes the need for another top level function that could cause breakage and isn’t really any more complex.

The syntax handler was to prevent arguments about how that gets handled, where eventually we could have a native syntax potentially.

The handler parameter and other improvements I would suggest be part of web components themselves and a separate topic to this. I suspect components are what you are after anyway:

class MyCard extends HTMLElement {
  constructor(properties) {
    this.properties = properties;
  }

  connectedCallback() {
    const {
      title="",
      id=0
    } = this;

    const template = `<h2>${title}</h2>
    <div><slot></slot></div>
    <a href="/card/${id}">Read more</a>
    <button>Edit</button>`;

    const shadow = this.attachShadow({mode: "closed"});
    shadow.innerHTML = template;

    shadow.querySelector("button").addEventListener("click", this);
  }

  handleEvent(e) {
    // If the user has defined a callback for edit call it here
    if (this.onEdit) {
      this.onEdit(e);
    }
  }

  set properties(properties) {
    Object.assign(this, properties);
  }
}
customElements.define('my-card', MyCard);

Usage like:

const myCardInstance = new MyCard({
  title: 'Card title',
  id: 42
});

myCardInstance.textContent = "Hello world";

myCardInstance.onEdit = (e) => {
  console.log('Button clicked', e);
};

document.body.appendChild(myCardInstance);

Or:

let other = document.createElement("div");
document.body.appendChild(other)
meb.innerHTML = `<my-card title="abs" id="111" disabled="true">Hello all</my-card>`;

Slack is a different discussion nor is it open etc and this is an official board provided by the W3C for discussion of standards. Personally I would prefer a persistent storage of messages rather than a chat room.


#18

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.

The most of opinions agree that there should be just a new method on Template prototype instead of global function.

There is also a polyfill for this proposal already in development and works for basic use cases: https://github.com/Mevrael/html-template

I still totally against “web components” and that weird syntax of “classes” and event handling is a basic feature which is not connected to web components. You already pointed out that Google with Polymer moves it into different angle and doesn’t cares about standardization.

So far we need to come up with syntax for logical operations and loops like {{ if () }} {{ endif }} {{ foreach (users as user) }} {{ endforeach }} and for event handlers.

There is difference between chat and summary. It is easier to chat/do a meeting when you need to discuss specific topics real-time and persistent storage with summary should be only at the end, otherwise there is only persistent storage with sometimes off-top, discussions in different directions and it is very disorganized and hard to find relevant information, what is current stage and all the main key points accepted/discussed/rejected.

However, I would say separate repo where anyone could contribute to wiki and docs would be the best option.

We sometimes discussing it on my gitter room https://gitter.im/bunny-js/Lobby and there is a fork of template implementation which somehow supports if statements.


#19

I suggest we move this to the whatwg github issue. my response to this will go there.