lit / rfcs

RFCs for changes to Lit
BSD 3-Clause "New" or "Revised" License
15 stars 10 forks source link

[RRFC] List and enum attribute converters #4

Open justinfagnani opened 2 years ago

justinfagnani commented 2 years ago

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

sorvell commented 2 years ago

Great suggestion, some questions:

justinfagnani commented 2 years ago

Reflection

I think they should support reflection, so should implement toAttribute and serializing to a string should be straight forward.

I don't think it's really necessary to implement a DOMTokenList-like thing. That would be a lot more infrastructure than just a converter and I don't think it's a great API, since the more natural thing is to mutate the array. Not that the DOM has an observable array, we might also consider offering an observable array that's also a reactive controller, like:

class MyElement extends LitElement {
  @property({converter: new ListConverter()})
  items = new ObservableArray(this, []);
}

Validating properties

Good point. input/type has the behavior where if you set it to an invalid value and then read it, the value is "text". I actually don't like this behavior much, since it violates the standard behavior of properties. I expect that when I set a property and read it right back, it'll have the value I set.

I weakly feel like if an author wants to go that far, maybe they should implement their own setter. I think not changing a property from the value that's set is coherent with the attribute behavior too: a converter will not change the value of an invalid attribute, it'll just reflect the default value to the property.

Separators

I mention that we could support custom separators. I think it's ok, but likely a bad pattern to use too much. A microsyntax like for exportparts will require a custom converter anyway.

Input

See above. I think <input> in general is a little problematic to use a prototype case to follow - it's far too overridden IMO. I'd look to see how other enum values behave in the platform, and even then I'm not personally sold on properties spontaneously changing without any other code apparently running, if that's done elsewhere.