/**
* @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