Coder Social home page Coder Social logo

rfcs's Introduction

Lit

Simple. Fast. Web Components.

Build Status Published on npm Join our Discord Mentioned in Awesome Lit

Lit is a simple library for building fast, lightweight web components.

At Lit's core is a boilerplate-killing component base class that provides reactive state, scoped styles, and a declarative template system that's tiny, fast and expressive.

Documentation

See the full documentation for Lit at lit.dev.

Additional documentation for developers looking to contribute or understand more about the project can be found in dev-docs.

npm

To install from npm:

npm i lit

Lit Monorepo

This is the monorepo for Lit packages.

lit 2.x source is available on the 2.x branch. lit-html 1.x source is available on the lit-html-1.x branch.

Packages

Contributing to Lit

Lit is open source and we appreciate issue reports and pull requests. See CONTRIBUTING.md for more information.

Setting up the lit monorepo for development

Initialize repo:

git clone https://github.com/lit/lit.git
cd lit
npm ci

Build all packages:

npm run build

Test all packages:

npm run test

Run benchmarks for all packages:

npm run benchmarks

See individual package READMEs for details on developing for a specific package.

rfcs's People

Contributors

andrewjakubowicz avatar drschlaubi avatar justinfagnani avatar kevinpschaaf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rfcs's Issues

[RRFC] Add ability to spread attributes, properties, and listeners to elements

  • [X ] I searched for an existing RRFC which might be relevant to my RRFC

Motivation

This has been a long standing request and is generally useful when passing around blobs of related data that should be handed down between elements together.

Supporting this capability could also be used to satisfy [RRFC] Better Declarative Host Manipulation.

Example

html`<x-foo ${spread({value: this.value, class: {selected: this.selected}, '@foo': this.handleFoo})}>...`

How

Here is a slightly opinionated prototype of a spread directive. Sample usage with a bit of explanation inline:

html`<x-foo ${spread({
  '.prop': 5, 
  '@event': fn, 
  '?boolAttr': true,
   attrOrProp: 'prop if "in"',
   class: 'x' || {x: true},
   style: 'top: 0;' || {top: 0}     
})>...`

Opinions (favoring simplicity)

  • attribute or property is auto-detected with an in check
  • attributes automatically convert to boolean when value is boolean and unset whenever null-ish
  • classMap/styleMap behavior is built in

In addition, the prototype provides a host directive that can be used in ChildPart position, e.g.

html`${host({tabIndex: 0, 'aria-hidden': true, '@click': this.handleClick})}...`

And a tag directive that obviates the need to use static-html, also building upon spread, e.g.

html`${tag('button', {'@click': fn}, `Hi `, html`<img ...>`)}...`

Caveats

Values that are spread into elements can conflict with otherwise bound values. While not necessarily harmful and following a simple "last one wins" rule, this behavior could be unexpected or a potential foot-gun. It's up to the user to understand this and ensure proper configuration.

[RRFC] Stateful/reactive render functions

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

Functions are the simplest unit of composition and the first tool a developer leverages to accomplish almost any task. lit empowers developers to write shareable interoperable code that creates rich interactive interfaces. Minimizing the distance and friction between starting with a function and ending with a great UI serves that goal.

There are 2 main pieces of this puzzle: (1) describing rendering, (2) managing reactive state.

Describing rendering: Lit's html and css TTL syntaxes are excellent at this, and can easily be returned from a function.

Managing reactive state:

  • Lit's reactive properties provide an excellent mechanism for managing reactive state and make sense to use when creating custom elements that extend LitElement. If a dev started with a function, they must move to a class, and this is fine when it makes sense.
  • However, the new @lit-labs/preact-signals library provides an alternative for simpler use cases and it more seamlessly builds on top of functions.
    • Signals are reactive state containers. Their reactivity does require using them within a scope that records dependencies (aka effects), but the lit signals package provides SignalWatcher and watch to deal with this.
    • The one remaining problem is having a convenient way to create signals within a function. They can certainly be passed in as arguments, but what if they are internal the ui being managed by the function?

This last issue is addressed below...

Example

Consider creating a simple counter:

In a LitElement, it looks like:

@state()
accessor count = 0;
  
render() {
  return html`<button @click=${() => this.count++}>${this.count}</button>`;
}

With a function using signals, this can be:

const renderCounter = () => html`<button @click=${() => count.value++}>${count}</button>`;

But this doesn't work because we need to get the count signal from somewhere.

How

React solves this problem with hooks, but these have tradeoffs. Lit can make this more straightforward by leveraging the fact that all Lit templates are rendered into persistent "parts," and access to them is provided via the directives API.

So, all we need is a simple directive that can initialize state. Here's the updated counter example:

const renderCounter = (use) => {
  const count = use.state(() => signal(0));
  return html`<button @click=${() => count.value++}>${count}</button>`;
}

Then use it like this:

  return html`counter: ${stateful(renderCounter)}`

See working prototype.

The stateful directive provides a state method which memoizes the result of its argument.

References

  • n/a

[RRFC] SlotController

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

Moved from lit/lit#2693

There are a few common patterns for using slots that could be nicely wrapped up in a reactive controller:

  • Determining when a slot has assigned content so you can style the component differently (ex: add padding when there's content).
  • Tracking slotted children to call APIs (set properties, add/remove event listeners, call methods) when child enter and exit the slot.

Example

We could make a SlotController(s) to help.

class MyElement extends LitElement {
  private _slotController = new SlotController(this);
    
  render() {
    const slotStyles = {
      padding: this._slotController.hasAssignedElements() ? '4px' : 0,
    };
    return html`<slot style=${styleMap(slotStyles)}></slot>`;
  }  
}

How

See https://gist.github.com/justinfagnani/a90eefb8da6bbfa4250a46732a8e5fbc

Current Behavior

Libraries write their own slot controller, like https://github.com/shoelace-style/shoelace/blob/next/src/internal/slot.ts or users use slotchange.

Desired Behavior

It's easier.

References

  • n/a

[RRFC] Worker-based templates and components

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

There are a couple of reasons why developers may want to run components in isolated workers:

  1. Applications may want to run untrusted code with UI, such as a third-party plugins. iframes are sometimes appropriate for this, but not always. A relevant example for Lit might be Photoshop for the web, which happens to be written in Lit, and would be likely to want a secure plugin model.
  2. Applications may be able to move some computationally expensive work off main thread to a worker.

I propose we focus on the first motivation, since experiments on moving UI work into workers generally haven't paid off due to the cost of communication overheard of workers. Especially without an expensive vdom diff/reconciliation phase, lit-html updates aren't often the bottleneck. For performance improvements, it's usually better to move expensive computation to the worker, and keep rendering on the main thread.

How

To run UI code in a worker we need a few things:

  1. Load a worker-rendering protocol module, plus template/component-containing modules, in a worker
  2. Register templates and components by identifier
  3. Send a request for a render to a worker with a template or component ID and data
  4. Instantiate components in the worker as necessary
  5. Send a result to be rendered to the main thread
  6. Render the result with standard Lit behavior, including stable, minimal DOM updates
  7. Handling UI events on the main thread and sending them to the worker

This might be nicely done by abstracting over workers in a way to install the infrastructure for the worker-rendering protocol, and creating a proxy element in the main thread to apply render results and maintain communication with the worker instantiated component.

Image a main thread API like:

import {createWorkerElement} from '@lit-labs/workers';

// Create a local element class that's a proxy to a worker element named 'my-worker-element'
// With this API we could have one worker per element instance, which might be expensive, but
// would maximally isolate sate for use cases like plugins. This component could still have child
// components in the same worker.
// Worker-to-instance affiliation could be configurable.
const workerElementClass = createWorkerElement({
  tagName: 'my-worker-component',
  url: new URL('./my-worker-component.js', import.meta.url),
});

// Register the local proxy
customElements.define('my-local-element', workerElementClass);

// Create a local instance which will automatically create a worker and start rendering
document.createElement('my-local-element');

In the worker, we would like the API to appear to be as plain as main thread Lit components. Possibly we patch the LitElement prototype to send the result of this.render() to the main thread.

Rendering lit-html templates across worker boundaries

lit-html TemplateResults are the objects that represent the evaluation of a template expression.

For a template like:

html`<h1>Hello, ${name}!</h1>`

A TemplateResult would look like:

{
  _$litType$: 1, // 1 for html, 2 for svg
  strings: ['<h1>Hello, ', '!</h1>'], // This is actually a template strings array
  values: ['Workers']
}

This object can be sent via postMessage() (as long as the binding values are), and a TemplateResult can be reconstructed on the main thread side.

The trickiest part is to keep the tagged-template-literal semantics that lit-html relies on so that the strings array is referentially identical across multiple results. We can do this, and optimize postMessage() payload size, with a cache in each thread.

The first time we send a TemplateResult for a particular template expression, we'll send the result, but add a unique and durable ID:

{
  _$litType$: 1,
  _$stringsId$: 1,
  strings: ['<h1>Hello, ', '!</h1>'],
  values: ['Workers']
}

The main thread will store the strings array, keyed by the _$stringsId$.

Subsequent messages with TemplateResults from this template expression will only contain the ID:

{
  _$litType$: 1,
  _$stringsId$: 1,
  values: ['Workers']
}

The main thread will then add back in the cached strings array resulting in a TemplateResult object that can be correctly rendered by lit-html.

We will have to do a similar thing for styles.

Compatible data types

postMessage() supports send objects compatible with the structured clone algorithm, which includes primitives, objects (with cyclical references supported), Arrays, Maps, Sets, Dates, Regexps, and more.

A few notable omissions:

  • Symbols
  • Functions
  • DOM objects including Node and Event
  • Prototype chains are not walked - only own properties are sent
  • Private fields will not be sent (nor their WeakMap equivalent)

This means that we will have to traverse messages and transform them to proxy certain unsupported data types, especially functions and events for event handling.

The local proxy

The local proxy element will have to send messages to the worker with new data to render. This should be doable in a performUpdate() override. It will send a payload with the current and old values of declared reactive properties and attributes. Attributes have to be sent separately from properties because attribute converter functions have to run in the worker. We can't rely on them being reflected to properties in the main thread.

The local proxy will have to add event handlers for worker components. This can be done by proxying event handlers in templates, and patching addEventListener() in the worker to cause it to register an event handler in the proxy.

Patches to LitElement in workers

Plain LitElement's update() implementation calls this.render() and then passes the result to lit-html's render() function. We will have to patch the LitElement in the worker to instead pass the result to the main thread as a message.

DOM restrictions

Since there is no DOM in workers, components will be restricted to APIs similarly to SSR-compatible Lit elements.

We could also more completely emulate the DOM in workers, such as with projects like Partytown or WorkerDOM.

Nested components

The most difficult design area is likely how to deal with nested elements.

There will be two kinds of nested elements:

  1. Components that should also run in the worker
  2. Components that can be assumed to be present in the main thread, provided by the host application.

Components that should also run in the worker

The first case is more complicated.

Typically we use the DOM itself to propagate state down the tree. When a parent renders, it may set properties on a child causing it to render. If we did that for nested worker components we would cause a cascade of messages to a worker for the parent, back to the main thread where we set properties on the child, causing another message to the worker for the child.

We would instead like to run the child in the worker, send properties to it in the worker while rendering the parent, and render both parent and child in one message/reply turn. This is actually very similar to how SSR works, so we should be able to reuse some of the SSR infrastructure. We can parse templates, find the child components, and instantiate and render them.

Main thread components

Main thread child components are similar to built-in HTML elements with the one exception that they may be imported by the parent element. This could be a problem if the child component is not worker compatible. For parent components specifically written for workers, they can just elide the imports for their built-in children.

[RRFC] Angular signals for lit

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

Since lit release @lit-labs/preact-signals community has adopted signals progressively, usually as a state management alternative. However, preact signals have not been created for sharing with other non-react frameworks or exposing its internal mechanism.

Some days before Angular announces how signals are production-ready, and how signals power some youtube related functionalities. See https://www.youtube.com/watch?v=nIBseTi6RVk

Now, Angular signals are designed to be introduced in non-angular ecosystems and offer low-level APIs to create custom reactive nodes (producers/consumers)

Example

Consider all the same examples of @lit-labs/preact-signals, but we can take more control over what kind of tracking we need, at the same time we update how signals are consumed

const count = signal(0);

cont increment = () => {
  count.update(count => count + 1)
};

html`<button>Count: ${watch(count)}</button>`

// optionaly, we can enable watch function
html`<button>Count: ${watch(() => `${count()} ${count() > 1 ? 'time' : 'times'}`)}<button>`; 

How

Currently angular offers its core signals primitives as an independient export, and we can build on top of this "building blocks" a more lit specific signals package.

See working prototype.

References

Relates to #19

[RRFC] Provide help writing accessible component libraries similar to Radix UI or ShadCN UI

  • [ x] I searched for an existing RRFC which might be relevant to my RRFC

Motivation

While I am not certain I agree with all design decisions of ShadCN/UI I cannot help but envy how simple it is to get started with a component library that is accessible and has a great UX. Sadly, ShadCN UI is React-only and I feel it would have been the perfect opportunity to use web components.

Since web components are used to develop components it would be great to aid the process of creating component libraries.

Example

How

My idea is mostly to take the things that have been solved in existing component libraries like Material Web Components and extract them, so they can be used by others. This has been done in the React ecosystem by Radix UI (used under the hood in ShadCN/UI) or headless UI.

I could imagine shipping directives that allow to create accessible, components that can be styled and customized to the component library's author's liking.

One benefit of shipping building blocks like directives and not complete components is that users have flexibility in their implementation. Take for example a button component. If you want to style a link like a button in ShadCN UI, you can just copy the button component and change its markup to use an a-tag instead.

This RRFC does not concern itself with much of the prior art on the subject, so I haven't given much thought if Tailwind should be used or a CLI should be created or it would be packaged as a library.

Current Behavior

This feature does not exist in Lit-land.

Desired Behavior

Provide help to implement components for common pain points such as combo boxes.

References

There is a clone of ShadCN UI for Angular: https://www.spartan.ng/components/#

  • n/a

Thanks to @augustjk who motivated me to open this RRFC in the first place.

[RRFC] Tools for inline css

Motivation

CSS libraries like Tailwind, StyleX, panda, etc are pretty popular. Some of the benefits they typically tout are:

  1. ability to write/co-locate css close to your template
  2. avoid cognitive load associated with naming
  3. efficiency
  4. integration with a design system

The Tailwind docs ask "why not inline styles"? and note the benefits of using a design system and lack of expressiveness.

Lit element directives can easily overcome these limitations...

Example

proof of concept playground

render() {
  return html`<div ${rule(`background: ${color('primary')} &:hover img { scale: 2; }`)}>
    <img src...>  
    <section>Caption...</section>
  </div>
  <button ${rule`${button({kind: 'secondary', circle: true})}`}>๐Ÿ˜‰</button>
  `
}

How

The primary tool here is the rule directive. It simply appends a uniquely named css rule to a managed stylesheet in the host element (cached by the css itself). With the use of modern css nesting, individual rules can expressively reference/target descendants, siblings, states, and media queries. Directive results can also be shared so it's possible to compose them as desired. The rule itself can also optionally be named.

Various other tools can accompany this to facilitate design system restrictions likely that leverage css custom properties under the hood. This is what the color and size functions demonstrate in the example above. This type of system could also easily provide patterns, recipes, and variants similar to what is supported in panda and similar libraries.

[RRFC] Add changed callback to Ref

  • [x ] I searched for an existing RRFC which might be relevant to my RRFC

Hello everyone,

Motivation

The ref directive is awesome. However, Ref.value can always be undefined. Recently, I found myself wanting to access value only once and only if Ref.value was defined. With knowledge of my rendering code I could use firstUpdated() instead, but this only works if the element is rendered initially. And the other way, when the element would be no longer rendered would be impossible to detect.

My use case is wanting to access an element to see if certain styles are set - such as display: none for example.

Example

let elementRef: Ref<HtmlElement> = createRef((oldValue: HtmlElement | undefined, value: HtmlElement | undefined) => {
// Do something here if the Ref changes.
});

How

https://github.com/lit/lit/blob/0275bd0bf1c96d1e27983cefd6c7c0e5ebe53d2e/packages/lit-html/src/directives/ref.ts#L52 detects already if the RefElement changed. So, it should be pretty easy to invoke an optional callback function defined on Ref there.

Current Behavior

Nothing.

Desired Behavior

The optional callback would invoked whenever the RefElement changes.

References

  • n/a

I dunno. Maybe I am alone with this, but recently, I thought it would be pretty useful.

Interested to hear what you think,
Christian

[RRFC] Lazy loaded Lit elements

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

Some developers would like to create a loader for a set of components that will automatically load element definitions as they are used. This is useful when you have a very large component set hosted on a CDN, so there's no specific module graph to load individual elements from, and you don't want to require your HTML-authoring users to write a script tag for every element they use.

It's a feature of Stencil, and Shoelace has built their own loader system.

Example

<head>
  <script type="module" src="https://my-element-cdn.com/my-element-collection-loader.js"></script>
</head>
<body>
  <my-element-1></my-element-1>
  <my-element-3></my-element-3>
</body>

In the case my-element-1, and my-element-3 would be loaded, but not my-element-2.

How

The basic approach is to register a stub element that loads the real definition when the first instance is created. The stub should contain static metadata needed for the definition like observed attributes, form association, etc.

We made an experiment for this a while back: https://github.com/PolymerLabs/split-element

Another approach would be to use a mutation observer, but that would only work in one HTML tree scope at a time. It would be fine for the main document.

Current Behavior

We have no lazy loading solution

Desired Behavior

References

[RRFC] Add additional decorators to help observe property changes and create event properties

  • [ X] I searched for an existing RRFC which might be relevant to my RRFC

Motivation

Lit has proven that declarative setup via decorators is exceptionally convenient and useful. Additional decorators could be explored in a lit-labs package for potential inclusion based on feedback. Two obvious candidates are @observe and @event.

The @observe decorator would be used on a class member function which would be called when the noted propery changes.

The @event decorator would be used on a property intended to behave like a platform event property, e.g. onclick.

Example

A simple @observe decorator is prototyped here.

A simple @event decorator is prototyped here.

class MyElement extends LitElement {

  @property()
  foo = 'foo';

  @observe('foo')
  #fooChanged(value, old, name) {...}

  @event('hiya')
  onHiya?: EventListener;
}

[RRFC] List and enum attribute converters

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

It's relatively common to have attributes that take a list of tokens (ie, class, part) or a token from a specific enum (input/type, rel, etc.). Lit doesn't provide converters for these common cases, but probably should.

Example

Enums

An attribute may have a set of valid values, say "happy" and "sad" in this example.

export type Mood = 'happy' | 'sad';

@customElement('mood-display')
export class MoodDisplay extends LitElement {
  @property({
    reflect: true,
    converter: new EnumConverter({values: ['happy', 'sad'], default: 'happy')})
  mood = 'happy';
}
<mood-display mood="happy"></mood-display>

When the attribute value is valid, the property will have the same value as the attribute. When the attribute value is invalid, we can choose the fallback value.

<mood-display mood="confused"></mood-display>
moodElement.mood; // "happy"

The default fallback would be undefined.

Option possible features

Case-insensitivity / Normalization

We can match attributes case insensitively and normalize property values to upper or lower case.

Non-string Enums

For non-string properties we could offer a mapping (bi-directional for reflection). That could be a function or an object provided to the converter.

Lists

@customElement('x-count')
export class XCount extends LitElement {
  @property({
    converter: new ListConverter()
  items = [];
}
<x-count items="one two five"></x-count>
countElement.items; // ['one', 'two', 'five']

Authors may want to support a different list separator, but we should encourage space as a separator to be compatible with the attribute selector operator |=.

How

Current Behavior

Authors have to write their own converters.

Desired Behavior

Authors can use one of our well-written ones.

References

  • n/a

[RRFC] Reduce the need to understand and mitigate Lit's asynchronous update model.

  • [X ] I searched for an existing RRFC which might be relevant to my RRFC

Motivation

Standard platform behavior for elements is that they update synchronously after being mutated. If an element's textContent is set in such a way that its height would change, checking offsetHeight on the element is synchronously valid.

For efficiency, Lit elements update asynchronously and do not have this guarantee. Instead, users must await element.updateComplete to ensure any pending work is completed.

This is often not a problem because all updates are complete at task timing and before painting via await new Promise(requestAnimationFrame). However, it comes up in some cases and can be hard to deal with. In addition, if updateComplete is awaited, this does not reflect the status of any nested Lit elements unless users explicitly implement the promise to do so.

To mitigate this behavior, a mixin can be provided which does 2 things:

  1. updating the element synchronously makes the element's entire shadow subtree update.
  2. any element properties will ensure any pending update is synchronously completed before returning their current value.

How

Here is a prototype implementing the two proposed behaviors via a ReactiveManager mixin.

  1. it tracks child elements updated as a result of an element itself updating, and ensure they also update before the element's update is finished.
  2. it customizes reactive property accessors so that gets call performUpdate before returning their value, being careful not to do so during actual rendering.

[RRFC] Add ability to set dynamic styles programatically after render

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

In the documentation for styles, there is a brief section dynamic styles and classes. In this section it provides the below example (modified for clarity):

constructor() {
    super();
    this.styles = {color: 'lightgreen', fontFamily: 'Roboto'};
  }
  render() {
    return html`
      <div style=${styleMap(this.styles)}>
        Some content
      </div>
    `;
  }

The stylemap directive simply iterates over Object.entries(this.styles) and uses Array.prototype.reduce() to form a string that can be rendered as part of the component's own lifecycle. Styles added this way, however, are blocked when using a CSP with the style-src directive (MDN)

From the MDN page above:

Inline style attributes are also blocked:

<div style="display:none">Foo</div>

As well as styles that are applied in JavaScript by setting the style attribute directly, or by setting cssText:

document.querySelector('div').setAttribute('style', 'display:none;');
document.querySelector('div').style.cssText = 'display:none;';

However, styles properties that are set directly on the element's style property will not be blocked, allowing users to safely manipulate styles via JavaScript:

document.querySelector('div').style.display = 'none';

Example

On a page with the CSP header Content-Security-Policy: style-src https://example.com/

This code adds the styles to the markup, but the browser does not apply those styles, blocking them per the content security policy.

constructor() {
    super();
    this.styles = {color: 'lightgreen', fontFamily: 'Roboto'};
  }
  render() {
    return html`
      <div style=${styleMap(this.styles)}>
        Some content
      </div>
    `;
  }

How

I propose adding a new @ expression or similar, as we would for attaching event listeners, to programatically attach stylemap-ready objects post-render:

constructor() {
    super();
    this.styles = {color: 'lightgreen', fontFamily: 'Roboto'};
  }
  render() {
    return html`
      <div @style=${this.styles}>
        Some content
      </div>
    `;
  }

In this situation, the html tagged literal wouldn't render style="color: lightgreen; font-family: 'Roboto';", but instead render the div without a style element and then post-render run:

div.style.color = lightgreen;
div.style.fontFamily = 'Roboto';

References

[RRFC] Better Declarative Host Manipulation

Description

  • Who: Developers who code declaratively
  • What: Be able to declaratively set event listeners and styles on host
  • Why: Declarative code is great and for northstar transformations
  • Description:

I would like better declarative host manipulation. Often you need to set lots of event listeners on host or even set an inline style on host e.g. transform: translate(x, y). It would be nice to have a declarative way to do this with things such as stylemap or listenermap or something. Perhaps another method alongside render and first render that gets called on each / first render cycle etc.

Or for event listeners maybe even a TS decorator that takes in event name and event options and passes the proper type to the event listener based on the event name.

Acceptance criteria

Be able to set an event listener and a style declaratively on host

[RRFC] Provide styling API supporting dynamic changes, theming, and applying document styling

  • [X ] I searched for an existing RRFC which might be relevant to my RRFC

Motivation

There are many longstanding requests to improve Lit's styling API to better support things like (1) dynamic style changes, (2) theming, (3) application of document styles, (4) slightly relaxing the restrictions of Shadow DOM's style scoping.

Related

This is a complex topic and a lot of customization is possible using css custom properties, shadow parts, and shared constructible CSSStyleSheets. However, it would be nice if Lit provided some convenient helpers and guidance/best practices for these needs. This would probably make sense as a lit-labs package that could be iterated on via extensive feedback.

How

Here is a prototype of new capability to support these needs. It includes:

  1. a new sheet TTL to use instead of css which mainly is less restrictive around accepting values and has customizable caching behavior via including an inline storeAs({key, scope}) value. This makes stylesheets shareable by key, without having to explicitly pass around the sheet itself.
  2. sheet mutation API: hasSheet, addSheets, removeSheets, toggleSheets. These helpers just make it more convenient to work with adoptedStyleSheets.
  3. getDocumentStyles: retrieves and tracks document styles/links and auto-convert them to adoptable sheets and Lit renderable link template results. This makes it possible easily to include document styles in elements. Caveats: only styles available when the element is defined are available and tracked.
  4. themeSheet({...part: "css"}): intended for theming this let's users provide styles for parts and custom properties that are not necessarily restricted by the DOM tree or export parts.
  5. fromSelectors({...class: "css"}): this provides type-safe access to a set of css classes defined by name, returning {sheet, selectors} where sheet is intended to be included in the element's styles and class=${selectors.name} is included in the template. The advantage is that TS will ensure only defined class selectors can be included.

[RRFC] Use `peerDependencies` for some lit dependencies

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

We've been getting reports of users having multiple versions of Lit packages being loaded due to package managers failing to dedupe core lit packages.
While our version constraints are such that it should be fine to have multiple versions of Lit within the designated sem ver loaded for an application, it is suboptimal to do so as it means shipping more code than necessary to the client.

Marking as peerDependency might signal to package managers that it should be sharing the package rather than creating a nested dependency.

I'm opening this as an RRFC because I don't know if package mangers like npm, pnpm, or yarn actually behave in this way, nor am sure of any potential downsides like getting hard npm install errors due to having conflicting peer dependencies.

Current Behavior

Depending on other installed packages that depend on lit and which version was installed first, we can end up with multiple copies of a package nested like:

node_modules/lit/node_modules/@lit/reactive-element
node_modules/lit/node_modules/lit-element/node_modules/@lit/reactive-element
node_modules/@lit/reactive-element
node_modules/@lit-labs/virtualizer/node_modules/lit...

Desired Behavior

Proper hoisting of shared dependencies with compatible versions.

How

We need to consider which packages should be marked as peer dependency.

For example, @lit-labs/motion depends on lit because it directly imports from it classes, functions, and sentinel values. However, its exports are a directive and a reactive controller that are always meant to used in a project already using lit. Making lit a peer dependency here would signal that the version of lit being used by @lit-labs/motion should always come along side it, rather than be nested under it.

This line is more blurry when we consider @lit-labs/virtualizer which exports the virtualize directive meant to be used along side in a lit project, but also exports the lit-virtualizer component which can be used stand alone where the version of lit being used does not matter.[1]

The situation with the core packages of lit, lit-element, and @lit/reactive-element is also open to debate. Currently lit depends on @lit/reactive-element as well as lit-element which itself depends on @lit/reactive-element, potentially leading to:

node_modules/lit/node_modules/@lit/reactive-element
node_modules/lit/node_modules/lit-element/node_modules/@lit/reactive-element

It could be possible that if @lit/reactive-element is a peer dependency of lit-element, npm would never nest @lit/reactive-element under lit-element and look for it as a sibling, or a parent in the package tree. This would at least ensure that we don't get multiple versions of @lit/reactive-element within a single lit package installation. This is conjecture based on npm docs on peer dependencies[2] and needs to be verified.

Peer dependencies that are not explicitly marked as optional are auto installed as of npm 7[3] so an argument can be made for marking lit's dependency on lit-element and @lit/reactive-element as peer to really promote hoisting, though this could increase the blast radius of causing conflicts.

Other considerations

Package managers should also be able to dedupe and hoist dependencies that satisfy semver across multiple packages so it shouldn't matter whether something is marked as a dependency or a peer dependency in that sense.

Having multiple versions of Lit is technically fine and not detrimental to most users. If users want to optimize, it is possible to manually tweak the installed dependencies, coerce package managers to hoist or dedupe by blowing away node_modules and the lockfile, or use npm overrides/yarn resolutions to force a single version.

References

[RRFC] Add first-class support for observables/signals in lit-html

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

Given the prevalence of subscribable reactive primitives in front-end development (such as RxJS Observables and Preact Signals), it would be win for developer experience and interop if lit-html supported binding to them directly without needing to use a custom Async Directive.

Example

These examples are live here.

RxJS

import { interval } from 'rxjs';
 
html`<div>${inverval(1000)}</div>`

Preact Signals

import { signal } from '@preact/signals-core';
 
const count$ = signal(0);
html`<input type="button" value=${count$} @click=${() => count$.value += 1}>`

How

The above examples use a naive implementation found here: https://github.com/willmartian/lit-signals

It works by utilizing an ObserveDirective, identical to the example in the Async Directive docs.

Any subscribable objects are automatically wrapped in the directive:

import { html as _html } from "lit-html";
import { observe } from "./ObserveDirective";
 
export const html = (strings: TemplateStringsArray, ...values: unknown[]) => {
   return _html(strings, ...values.map(
       val => isSubscribable(val) ? observe(val) : val)
   );
}
 
const isSubscribable = (value: any): value is Subscribable<unknown> => {
   return 'subscribe' in value && typeof value.subscribe === 'function';
}

I am sure there is a more nuanced way to actually implement this, but my assumption is that this is a fairly light weight addition.

Current Behavior

The current behavior requires the developer to wrap wrap any subscribable values in a custom Async Directive.

html`<div>${observe(observable)}</div>`

Desired Behavior

html`<div>${observable}</div>`

References

  • n/a

[RRFC] Improve decorator support for JS users

  • [X ] I searched for an existing RRFC which might be relevant to my RRFC

Motivation

It's a major design goal of Lit to hew close to the web platform while at the same time providing excellent developer experience (DX). The use of decorators strongly contributes to the DX goal before they provide convenient, declarative configuration. However, although decorators are stage 3 and therefore will be included in the web platform, they have not shipped in any browser yet and therefore using them requires a polyfill like TypeScript or Babel which degrades. This degrades DX for developers who wouldn't otherwise use these tools.

To provide good DX for pure JS users, lit currently allows special declaration of reactive properties using the static get properties() { return {...}} class property.

However, the declarative configuration provided by Lit's other decorators are not supported, which is not ideal.

There is another motivation for this change: Lit 3.x has added support for stage 3 standard decorators in addition to supporting the previous abandoned stage 2 proposal. Unfortunately, the static get properties() code path is distinct from either the stage 2 or stage 3 decorator code. This creates a maintenance burden and has the potential to ship extra, unneeded code.

Proposal

Lit should provide a way to easily use all supported decorators without the need for a decorator polyfill. To do this, a decorate method could be added which takes the class constructor and a set of decorator instructions provided via a config object. This would be used in a class static block, which are supported in all current browsers.

The decorate method would use Lit's decorators behind the scene thus reducing code complexity. It should also use the stage 3 standard decorators to support eventual removal of the stage 2 decorator support. Lit's static get properties() feature should also be deprecated.

Example

https://lit.dev/playground/#gist=254582877f3a7fa416f069b7c284d4d4

export class MyElement extends LitElement {
  
  static {
    decorate(this, {
      customElement: 'my-element',
      property: {
        count: {type: Number}
      },
      query: {
        hi: '.hi'
      },
      eventOptions: {
        clickHandler: {once: true}
      }
    });
  }
//...

References

  • n/a

[RRFC] Keeping and reverting to default values for properties

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

Currently the common pattern of setting default values for properties to do it in the constructor or class field instantiation. These are really more initial values that we lose after instantiation. Users may actually want some behavior where "unsetting" a property will revert to a default value.

It's possible to manually add this behavior by falling back to some value in render(), willUpdate(), or custom accessors[1] or even attribute converters if we wish to consider removing an attribute as "unsetting" as well.

There have also been calls for this behavior for or React wrapper[2] which we could add on at the wrapper level but would also be nice to have it for the underlying Lit element.

We do need to decide what "unsetting" means

  • Setting property to undefined
  • Setting property to null (probably not this?)
  • delete el[prop]; (unlikely to happen?)
  • Removing associated attribute
  • If we implement spread, encountering a missing property from a previously rendered spread.

How

Eventually when the only way to declare a property is with standard decorators, I believe we get access to the initial value set on the class field (with auto-accessor). Until then, we would need to add a property option for the default value.

Once we collect the default value, we can add it as a fallback value in an appropriate place like property setter or attribute converter.

References

[RRFC] Breaking change policy and tracking post-3.0 breaking changes

  • I searched for an existing RRFC which might be relevant to my RRFC

Motivation

We should develop a breaking change policy so our users know what to expect in future releases and how we approach stability and backwards compatibility. This can include hard policies like deprecating an API in a release before removing it, or softer policies like trying to not make breaking changes unless we need to, improves our code quality, performance, maintenance burden, etc.

We should also try to develop some more precise browser-support policies for relying on new features, possibly based on usage and whether there's a polyfill. For example, we could not rely on a new non-polyfillable feature if > X% of browser usage doesn't support the feature, and not rely on a polyfillable feature of > Y% of browser usage doesn't support the feature.

And not a request for a specific RFC per se, but I'd like for us to keep track of potential breaking changes that we might want to make in order to help with longer term planning and carefully minimize disruptions to developers and users.

Example

Examples of breaking changes would could make that could improve our source and published code:

  • Using and publishing private fields (ES2022)
  • Relying on Constructible StyleSheets (Supported in Chrome 73, FF 101, Safari 16.4)
  • Removing support for TypeScript experimental decorators
  • Not reflecting initial property values to attributes
  • Removing ReactiveElement.createProperty(), .getPropertyDescriptor(), etc.
  • Using the DOM Parts API when it lands.
  • etc

How

For the breaking change policy, the RFC itself can serve as documentation of it. We may find a good place in the lit.dev docs as well.

For tracking ideas, we can use several methods, though it's important to remember that all breaking changes will need to be part of an RFC to get approval for actually making it.

  1. Mention the idea in this issue
  2. Propose a standalone RFC
  3. Include the breaking change as part of a larger RFC
  4. File an issue in lit/lit

Current Behavior

Desired Behavior

References

  • n/a

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.