Skip to content
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

Ideas for lit-html 2.0 #1182

Closed
justinfagnani opened this issue Jul 15, 2020 · 30 comments
Closed

Ideas for lit-html 2.0 #1182

justinfagnani opened this issue Jul 15, 2020 · 30 comments

Comments

@justinfagnani
Copy link
Collaborator

justinfagnani commented Jul 15, 2020

lit-html 1.x has been out for about a year and a half now. It's seen good uptake, and it's still among the fastest, smallest, and arguably simplest DOM rendering libraries out there. The API is extremely flexible and has worked well for a wide variety of use cases - we haven't seen a lot of places where we want to drastically change the API based on current usage. There are some long-standing requests - like static bindings and spread expressions - that still pop-up every once and awhile, but don't seem to be urgent deal-breakers for most users.

So we're in a good place and are not out looking for reasons to release a new major version - we really value API stability! - but we have some new goals, the browser landscape is changing, and we've been able to see what the common usage patterns are. We have a chance to integrate this and make an even better lit-html.

The team has been experimenting with some ideas for a lit-html 2.0, and it seems like a good time to write down some of the goals and possibilities to get feedback on them. Note that nothing is set in stone, and some of the changes we might be able to back-port into lit-html 1.x

Goals

Backwards compatibility for most users

The simplicity of the main public APIs - html and render - has worked really well, and has even been adopted by other libraries. So we want to keep that, and want API changes to be backward compatible for most users. This also includes users of directives: the built-in directives should be backwards compatible. Far fewer developers write directives though, so we're willing to consider breaking changes there.

Backwards compatibility is especially important for internal Google use. Google's monorepo only allows one version of a library, so we'll have to update all of Google at once.

Performance

We always want to be as fast as possible :) Some changes that can make us faster are technically breaking though, and require a new major version. Ideas for improving perf range from removing abstractions, to doing less DOM work on template prep, to building an optional template pre-compiler. We also want to drive down the cost of template composition as much as possible, and optimize more for first render over updates.

Memory efficiency

lit-html is already decent with respect to memory consumption, but we can do better. Our architecture inherently requires memory usage between renders to store information for expressions (what we call "parts"). We can reduce this with API changes that let us keep fewer objects with fewer fields around. We especially want to optimize for the more common case of single-valued bindings.

Size reductions

lit-html is currently weighing in at ~3.4KB minzipped. We would love to get this back under ~3K. Removing some abstractions and browser bug workarounds can help, as well as exporting fewer public symbols so minification can work better. More concise modern DOM APIs are available if we can change browser support.

In addition, we can look at publishing a minified, bundled build so that we can crank the minifier settings to 11 and ensure the result works correctly. While we believe minification is ideally an app concern, we realize that apps won't know all the transforms that can be safely applied to our code.

Developer Experience

We would like to improve error messages, and not let publishing minified code negatively impact development. We'll look into producing both production and development builds.

Easier SSR

We are still working on SSR with the 1.x codebase, but we have learned a lot about how parts of the API make it easier or harder to implement SSR. The largest impediment is the sheer amount of flexibility our API gives developers. lit-html is in some ways more of a template library construction kit than just one template library. With pluggable template factories, template processors, polymorphic template results, and the ability to dynamically construct and insert parts into the DOM, we can't guarantee that we can represent all templates when SSR'ing, or understand how to reconstruct the JavaScript relationship between template constructs when hydrating.

Our SSR system will only be reliable when using a pretty non-customized version of lit-html, as the vast majority of users do. We've seen very little usage of these customization features, but their existence limits us, so we're going to explore removing some of them. We especially need to hear from developers using these features first though.

In addition, directives are a rough spot for SSR. To be server-renderable, directives currently need to stick within a subset of the API during first-render. We want a new directive API clarifies the allowable APIs and works on the server by default.

Better compile target

Some partners have existing template systems they may be interested in compiling to lit-html. Some new features would be useful for this, like unbalanced tags, statically verifiable spread attribute bindings, tag-name position bindings, and XSS-safe CSS bindings.

New features

Long-standing requests like static and spread bindings should be looked at, especially now that all current browsers correctly implement tagged template literals (which was preventing static bindings before now).

API cleanup

Some parts of the API are less-than-ideal and we shipped 1.0 knowing that we'd like to improve them. The directive and part APIs are prime examples. We want to make directives easier to write, and as mentioned, make them SSR compatible by default.

Potential Changes

Reduce the API surface

We see very little evidence of broad usage of extension and customization points like TemplateProcessor, TemplateFactory, and freely constructible Parts. Removing these abstractions would eliminate code, allow some APIs to be minified that weren't before, and reduce function calls and indirections in very hot code paths.

As noted in goals, we want the vast majority of existing templates to work as-is in lit-html 2.0.

Prod and dev builds

We don't want minified code to make debugging more difficult, so we should create an unminified development build with nice error messages and maybe even more template validation. We'll publish the production build by default so that apps don't accidentally deploy the dev build, and allow the development build to be used via various module replacement / aliasing tools, such as @rollup/plugin-alias.

Remove the workaround for broken template literals in Safari

Safari 12 had a bug where it would occasionally garbage collect template literal strings that shouldn't have been. This caused lit-html's caching to fail and re-render DOM that should have been updated. Our workaround was to use a secondary cache keyed by the joined strings of the template. This works without UA sniffing, but this has a perf and memory impact for all browsers. We should remove the workaround and consider Safari 12 to be a browser that doesn't support ES6 properly.

Simplify template caching by not allowing template tags to be dynamic.

Currently we cache by both the tag type (svg or html) and the template strings object. This lets you dynamically switch the template tag, but… who does that? We can eliminate a cache lookup completely by only caching by the template strings object.

Better HTML scanning

Before templates are parsed by the browser, lit-html prepares the HTML string by inserting markers where expressions are. We need to insert comments in some places, like text nodes, and non-comments in others, like attribute values. Our current scanner is good, but sometimes thinks that an expression is in an attribute when it's in text. This means we have to search all text nodes for markers, which has a high cost in template prep. If we can make a more precise scanner, so that we always insert comment markers in text nodes, we can skip processing them (except for raw text nodes like <script>, <style>, and <textarea>) and speed up template prep and thus first render.

There is a careful balance here because the current scanner is so small and we want to reduce code size overall. We'll have to be lucky and/or good to make this work. If it does work we'll be able to support new binding types like spread and tag-name - or at least we'll be able to produce better error messages when people try to write them.

Spread bindings / Attribute position bindings

Attribute directives are very useful for running some logic on an element because the reference to the element is inherent in the template syntax, and the directive is run after the element is created. Sometimes you just need logic on the element and don't need to bind to an attribute, but in lit-html 1.x you need to invent an attribute name to place a binding. We've seen this when MWC used a directive for ripple:

html`<button .ripple=${ripple()}>${label}</button>`

With the better scanner we can add attribute position bindings:

html`<button ${ripple()}>${label}</button>`

Another example is Vaadin's field() directive:

html`
  <vaadin-text-field
    label="Full name"
     ...=${field(model.fullName)}>
  </vaadin-text-field>`

Which could be written as:

html`
  <vaadin-text-field
    label="Full name"
    ${field(model.fullName)}>
  </vaadin-text-field>`

This will let us do nice things like ref and spread directives:

html`<div ${ref((e) => console.log(e))} ${spread(props)}></div>`

More template types

We have seen some need for template types like attr and css.

attr would be a template that just contains attributes (and possibly properties, events, etc), and would enable passing and setting multiple attributes in an XSS-safe manner.

const exampleAttrs = (foo, bar) => attr`foo=${foo} bar=${bar}`;
html`<div ${exampleAttrs('a', 'b'}></div>`

It's really an element partial, which has a number of interesting use cases, from parity with template systems like Closure Templates, to being able to declaratively set host attributes.

css would be for bindings into CSS text in <style> tags and possibly LitElement's styles property. It would make style bindings more XSS-safe by only allowing developer provided and sanitized values to be bound to CSS.

LitElement's css is a bit different from a lit-html template, but if we can combine the two we can share CSS-safe values between them, and enable other component base-classes to use the Constructible StyleSheets-producing system.

Remove template part's two-pass update

Due to how attributes with multi bindings interact with directives, we have a two-pass update process: first all parts in a template have their value set, then all parts are committed. This allows multi-binding attributes to set the attribute value only once. Removing this will reduce code, an entire pass over all parts in an app, and simplify the Part API for directive authors.

Remove attribute part committers

As part of removing two-pass updates we should remove the part/committer split. Currently we have a part for each attribute expression and a single committer shared between parts on the same attribute. This is extra work to setup and incurs extra memory overhead. An early version of lit-html had a multi-valued part type that could update with an array of values. We changed to allow parts to have their value set externally to rendering and to more closely align with the W3C Template Instantiation proposal. We don't want to let parts be updated externally anymore and we should only align with template instantiation once it's more concrete, so we should explore the old design again.

Remove async directives: until(), asyncAppend(), asyncReplace()

Dealing with asynchronicity in components is hard. until() and friends are helpful in some simple cases, but they are actually quite tricky to use correctly. It's pretty easy to generate a new Promise every render and pass it to until() so that the binding it's used in actually updates twice for every render: once for the fallback, and once again with the resolved value.

Async templates are also hard to test if the async values aren't exposed outside the template in some way.

We're not 100% on this, but we suspect that async helpers should be moved up into the component model, leaving templates synchronous. It should still be possible to write an until-like directive, though such directives might require cooperation with a host component that can perform a re-render. TBD.

A better directive API

Directives have a few issues. First, they often directly interact with the DOM, making them difficult to SSR (our SSR library doesn't emulate the DOM). Second the API is stateless, but directives are really only needed when they either mutate the DOM or require state. Directives that require state then have to keep WeakMaps keyed off parts. This is cumbersome. Third, directives don't have a lifecycle, but we've had requests for one, and it could be useful for first-render performance.

We should investigate a stateful, class-based directive API. This may seem to fly in the face of other libraries eschewing classes, but we think we have something nice in the works.

Directives would extend or implement the Directive base class and implement one or more lifecycle methods, currently render() and update(). render() is for the initial render and SSR and can only return a lit-html value (including a template). update() is called only on the client and can access a directive's part.

lit-html will instantiate the directive instance and keep it around between renders. State can be class fields: no more WeakMaps!

/** Renders a count of the number of render calls before the value */
class Count extends Directive {
  count = 1;

  render(value) {
    return html`${this.count}:${value}`
  }

  update(part, args) {
    // mutate DOM here if neccessary
  }
}
export count = directive(Count);

This seems to be easy to implement and already much better for SSR. We'll also investigate a disconnect callback to allow cleaning up any held resources.

Other changes to explore for directives include making it less likely that they need to directly manipulate DOM or Part internals. Handling the nothing sentinel in the core library so that it removes attributes means ifDefined() doesn't need to call removeAttribute(). Adding part state detach and attach methods means cache() and repeat() don't have to move the DOM for a part.

Better animation support

When directives have lifecycles, we may be able to tie them into the host component's lifecycle to better enable animation directives. Animation techniques like FLIP work by measuring layout before DOM changes. If we can get a performant disconnected callback, we might be able to get a performant beforeRender() callback too, which would allow this. Another option would be to let the directive directly get a reference to the host component in its constructor and set a before render callback on the host, an API that would have to be added to LitElement and other component hosts.

Remove support for some data types

lit-html takes care to do nice things with data types like Symbol and arrays in attribute bindings. This costs bytes and exists on the hot paths. We can require developers to use patterns like String(symbol) and array.join(' ') instead. We'll obviously keep array support for text bindings so that array.map((i) => html${i}) still works.

Move IE11 support out of the core library

IE11 usage is dropping and we'd love to drop it, but we probably can't do it quite yet since some of our customers are stuck supporting it for a while. We might be able to change our support policy and move any additional code we need for IE11 and Edge Classic into auxiliary files that are loaded only by the applications that need it. lit-html (and by extension LitElement) wouldn't support IE11 by default then, and apps that don't need IE support wouldn't pay the cost of it, but an app could include a top-level script tag that would patch lit-html and make it work.

We can also use more modern DOM APIs that will save some bytes and just require more polyfills for IE. There are some tricky parts of IE support we might not be able to patch in though. IE and Edge Classic iterate attributes in an undefined order, making it harder to associate them with expressions. In-order iteration isn't polyfillable. IE and Edge Classic also remove style and other attributes with invalid content, which happens when they're bound, so we'll need to still change bound attribute names.

There is a lot of experimentation to be done here.

Remove use of instanceof

Using instanceof to detect lit-html's own objects is problematic in cases where npm installs multiple versions of the lit-html package. It's also slow for false cases as it has to traverse the prototype chain (we need to measure and confirm this). We can move to looking for a sentinel own property on TemplateResult and other objects.

Template pre-compiler

While we think we can bring down template processing overhead a bit, we could possibly eliminate most of the work by pre-compiling templates. We need to let TemplateResult be able to carry a reference to pre-compiled template strings and part metadata, rather than just a template literal strings.

Pre-compiling templates may be an alternative way to supporting Safari 12, IE11, and Edge Classic since it skips some usage of template literals and DOM APIs that are problematic for them.

Next steps

That's a lot of potential changes! We'll be lucky if the majority of them pan out. Some changes, like new features, are potentially in conflict with goals for performance and code size. Other changes, like reducing configurability, may be hard on some customers.

One thing we know we need to do is drive the whole process from science, which means lots of benchmarking. Our current "Shack" benchmark, and the community JS framework benchmarks don't exercise enough of the features and code paths to give us enough information. So we'll develop better benchmarks, and a parameterized benchmark generator to be able to tell where performance tradeoffs start paying off (ie, benchmarks with adjustable mix of static vs dynamic content in a template).

We're experimenting with some of the ideas in this issue already, and need to figure out when, where, and how to integrate the changes into this repo. Details TBD.

Meanwhile, we encourage feedback on these ideas and any more that our users have.

@bennypowers
Copy link
Collaborator

bennypowers commented Jul 15, 2020

Would you consider shortened urls for error messages in prod?

Error LH2791 https://goo.gl/juid789x

@justinfagnani
Copy link
Collaborator Author

@bennypowers that's the idea for the errors that remain in prod. Some warnings might require string or DOM introspection that we just don't want to do in the prod build. Things like trying to verify that the template is well-formed. That may be better served still by static analysis with lit-analyzer though.

@bennypowers
Copy link
Collaborator

lit-analyzer

oh ok cool :D

@fardarter
Copy link

Lack of spread expressions are a big deal for me for design system projects. It's really valuable when designing component to wrap arbitrary components which can lift the props they need.

@Westbrook
Copy link
Contributor

I saw another member of the Google make a comment on a similarly shaped issue in a repo for a similarly small renderer wherein he implied that said renderer's future version should target being "2kb 2kb 2kb". Would it even be feasible to target a similar goal here? Keeping the library under 3kb would certainly seem easier while targetting getting it down to that level.

@Christian24
Copy link
Contributor

I will try to give feedback on some of the things that came to mind.

Reduce the API surface

I always thought this was "rocket science" and there has never been a need to dig deeper, for me. I wouldn't even know where to find further info to unleash the potential maybe buried there. So, I am team reducing the surface. :)

Remove async directives: until(), asyncAppend(), asyncReplace()

My gut reaction was that I find these a good reference point to see how something like this could be implemented. We do not use them, but we used them as kinda reference points to see how we could get where we wanted to get. I have a couple feelings about these:

  • putting one or two as a kind of recipe in the documentation and remove them from the code
  • splitting them into an own, "extended" package. not sure that is necessary, but for people using them would provide an update path.
  • I think it is okay to remove them, because not sure the assumption that Promises are being used is fair. Maybe people would also see use cases for Observables or something else.

Remove support for some data types

Am I reading this correctly? You would remove support for attribute binding if the bound value is an array? I can see this being useful, while in those cases I prefer property bindings. Not sure I would support this change if it can be avoided.

Move IE11 support out of the core library

I agree. I feel the library should not carry fixes for browsers that are not the release target of the ES version. For IE 11 we need to transpile lit-html anyway, so I think making polyfills an addon is fine. On a different note I think the ease of use of the web component polyfills in general could be improved.

Template pre-compiler

So, this is a build step? Would this be opt in or the only way to use lit-html going forward? I think one of the beauties of lit-html is that you can just drop it in and it just works. I would also argue it makes it easier to understand. If I have to understand a template, a library, a compiler and possibly a build chain just to debug a bug it would increase complexity a lot I think. That does not mean I am against an optional webpack plugin or whatever though.

@justinfagnani
Copy link
Collaborator Author

@Westbrook I think we could make a very minimal lit-html in under 2KB, but we can deliver a lot of functionality in another 1KB or so. It's all tradeoffs. For apps there's diminishing returns on removing 1KB out of 100s or 1000s. For individual components it might really matter.

We might be able to make a version of lit-html that only works if you're pre-compiled the templates (and is automatically swapped in), and individual components could use that in a build process to produce the smallest possible output.

@dlockhart
Copy link

Move IE11 support out of the core library

If you'd like another data point, we were just able to finally EOL IE11 in our application -- it took a 1.5 year company-wide effort. So we'd definitely support this! We do however still support legacy-Edge, but are monitoring Edgium uptake closely and hope to fast-track its EOL in the next 6-12 months.

@DeadWisdom
Copy link

Maybe a renderServer() method for the Directives, can at simplest merely call render(), but signifies that it's meant to be called on the server.

Also +1 on the css - I remember being rather confused that I couldn't import { css } from 'lit-html'.

@justinfagnani
Copy link
Collaborator Author

@Christian24

Remove support for some data types

Am I reading this correctly? You would remove support for attribute binding if the bound value is an array? I can see this being useful, while in those cases I prefer property bindings. Not sure I would support this change if it can be avoided.

This would only be for attributes, where right now we join the array items with a space.

Template pre-compiler

So, this is a build step? Would this be opt in or the only way to use lit-html going forward?

Optional. It would perform the template preparation steps (adding markers, recording part metadata) at build-time.

@cintaccs
Copy link

cintaccs commented Jul 16, 2020

Thanks for your great work and efforts to keep lit-html one of the most well documented, well maintained, well supported etc. and stabile libraries that I have come across! This issue "Ideas for .." is just yet another example of that! Thanks!!!

I am not using TypeScript - and I believe all you can do to improve the build process especially minifying html, css (less important as it is written more flat already than html nested syntax...) for a pure js ES6+ webpack or roll-up environment will be highly appreciated. Although there are examples it is not straight forward to get started and get a good minified version (which I believe are also more performant) of the render templates.

I am using lit-virtualizer - but ran into the problem that it can't do more than rather simple fixed width listings. As soon as you go for MDC-cards in a grid or a bigger old-fashioned TABLE (where column width per definition are adjusted to actual cell content) - it is a no-go with lit-virtualizer. I have made some attempts with lit-element + Observer and simply re-render new entries to the end of the TABLE as you scroll down - and that works. It is simple and straight forward - however as soon as you reach a few 100 records there is a need to remove hidden previous records to maintain scroll speed and keep performance / memory down. That is more tricky (scroll position needs to be handled etc.) and I haven't got the solution to that (...yet)... However from that work I believe the main speed improvement for lit-html could actually be to guard (optionally) what is actually in the visible part of DOM / viewport... not sure whether that is addressed elsewhere, here or? (or is that what the repeat directive is actually doing / supposed to be doing...??)

Streaming contents into a render process is also an area that I believe could be a (potentially more than SSR... or in combination with) performance improvement that could benefit from prepared features around lit-element and lit-html.

@Christian24
Copy link
Contributor

@Christian24

Remove support for some data types

Am I reading this correctly? You would remove support for attribute binding if the bound value is an array? I can see this being useful, while in those cases I prefer property bindings. Not sure I would support this change if it can be avoided.

This would only be for attributes, where right now we join the array items with a space.

Okay, let me explain a bit more where I am coming from. My idea has always been that when we write custom elements we, as development team, should provide attributes for our elements, because it is the HTML standard way of doing this. Because quite frankly nobody knows how and with which technology our elements might be used in the future. We are using LitElement, but tomorrow somebody might want to use them with Angular or whatever. That was my motivation for always providing attributes. However, upon further inspection this might have been a bit shortsighted: Since HTML attributes are per spec strings, there seems to be not really a native way to provide array or object attributes anyway. Maybe one could argue that the HTML spec tries to solve arrays more through the template:

<fieldset>
<input type="radio" value="One">
<input type="radio" value="Two">
</fieldset>

So, for element authoring array attribute binding is probably not the way to go anyway. And in lit-html we can always use property bindings. Not entirely sure, about object attribute bindings, it would kinda be the same thing right?

Template pre-compiler

So, this is a build step? Would this be opt in or the only way to use lit-html going forward?

Optional. It would perform the template preparation steps (adding markers, recording part metadata) at build-time.

Sounds like a good idea.

I am generally excited for the direction lit-html is going.

@43081j
Copy link
Collaborator

43081j commented Jul 16, 2020

Template pre-compiler

Would this involve exposing a way at run-time to create instances of a single template programmatically?

In our case, we're struggling to come up with a clean solution whereby we can take rows of data from the backend and render them with a single template on the frontend. Normally, this is easy but in our case which template we use depends on the data which came back (or should...).

This sounds similar SSR i guess, in that SSR has to have some way of instantiating a template (re-using it) with data?

Basically i think the ultimate solution for us is the "template instantiation" RFC ive seen floating around for the past few years. But that won't come any time soon i imagine.

i.e. we want this:

render() {
  return someAlreadyCompiledTemplate.apply({some: "data"});
}

There are ways to achieve this in a more hard-coded/strongly-typed fashion but this could still be useful.

@grimly
Copy link

grimly commented Jul 18, 2020

I've been using lit-html for some time now and here are my suggestions :

  • Drop classes / instanceof checks / internal Weakmap registries. Start using duck typing.
    I came across the need to let other developers make templates but not call the render functions themselves. To make it work I had to create a package that store all exports to the globalThis object, export them again and make everyone use my new package. Also Typescript support for these cases is aweful.
    As an implementation suggestion, you may use either Symbol.* just like Symbol.observable or expect an object with specific properties like the then of promises.
    As a test suggestion. Ensure that given two instances of the library, one render may accept the result of the other html function.
  • For directives, emphasize on setValue. We should not have access to the underlying element. For cases where HTMLElement manipulation is required (and I have yet to find one), you already support those elements as text binding values.
  • Again for directives, let them return a function to call on the next render. This would solve the cancellation issues.
    An other solution would be to look for functions in a directive object like the custom elements API does.
  • Either support asynchronous directives giving these a life cycle or just don't and use synchronous mapping functions only. Make it an explicit goal or non-goal.
    It is fine not to support it if you let people know they should use other libraries such as lit-element or redux to manage changes.
  • Change property bindings to transform kebab case to camel case.
    Out of the box, you cannot bind to <input .valueAsDate="${...}".
    While it can be done by overriding the template factory and slap a .replace(/-(.)/g, (_,c) => c.toUppercase()) to the name. I think this should be the default. And note that it doesn't break make current bindings impossible since my--property would bind to the my-property property.
    This would also make easier things like the classmap directive. You would just bind the directive to .class-list and make the changes like part.value.add('my-class')
  • Just like you said, clear the API of its clutter. To me html and render are the key elements and other exports should be Typescript interfaces or constants (reproductible symbols or strings). Let's apply the YAGNI and KISS principles.
  • I have so little ideas of what SSR imply but to me, hydrated rendering takes it to a whole different level. You would have to inspect what is already there, look for clues to catch up on parts, not mentioning the conditional rendering... I wonder if replacing the whole content would not be faster.
    If you use duck typing as I suggested, make SSR support in its own package that would provide its own version of the render function.

@justinfagnani
Copy link
Collaborator Author

@grimly thanks for the suggestions.

Out of the box, you cannot bind to <input .valueAsDate="${...}"

You absolutely can. If you're having an problem with this can you open an issue?

@Westbrook
Copy link
Contributor

Yay to "Remove use of instanceof"...while it's certainly manageable, it's always a chore and if we get that library all the way down to 2KB 😉 , it won't matter much if there are multiple copies on the page.

With the continued growth of tools like https://medium.com/storybookjs/storybook-controls-ce82af93e430 the need for an updated scanner and native support for spreading objects onto elements is growing as well. Do you see some alternatives to needing to mutate object properties to be pre-fixed with sigils in order to acheive event and property binding? I like the overall drive to reduce layers of unneeded complexity and see this as yet another that it would be great to see a path towards disolving.

Is there anything that an updated scanner or approach to directives could do inorder to increase the compatibility between lit-html and something like Preact's htm pragma? They are very close already, and maybe a compilation step will always be required (particularly for specific feature differentiation, like dynamic tag names), but the flexibility to be able to adopt code/tools from other projects across the boundary would be of particular interest.

On the subject of Preact and in a way the subject of getting down to 2KB (or less?), there is a proposal that would bring a (virtual) DOM diffing approach to templates natively to the browser in a way that could be naively related to what the Template Instantiation proposal also attempts to acheive. While you mention stepping back a little from the Template Instantiation spec while it bakes a little further, do you see any benefit of doing explorations into how lit-html might align with the "DOM Diffing Web API" or other template centric proposals (if they exist) during this time of "reassesment" on the way towards a 2.0 release? It would seem like a good future proofing especially if such investigations were to discover benefits that could be applied to the library or either of the two very exciting spec proposals.

Looking forward to ongoing expirementation!

@grimly
Copy link

grimly commented Jul 22, 2020

@grimly thanks for the suggestions.

Out of the box, you cannot bind to <input .valueAsDate="${...}"

You absolutely can. If you're having an problem with this can you open an issue?

Oops. That was an issue of mine and I didn't take the time to look further. I stroke this point.

@justinfagnani
Copy link
Collaborator Author

@Westbrook

Yay to "Remove use of instanceof"...while it's certainly manageable, it's always a chore and if we get that library all the way down to 2KB 😉 , it won't matter much if there are multiple copies on the page.

For the record, I don't think we'll get to 2K. Let's say I'll be happy with a 20% improvement :)

With the continued growth of tools like https://medium.com/storybookjs/storybook-controls-ce82af93e430 the need for an updated scanner and native support for spreading objects onto elements is growing as well.

Can you clarify? I see the need to set properties based on the "arguments", in our case the component's public properties. But why would spread bindings in lit-html help? Seems like the controls system could just set properties on the components directly, independent of lit-html.

Do you see some alternatives to needing to mutate object properties to be pre-fixed with sigils in order to achieve event and property binding?

What I've been experimenting with is two things: 1) The ability to have attribute-positioned expressions. Directives placed there could then do whatever they want in terms of setting properties, attributes, etc. (module the restrictions we want to ensure SSR compatibility. TBD). 2) an attr template tag that looks like:

attr`foo=${foo} bar=${bar}`

This is nice because it's statically analyzable, doesn't require imperative DOM calls, and should be SSR-friendly. It could be used directly by template authors or produced by directives, but doesn't allow for dynamic bindings.

For the case of turning objects into attribute bindings, I'd want to make that SSR-compatible, and without requiring custom SSR implementations for the directives. I'm not sure how possible that is without some tough code size/complexity tradeoffs. We'll see. It could be that for a lot of use-cases we're either fine with the more static attr approach, or don't need SSR at all. Maybe Storybook is one of those cases.

@yorrd
Copy link

yorrd commented Sep 1, 2020

Thanks for the transparency in this thread, really appreciate it. Love all of it! Here's some of what we're doing with lit:


TemplateProcessor

Here are the lit-html internals that we're using currently to customize lit for our projects. Mainly this comes down to us using our own TemplateProcessor on top of the default one (note the OptionallyBinding... parts):

  • we enable refs. That's obsolete with the changes you're proposing
  • we enable lit to subscribe to (and unsubscribe from) rjxs observables. That has been a game-changer for our framework because the data layer is written in rxjs (network updates are mirrored immediately in the UI without weird bindings)
  • we've enabled the style attribute to use emotion.js Interpolations. Basically you get CSS in JS just by style=${{...}} without writing the misleading class=${css({...})} every time. This will be even more amazing with <div ${css({...})}></div>

Directives

Also, we use directives heavily.

For example, we do virtualization with a directive that loads and unloads the content depending on an automatically spawned IntersectionObserver. We just started using this, needs a lot of improvement but I really like the API using directives for this.

export const virtualize = directive( (actualTemplateResult: TemplateResult) => (nodePart: NodePart) => {
    const node = nodePart.startNode.parentElement!;

    const render = () => {
        ...
    };

    const unrender = () => {
        ...
    };

    requestAnimationFrame(() => {
      const intersectionObserver = new IntersectionObserver(
        x => {
          if (x[0].isIntersecting) render();
          else unrender();
        },
        { options },
      );
      intersectionObserver.observe(node);

      // register unmount listeners // EDIT: this is something we add to parts to be able to stop rxjs subscriptions in parts
      (nodePart as OptionallyBindingNodePart).subscription.add(() => {
        intersectionObserver.unobserve(nodePart.startNode.parentElement!);
        intersectionObserver.disconnect();
      });
    });
  },
);

Another example where we use directives is a preloader that spawns all components and sets style so that you can seamlessly switch between templates without a render flicker. That could probably be done with a standard web component as well though.

Really excited to see where lit is going. Glad to provide feedback and beta testing at any time.

@justinfagnani
Copy link
Collaborator Author

Thanks for the info @yorrd! Hopefully you'll be able to handle the removal of TemplateProcessor ok. At a glance it seems like directives and "spread parts" (we need a better name, since it's not just spread) should handle those cases ok.

We'll publish preview builds relatively soon. They'll have some missing features and won't have full browser support at first, but hopefully you can see how your needs fit.

@43081j
Copy link
Collaborator

43081j commented Sep 2, 2020

Couple of things im sure you're already aware of but these are what we've been hoping for:

  • Some way of spreading bindings (you mention this in your original post, it'd be great for completing the storybook integration of web components)
  • A directive or fix for the old select problem you and i have discussed before - where if a select has children computed dynamically, they get appended after the select's bindings are processed which means you can't possibly set value since the options won't yet exist at that point
  • A way to render to light DOM as well as shadow so we don't have to choose one or the other (you mentioned this once before so im sure you have it on a branch somewhere)

Is there anywhere we can read about what work you've been doing in regards to SSR too? it would be nice to have an explainer of how you're looking to make it work so we're prepared/can track it.

@grimly
Copy link

grimly commented Sep 2, 2020

  • Some way of spreading bindings (you mention this in your original post, it'd be great for completing the storybook integration of web components)

Coupling this kind of feature with directives may be dangerous. What if a second commit from a directive does not spread a previously given attribute ? Remove it ? Let it as is ?

  • A directive or fix for the old select problem you and i have discussed before - where if a select has children computed dynamically, they get appended after the select's bindings are processed which means you can't possibly set value since the options won't yet exist at that point

For the select problem itself, you have the selected attribute on the options.
As for this as a general issue, maybe committing the values in reverse order of their declaration might be a solution since attributes of a parent element always precede the element's children.

  • A way to render to light DOM as well as shadow so we don't have to choose one or the other (you mentioned this once before so im sure you have it on a branch somewhere)

Are you talking about lit elements ? I can't find the use case otherwise.

Is there anywhere we can read about what work you've been doing in regards to SSR too? it would be nice to have an explainer of how you're looking to make it work so we're prepared/can track it.

There are a lot of new branches. Have a look in the commit network

As for new suggestions :

- export const nothing = {};
- export const noChange = {};
+ export const nothing = undefined;
+ export const noChange = Symbol.noChange = Symbol.for('noChange');

@mrin9
Copy link

mrin9 commented Sep 3, 2020

as @43081j already mentioned, I like to vote +1 to know a little more on the SSR and how would lit-element leverage this would be great.

@yorrd
Copy link

yorrd commented Sep 3, 2020

@justinfagnani thanks for your take on this. Appreciate the insight. We'll definitely beta-test as early as possible :)

As for rxjs parts: I'm not sure whether directives are the way to go there just because you end up writing a whole lot more (s and )s. We tried, it does make a real difference to readability. But hey, we'll figure something out - worst case we'll do a fork. Looking forward to testing!

@rictic
Copy link
Collaborator

rictic commented Oct 5, 2020

+1 to clarifying the type of nothing so that it's easier to return from a template function

@zandaqo
Copy link

zandaqo commented Oct 12, 2020

Are there any plans for implementing this #1143 in the new Directives API? This would be useful in lit-element when we need to make directives aware of the custom element they are used in.

@vegarringdal
Copy link

vegarringdal commented Oct 17, 2020

Just tried version 2.0
Dont know if Im doing something wrong, but version 2.0 does not handle null/undefined with .valueAsDate.

  <div>
      <input type="date" .valueAsDate=${null} />
      <input type="date" .valueAsDate=${undefined} />
      <input type="date" .valueAsDate=${new Date()} />
    </div>

Use this and set to version 2.0.0-pre3
https://codesandbox.io/s/lit-html-forked-njyi8?file=/index.html

image

@vegarringdal
Copy link

Question about my last comment.

Should I create a seperate issue about this ?
I use lit-html in some projects now with a custom wrapper abound it.
Love it, its fast any only had one issue so far with unpredictable behavoir before 2.0 alpha. So thats a win in my book.

Really want 2.0 to be better and faster :-)

@sorvell sorvell unpinned this issue Nov 20, 2020
@sorvell
Copy link
Member

sorvell commented Nov 21, 2020

For issues on the prerelease, please file an issue with the prefix and/or label [lit-next], thanks.

@justinfagnani
Copy link
Collaborator Author

The 2.0 release is imminent... thanks for the ideas!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests