UNPKG

@material/web

Version:
185 lines 8.08 kB
/** * @license * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { isServer } from 'lit'; /** * A symbol property used for a callback when validity has been reported. */ export const onReportValidity = Symbol('onReportValidity'); // Private symbol members, used to avoid name clashing. const privateCleanupFormListeners = Symbol('privateCleanupFormListeners'); const privateDoNotReportInvalid = Symbol('privateDoNotReportInvalid'); /** * Mixes in a callback for constraint validation when validity should be * styled and reported to the user. * * This is commonly used in text-field-like controls that display error styles * and error messages. * * @example * ```ts * const baseClass = mixinOnReportValidity( * mixinConstraintValidation( * mixinFormAssociated(mixinElementInternals(LitElement)), * ), * ); * * class MyField extends baseClass { * \@property({type: Boolean}) error = false; * \@property() errorMessage = ''; * * [onReportValidity](invalidEvent: Event | null) { * this.error = !!invalidEvent; * this.errorMessage = this.validationMessage; * * // Optionally prevent platform popup from displaying * invalidEvent?.preventDefault(); * } * } * ``` * * @param base The class to mix functionality into. * @return The provided class with `OnReportValidity` mixed in. */ export function mixinOnReportValidity(base) { var _a, _b; class OnReportValidityElement extends base { // Mixins must have a constructor with `...args: any[]` // tslint:disable-next-line:no-any constructor(...args) { super(...args); /** * Used to clean up event listeners when a new form is associated. */ this[_a] = new AbortController(); /** * Used to determine if an invalid event should report validity. Invalid * events from `checkValidity()` do not trigger reporting. */ this[_b] = false; if (isServer) { return; } this.addEventListener('invalid', (invalidEvent) => { // Listen for invalid events dispatched by a `<form>` when it tries to // submit and the element is invalid. We ignore events dispatched when // calling `checkValidity()` as well as untrusted events, since the // `reportValidity()` and `<form>`-dispatched events are always // trusted. if (this[privateDoNotReportInvalid] || !invalidEvent.isTrusted) { return; } this.addEventListener('invalid', () => { // A normal bubbling phase event listener. By adding it here, we // ensure it's the last event listener that is called during the // bubbling phase. if (!invalidEvent.defaultPrevented) { this[onReportValidity](invalidEvent); } }, { once: true }); }, { // Listen during the capture phase, which will happen before the // bubbling phase. That way, we can add a final event listener that // will run after other event listeners, and we can check if it was // default prevented. This works because invalid does not bubble. capture: true, }); } checkValidity() { this[privateDoNotReportInvalid] = true; const valid = super.checkValidity(); this[privateDoNotReportInvalid] = false; return valid; } reportValidity() { const valid = super.reportValidity(); // Constructor's invalid listener will handle reporting invalid events. if (valid) { this[onReportValidity](null); } return valid; } [(_a = privateCleanupFormListeners, _b = privateDoNotReportInvalid, onReportValidity)](invalidEvent) { throw new Error('Implement [onReportValidity]'); } formAssociatedCallback(form) { // can't use super.formAssociatedCallback?.() due to closure if (super.formAssociatedCallback) { super.formAssociatedCallback(form); } // Clean up previous submit listener this[privateCleanupFormListeners].abort(); if (!form) { return; } this[privateCleanupFormListeners] = new AbortController(); // If the element's form submits, then all controls are valid. This lets // the element remove its error styles that may have been set when // `reportValidity()` was called. form.addEventListener('submit', () => { this[onReportValidity](null); }, { signal: this[privateCleanupFormListeners].signal, }); // Inject a callback when `form.reportValidity()` is called and the form // is valid. There isn't an event that is dispatched to alert us (unlike // the 'invalid' event), and we need to remove error styles when // `form.reportValidity()` is called and returns true. let reportedInvalidEventFromForm = false; let formReportValidityCleanup = new AbortController(); injectFormReportValidityHooks({ form, cleanup: this[privateCleanupFormListeners].signal, beforeReportValidity: () => { reportedInvalidEventFromForm = false; this.addEventListener('invalid', (invalidEvent) => { reportedInvalidEventFromForm = true; if (!invalidEvent.defaultPrevented) { this[onReportValidity](invalidEvent); } }, { signal: formReportValidityCleanup.signal }); }, afterReportValidity: () => { formReportValidityCleanup.abort(); formReportValidityCleanup = new AbortController(); if (reportedInvalidEventFromForm) { reportedInvalidEventFromForm = false; return; } // Report successful form validation if an invalid event wasn't // fired. this[onReportValidity](null); }, }); } } return OnReportValidityElement; } const FORM_REPORT_VALIDITY_HOOKS = new WeakMap(); function injectFormReportValidityHooks({ form, beforeReportValidity, afterReportValidity, cleanup, }) { if (!FORM_REPORT_VALIDITY_HOOKS.has(form)) { // Patch form.reportValidity() to add an event target that can be used to // react when the method is called. // We should only patch this method once, since multiple controls and other // forces may want to patch this method. We cannot reliably clean it up by // resetting the method to "superReportValidity", which may be a patched // function. // Instead, we never clean up the patch but add and clean up event listener // hooks once it's patched. const hooks = new EventTarget(); const superReportValidity = form.reportValidity; form.reportValidity = function () { hooks.dispatchEvent(new Event('before')); const valid = superReportValidity.call(this); hooks.dispatchEvent(new Event('after')); return valid; }; FORM_REPORT_VALIDITY_HOOKS.set(form, hooks); } const hooks = FORM_REPORT_VALIDITY_HOOKS.get(form); hooks.addEventListener('before', beforeReportValidity, { signal: cleanup }); hooks.addEventListener('after', afterReportValidity, { signal: cleanup }); } //# sourceMappingURL=on-report-validity.js.map