Skip to content

Latest commit

 

History

History
615 lines (420 loc) · 23 KB

File metadata and controls

615 lines (420 loc) · 23 KB
title eleventyNavigation
Reactive properties
key parent order
Reactive properties
Components
3

Lit components receive input and store their state as JavaScript class fields or properties. Reactive properties are properties that can trigger the reactive update cycle when changed, re-rendering the component, and optionally be read or written to attributes.

{% switchable-sample %}

class MyElement extends LitElement {
  @property()
  name: string;
}
class MyElement extends LitElement {
  static properties = {
    name: {},
  };
}

{% endswitchable-sample %}

Lit manages your reactive properties and their corresponding attributes. In particular:

  • Reactive updates. Lit generates a getter/setter pair for each reactive property. When a reactive property changes, the component schedules an update.
  • Attribute handling. By default, Lit sets up an observed attribute corresponding to the property, and updates the property when the attribute changes. Property values can also, optionally, be reflected back to the attribute.
  • Superclass properties. Lit automatically applies property options declared by a superclass. You don't need to redeclare properties unless you want to change options.
  • Element upgrade. If a Lit component is defined after the element is already in the DOM, Lit handles upgrade logic, ensuring that any properties set on an element before it was upgraded trigger the correct reactive side effects when the element upgrades.

Public properties and internal state

Public properties are part of the component's public API. In general, public properties—especially public reactive properties—should be treated as input.

The component shouldn't change its own public properties, except in response to user input. For example, a menu component might have a public selected property that can be initialized to a given value by the owner of the element, but that is updated by the component itself when the user selects an item. In these instances, the component should dispatch an event to indicate to the component's owner that the selected property changed. See Dispatching events for more details.

Lit also supports internal reactive state. Internal reactive state refers to reactive properties that aren't part of the component's API. These properties don't have a corresponding attribute, and are typically marked protected or private in TypeScript.

{% switchable-sample %}

@state()
private _counter = 0;
static properties = {
  _counter: {state: true};
};

constructor()
  super();
  this._counter = 0;
}

{% endswitchable-sample %}

The component manipulates its own internal reactive state. In some cases, internal reactive state may be initialized from public properties—for example, if there is a expensive transformation between the user-visible property and the internal state.

As with public reactive properties, updating internal reactive state triggers an update cycle. For more information, see Internal reactive state.

Public reactive properties {#declare}

Declare your element's public reactive properties using decorators or the static properties field.

In either case, you can pass an options object to configure features for the property.

Declaring properties with decorators {#declare-with-decorators}

Use the @property decorator with a class field declaration to declare a reactive property.

class MyElement extends LitElement {
  @property({type: String})
  mode: string;

  @property({attribute: false})
  data = {};
}

The argument to the @property decorators is an options object. Omitting the argument is equivalent to specifying the default value for all options.

Using decorators. Decorators are a proposed JavaScript feature, so you'll need to use a compiler like Babel or the TypeScript compiler to use decorators. See Enabling decorators for details.

Declaring properties in a static properties class field

To declare properties in a static properties class field:

class MyElement extends LitElement {
  static properties = {
    mode: {type: String},
    data: {attribute: false},
  };

  constructor() {
    super();
    this.data = {};
  }
}

An empty option object is equivalent to specifying the default value for all options.

Avoiding issues with class fields when declaring properties {#avoiding-issues-with-class-fields}

Class fields have a problematic interaction with reactive properties. Class fields are defined on the element instance. Reactive properties are defined as accessors on the element prototype. According to the rules of JavaScript, an instance property takes precedence over and effectively hides a prototype property. This means that reactive property accessors do not function when class fields are used. When a property is set, the element does not update.

In JavaScript you must not use class fields when declaring reactive properties. Instead, properties must be initialized in the element constructor:

constructor() {
  super();
  this.data = {};
}

For TypeScript, you may use class fields for declaring reactive properties as long as the useDefineForClassFields setting in your tsconfig is set to false. Note, this is not required for some configurations of TypeScript, but it's recommended to explicitly set it to false.

For TypeScript, you can use declare syntax for class fields that you explicitly want to exclude from the JS output regardless of the useDefineForClassFields setting. This will allow you to use decorators, but cannot be initialised inline. Similar to JS declared fields can only be initialised in the constructor.

When compiling JavaScript with Babel, you may use class fields for declaring reactive properties as long as you set setPublicClassFields to true in the assumptions config of your babelrc. Note, for older versions of Babel, you also need to include the plugin @babel/plugin-proposal-class-properties:

assumptions = {
  "setPublicClassFields": true
};

plugins = [
  ["@babel/plugin-proposal-class-properties"],
];

For information about using class fields with decorators, see Avoiding issues with class fields and decorators.

Property options

The options object can have the following properties:

attribute

Whether the property is associated with an attribute, or a custom name for the associated attribute. Default: true. If attribute is false, the converter, reflect and type options are ignored. For more information, see Setting the attribute name.

converter

A custom converter for converting between properties and attributes. If unspecified, use the default attribute converter.

hasChanged

A function called whenever the property is set to determine if the property has changed, and should trigger an update. If unspecified, LitElement uses a strict inequality check (newValue !== oldValue) to determine whether the property value has changed. For more information, see Customizing change detection.

noAccessor

Set to true to avoid generating the default property accessors. This option is rarely necessary. Default: false. For more information, see Preventing Lit from generating a property accessor.

reflect

Whether property value is reflected back to the associated attribute. Default: false. For more information, see Enabling attribute reflection.

state

Set to true to declare the property as internal reactive state. Internal reactive state triggers updates like public reactive properties, but Lit doesn't generate an attribute for it, and users shouldn't access it from outside the component. Equivalent to using the @state decorator. Default: false. For more information, see Internal reactive state.

type

When converting a string-valued attribute into a property, Lit's default attribute converter will parse the string into the type given, and vice-versa when reflecting a property to an attribute. If converter is set, this field is passed to the converter. If type is unspecified, the default converter treats it as type: String. See Using the default converter.

When using TypeScript, this field should generally match the TypeScript type declared for the field. However, the type option is used by the Lit's runtime for string serialization/deserialization, and should not be confused with a type-checking mechanism.

Omitting the options object or specifying an empty options object is equivalent to specifying the default value for all options.

Internal reactive state

Internal reactive state refers to reactive properties that are not part of the component's public API. These state properties don't have corresponding attributes, and aren't intended to be used from outside the component. Internal reactive state should be set by the component itself.

Use the @state decorator to declare internal reactive state:

@state()
protected _active = false;

Using the static properties class field, you can declare internal reactive state by using the state: true option.

static properties = {
  _active: {state: true}
};

constructor() {
  this._active = false;
}

Internal reactive state shouldn't be referenced from outside the component. In TypeScript, these properties should be marked as private or protected. We also recommend using a convention like a leading underscore (_) to identify private or protected properties for JavaScript users.

Internal reactive state works just like public reactive properties, except that there is no attribute associated with the property. The only option you can specify for internal reactive state is the hasChanged function.

The @state decorator can also serve as a hint to a code minifier that the property name can be changed during minification.

What happens when properties change {#when-properties-change}

A property change can trigger a reactive update cycle, which causes the component to re-render its template.

When a property changes, the following sequence occurs:

  1. The property's setter is called.
  2. The setter calls the component's requestUpdate method.
  3. The property's old and new values are compared. If the property has a hasChanged function, it's called with the property's old and new values.
  4. If the property change is detected, an update is scheduled asynchronously. If an update is already scheduled, only a single update is executed.
  5. The component's update method is called, reflecting changed properties to attributes and re-rendering the component's templates.

There are many ways to hook into and modify the reactive update cycle. For more information, see Reactive update cycle.

For more information about property change detection, see Customizing change detection.

Attributes {#attributes}

While properties are great for receiving JavaScript data as input, attributes are the standard way HTML allows configuring elements from markup, without needing to use JavaScript to set properties. Providing both a property and attribute interface for their reactive properties is a key way Lit components can be useful in a wide variety of environments, including those rendered without a client-side templating engine, such as static HTML pages served from CMSs.

By default, Lit sets up an observed attribute corresponding to each public reactive property, and updates the property when the attribute changes. Property values can also, optionally, be reflected (written back to the attribute).

While element properties can be of any type, attributes are always strings. This impacts the observed attributes and reflected attributes of non-string properties:

  • To observe an attribute (set a property from an attribute), the attribute value must be converted from a string to match the property type.

  • To reflect an attribute (set an attribute from a property), the property value must be converted to a string.

Setting the attribute name {#observed-attributes}

By default, Lit creates a corresponding observed attribute for all public reactive properties. The name of the observed attribute is the property name, lowercased:

{% switchable-sample %}

// observed attribute name is "myvalue"
@property({ type: Number })
myValue = 0;
// observed attribute name is "myvalue"
static properties = {
  myValue: { type: Number },
};

constructor() {
  super();
  this.myValue = 0;
}

{% endswitchable-sample %}

To create an observed attribute with a different name, set attribute to a string:

{% switchable-sample %}

// Observed attribute will be called my-name
@property({ attribute: 'my-name' })
myName = 'Ogden';
// Observed attribute will be called my-name
static properties = {
  myName: { attribute: 'my-name' },
};

constructor() {
  super();
  this.myName = 'Ogden'
}

{% endswitchable-sample %}

To prevent an observed attribute from being created for a property, set attribute to false. The property will not be initialized from attributes in markup, and attribute changes won't affect it.

{% switchable-sample %}

// No observed attribute for this property
@property({ attribute: false })
myData = {};
// No observed attribute for this property
static properties = {
  myData: { attribute: false },
};

constructor() {
  super();
  this.myData = {};
}

{% endswitchable-sample %}

Internal reactive state never has an associated attribute.

An observed attribute can be used to provide an initial value for a property from markup. For example:

<my-element myvalue="99"></my-element>

Using the default converter {#conversion-type}

Lit has a default converter that handles String, Number, Boolean, Array, and Object property types.

To use the default converter, specify the type option in your property declaration:

{% switchable-sample %}

// Use the default converter
@property({ type: Number })
count = 0;
// Use the default converter
static properties = {
  count: { type: Number },
};

constructor() {
  super();
  this.count = 0;
}

{% endswitchable-sample %}

If you don't specify a type or a custom converter for a property, it behaves as if you'd specified type: String.

The tables below shows how the default converter handles conversion for each type.

From attribute to property

Type Conversion
String If the element has the corresponding attribute, set the property to the attribute value.
Number If the element has the corresponding attribute, set the property to Number(attributeValue).
Boolean If the element has the corresponding attribute, set the property to true.
If not, set the property to false.
Object, Array If the element has the corresponding attribute, set the property value to JSON.parse(attributeValue).

For any case except Boolean, if the element doesn't have the corresponding attribute, the property keeps its default value, or undefined if no default is set.

From property to attribute

Type Conversion
String, Number If property is defined and non-null, set the attribute to the property value.
If property is null or undefined, remove the attribute.
Boolean If property is truthy, create the attribute and set its value to an empty string.
If property is falsy, remove the attribute
Object, Arrray If property is defined and non-null, set the attribute to JSON.stringify(propertyValue).
If property is null or undefined, remove the attribute.

Providing a custom converter {#conversion-converter}

You can specify a custom property converter in your property declaration with the converter option:

myProp: {
  converter: // Custom property converter
}

converter can be an object or a function. If it is an object, it can have keys for fromAttribute and toAttribute:

prop1: {
  converter: {
    fromAttribute: (value, type) => {
      // `value` is a string
      // Convert it to a value of type `type` and return it
    },
    toAttribute: (value, type) => {
      // `value` is of type `type`
      // Convert it to a string and return it
    }
  }
}

If converter is a function, it is used in place of fromAttribute:

myProp: {
  converter: (value, type) => {
    // `value` is a string
    // Convert it to a value of type `type` and return it
  }
}

If no toAttribute function is supplied for a reflected attribute, the attribute is set to the property value using the default converter.

If toAttribute returns null or undefined, the attribute is removed.

Enabling attribute reflection {#reflected-attributes}

You can configure a property so that whenever it changes, its value is reflected to its corresponding attribute. Reflected attributes are useful because attributes are visible to CSS, and to DOM APIs like querySelector.

For example:

// Value of property "active" will reflect to attribute "active"
active: {reflect: true}

When the property changes, Lit sets the corresponding attribute value as described in Using the default converter or Providing a custom converter.

{% playground-example "properties/attributereflect" "my-element.ts" %}

Attributes should generally be considered input to the element from its owner, rather than under control of the element itself, so reflecting properties to attributes should be done sparingly. It's necessary today for cases like styling and accessibility, but this is likely to change as the platform adds features like the :state pseudo selector and the Accessibility Object Model, which fill these gaps.

Reflecting properties of type object or array is not recommended. This can cause large objects to serialize to the DOM which can result in poor performance.

Lit tracks reflection state during updates. You may have realized that if property changes are reflected to an attribute and attribute changes update the property, it has the potential to create an infinite loop. However, Lit tracks when properties and attributes are set specifically to prevent this from happening

Custom property accessors {#accessors}

By default, LitElement generates a getter/setter pair for all reactive properties. The setter is invoked whenever you set the property:

{% switchable-sample %}

// Declare a property
@property()
greeting: string = 'Hello';
...
// Later, set the property
this.greeting = 'Hola'; // invokes greeting's generated property accessor
// Declare a property
static properties = {
  greeting: {},
}
constructor() {
  this.super();
  this.greeting = 'Hello';
}
...
// Later, set the property
this.greeting = 'Hola'; // invokes greeting's generated property accessor

{% endswitchable-sample %}

Generated accessors automatically call requestUpdate(), initiating an update if one has not already begun.

Creating custom property accessors {#accessors-custom}

To specify how getting and setting works for a property, you can define your own getter/setter pair. For example:

{% switchable-sample %}

private _prop = 0;

set prop(val: number) {
  let oldVal = this._prop;
  this._prop = Math.floor(val);
  this.requestUpdate('prop', oldVal);
}

@property()
get prop() { return this._prop; }
static properties = {
  prop: {},
};

_prop = 0;

set prop(val) {
  let oldVal = this._prop;
  this._prop = Math.floor(val);
  this.requestUpdate('prop', oldVal);
}

get prop() { return this._prop; }

{% endswitchable-sample %}

To use custom property accessors with the @property or @state decorators, put the decorator on the getter, as shown above.

The setters that Lit generates automatically call requestUpdate(). If you write your own setter you must call requestUpdate() manually, supplying the property name and its old value.

In most cases, you do not need to create custom property accessors. To compute values from existing properties, we recommend using the willUpdate callback, which allows you to set values during the update cycle without triggering an additional update. To perform a custom action after the element updates, we recommend using the updated callback. A custom setter can be used in rare cases when it's important to synchronously validate any value the user sets.

If your class defines its own accessors for a property, Lit will not overwrite them with generated accessors. If your class does not define accessors for a property, Lit will generate them, even if a superclass has defined the property or accessors.

Prevent Lit from generating a property accessor {#accessors-noaccessor}

In rare cases, a subclass may need to change or add property options for a property that exists on its superclass.

To prevent Lit from generating a property accessor that overwrites the superclass's defined accessor, set noAccessor to true in the property declaration:

static properties = {
  myProp: { type: Number, noAccessor: true }
};

You don't need to set noAccessor when defining your own accessors.

Customizing change detection {#haschanged}

All reactive properties have a function, hasChanged(), which is called when the property is set.

hasChanged compares the property's old and new values, and evaluates whether or not the property has changed. If hasChanged() returns true, Lit starts an element update if one is not already scheduled. For more information on updates, see Reactive update cycle .

The default implementation of hasChanged() uses a strict inequality comparison: hasChanged() returns true if newVal !== oldVal.

To customize hasChanged() for a property, specify it as a property option:

{% switchable-sample %}

@property({
  hasChanged(newVal: string, oldVal: string) {
    return newVal?.toLowerCase() !== oldVal?.toLowerCase();
  }
})
myProp: string | undefined;
static properties = {
  myProp: {
    hasChanged(newVal, oldVal) {
      return newVal?.toLowerCase() !== oldVal?.toLowerCase();
    }
  }
};

{% endswitchable-sample %}

In the following example, hasChanged() only returns true for odd values.

{% playground-example "properties/haschanged" "my-element.ts" %}