1122 lines
46 KiB
JavaScript
1122 lines
46 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
*/
|
|
/**
|
|
* Use this module if you want to create your own base class extending
|
|
* {@link ReactiveElement}.
|
|
* @packageDocumentation
|
|
*/
|
|
import { getCompatibleStyle, adoptStyles, } from './css-tag.js';
|
|
// In the Node build, this import will be injected by Rollup:
|
|
// import {HTMLElement, customElements} from '@lit-labs/ssr-dom-shim';
|
|
export * from './css-tag.js';
|
|
// TODO (justinfagnani): Add `hasOwn` here when we ship ES2022
|
|
const { is, defineProperty, getOwnPropertyDescriptor, getOwnPropertyNames, getOwnPropertySymbols, getPrototypeOf, } = Object;
|
|
const NODE_MODE = false;
|
|
// Lets a minifier replace globalThis references with a minified name
|
|
const global = globalThis;
|
|
if (NODE_MODE) {
|
|
global.customElements ??= customElements;
|
|
}
|
|
const DEV_MODE = true;
|
|
let issueWarning;
|
|
const trustedTypes = global
|
|
.trustedTypes;
|
|
// Temporary workaround for https://crbug.com/993268
|
|
// Currently, any attribute starting with "on" is considered to be a
|
|
// TrustedScript source. Such boolean attributes must be set to the equivalent
|
|
// trusted emptyScript value.
|
|
const emptyStringForBooleanAttribute = trustedTypes
|
|
? trustedTypes.emptyScript
|
|
: '';
|
|
const polyfillSupport = DEV_MODE
|
|
? global.reactiveElementPolyfillSupportDevMode
|
|
: global.reactiveElementPolyfillSupport;
|
|
if (DEV_MODE) {
|
|
// Ensure warnings are issued only 1x, even if multiple versions of Lit
|
|
// are loaded.
|
|
global.litIssuedWarnings ??= new Set();
|
|
/**
|
|
* Issue a warning if we haven't already, based either on `code` or `warning`.
|
|
* Warnings are disabled automatically only by `warning`; disabling via `code`
|
|
* can be done by users.
|
|
*/
|
|
issueWarning = (code, warning) => {
|
|
warning += ` See https://lit.dev/msg/${code} for more information.`;
|
|
if (!global.litIssuedWarnings.has(warning) &&
|
|
!global.litIssuedWarnings.has(code)) {
|
|
console.warn(warning);
|
|
global.litIssuedWarnings.add(warning);
|
|
}
|
|
};
|
|
queueMicrotask(() => {
|
|
issueWarning('dev-mode', `Lit is in dev mode. Not recommended for production!`);
|
|
// Issue polyfill support warning.
|
|
if (global.ShadyDOM?.inUse && polyfillSupport === undefined) {
|
|
issueWarning('polyfill-support-missing', `Shadow DOM is being polyfilled via \`ShadyDOM\` but ` +
|
|
`the \`polyfill-support\` module has not been loaded.`);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Useful for visualizing and logging insights into what the Lit template system is doing.
|
|
*
|
|
* Compiled out of prod mode builds.
|
|
*/
|
|
const debugLogEvent = DEV_MODE
|
|
? (event) => {
|
|
const shouldEmit = global
|
|
.emitLitDebugLogEvents;
|
|
if (!shouldEmit) {
|
|
return;
|
|
}
|
|
global.dispatchEvent(new CustomEvent('lit-debug', {
|
|
detail: event,
|
|
}));
|
|
}
|
|
: undefined;
|
|
/*
|
|
* When using Closure Compiler, JSCompiler_renameProperty(property, object) is
|
|
* replaced at compile time by the munged name for object[property]. We cannot
|
|
* alias this function, so we have to use a small shim that has the same
|
|
* behavior when not compiling.
|
|
*/
|
|
/*@__INLINE__*/
|
|
const JSCompiler_renameProperty = (prop, _obj) => prop;
|
|
export const defaultConverter = {
|
|
toAttribute(value, type) {
|
|
switch (type) {
|
|
case Boolean:
|
|
value = value ? emptyStringForBooleanAttribute : null;
|
|
break;
|
|
case Object:
|
|
case Array:
|
|
// if the value is `null` or `undefined` pass this through
|
|
// to allow removing/no change behavior.
|
|
value = value == null ? value : JSON.stringify(value);
|
|
break;
|
|
}
|
|
return value;
|
|
},
|
|
fromAttribute(value, type) {
|
|
let fromValue = value;
|
|
switch (type) {
|
|
case Boolean:
|
|
fromValue = value !== null;
|
|
break;
|
|
case Number:
|
|
fromValue = value === null ? null : Number(value);
|
|
break;
|
|
case Object:
|
|
case Array:
|
|
// Do *not* generate exception when invalid JSON is set as elements
|
|
// don't normally complain on being mis-configured.
|
|
// TODO(sorvell): Do generate exception in *dev mode*.
|
|
try {
|
|
// Assert to adhere to Bazel's "must type assert JSON parse" rule.
|
|
fromValue = JSON.parse(value);
|
|
}
|
|
catch (e) {
|
|
fromValue = null;
|
|
}
|
|
break;
|
|
}
|
|
return fromValue;
|
|
},
|
|
};
|
|
/**
|
|
* Change function that returns true if `value` is different from `oldValue`.
|
|
* This method is used as the default for a property's `hasChanged` function.
|
|
*/
|
|
export const notEqual = (value, old) => !is(value, old);
|
|
const defaultPropertyDeclaration = {
|
|
attribute: true,
|
|
type: String,
|
|
converter: defaultConverter,
|
|
reflect: false,
|
|
useDefault: false,
|
|
hasChanged: notEqual,
|
|
};
|
|
// Ensure metadata is enabled. TypeScript does not polyfill
|
|
// Symbol.metadata, so we must ensure that it exists.
|
|
Symbol.metadata ??= Symbol('metadata');
|
|
// Map from a class's metadata object to property options
|
|
// Note that we must use nullish-coalescing assignment so that we only use one
|
|
// map even if we load multiple version of this module.
|
|
global.litPropertyMetadata ??= new WeakMap();
|
|
/**
|
|
* Base element class which manages element properties and attributes. When
|
|
* properties change, the `update` method is asynchronously called. This method
|
|
* should be supplied by subclasses to render updates as desired.
|
|
* @noInheritDoc
|
|
*/
|
|
export class ReactiveElement
|
|
// In the Node build, this `extends` clause will be substituted with
|
|
// `(globalThis.HTMLElement ?? HTMLElement)`.
|
|
//
|
|
// This way, we will first prefer any global `HTMLElement` polyfill that the
|
|
// user has assigned, and then fall back to the `HTMLElement` shim which has
|
|
// been imported (see note at the top of this file about how this import is
|
|
// generated by Rollup). Note that the `HTMLElement` variable has been
|
|
// shadowed by this import, so it no longer refers to the global.
|
|
extends HTMLElement {
|
|
/**
|
|
* Adds an initializer function to the class that is called during instance
|
|
* construction.
|
|
*
|
|
* This is useful for code that runs against a `ReactiveElement`
|
|
* subclass, such as a decorator, that needs to do work for each
|
|
* instance, such as setting up a `ReactiveController`.
|
|
*
|
|
* ```ts
|
|
* const myDecorator = (target: typeof ReactiveElement, key: string) => {
|
|
* target.addInitializer((instance: ReactiveElement) => {
|
|
* // This is run during construction of the element
|
|
* new MyController(instance);
|
|
* });
|
|
* }
|
|
* ```
|
|
*
|
|
* Decorating a field will then cause each instance to run an initializer
|
|
* that adds a controller:
|
|
*
|
|
* ```ts
|
|
* class MyElement extends LitElement {
|
|
* @myDecorator foo;
|
|
* }
|
|
* ```
|
|
*
|
|
* Initializers are stored per-constructor. Adding an initializer to a
|
|
* subclass does not add it to a superclass. Since initializers are run in
|
|
* constructors, initializers will run in order of the class hierarchy,
|
|
* starting with superclasses and progressing to the instance's class.
|
|
*
|
|
* @nocollapse
|
|
*/
|
|
static addInitializer(initializer) {
|
|
this.__prepare();
|
|
(this._initializers ??= []).push(initializer);
|
|
}
|
|
/**
|
|
* Returns a list of attributes corresponding to the registered properties.
|
|
* @nocollapse
|
|
* @category attributes
|
|
*/
|
|
static get observedAttributes() {
|
|
// Ensure we've created all properties
|
|
this.finalize();
|
|
// this.__attributeToPropertyMap is only undefined after finalize() in
|
|
// ReactiveElement itself. ReactiveElement.observedAttributes is only
|
|
// accessed with ReactiveElement as the receiver when a subclass or mixin
|
|
// calls super.observedAttributes
|
|
return (this.__attributeToPropertyMap && [...this.__attributeToPropertyMap.keys()]);
|
|
}
|
|
/**
|
|
* Creates a property accessor on the element prototype if one does not exist
|
|
* and stores a {@linkcode PropertyDeclaration} for the property with the
|
|
* given options. The property setter calls the property's `hasChanged`
|
|
* property option or uses a strict identity check to determine whether or not
|
|
* to request an update.
|
|
*
|
|
* This method may be overridden to customize properties; however,
|
|
* when doing so, it's important to call `super.createProperty` to ensure
|
|
* the property is setup correctly. This method calls
|
|
* `getPropertyDescriptor` internally to get a descriptor to install.
|
|
* To customize what properties do when they are get or set, override
|
|
* `getPropertyDescriptor`. To customize the options for a property,
|
|
* implement `createProperty` like this:
|
|
*
|
|
* ```ts
|
|
* static createProperty(name, options) {
|
|
* options = Object.assign(options, {myOption: true});
|
|
* super.createProperty(name, options);
|
|
* }
|
|
* ```
|
|
*
|
|
* @nocollapse
|
|
* @category properties
|
|
*/
|
|
static createProperty(name, options = defaultPropertyDeclaration) {
|
|
// If this is a state property, force the attribute to false.
|
|
if (options.state) {
|
|
options.attribute = false;
|
|
}
|
|
this.__prepare();
|
|
// Whether this property is wrapping accessors.
|
|
// Helps control the initial value change and reflection logic.
|
|
if (this.prototype.hasOwnProperty(name)) {
|
|
options = Object.create(options);
|
|
options.wrapped = true;
|
|
}
|
|
this.elementProperties.set(name, options);
|
|
if (!options.noAccessor) {
|
|
const key = DEV_MODE
|
|
? // Use Symbol.for in dev mode to make it easier to maintain state
|
|
// when doing HMR.
|
|
Symbol.for(`${String(name)} (@property() cache)`)
|
|
: Symbol();
|
|
const descriptor = this.getPropertyDescriptor(name, key, options);
|
|
if (descriptor !== undefined) {
|
|
defineProperty(this.prototype, name, descriptor);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Returns a property descriptor to be defined on the given named property.
|
|
* If no descriptor is returned, the property will not become an accessor.
|
|
* For example,
|
|
*
|
|
* ```ts
|
|
* class MyElement extends LitElement {
|
|
* static getPropertyDescriptor(name, key, options) {
|
|
* const defaultDescriptor =
|
|
* super.getPropertyDescriptor(name, key, options);
|
|
* const setter = defaultDescriptor.set;
|
|
* return {
|
|
* get: defaultDescriptor.get,
|
|
* set(value) {
|
|
* setter.call(this, value);
|
|
* // custom action.
|
|
* },
|
|
* configurable: true,
|
|
* enumerable: true
|
|
* }
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @nocollapse
|
|
* @category properties
|
|
*/
|
|
static getPropertyDescriptor(name, key, options) {
|
|
const { get, set } = getOwnPropertyDescriptor(this.prototype, name) ?? {
|
|
get() {
|
|
return this[key];
|
|
},
|
|
set(v) {
|
|
this[key] = v;
|
|
},
|
|
};
|
|
if (DEV_MODE && get == null) {
|
|
if ('value' in (getOwnPropertyDescriptor(this.prototype, name) ?? {})) {
|
|
throw new Error(`Field ${JSON.stringify(String(name))} on ` +
|
|
`${this.name} was declared as a reactive property ` +
|
|
`but it's actually declared as a value on the prototype. ` +
|
|
`Usually this is due to using @property or @state on a method.`);
|
|
}
|
|
issueWarning('reactive-property-without-getter', `Field ${JSON.stringify(String(name))} on ` +
|
|
`${this.name} was declared as a reactive property ` +
|
|
`but it does not have a getter. This will be an error in a ` +
|
|
`future version of Lit.`);
|
|
}
|
|
return {
|
|
get,
|
|
set(value) {
|
|
const oldValue = get?.call(this);
|
|
set?.call(this, value);
|
|
this.requestUpdate(name, oldValue, options);
|
|
},
|
|
configurable: true,
|
|
enumerable: true,
|
|
};
|
|
}
|
|
/**
|
|
* Returns the property options associated with the given property.
|
|
* These options are defined with a `PropertyDeclaration` via the `properties`
|
|
* object or the `@property` decorator and are registered in
|
|
* `createProperty(...)`.
|
|
*
|
|
* Note, this method should be considered "final" and not overridden. To
|
|
* customize the options for a given property, override
|
|
* {@linkcode createProperty}.
|
|
*
|
|
* @nocollapse
|
|
* @final
|
|
* @category properties
|
|
*/
|
|
static getPropertyOptions(name) {
|
|
return this.elementProperties.get(name) ?? defaultPropertyDeclaration;
|
|
}
|
|
/**
|
|
* Initializes static own properties of the class used in bookkeeping
|
|
* for element properties, initializers, etc.
|
|
*
|
|
* Can be called multiple times by code that needs to ensure these
|
|
* properties exist before using them.
|
|
*
|
|
* This method ensures the superclass is finalized so that inherited
|
|
* property metadata can be copied down.
|
|
* @nocollapse
|
|
*/
|
|
static __prepare() {
|
|
if (this.hasOwnProperty(JSCompiler_renameProperty('elementProperties', this))) {
|
|
// Already prepared
|
|
return;
|
|
}
|
|
// Finalize any superclasses
|
|
const superCtor = getPrototypeOf(this);
|
|
superCtor.finalize();
|
|
// Create own set of initializers for this class if any exist on the
|
|
// superclass and copy them down. Note, for a small perf boost, avoid
|
|
// creating initializers unless needed.
|
|
if (superCtor._initializers !== undefined) {
|
|
this._initializers = [...superCtor._initializers];
|
|
}
|
|
// Initialize elementProperties from the superclass
|
|
this.elementProperties = new Map(superCtor.elementProperties);
|
|
}
|
|
/**
|
|
* Finishes setting up the class so that it's ready to be registered
|
|
* as a custom element and instantiated.
|
|
*
|
|
* This method is called by the ReactiveElement.observedAttributes getter.
|
|
* If you override the observedAttributes getter, you must either call
|
|
* super.observedAttributes to trigger finalization, or call finalize()
|
|
* yourself.
|
|
*
|
|
* @nocollapse
|
|
*/
|
|
static finalize() {
|
|
if (this.hasOwnProperty(JSCompiler_renameProperty('finalized', this))) {
|
|
return;
|
|
}
|
|
this.finalized = true;
|
|
this.__prepare();
|
|
// Create properties from the static properties block:
|
|
if (this.hasOwnProperty(JSCompiler_renameProperty('properties', this))) {
|
|
const props = this.properties;
|
|
const propKeys = [
|
|
...getOwnPropertyNames(props),
|
|
...getOwnPropertySymbols(props),
|
|
];
|
|
for (const p of propKeys) {
|
|
this.createProperty(p, props[p]);
|
|
}
|
|
}
|
|
// Create properties from standard decorator metadata:
|
|
const metadata = this[Symbol.metadata];
|
|
if (metadata !== null) {
|
|
const properties = litPropertyMetadata.get(metadata);
|
|
if (properties !== undefined) {
|
|
for (const [p, options] of properties) {
|
|
this.elementProperties.set(p, options);
|
|
}
|
|
}
|
|
}
|
|
// Create the attribute-to-property map
|
|
this.__attributeToPropertyMap = new Map();
|
|
for (const [p, options] of this.elementProperties) {
|
|
const attr = this.__attributeNameForProperty(p, options);
|
|
if (attr !== undefined) {
|
|
this.__attributeToPropertyMap.set(attr, p);
|
|
}
|
|
}
|
|
this.elementStyles = this.finalizeStyles(this.styles);
|
|
if (DEV_MODE) {
|
|
if (this.hasOwnProperty('createProperty')) {
|
|
issueWarning('no-override-create-property', 'Overriding ReactiveElement.createProperty() is deprecated. ' +
|
|
'The override will not be called with standard decorators');
|
|
}
|
|
if (this.hasOwnProperty('getPropertyDescriptor')) {
|
|
issueWarning('no-override-get-property-descriptor', 'Overriding ReactiveElement.getPropertyDescriptor() is deprecated. ' +
|
|
'The override will not be called with standard decorators');
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Takes the styles the user supplied via the `static styles` property and
|
|
* returns the array of styles to apply to the element.
|
|
* Override this method to integrate into a style management system.
|
|
*
|
|
* Styles are deduplicated preserving the _last_ instance in the list. This
|
|
* is a performance optimization to avoid duplicated styles that can occur
|
|
* especially when composing via subclassing. The last item is kept to try
|
|
* to preserve the cascade order with the assumption that it's most important
|
|
* that last added styles override previous styles.
|
|
*
|
|
* @nocollapse
|
|
* @category styles
|
|
*/
|
|
static finalizeStyles(styles) {
|
|
const elementStyles = [];
|
|
if (Array.isArray(styles)) {
|
|
// Dedupe the flattened array in reverse order to preserve the last items.
|
|
// Casting to Array<unknown> works around TS error that
|
|
// appears to come from trying to flatten a type CSSResultArray.
|
|
const set = new Set(styles.flat(Infinity).reverse());
|
|
// Then preserve original order by adding the set items in reverse order.
|
|
for (const s of set) {
|
|
elementStyles.unshift(getCompatibleStyle(s));
|
|
}
|
|
}
|
|
else if (styles !== undefined) {
|
|
elementStyles.push(getCompatibleStyle(styles));
|
|
}
|
|
return elementStyles;
|
|
}
|
|
/**
|
|
* Returns the property name for the given attribute `name`.
|
|
* @nocollapse
|
|
*/
|
|
static __attributeNameForProperty(name, options) {
|
|
const attribute = options.attribute;
|
|
return attribute === false
|
|
? undefined
|
|
: typeof attribute === 'string'
|
|
? attribute
|
|
: typeof name === 'string'
|
|
? name.toLowerCase()
|
|
: undefined;
|
|
}
|
|
constructor() {
|
|
super();
|
|
this.__instanceProperties = undefined;
|
|
/**
|
|
* True if there is a pending update as a result of calling `requestUpdate()`.
|
|
* Should only be read.
|
|
* @category updates
|
|
*/
|
|
this.isUpdatePending = false;
|
|
/**
|
|
* Is set to `true` after the first update. The element code cannot assume
|
|
* that `renderRoot` exists before the element `hasUpdated`.
|
|
* @category updates
|
|
*/
|
|
this.hasUpdated = false;
|
|
/**
|
|
* Name of currently reflecting property
|
|
*/
|
|
this.__reflectingProperty = null;
|
|
this.__initialize();
|
|
}
|
|
/**
|
|
* Internal only override point for customizing work done when elements
|
|
* are constructed.
|
|
*/
|
|
__initialize() {
|
|
this.__updatePromise = new Promise((res) => (this.enableUpdating = res));
|
|
this._$changedProperties = new Map();
|
|
// This enqueues a microtask that must run before the first update, so it
|
|
// must be called before requestUpdate()
|
|
this.__saveInstanceProperties();
|
|
// ensures first update will be caught by an early access of
|
|
// `updateComplete`
|
|
this.requestUpdate();
|
|
this.constructor._initializers?.forEach((i) => i(this));
|
|
}
|
|
/**
|
|
* Registers a `ReactiveController` to participate in the element's reactive
|
|
* update cycle. The element automatically calls into any registered
|
|
* controllers during its lifecycle callbacks.
|
|
*
|
|
* If the element is connected when `addController()` is called, the
|
|
* controller's `hostConnected()` callback will be immediately called.
|
|
* @category controllers
|
|
*/
|
|
addController(controller) {
|
|
(this.__controllers ??= new Set()).add(controller);
|
|
// If a controller is added after the element has been connected,
|
|
// call hostConnected. Note, re-using existence of `renderRoot` here
|
|
// (which is set in connectedCallback) to avoid the need to track a
|
|
// first connected state.
|
|
if (this.renderRoot !== undefined && this.isConnected) {
|
|
controller.hostConnected?.();
|
|
}
|
|
}
|
|
/**
|
|
* Removes a `ReactiveController` from the element.
|
|
* @category controllers
|
|
*/
|
|
removeController(controller) {
|
|
this.__controllers?.delete(controller);
|
|
}
|
|
/**
|
|
* Fixes any properties set on the instance before upgrade time.
|
|
* Otherwise these would shadow the accessor and break these properties.
|
|
* The properties are stored in a Map which is played back after the
|
|
* constructor runs.
|
|
*/
|
|
__saveInstanceProperties() {
|
|
const instanceProperties = new Map();
|
|
const elementProperties = this.constructor
|
|
.elementProperties;
|
|
for (const p of elementProperties.keys()) {
|
|
if (this.hasOwnProperty(p)) {
|
|
instanceProperties.set(p, this[p]);
|
|
delete this[p];
|
|
}
|
|
}
|
|
if (instanceProperties.size > 0) {
|
|
this.__instanceProperties = instanceProperties;
|
|
}
|
|
}
|
|
/**
|
|
* Returns the node into which the element should render and by default
|
|
* creates and returns an open shadowRoot. Implement to customize where the
|
|
* element's DOM is rendered. For example, to render into the element's
|
|
* childNodes, return `this`.
|
|
*
|
|
* @return Returns a node into which to render.
|
|
* @category rendering
|
|
*/
|
|
createRenderRoot() {
|
|
const renderRoot = this.shadowRoot ??
|
|
this.attachShadow(this.constructor.shadowRootOptions);
|
|
adoptStyles(renderRoot, this.constructor.elementStyles);
|
|
return renderRoot;
|
|
}
|
|
/**
|
|
* On first connection, creates the element's renderRoot, sets up
|
|
* element styling, and enables updating.
|
|
* @category lifecycle
|
|
*/
|
|
connectedCallback() {
|
|
// Create renderRoot before controllers `hostConnected`
|
|
this.renderRoot ??=
|
|
this.createRenderRoot();
|
|
this.enableUpdating(true);
|
|
this.__controllers?.forEach((c) => c.hostConnected?.());
|
|
}
|
|
/**
|
|
* Note, this method should be considered final and not overridden. It is
|
|
* overridden on the element instance with a function that triggers the first
|
|
* update.
|
|
* @category updates
|
|
*/
|
|
enableUpdating(_requestedUpdate) { }
|
|
/**
|
|
* Allows for `super.disconnectedCallback()` in extensions while
|
|
* reserving the possibility of making non-breaking feature additions
|
|
* when disconnecting at some point in the future.
|
|
* @category lifecycle
|
|
*/
|
|
disconnectedCallback() {
|
|
this.__controllers?.forEach((c) => c.hostDisconnected?.());
|
|
}
|
|
/**
|
|
* Synchronizes property values when attributes change.
|
|
*
|
|
* Specifically, when an attribute is set, the corresponding property is set.
|
|
* You should rarely need to implement this callback. If this method is
|
|
* overridden, `super.attributeChangedCallback(name, _old, value)` must be
|
|
* called.
|
|
*
|
|
* See [responding to attribute changes](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#responding_to_attribute_changes)
|
|
* on MDN for more information about the `attributeChangedCallback`.
|
|
* @category attributes
|
|
*/
|
|
attributeChangedCallback(name, _old, value) {
|
|
this._$attributeToProperty(name, value);
|
|
}
|
|
__propertyToAttribute(name, value) {
|
|
const elemProperties = this.constructor.elementProperties;
|
|
const options = elemProperties.get(name);
|
|
const attr = this.constructor.__attributeNameForProperty(name, options);
|
|
if (attr !== undefined && options.reflect === true) {
|
|
const converter = options.converter?.toAttribute !==
|
|
undefined
|
|
? options.converter
|
|
: defaultConverter;
|
|
const attrValue = converter.toAttribute(value, options.type);
|
|
if (DEV_MODE &&
|
|
this.constructor.enabledWarnings.includes('migration') &&
|
|
attrValue === undefined) {
|
|
issueWarning('undefined-attribute-value', `The attribute value for the ${name} property is ` +
|
|
`undefined on element ${this.localName}. The attribute will be ` +
|
|
`removed, but in the previous version of \`ReactiveElement\`, ` +
|
|
`the attribute would not have changed.`);
|
|
}
|
|
// Track if the property is being reflected to avoid
|
|
// setting the property again via `attributeChangedCallback`. Note:
|
|
// 1. this takes advantage of the fact that the callback is synchronous.
|
|
// 2. will behave incorrectly if multiple attributes are in the reaction
|
|
// stack at time of calling. However, since we process attributes
|
|
// in `update` this should not be possible (or an extreme corner case
|
|
// that we'd like to discover).
|
|
// mark state reflecting
|
|
this.__reflectingProperty = name;
|
|
if (attrValue == null) {
|
|
this.removeAttribute(attr);
|
|
}
|
|
else {
|
|
this.setAttribute(attr, attrValue);
|
|
}
|
|
// mark state not reflecting
|
|
this.__reflectingProperty = null;
|
|
}
|
|
}
|
|
/** @internal */
|
|
_$attributeToProperty(name, value) {
|
|
const ctor = this.constructor;
|
|
// Note, hint this as an `AttributeMap` so closure clearly understands
|
|
// the type; it has issues with tracking types through statics
|
|
const propName = ctor.__attributeToPropertyMap.get(name);
|
|
// Use tracking info to avoid reflecting a property value to an attribute
|
|
// if it was just set because the attribute changed.
|
|
if (propName !== undefined && this.__reflectingProperty !== propName) {
|
|
const options = ctor.getPropertyOptions(propName);
|
|
const converter = typeof options.converter === 'function'
|
|
? { fromAttribute: options.converter }
|
|
: options.converter?.fromAttribute !== undefined
|
|
? options.converter
|
|
: defaultConverter;
|
|
// mark state reflecting
|
|
this.__reflectingProperty = propName;
|
|
const convertedValue = converter.fromAttribute(value, options.type);
|
|
this[propName] =
|
|
convertedValue ??
|
|
this.__defaultValues?.get(propName) ??
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
convertedValue;
|
|
// mark state not reflecting
|
|
this.__reflectingProperty = null;
|
|
}
|
|
}
|
|
/**
|
|
* Requests an update which is processed asynchronously. This should be called
|
|
* when an element should update based on some state not triggered by setting
|
|
* a reactive property. In this case, pass no arguments. It should also be
|
|
* called when manually implementing a property setter. In this case, pass the
|
|
* property `name` and `oldValue` to ensure that any configured property
|
|
* options are honored.
|
|
*
|
|
* @param name name of requesting property
|
|
* @param oldValue old value of requesting property
|
|
* @param options property options to use instead of the previously
|
|
* configured options
|
|
* @param useNewValue if true, the newValue argument is used instead of
|
|
* reading the property value. This is important to use if the reactive
|
|
* property is a standard private accessor, as opposed to a plain
|
|
* property, since private members can't be dynamically read by name.
|
|
* @param newValue the new value of the property. This is only used if
|
|
* `useNewValue` is true.
|
|
* @category updates
|
|
*/
|
|
requestUpdate(name, oldValue, options, useNewValue = false, newValue) {
|
|
// If we have a property key, perform property update steps.
|
|
if (name !== undefined) {
|
|
if (DEV_MODE && name instanceof Event) {
|
|
issueWarning(``, `The requestUpdate() method was called with an Event as the property name. This is probably a mistake caused by binding this.requestUpdate as an event listener. Instead bind a function that will call it with no arguments: () => this.requestUpdate()`);
|
|
}
|
|
const ctor = this.constructor;
|
|
if (useNewValue === false) {
|
|
newValue = this[name];
|
|
}
|
|
options ??= ctor.getPropertyOptions(name);
|
|
const changed = (options.hasChanged ?? notEqual)(newValue, oldValue) ||
|
|
// When there is no change, check a corner case that can occur when
|
|
// 1. there's a initial value which was not reflected
|
|
// 2. the property is subsequently set to this value.
|
|
// For example, `prop: {useDefault: true, reflect: true}`
|
|
// and el.prop = 'foo'. This should be considered a change if the
|
|
// attribute is not set because we will now reflect the property to the attribute.
|
|
(options.useDefault &&
|
|
options.reflect &&
|
|
newValue === this.__defaultValues?.get(name) &&
|
|
!this.hasAttribute(ctor.__attributeNameForProperty(name, options)));
|
|
if (changed) {
|
|
this._$changeProperty(name, oldValue, options);
|
|
}
|
|
else {
|
|
// Abort the request if the property should not be considered changed.
|
|
return;
|
|
}
|
|
}
|
|
if (this.isUpdatePending === false) {
|
|
this.__updatePromise = this.__enqueueUpdate();
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_$changeProperty(name, oldValue, { useDefault, reflect, wrapped }, initializeValue) {
|
|
// Record default value when useDefault is used. This allows us to
|
|
// restore this value when the attribute is removed.
|
|
if (useDefault && !(this.__defaultValues ??= new Map()).has(name)) {
|
|
this.__defaultValues.set(name, initializeValue ?? oldValue ?? this[name]);
|
|
// if this is not wrapping an accessor, it must be an initial setting
|
|
// and in this case we do not want to record the change or reflect.
|
|
if (wrapped !== true || initializeValue !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
// TODO (justinfagnani): Create a benchmark of Map.has() + Map.set(
|
|
// vs just Map.set()
|
|
if (!this._$changedProperties.has(name)) {
|
|
// On the initial change, the old value should be `undefined`, except
|
|
// with `useDefault`
|
|
if (!this.hasUpdated && !useDefault) {
|
|
oldValue = undefined;
|
|
}
|
|
this._$changedProperties.set(name, oldValue);
|
|
}
|
|
// Add to reflecting properties set.
|
|
// Note, it's important that every change has a chance to add the
|
|
// property to `__reflectingProperties`. This ensures setting
|
|
// attribute + property reflects correctly.
|
|
if (reflect === true && this.__reflectingProperty !== name) {
|
|
(this.__reflectingProperties ??= new Set()).add(name);
|
|
}
|
|
}
|
|
/**
|
|
* Sets up the element to asynchronously update.
|
|
*/
|
|
async __enqueueUpdate() {
|
|
this.isUpdatePending = true;
|
|
try {
|
|
// Ensure any previous update has resolved before updating.
|
|
// This `await` also ensures that property changes are batched.
|
|
await this.__updatePromise;
|
|
}
|
|
catch (e) {
|
|
// Refire any previous errors async so they do not disrupt the update
|
|
// cycle. Errors are refired so developers have a chance to observe
|
|
// them, and this can be done by implementing
|
|
// `window.onunhandledrejection`.
|
|
Promise.reject(e);
|
|
}
|
|
const result = this.scheduleUpdate();
|
|
// If `scheduleUpdate` returns a Promise, we await it. This is done to
|
|
// enable coordinating updates with a scheduler. Note, the result is
|
|
// checked to avoid delaying an additional microtask unless we need to.
|
|
if (result != null) {
|
|
await result;
|
|
}
|
|
return !this.isUpdatePending;
|
|
}
|
|
/**
|
|
* Schedules an element update. You can override this method to change the
|
|
* timing of updates by returning a Promise. The update will await the
|
|
* returned Promise, and you should resolve the Promise to allow the update
|
|
* to proceed. If this method is overridden, `super.scheduleUpdate()`
|
|
* must be called.
|
|
*
|
|
* For instance, to schedule updates to occur just before the next frame:
|
|
*
|
|
* ```ts
|
|
* override protected async scheduleUpdate(): Promise<unknown> {
|
|
* await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
* super.scheduleUpdate();
|
|
* }
|
|
* ```
|
|
* @category updates
|
|
*/
|
|
scheduleUpdate() {
|
|
const result = this.performUpdate();
|
|
if (DEV_MODE &&
|
|
this.constructor.enabledWarnings.includes('async-perform-update') &&
|
|
typeof result?.then ===
|
|
'function') {
|
|
issueWarning('async-perform-update', `Element ${this.localName} returned a Promise from performUpdate(). ` +
|
|
`This behavior is deprecated and will be removed in a future ` +
|
|
`version of ReactiveElement.`);
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Performs an element update. Note, if an exception is thrown during the
|
|
* update, `firstUpdated` and `updated` will not be called.
|
|
*
|
|
* Call `performUpdate()` to immediately process a pending update. This should
|
|
* generally not be needed, but it can be done in rare cases when you need to
|
|
* update synchronously.
|
|
*
|
|
* @category updates
|
|
*/
|
|
performUpdate() {
|
|
// Abort any update if one is not pending when this is called.
|
|
// This can happen if `performUpdate` is called early to "flush"
|
|
// the update.
|
|
if (!this.isUpdatePending) {
|
|
return;
|
|
}
|
|
debugLogEvent?.({ kind: 'update' });
|
|
if (!this.hasUpdated) {
|
|
// Create renderRoot before first update. This occurs in `connectedCallback`
|
|
// but is done here to support out of tree calls to `enableUpdating`/`performUpdate`.
|
|
this.renderRoot ??=
|
|
this.createRenderRoot();
|
|
if (DEV_MODE) {
|
|
// Produce warning if any reactive properties on the prototype are
|
|
// shadowed by class fields. Instance fields set before upgrade are
|
|
// deleted by this point, so any own property is caused by class field
|
|
// initialization in the constructor.
|
|
const ctor = this.constructor;
|
|
const shadowedProperties = [...ctor.elementProperties.keys()].filter((p) => this.hasOwnProperty(p) && p in getPrototypeOf(this));
|
|
if (shadowedProperties.length) {
|
|
throw new Error(`The following properties on element ${this.localName} will not ` +
|
|
`trigger updates as expected because they are set using class ` +
|
|
`fields: ${shadowedProperties.join(', ')}. ` +
|
|
`Native class fields and some compiled output will overwrite ` +
|
|
`accessors used for detecting changes. See ` +
|
|
`https://lit.dev/msg/class-field-shadowing ` +
|
|
`for more information.`);
|
|
}
|
|
}
|
|
// Mixin instance properties once, if they exist.
|
|
if (this.__instanceProperties) {
|
|
// TODO (justinfagnani): should we use the stored value? Could a new value
|
|
// have been set since we stored the own property value?
|
|
for (const [p, value] of this.__instanceProperties) {
|
|
this[p] = value;
|
|
}
|
|
this.__instanceProperties = undefined;
|
|
}
|
|
// Trigger initial value reflection and populate the initial
|
|
// `changedProperties` map, but only for the case of properties created
|
|
// via `createProperty` on accessors, which will not have already
|
|
// populated the `changedProperties` map since they are not set.
|
|
// We can't know if these accessors had initializers, so we just set
|
|
// them anyway - a difference from experimental decorators on fields and
|
|
// standard decorators on auto-accessors.
|
|
// For context see:
|
|
// https://github.com/lit/lit/pull/4183#issuecomment-1711959635
|
|
const elementProperties = this.constructor
|
|
.elementProperties;
|
|
if (elementProperties.size > 0) {
|
|
for (const [p, options] of elementProperties) {
|
|
const { wrapped } = options;
|
|
const value = this[p];
|
|
if (wrapped === true &&
|
|
!this._$changedProperties.has(p) &&
|
|
value !== undefined) {
|
|
this._$changeProperty(p, undefined, options, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let shouldUpdate = false;
|
|
const changedProperties = this._$changedProperties;
|
|
try {
|
|
shouldUpdate = this.shouldUpdate(changedProperties);
|
|
if (shouldUpdate) {
|
|
this.willUpdate(changedProperties);
|
|
this.__controllers?.forEach((c) => c.hostUpdate?.());
|
|
this.update(changedProperties);
|
|
}
|
|
else {
|
|
this.__markUpdated();
|
|
}
|
|
}
|
|
catch (e) {
|
|
// Prevent `firstUpdated` and `updated` from running when there's an
|
|
// update exception.
|
|
shouldUpdate = false;
|
|
// Ensure element can accept additional updates after an exception.
|
|
this.__markUpdated();
|
|
throw e;
|
|
}
|
|
// The update is no longer considered pending and further updates are now allowed.
|
|
if (shouldUpdate) {
|
|
this._$didUpdate(changedProperties);
|
|
}
|
|
}
|
|
/**
|
|
* Invoked before `update()` to compute values needed during the update.
|
|
*
|
|
* Implement `willUpdate` to compute property values that depend on other
|
|
* properties and are used in the rest of the update process.
|
|
*
|
|
* ```ts
|
|
* willUpdate(changedProperties) {
|
|
* // only need to check changed properties for an expensive computation.
|
|
* if (changedProperties.has('firstName') || changedProperties.has('lastName')) {
|
|
* this.sha = computeSHA(`${this.firstName} ${this.lastName}`);
|
|
* }
|
|
* }
|
|
*
|
|
* render() {
|
|
* return html`SHA: ${this.sha}`;
|
|
* }
|
|
* ```
|
|
*
|
|
* @category updates
|
|
*/
|
|
willUpdate(_changedProperties) { }
|
|
// Note, this is an override point for polyfill-support.
|
|
// @internal
|
|
_$didUpdate(changedProperties) {
|
|
this.__controllers?.forEach((c) => c.hostUpdated?.());
|
|
if (!this.hasUpdated) {
|
|
this.hasUpdated = true;
|
|
this.firstUpdated(changedProperties);
|
|
}
|
|
this.updated(changedProperties);
|
|
if (DEV_MODE &&
|
|
this.isUpdatePending &&
|
|
this.constructor.enabledWarnings.includes('change-in-update')) {
|
|
issueWarning('change-in-update', `Element ${this.localName} scheduled an update ` +
|
|
`(generally because a property was set) ` +
|
|
`after an update completed, causing a new update to be scheduled. ` +
|
|
`This is inefficient and should be avoided unless the next update ` +
|
|
`can only be scheduled as a side effect of the previous update.`);
|
|
}
|
|
}
|
|
__markUpdated() {
|
|
this._$changedProperties = new Map();
|
|
this.isUpdatePending = false;
|
|
}
|
|
/**
|
|
* Returns a Promise that resolves when the element has completed updating.
|
|
* The Promise value is a boolean that is `true` if the element completed the
|
|
* update without triggering another update. The Promise result is `false` if
|
|
* a property was set inside `updated()`. If the Promise is rejected, an
|
|
* exception was thrown during the update.
|
|
*
|
|
* To await additional asynchronous work, override the `getUpdateComplete`
|
|
* method. For example, it is sometimes useful to await a rendered element
|
|
* before fulfilling this Promise. To do this, first await
|
|
* `super.getUpdateComplete()`, then any subsequent state.
|
|
*
|
|
* @return A promise of a boolean that resolves to true if the update completed
|
|
* without triggering another update.
|
|
* @category updates
|
|
*/
|
|
get updateComplete() {
|
|
return this.getUpdateComplete();
|
|
}
|
|
/**
|
|
* Override point for the `updateComplete` promise.
|
|
*
|
|
* It is not safe to override the `updateComplete` getter directly due to a
|
|
* limitation in TypeScript which means it is not possible to call a
|
|
* superclass getter (e.g. `super.updateComplete.then(...)`) when the target
|
|
* language is ES5 (https://github.com/microsoft/TypeScript/issues/338).
|
|
* This method should be overridden instead. For example:
|
|
*
|
|
* ```ts
|
|
* class MyElement extends LitElement {
|
|
* override async getUpdateComplete() {
|
|
* const result = await super.getUpdateComplete();
|
|
* await this._myChild.updateComplete;
|
|
* return result;
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @return A promise of a boolean that resolves to true if the update completed
|
|
* without triggering another update.
|
|
* @category updates
|
|
*/
|
|
getUpdateComplete() {
|
|
return this.__updatePromise;
|
|
}
|
|
/**
|
|
* Controls whether or not `update()` should be called when the element requests
|
|
* an update. By default, this method always returns `true`, but this can be
|
|
* customized to control when to update.
|
|
*
|
|
* @param _changedProperties Map of changed properties with old values
|
|
* @category updates
|
|
*/
|
|
shouldUpdate(_changedProperties) {
|
|
return true;
|
|
}
|
|
/**
|
|
* Updates the element. This method reflects property values to attributes.
|
|
* It can be overridden to render and keep updated element DOM.
|
|
* Setting properties inside this method will *not* trigger
|
|
* another update.
|
|
*
|
|
* @param _changedProperties Map of changed properties with old values
|
|
* @category updates
|
|
*/
|
|
update(_changedProperties) {
|
|
// The forEach() expression will only run when __reflectingProperties is
|
|
// defined, and it returns undefined, setting __reflectingProperties to
|
|
// undefined
|
|
this.__reflectingProperties &&= this.__reflectingProperties.forEach((p) => this.__propertyToAttribute(p, this[p]));
|
|
this.__markUpdated();
|
|
}
|
|
/**
|
|
* Invoked whenever the element is updated. Implement to perform
|
|
* post-updating tasks via DOM APIs, for example, focusing an element.
|
|
*
|
|
* Setting properties inside this method will trigger the element to update
|
|
* again after this update cycle completes.
|
|
*
|
|
* @param _changedProperties Map of changed properties with old values
|
|
* @category updates
|
|
*/
|
|
updated(_changedProperties) { }
|
|
/**
|
|
* Invoked when the element is first updated. Implement to perform one time
|
|
* work on the element after update.
|
|
*
|
|
* ```ts
|
|
* firstUpdated() {
|
|
* this.renderRoot.getElementById('my-text-area').focus();
|
|
* }
|
|
* ```
|
|
*
|
|
* Setting properties inside this method will trigger the element to update
|
|
* again after this update cycle completes.
|
|
*
|
|
* @param _changedProperties Map of changed properties with old values
|
|
* @category updates
|
|
*/
|
|
firstUpdated(_changedProperties) { }
|
|
}
|
|
/**
|
|
* Memoized list of all element styles.
|
|
* Created lazily on user subclasses when finalizing the class.
|
|
* @nocollapse
|
|
* @category styles
|
|
*/
|
|
ReactiveElement.elementStyles = [];
|
|
/**
|
|
* Options used when calling `attachShadow`. Set this property to customize
|
|
* the options for the shadowRoot; for example, to create a closed
|
|
* shadowRoot: `{mode: 'closed'}`.
|
|
*
|
|
* Note, these options are used in `createRenderRoot`. If this method
|
|
* is customized, options should be respected if possible.
|
|
* @nocollapse
|
|
* @category rendering
|
|
*/
|
|
ReactiveElement.shadowRootOptions = { mode: 'open' };
|
|
// Assigned here to work around a jscompiler bug with static fields
|
|
// when compiling to ES5.
|
|
// https://github.com/google/closure-compiler/issues/3177
|
|
ReactiveElement[JSCompiler_renameProperty('elementProperties', ReactiveElement)] = new Map();
|
|
ReactiveElement[JSCompiler_renameProperty('finalized', ReactiveElement)] = new Map();
|
|
// Apply polyfills if available
|
|
polyfillSupport?.({ ReactiveElement });
|
|
// Dev mode warnings...
|
|
if (DEV_MODE) {
|
|
// Default warning set.
|
|
ReactiveElement.enabledWarnings = [
|
|
'change-in-update',
|
|
'async-perform-update',
|
|
];
|
|
const ensureOwnWarnings = function (ctor) {
|
|
if (!ctor.hasOwnProperty(JSCompiler_renameProperty('enabledWarnings', ctor))) {
|
|
ctor.enabledWarnings = ctor.enabledWarnings.slice();
|
|
}
|
|
};
|
|
ReactiveElement.enableWarning = function (warning) {
|
|
ensureOwnWarnings(this);
|
|
if (!this.enabledWarnings.includes(warning)) {
|
|
this.enabledWarnings.push(warning);
|
|
}
|
|
};
|
|
ReactiveElement.disableWarning = function (warning) {
|
|
ensureOwnWarnings(this);
|
|
const i = this.enabledWarnings.indexOf(warning);
|
|
if (i >= 0) {
|
|
this.enabledWarnings.splice(i, 1);
|
|
}
|
|
};
|
|
}
|
|
// IMPORTANT: do not change the property name or the assignment expression.
|
|
// This line will be used in regexes to search for ReactiveElement usage.
|
|
(global.reactiveElementVersions ??= []).push('2.1.2');
|
|
if (DEV_MODE && global.reactiveElementVersions.length > 1) {
|
|
queueMicrotask(() => {
|
|
issueWarning('multiple-versions', `Multiple versions of Lit loaded. Loading multiple versions ` +
|
|
`is not recommended.`);
|
|
});
|
|
}
|
|
//# sourceMappingURL=reactive-element.js.map
|