-
Notifications
You must be signed in to change notification settings - Fork 378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discussion: Templates that Don't Destroy Element References #777
Comments
There is a proposal along the lines of what you are suggesting, I think. |
Besides editing it. How can I get my comment above in front of the eyes of the people making that proposal? |
If you'd like, we can converse over email or video conf about this. Shoot me an email me at webkit.org or apple.com (username is same as my Github username). |
Yes, let's discuss. I sent an email @webkit.org (same handle as here). |
This is how I'd like the process of creating a web component to be.
At this point pretend we've created actual DOM that contains the static and dynamic parts. And most importantly, the reference to this.div remains accessible throughout the life cycle of our web component instance. Any update method would be able to modify this.div (fast as possible) with a direct reference and the event listener we created at the beginning continues to work. The template tag, never needs to be re-rendered. If you need to update something. You modify the element directly (by reference). This how I'd like it to work. hyperHTML and lit-html accomplish this same type of thing, but if that .buildFromLiteral method above worked natively, it would be even faster than they've accomplished. Because, in order for them to do it, they are having to replace interpolations with comment nodes before cloning the HTML template. Then, after cloning the template, they have to retrieve references for all those comment nodes, something like this:
Lastly, they are having to replace each comment node with the actual element created within the web-component's constructor. And they are not just doing element-interpolations either. They support strings, fragments, promises and other interpolations/expression output types. So if you could generate an HTML The only thing I care about is element-interpolations. I'll separate static from dynamic myself. As long as I can reference the dynamic parts via element-reference, I've got all I'll ever need. |
Thanks. I think what we proposed would achieve something very close to what you want / are proposing. Fundamentally, there are two important pieces to the template API we’ve been discussing.
Let's say we have the HTML like this: <template id="myTemplate">
<h1>See Below</h1>{{content}}<p>Click the sentence above to see alert.</p>
</template> Then we can do something like this (the exact shape & naming of API is not yet settled but so take it with a grain of salt; e.g. which properties exist where, etc... can change): const div = document.createElement("div");
div.textContent = "Click Here to See an Alert";
div.addEventListener('click', (event)=>{alert(`You click a div that contained this text: ${this.div.textContent}`)});
const instance = myTemplate.createInstance({content: div}); Here, Once you created an instance, you can insert that context into anywhere in the DOM: document.body.appendChild(instance.content); Obviously, you can access & mutate div.innerHTML = '<b>Click Here to See an Alert</b>'; In addition, the const b = document.createElement('b');
b.textContent = 'Open an alert';
instance.parts['content'].replace(b); |
What I'm proposing is more fundamental (and more efficient) than Template Instantiation, but not mutually exclusive to Template Instantiation. Here's how the two would complement each other. First I'll discuss what I'm proposing and later I'll tie that into the Template Instantiation proposal. Template Literals already know the context from which to determine the values of the ${interpolations/expressions} they contain. They also already inherently know which portions of the string are "parts". Template Literals, without the help of any type of HTML template element, already contain everything that needs to be known to generate DOM. Therefore, I think it would be ideal for Browsers to add 2 prototype method to all strings. One called toDOM() and the other called toTemplate(). Here's how this would work. toDOM()If you want to create live Dom (that's ready for the appendChild method) you use the toDOM() method:
Notice that the this.btn element reference, created at the beginning of the constructor, survived the template literal all the way to DOM, and so did its click eventListener; if you click this.btn it will still fire. this.btn remains accessible to the web component for efficient changes throughout the entire life cycle of the web component. Above is what I'd primarily use, because if I already have a direct object reference to the dynamic elements that change (which what I'm proposing would give you), there is no need for template updating. Therefore, there's no need for an intermediary template element and the added change-evaluation processing it brings with it for each interpolation each time the template is updated. An event fires, and you change only what needs to change, without ever even evaluating any of the interpolations that didn't change! This is as efficient as it gets. toTemplate()Here's where your proposal comes into the picture. If you want each template part to have a name, you can pass names in during the toTemplate() method: Then, later when you call htmlTemplate.createInstance(), you pass in an object literal instead of an array if you originally gave the parts a name during .toTemplate(). And also, the examples you gave, where the template element was created in HTML, this will be fine too. SummaryThese two methods are fundamentally different ways of updating the dynamic aspects of a web component. The first method only gets called once and stuff that doesn't change doesn't even get evaluated (an event tells you exactly what changed and at most you evaluation only the one thing the event said changed). The Template Instantiation method has to evaluation each part on each update, looking for everything that might have changed. By handling your updates at the more fundamental level, the events themselves inherently tell you exactly what changed. Evaluating things that didn't change is unnecessary, but hyperHTML and lit-html do this for every interpolation on every template update. You don't have to compare anything to see what changed, the event inherently tells you what changed and you can go update that alone. What I'm proposing is fundamental, but simple. templateLiteral.toDOM() is like a fancy innerHTML that understands when a ${JavaScriptExpression} is an element. This is all the "templating" I'll ever need. |
This seems a bit weird to me. The interpolation of: `<h1>See Below</h1>${this.btn}<p>Click the sentence above to see alert.</p>` produces: "<h1>See Below</h1>[object HTMLButtonElement]<p>Click the sentence above to see alert.</p>" You want the thing the literal produces to not be a string. To do that you need a tagged template literal like lit-html does. Such a function could work with template instantiation to produce what you want. |
@matthewp : Yes, you're right, that's what string alone produces, and that's what the .toDom() method would handle. It ultimately converts that I'm saying that, when javascript is running in a web browser. There could be a prototype method called toDOM() on string (for all strings, including template literal strings). However, if the string is a template literal, the method should be smart enough to interpret the ${elementNode} expression just like what is accomplished using tagged template literals, before producing the DOM. My basic need is the ability to just mix a string of HTML with my own hand-crafted elementNode objects. The toDOM() method would produce the DOM I need very easily with nothing more. The events on my web components tell me exactly what changed without having to evaluate every other interpolation. I would never want to do a string lookup on a clone to replace a node. Each dynamic thing in my web components can be directly accessed via What I'm saying is not better than Template Instantiation. It just seems like a more fundamental thing that should precede Template Instantiation. After I get the DOM, you can garbage collect any template that may have been needed to give it to me. I agree that the end result can be accomplished by lit-html, hyperHTML, and the Template Instantiation proposal. I look forward to seeing what is ultimately the standard on these concepts. |
For that to work the string would need to keep a reference to |
Well, then I'd expect When you pass a template literal into a tag function, the tag function receives an array of static strings and an argument of each expression result. Couldn't a method on a template literal receive these same parameters automatically? Does it have to become a literal string value during the dot of |
Sounds like to make this method work you're going to have to make a lot of changes to JavaScript. Is it really that important to you that the method be on the string prototype? Why not just have a method |
Maybe I'm missing something here, but what's wrong with a template tag? It cal already do all the things asked for here and makes it clear to the author that some special is going on compared to plain strings. |
...
There is a difference between the template literal and the string it generates. " This is precisely why our proposal uses An alternative approach is to use tagged template literal like lit-html and hyperHTML. One big difference between a regular template literal and a tagged template literals is that the latter can return a non-string object.
That's going to be an extremely exotic behavior because this
That would be a massive layering violation. Because ECMAScript is used in non-browser environments without DOM like node.js these days, adding any sort of DOM related API directly onto String would be a bad idea. Having said that, I'm amendable to the idea that there probably is a valid use case for creating a template instance directly from a string. For example, I could imagine an alternative approach to instantiating a TemplateInstance perhaps via a constructor as in: <template id="boldTemplate"><b>{{text}}</b></template>
<script>
const content = {text: 'hello, world'};
const instance1 = new TemplateInstance('<b>{{text}}</b>', content);
const instance2 = new TemplateInstance(boldTemplate, content);
</script> where Alternatively, using tagged template literal, we could introduce something like <template id="template3"><b>{{text}}</b></template>
<script>
const content = {text: 'hello, world'};
const template4 = htmlTemplate`<b>{{text}}</b>`;
const instance3 = template3.createInstance(content);
const instance4 = template4.createInstance(content);
</script> Again, const br = document.createElement('br');
const template5 = htmlTemplate`<b>{{text}}</b>${br}`;
const instance5 = template5.createInstance({text: "hello"}); Then Note that const template5 = htmlTemplate`<b>{{text}}</b>{{suffix}}`;
const br = document.createElement('br');
const instance5 = template5.createInstance({text: "hello", suffix: br}); This is the closest thing I can come up with to what you're proposing without all the issues I listed above. |
@matthewp : When you create a string, it becomes a string object instance having numerous methods. Since converting strings to DOM is such a common task in client-side development, it seems quite natural to me that browsers would add a Tag Template literals are a wonderful but strange beast in JavaScript; somewhere between the template literal itself and the tag function, a magic hidden splitter-function occurs that feeds the template literal to your tag function in pieces: You mentioned the window object, while my first thought of an alternative would be for browsers to add a static class-method on String: @justinfagnani : Actually, I must admit that the If I do not want an update-able template . . . if all I want is DOM from the template tag function, then why must I call a template function that returns a template result which is NOT DOM? It seems like some efficiency would be gained from not returning something that facilitates more than I need. In my web components, I create the dynamic elements manually, so that I'll have direct object references to each thing that needs to change. When my web component receives an event from the browser, methods get fired and the only thing updated is exactly what changed (without having to re-evaluate a template at all). So, the only thing I want, is the ability to convert a template literal directly into DOM. I realize that Template Instantiation, lit-html, and hyperHTML all allow me to do just that, but its done at the price of what it costs to make a fully functional update-able template, and I'd like to do it at the cost (in memory and performance) to just generate the DOM (once, and garbage collect the rest). Surely, more resources are required to make a fully functional, update-able template, compared to a tag function that is elusively dedicated to only creating DOM once? No? |
hyperHTML works this way if you want that. lit-html returns a light-weight result object so that the cost of creating DOM is only done when rendering. You can convert the template approach to the immediate DOM approach easy enough: import {render} from 'lit-html';
export const renderFragment = (v) => {
const fragment = document.createDocumentFragment();
render(v, fragment);
return fragment;
}; As for the intermediate It may be possible to create another code-path that directly creates DOM without the I should note that with lit-html you don't need to re-render at all. If you give a template your dynamic elements and only change those, that technique will work. Using a If you think that re-parsing the HTML string is faster than cloning for multiple instances, it'd be good to verify that with some benchmarks. I'd be curious about the results. |
@rniwa I wonder if we want to consider a more generic name for Also, another approach could be a template tag that does what you have in the very first example and directly creates an instance. Maybe something like: let text = 'hello';
const br = document.createElement('br');
const instance5 = htmlInstance`<b>${text}</b>${suffix}`;
document.body.appendChild(instance5.fragment);
instance5.update({0: 'goodbye'}); edit: forgot |
@Lonniebiz just to clarify, hyperHTML doesn't replace comments, it uses these as "pin" in the DOM and relate changes to these without ever replacing a thing. The eventual update is handled by domdiff which takes care of adding, removing, or swapping only nodes that have changed since last update. Since I've recently refactored out all the major parts of hyperHTML, what might interest you here is the domtagger, which is the hyperHTML engine in charge of using A hole can be a textContent, as example if inside a textarea or a text only node such a style one, an attribute, or "any" hole, meaning any sparse hole within a node. The most basic example (note that hyperHTML does way more than just this): var html = domtagger({
type: 'html',
attribute: (node, name) => value => {
var type = typeof value;
if (type === 'boolean' || type === 'function')
node[name] = value;
else if (value == null)
node.removeAttribute(name);
else
node.setAttribute(name, value);
},
any: comment => html => {
const {parentNode} = comment;
parentNode.innerHTML = html;
parentNode.appendChild(comment);
},
text: node => textContent => {
node.textContent = textContent;
}
});
// render example
function render(model) {
return html`
<div onclick=${model.onclick}>
<div>${model.html}</div>
<textarea>${model.text}</textarea>
</div>
`;
}
document.body.appendChild(
render({
onclick: function (e) {
alert(e.currentTarget.outerHTML);
},
html: 'Hello <strong>domtagger</strong>!',
text: "isn't this cool?"
})
); So, pretty much all the primitives I need are there, the only missing part to me is a persistent fragment, so that I can keep comparing, or appending, a fragment without it ever losing its childNodes. Such fragment would be smart enough to move its childNodes over a new node only if the node it's appended into changes from the previous one. That is honestly the only thing the platform doesn't provide, everything else can be done with relative ease on user-land, and performances are already better than pretty much every relevant competitor. |
Just to clarify, since apparently my latest example has been already misunderstood, hyperHTML does never replace same content and above example is just 1/16th of the logic involved in hyperHTML updates, absolutely not what hyperHTML does. |
What does inertness get you here? Typically, in a template system you want to delay side-effects like network fetches or code loading until render time. Inertness lets you do that for trusted template elements, but it's unclear to me how that helps fill holes. For example, in So I'm unsure of the value of inertness for trusted template code without inertness for untrusted inputs, and I'm unclear on how the latter kind of inertness falls out of these changes. It's also unclear to me how this helps with untrusted inputs that specify bytes in a language other than HTML, like Having a DOM structure helps you derive context for holes, but it doesn't help preserve developer intent unless you use that context to reinterpret inputs. By the time you've parsed inputs to DOM fragments, I worry that it will be too late to reinterpret them. https://github.com/wicg/trusted-types provides a type-safe taxonomy for distinguishing trusted and untrusted inputs in important contexts but depends on delaying parsing. |
@mikesamuel in the most basic Template Instantiation proposal, the contents of |
The fastest way to access an element is by direct object reference. What's the 2nd fastest way? A Map object maybe? This got me thinking. What if you had a really huge template. Could performance be gained on first render if the "comments" knew how to replace themselves without having to walk the DOM to extract comment nodes? So, instead of replacing expressions with comments, why not replace them with something smart enough to know how to manage itself: a custom-element:
This is probably a silly idea, but here's a proof of concept. |
That's exactly what |
'cause you want to address attributes too, and. custom element inside an attribute isn't really working well, isn't is? On top of that, a custom element breaks certain layouts, like tables: <table>
<tr>
<broken-table><td></td></broken-table>
</tr>
</table> |
If I've used these library improperly
Yes, you 100% misused hyperHTML which otherwise creates 10K buttons in
~20ms, as explained already in SO [1]
[1] https://stackoverflow.com/a/53788734/2800218
…On Fri, Dec 14, 2018 at 2:55 PM Lonnie Best ***@***.***> wrote:
I've created at test called the 10,000 Button Test. Basically, it
generates a template that has 10,000 expressions containing button elements
with a little text in between each button.
I wanted to see how my code, that I wrote before posting this issue 777,
compared with the "self-replacing custom elements" (proof of concept) in my
last post above.
Well, it turns out that my first attempt (that replaces comments) is
faster than the one where custom elements replace themselves.
Comment Replacement Code
<http://chickenpecktech.com/LonnieBest/TemplateTesting/Comments/> -
finishes in 35ms.
Self-Replacing Custom Elements
<http://chickenpecktech.com/LonnieBest/TemplateTesting/Commentless/>-
finishes in 79ms
lit-html <http://chickenpecktech.com/LonnieBest/TemplateTesting/lit-html/>
- finishes in 59ms
hyperHTML
<http://chickenpecktech.com/LonnieBest/TemplateTesting/hyperHTML/> -
finishes in 7s
@justinfagnani <https://github.com/justinfagnani> and @WebRefection : If
I've used these library improperly, I will update this comment to reflect
proper usage per any suggested optimizations you might make.
Comparing my code to lit-html or hyperHTML is like comparing a prison
shank to a Swiss Army Knife.
However, my point is: in additional to offering fully updatable templates,
it would also be nice if web standards offered a performance optimized
tagged template literal that can simple allow users to mix ${elementNodes},
${fragments}, ${textNotes} with strings to produce DOM (without any of the
overhead that comes with facilitating a fully updatable template. To me, it
seems like syntax that specifies you're only wanting these limited
capabilities (upfront) will out perform code designed to handle updating
attributes and all the other tough things that lit-html and hyperHTML make
updatable.
let element = dom<h1>String with ${elementNode}.</h1>; //returns DOM
without template.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#777 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAFO9VO1cLp5eSOGLbveM1hbJ1B9FHxNks5u40trgaJpZM4ZBZW2>
.
|
Regarding templates, here's something to think about: |
In the same manner that tagged template literals allow the embedding of interpolated JavaScript expressions, I think it would be awesome if HTML
<template>
tags (or some new tag type) allowed ${elementReferenceName} expressions also. Then, instead of cloning a template, you could just instantiate it in the same manner you would an object from a class (without losing your element references), and when you do this the interpolated values could be based on the original context from where the instantiation occurs (or through .bind to specify an alternative context), kind of like arrow function scoping but with bindablity.Also, I think if an interpolation's output is anything other than an element, the template-instantiation should covert that output to a static string that cannot be modified. There's no need to update the template if you can instead update the dynamic elements that were place into it. Cloning destroys your element references. Template instantiation should keep those element references! This way, at the custom class level, you'll always be able to modify the vary same elements you created before injecting it into the template.
I need something like the
<template>
tag that I can stick inside my custom class's constructors, where each time I instantiate it, it will contain the same referenced element I created in that constructor. Cloning is a pain; you clone it and then you have modify it before it goes out so that you can maintain element references. Cloning is fine for some things, but not ideal inside a class constructor.If we had a
<template>
tag like this; it would be all I'd ever need.The text was updated successfully, but these errors were encountered: