UNPKG

@material/web

Version:
175 lines 6.29 kB
/** * @license * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { __decorate } from "tslib"; import { property } from 'lit/decorators.js'; import { internals } from './element-internals.js'; /** * A symbol property to retrieve the form value for an element. */ export const getFormValue = Symbol('getFormValue'); /** * A symbol property to retrieve the form state for an element. */ export const getFormState = Symbol('getFormState'); /** * Mixes in form-associated behavior for a class. This allows an element to add * values to `<form>` elements. * * Implementing classes should provide a `[formValue]` to return the current * value of the element, as well as reset and restore callbacks. * * @example * ```ts * const base = mixinFormAssociated(mixinElementInternals(LitElement)); * * class MyControl extends base { * \@property() * value = ''; * * override [getFormValue]() { * return this.value; * } * * override formResetCallback() { * const defaultValue = this.getAttribute('value'); * this.value = defaultValue; * } * * override formStateRestoreCallback(state: string) { * this.value = state; * } * } * ``` * * Elements may optionally provide a `[formState]` if their values do not * represent the state of the component. * * @example * ```ts * const base = mixinFormAssociated(mixinElementInternals(LitElement)); * * class MyCheckbox extends base { * \@property() * value = 'on'; * * \@property({type: Boolean}) * checked = false; * * override [getFormValue]() { * return this.checked ? this.value : null; * } * * override [getFormState]() { * return String(this.checked); * } * * override formResetCallback() { * const defaultValue = this.hasAttribute('checked'); * this.checked = defaultValue; * } * * override formStateRestoreCallback(state: string) { * this.checked = Boolean(state); * } * } * ``` * * IMPORTANT: Requires declares for lit-analyzer * @example * ```ts * const base = mixinFormAssociated(mixinElementInternals(LitElement)); * class MyControl extends base { * // Writable mixin properties for lit-html binding, needed for lit-analyzer * declare disabled: boolean; * declare name: string; * } * ``` * * @param base The class to mix functionality into. The base class must use * `mixinElementInternals()`. * @return The provided class with `FormAssociated` mixed in. */ export function mixinFormAssociated(base) { class FormAssociatedElement extends base { get form() { return this[internals].form; } get labels() { return this[internals].labels; } // Use @property for the `name` and `disabled` properties to add them to the // `observedAttributes` array and trigger `attributeChangedCallback()`. // // We don't use Lit's default getter/setter (`noAccessor: true`) because // the attributes need to be updated synchronously to work with synchronous // form APIs, and Lit updates attributes async by default. get name() { return this.getAttribute('name') ?? ''; } set name(name) { // Note: setting name to null or empty does not remove the attribute. this.setAttribute('name', name); // We don't need to call `requestUpdate()` since it's called synchronously // in `attributeChangedCallback()`. } get disabled() { return this.hasAttribute('disabled'); } set disabled(disabled) { this.toggleAttribute('disabled', disabled); // We don't need to call `requestUpdate()` since it's called synchronously // in `attributeChangedCallback()`. } attributeChangedCallback(name, old, value) { // Manually `requestUpdate()` for `name` and `disabled` when their // attribute or property changes. // The properties update their attributes, so this callback is invoked // immediately when the properties are set. We call `requestUpdate()` here // instead of letting Lit set the properties from the attribute change. // That would cause the properties to re-set the attribute and invoke this // callback again in a loop. This leads to stale state when Lit tries to // determine if a property changed or not. if (name === 'name' || name === 'disabled') { // Disabled's value is only false if the attribute is missing and null. const oldValue = name === 'disabled' ? old !== null : old; // Trigger a lit update when the attribute changes. this.requestUpdate(name, oldValue); return; } super.attributeChangedCallback(name, old, value); } requestUpdate(name, oldValue, options) { super.requestUpdate(name, oldValue, options); // If any properties change, update the form value, which may have changed // as well. // Update the form value synchronously in `requestUpdate()` rather than // `update()` or `updated()`, which are async. This is necessary to ensure // that form data is updated in time for synchronous event listeners. this[internals].setFormValue(this[getFormValue](), this[getFormState]()); } [getFormValue]() { // Closure does not allow abstract symbol members, so a default // implementation is needed. throw new Error('Implement [getFormValue]'); } [getFormState]() { return this[getFormValue](); } formDisabledCallback(disabled) { this.disabled = disabled; } } /** @nocollapse */ FormAssociatedElement.formAssociated = true; __decorate([ property({ noAccessor: true }) ], FormAssociatedElement.prototype, "name", null); __decorate([ property({ type: Boolean, noAccessor: true }) ], FormAssociatedElement.prototype, "disabled", null); return FormAssociatedElement; } //# sourceMappingURL=form-associated.js.map