diff --git a/addon/components/-focusable.js b/addon/components/-focusable.js
index 6c94e0304..e5bffaa95 100644
--- a/addon/components/-focusable.js
+++ b/addon/components/-focusable.js
@@ -89,6 +89,11 @@ export default class Focusable extends Component {
* They bubble by default.
*/
@action handleFocusIn(e) {
+ if (this.disabled) {
+ // elements should not be able to be focused if disabled.
+ return;
+ }
+
if ((!this.disabled && !this.focusOnlyOnKey) || !this.pressed) {
this.focused = true;
if (this.args.onFocusIn) {
diff --git a/addon/components/paper-input.hbs b/addon/components/paper-input.hbs
index 2041b50bd..ac21aa579 100644
--- a/addon/components/paper-input.hbs
+++ b/addon/components/paper-input.hbs
@@ -1,115 +1,159 @@
-{{! template-lint-disable no-action no-curly-component-invocation no-down-event-binding no-positive-tabindex }}
-{{#if @label}}
-
-{{/if}}
-
-{{#if @icon}}
- {{component this.iconComponent @icon}}
-{{/if}}
-
-{{#if @textarea}}
-
-{{else}}
-
-{{/if}}
+ {{#if @textarea}}
+
+ {{else}}
+
+ {{/if}}
-{{#unless this.hideAllMessages}}
-
- {{#each this.validationErrorMessages as |error index|}}
-
- {{error.message}}
-
- {{/each}}
+ {{#unless @hideAllMessages}}
+
+ {{#if @maxlength}}
+
{{this.currentLength}}/{{@maxlength}}
+ {{/if}}
- {{/if}}
-{{/unless}}
+ {{#if this.validation.isInvalidAndTouched}}
+
+ {{/if}}
+ {{/unless}}
-{{yield (hash
- charCount=this.currentLength
- isInvalid=this.isInvalid
- isTouched=this.isTouched
- isInvalidAndTouched=this.isInvalidAndTouched
- hasValue=this.hasValue
- validationErrorMessages=this.validationErrorMessages
-)}}
+ {{yield
+ (hash
+ charCount=this.currentLength
+ isInvalid=this.validation.isInvalid
+ isTouched=this.validation.isTouched
+ isInvalidAndTouched=this.validation.isInvalidAndTouched
+ hasValue=this.hasValue
+ validationErrorMessages=this.validation.errorMessages
+ )
+ }}
-{{#if @iconRight}}
- {{component this.iconComponent @iconRight}}
-{{/if}}
+ {{#if @iconRight}}
+ {{component this.iconComponent @iconRight}}
+ {{/if}}
+
\ No newline at end of file
diff --git a/addon/components/paper-input.js b/addon/components/paper-input.js
index 46702fc6d..5426f0d2b 100644
--- a/addon/components/paper-input.js
+++ b/addon/components/paper-input.js
@@ -1,150 +1,334 @@
-/* eslint-disable ember/no-actions-hash, ember/no-classic-components, ember/no-component-lifecycle-hooks, ember/no-get, ember/no-mixins, ember/require-computed-property-dependencies, ember/require-tagless-components */
/**
* @module ember-paper
*/
-import { or, bool, and } from '@ember/object/computed';
-
-import Component from '@ember/component';
-import { computed, set } from '@ember/object';
+import Focusable from './-focusable';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { guidFor } from '@ember/object/internals';
import { isEmpty } from '@ember/utils';
-import { bind, next } from '@ember/runloop';
import { assert } from '@ember/debug';
-import FocusableMixin from 'ember-paper/mixins/focusable-mixin';
-import ChildMixin from 'ember-paper/mixins/child-mixin';
-import ValidationMixin from 'ember-paper/mixins/validation-mixin';
-import { invokeAction } from 'ember-paper/utils/invoke-action';
+import Validation from '../lib/validation';
+import { A } from '@ember/array';
/**
* @class PaperInput
- * @extends Ember.Component
- * @uses FocusableMixin
- * @uses ChildMixin
- * @uses ValidationMixin
+ * @extends Component
*/
-export default Component.extend(FocusableMixin, ChildMixin, ValidationMixin, {
- tagName: 'md-input-container',
- classNames: ['md-default-theme'],
-
- classNameBindings: [
- 'hasValue:md-input-has-value',
- 'isInvalidAndTouched:md-input-invalid',
- 'hasLeftIcon:md-icon-left',
- 'hasRightIcon:md-icon-right',
- 'focused:md-input-focused',
- 'block:md-block',
- 'placeholder:md-input-has-placeholder',
- 'warn:md-warn',
- 'accent:md-accent',
- 'primary:md-primary',
- ],
-
- type: 'text',
- autofocus: false,
- tabindex: null,
- hideAllMessages: false,
- isTouched: false,
- iconComponent: 'paper-icon',
-
- // override validation mixin `isInvalid` to account for the native input validity
- isInvalid: or('hasErrorMessages', 'isNativeInvalid'),
-
- hasValue: computed('value', 'isNativeInvalid', function () {
- let value = this.value;
- let isNativeInvalid = this.isNativeInvalid;
- return !isEmpty(value) || isNativeInvalid;
- }),
-
- shouldAddPlaceholder: computed('label', 'focused', function () {
- // if has label, only add placeholder when focused
- return isEmpty(this.label) || this.focused;
- }),
-
- inputElementId: computed('elementId', {
- get() {
- return `input-${this.elementId}`;
- },
- // elementId can be set from outside and it will override the computed value.
- // Please check the deprecations for further details
- // https://deprecations.emberjs.com/v3.x/#toc_computed-property-override
- set(key, value) {
- // To make sure the context updates properly, We are manually set value using @ember/object#set as recommended.
- return set(this, 'elementId', value);
- },
- }),
-
- currentLength: computed('value', function () {
- return this.value ? this.value.length : 0;
- }),
+export default class PaperInput extends Focusable {
+ /**
+ * tracks the validity of an input.
+ *
+ * @type {Validation}
+ * @private
+ */
+ validation;
+
+ /**
+ * A unique id to identify the input element.
+ *
+ * @type{string}
+ * @readonly
+ */
+ inputElementId;
+ /**
+ * Stores a reference to the component's input element.
+ *
+ * @type {HTMLInputElement}
+ * @private
+ */
+ #inputElement;
+ /**
+ * The parent this component is bound to.
+ *
+ * @type {PaperRadioGroup|PaperForm|PaperItem|PaperTabs}
+ * @private
+ */
+ #parent;
+ /**
+ * Marks whether the component should register itself to the supplied parent.
+ *
+ * @type {Boolean}
+ * @public
+ */
+ shouldRegister;
+ /**
+ * Marks whether the component should skip being proxied.
+ *
+ * @type {Boolean}
+ * @public
+ */
+ skipProxy;
+ /**
+ * iconComponent specifies the icon component to use.
+ *
+ * @type {string}
+ * @readonly
+ * @default "paper-icon"
+ */
+ iconComponent;
+ /**
+ * type specifies the input's type. See {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types MDN}
+ * for a list of available input types.
+ *
+ * @type {string}
+ * @readonly
+ * @default "text"
+ */
+ type;
+ /**
+ * used to calculate how much to grow a textarea by.
+ *
+ * @type {number}
+ * @private
+ */
+ @tracked lineHeight;
+
+ /**
+ * @constructor
+ * @param owner
+ * @param args
+ */
+ constructor(owner, args) {
+ super(owner, args);
- hasLeftIcon: bool('icon'),
- hasRightIcon: bool('iconRight'),
- isInvalidAndTouched: and('isInvalid', 'isTouched'),
+ this.iconComponent = this.args.iconComponent || 'paper-icon';
+ this.type = this.args.type || 'text';
+ const elementId =
+ this.args.elementId || this.args.inputElementId || guidFor(this);
+ this.inputElementId = this.args.inputElementId || `input-${elementId}`;
+ this.lineHeight = this.args.lineHeight || null;
- // property that validations should be run on
- validationProperty: 'value',
+ // Construct Input Validation and pass through of custom attributes.
+ this.validation = new Validation(
+ elementId,
+ this.args.onValidityChange || null,
+ this.args.validations,
+ this.args.customValidations,
+ this.args.errors,
+ this.args.errorMessages,
+ this.args.isTouched
+ );
+
+ if (this.shouldRegister) {
+ assert(
+ 'A parent component should be supplied to
when shouldRegister=true',
+ this.args.parentComponent
+ );
+ this.#parent = this.args.parentComponent;
+ }
- // Lifecycle hooks
- didReceiveAttrs() {
- this._super(...arguments);
assert(
- '{{paper-input}} requires an `onChange` action or null for no action.',
- this.onChange !== undefined
+ ' requires an `onChange` action or null for no action.',
+ this.args.onChange !== undefined
);
+ }
+
+ /**
+ * Performs any required DOM setup.
+ *
+ * @param {HTMLElement} element - the node that has been added to the DOM.
+ */
+ @action didInsertNode(element) {
+ this.registerListeners(element);
+
+ let inputElement = element.querySelector('input, textarea');
+ this.#inputElement = inputElement;
+ this.validation.didInsertNode(inputElement);
+
+ // setValue ensures that the input value is the same as this.value
+ this.setValue(this.value);
+ this.growTextarea();
+
+ if (this.args.textarea) {
+ window.addEventListener('resize', this.growTextarea.bind(this));
+ }
- let { value, errors } = this;
- let { _prevValue, _prevErrors } = this;
- if (value !== _prevValue || errors !== _prevErrors) {
- this.notifyValidityChange();
+ if (this.shouldRegister) {
+ this.#parent.registerChild(this);
}
- this._prevValue = value;
- this._prevErrors = errors;
- },
-
- didInsertElement() {
- this._super(...arguments);
- if (this.textarea) {
- this._growTextareaOnResize = bind(this, this.growTextarea);
- window.addEventListener('resize', this._growTextareaOnResize);
+ }
+
+ /**
+ * didUpdateNode is called when tracked component attributes change.
+ */
+ @action didUpdateNode() {
+ if (this.args.errors) {
+ this.validation.errors = this.args.errors;
}
- },
- didRender() {
- this._super(...arguments);
- // setValue below ensures that the input value is the same as this.value
+ // setValue ensures that the input value is the same as this.value
this.setValue(this.value);
this.growTextarea();
- },
+ }
+
+ /**
+ * Performs any required DOM teardown.
+ *
+ * @param {HTMLElement} element - the node to be removed from the DOM.
+ */
+ @action willDestroyNode(element) {
+ this.unregisterListeners(element);
+
+ if (this.args.textarea) {
+ window.removeEventListener('resize', this.growTextarea.bind(this));
+ }
+ }
+
+ /**
+ * lifecycle hook to perform non-DOM related teardown.
+ */
+ willDestroy() {
+ super.willDestroy(...arguments);
+
+ if (this.shouldRegister) {
+ this.#parent.unregisterChild(this);
+ }
+ }
+
+ /**
+ * isBlock adds css class `md-block` which sets `display: block`.
+ *
+ * This indirection is required to maintain api compatibility as @block is
+ * reserved by glimmer.
+ *
+ * @returns {boolean}
+ */
+ get isBlock() {
+ return this.args.block || false;
+ }
+
+ /**
+ * This is a little bit of a hack to un-proxy the values in args so that we
+ * can track all changes. As users can define dynamic validations, we need to
+ * be able to account for random args coming in.
+ *
+ * @returns {Object}
+ */
+ get params() {
+ return { ...this.args };
+ }
+
+ /**
+ * returns an array of errors supplied as an argument to the component.
+ *
+ * @returns {A}
+ */
+ get errors() {
+ return this.args.errors || A([]);
+ }
+
+ /**
+ * value returns the value passed in to the component, or an empty string if
+ * undefined.
+ *
+ * @returns {string}
+ */
+ get value() {
+ return this.args.value || '';
+ }
- willDestroyElement() {
- this._super(...arguments);
- if (this.textarea) {
- window.removeEventListener('resize', this._growTextareaOnResize);
- this._growTextareaOnResize = null;
+ /**
+ * returns true if we have a non-empty value, or is considered natively
+ * invalid.
+ *
+ * @returns {boolean}
+ */
+ get hasValue() {
+ return !isEmpty(this.value) || this.validation.isNativeInvalid;
+ }
+
+ /**
+ * returns true if a label has been supplied, or if the input is focused.
+ *
+ * @returns {boolean}
+ */
+ get shouldAddPlaceholder() {
+ // if input has label, only add placeholder when focused
+ return isEmpty(this.args.label) || this.focused;
+ }
+
+ /**
+ * returns the current number of characters that {@link value} contains.
+ *
+ * @returns {number}
+ */
+ get currentLength() {
+ return this.value ? this.value.length : 0;
+ }
+
+ /**
+ * returns true if icon has been passed in to the component.
+ *
+ * @returns {boolean}
+ */
+ get hasLeftIcon() {
+ return !isEmpty(this.args.icon);
+ }
+
+ /**
+ * returns true if iconRight has been passed in to the component.
+ *
+ * @returns {boolean}
+ */
+ get hasRightIcon() {
+ return !isEmpty(this.args.iconRight);
+ }
+
+ /**
+ * minRows returns the user specified minimum number of rows.
+ *
+ * @returns {number}
+ * @default 0
+ */
+ get minRows() {
+ if (this.args.passThru && this.args.passThru.rows) {
+ return this.args.passThru.rows;
}
- },
+ return 0;
+ }
+
+ /**
+ * minRows returns the user specified maximum number of rows.
+ *
+ * @returns {number}
+ * @default Number.MAX_VALUE
+ */
+ get maxRows() {
+ if (this.args.passThru && this.args.passThru.maxRows) {
+ return this.args.passThru.maxRows;
+ }
+
+ return Number.MAX_VALUE;
+ }
+
+ /**
+ * calculates and grows a text area based on line-height.
+ */
growTextarea() {
- if (this.textarea) {
- let inputElement = this.element.querySelector('input, textarea');
+ if (this.args.textarea) {
+ let inputElement = this.#inputElement;
+
inputElement.classList.add('md-no-flex');
- inputElement.setAttribute('rows', 1);
+ inputElement.setAttribute('rows', '1');
- let minRows = this.get('passThru.rows');
+ let minRows = this.minRows;
let height = this.getHeight(inputElement);
if (minRows) {
- if (!this.lineHeight) {
- inputElement.style.minHeight = 0;
- this.lineHeight = inputElement.clientHeight;
+ let lineHeight = this.lineHeight;
+ if (!lineHeight) {
+ inputElement.style.minHeight = '0';
+ lineHeight = this.#inputElement.clientHeight;
inputElement.style.minHeight = null;
}
- if (this.lineHeight) {
- height = Math.max(height, this.lineHeight * minRows);
+ if (lineHeight) {
+ height = Math.max(height, lineHeight * minRows);
}
- let proposedHeight = Math.round(height / this.lineHeight);
- let maxRows = this.get('passThru.maxRows') || Number.MAX_VALUE;
- let rowsToSet = Math.min(proposedHeight, maxRows);
+ let proposedHeight = Math.round(height / lineHeight);
+ let maxRows = this.maxRows;
+ let rowsToSet = Math.min(proposedHeight, maxRows).toString();
- inputElement.style.height = `${this.lineHeight * rowsToSet}px`;
+ inputElement.style.height = `${lineHeight * rowsToSet}px`;
inputElement.setAttribute('rows', rowsToSet);
if (proposedHeight >= maxRows) {
@@ -152,6 +336,8 @@ export default Component.extend(FocusableMixin, ChildMixin, ValidationMixin, {
} else {
inputElement.classList.remove('md-textarea-scrollable');
}
+
+ this.lineHeight = lineHeight;
} else {
inputElement.style.height = 'auto';
inputElement.scrollTop = 0;
@@ -163,51 +349,72 @@ export default Component.extend(FocusableMixin, ChildMixin, ValidationMixin, {
inputElement.classList.remove('md-no-flex');
}
- },
+ }
+ /**
+ * returns the input elements current height.
+ *
+ * @param {HTMLInputElement} inputElement
+ * @returns {number}
+ */
getHeight(inputElement) {
let { offsetHeight } = inputElement;
let line = inputElement.scrollHeight - offsetHeight;
return offsetHeight + (line > 0 ? line : 0);
- },
+ }
+ /**
+ * pushes the given value into the input field.
+ *
+ * @param {*} value
+ */
setValue(value) {
// normalize falsy values to empty string
value = isEmpty(value) ? '' : value;
- if (this.element.querySelector('input, textarea').value !== value) {
- this.element.querySelector('input, textarea').value = value;
+ if (this.#inputElement.value !== value) {
+ this.#inputElement.value = value;
}
- },
-
- actions: {
- handleInput(e) {
- invokeAction(this, 'onChange', e.target.value);
- // setValue below ensures that the input value is the same as this.value
- next(() => {
- if (this.isDestroyed) {
- return;
- }
- this.setValue(this.value);
- });
- this.growTextarea();
- let inputElement = this.element.querySelector('input');
- let isNativeInvalid =
- inputElement && inputElement.validity && inputElement.validity.badInput;
- if (this.type === 'date' && e.target.value === '') {
- // Chrome doesn't fire the onInput event when clearing the second and third date components.
- // This means that we won't see another event when badInput becomes false if the user is clearing
- // the date field. The reported value is empty, though, so we can already mark it as valid.
- isNativeInvalid = false;
- }
- this.set('isNativeInvalid', isNativeInvalid);
- this.notifyValidityChange();
- },
-
- handleBlur(e) {
- invokeAction(this, 'onBlur', e);
- this.set('isTouched', true);
- this.notifyValidityChange();
- },
- },
-});
+
+ // Calculate Input Validity
+ this.validation.value = value;
+ this.validation.validate(this.args);
+ this.validation.notifyOnChange();
+ }
+
+ /**
+ * handleInput is called when input is received.
+ * Calls onChange if supplied.
+ *
+ * @param {Event} e - the input event.
+ */
+ @action handleInput(e) {
+ if (this.args.onChange) {
+ this.args.onChange(e.target.value);
+ }
+
+ if (this.isDestroyed) {
+ return;
+ }
+
+ // setValue below ensures that the input value is the same as this.value
+ this.setValue(this.value);
+ this.growTextarea();
+ }
+
+ /**
+ * handleBlur is called when the input element has lost focus.
+ * Calls onBlur if supplied.
+ *
+ * @param {Event} e - the input event.
+ */
+ @action handleBlur(e) {
+ if (this.args.onBlur) {
+ this.args.onBlur(e);
+ }
+
+ this.validation.isTouched = true;
+ this.validation.validate(this.args);
+ this.validation.notifyOnValidityChange();
+ }
+}
diff --git a/addon/lib/validation.js b/addon/lib/validation.js
new file mode 100644
index 000000000..72853780a
--- /dev/null
+++ b/addon/lib/validation.js
@@ -0,0 +1,387 @@
+import requiredValidator from 'ember-paper/validators/required';
+import minValidator from 'ember-paper/validators/min';
+import maxValidator from 'ember-paper/validators/max';
+import minlengthValidator from 'ember-paper/validators/minlength';
+import maxlengthValidator from 'ember-paper/validators/maxlength';
+import { isArray, A } from '@ember/array';
+import { assert, warn } from '@ember/debug';
+import { isBlank } from '@ember/utils';
+import { tracked } from '@glimmer/tracking';
+
+/**
+ * A validator provides a function that can validate the specified input.
+ *
+ * @typedef {Object} validator
+ * @property {string} param - the input attribute this is validating.
+ * @property {string} message - the error message to show to the end user if input is considered invalid.
+ * @property {function} validate - the function to be called to validate the param's value against the param.
+ */
+
+/**
+ * This callback is used to notify when the validity changes on the input field.
+ *
+ * @callback onValidityChange
+ * @param {boolean} isValid - whether the input element is considered valid.
+ * @param {string} elementId - The unique paper-input identifier.
+ */
+
+/**
+ * Validation provides input validation.
+ *
+ * @class Validation
+ */
+export default class Validation {
+ /**
+ * id is returned on notifying on validity to enable unique identification of
+ * the input that is in an invalid state.
+ *
+ * @type {string}
+ * @private
+ */
+ #id;
+ /**
+ * #inputElement stores a reference to the input element for updating the
+ * underlying value and checking the browser's native validity status.
+ *
+ * @type {HTMLInputElement}
+ * @private
+ */
+ #inputElement;
+ /**
+ *
+ * @type {onValidityChange|undefined}
+ * @private
+ */
+ #onValidityChange = null;
+ /**
+ * an array of validators to be run to ascertain the input's validity.
+ *
+ * @type {validator[]}
+ */
+ #validators = defaultValidations();
+
+ // State
+ /**
+ * Key-Value map of param to the message the user wants to override.
+ * For example, `@errorMessages={{hash required="Address is required."}}`
+ *
+ * @type {A}
+ */
+ #errorMessageOverrides = A();
+ /**
+ * #previousErrors stores the last state of errors to be able to track and
+ * notify on changes.
+ *
+ * @type {A}
+ * @private
+ */
+ #previousErrors;
+ /**
+ * previousIsTouched stores the last state of isTouched to be able to track
+ * and notify on changes.
+ *
+ * @type {boolean}
+ * @private
+ */
+ #previousIsTouched = undefined;
+ /**
+ * previousIsValid stores the last state of isValid to be able to track and
+ * notify on changes.
+ *
+ * @type {boolean}
+ * @private
+ */
+ #previousIsValid = undefined;
+ /**
+ * previousValue stores the last state of value to be able to track and
+ * notify on changes.
+ *
+ * @type {*}
+ * @private
+ */
+ #previousValue = '';
+
+ /**
+ * an array of the latest validation error messages, suitable for end user
+ * display.
+ *
+ * @type {A}
+ * @public
+ */
+ @tracked errorMessages = A();
+ /**
+ * An array of the latest validation errors.
+ * Can be overridden in order to supply your own external errors.
+ *
+ * @type {A}
+ * @public
+ */
+ @tracked errors = A();
+ /**
+ * tracks if the input has been touched.
+ * Useful for detecting when validation error messages should be displayed to
+ * the end user.
+ *
+ * @type {boolean}
+ * @public
+ */
+ @tracked isTouched = false;
+ /**
+ * value contains the datum to be validated.
+ *
+ * @type {any}
+ * @public
+ */
+ @tracked value = null;
+
+ /**
+ * @constructor
+ * @param {string} id - a unique identifier to identify the input element.
+ * @param {onValidityChange} onValidityChange - callback to notify when a change has occurred to the validity of a field.
+ * @param {validator[]|undefined} [validations] - validators to override the default set.
+ * @param {validator[]|undefined} [customValidations] - validators to append to the default set.
+ * @param {A|undefined} [errors] - input errors that should be reported.
+ * @param {A|undefined} [errorMessageOverrides] - validation error message overrides.
+ * @param {boolean|undefined} [isTouched] - a flag to mark the input as touched on creation.
+ */
+ constructor(
+ id,
+ onValidityChange,
+ validations,
+ customValidations,
+ errors,
+ errorMessageOverrides,
+ isTouched
+ ) {
+ this.#id = id;
+
+ if (validations) {
+ this.#validators = validations
+ .map((v) => v.param)
+ .filter((v) => !isBlank(v));
+ }
+ if (customValidations) {
+ assert(
+ '`customValidations` must be an array',
+ isArray(customValidations)
+ );
+ this.#validators.pushObjects(customValidations);
+ }
+ if (errorMessageOverrides) {
+ this.#errorMessageOverrides = errorMessageOverrides;
+ }
+ if (isTouched) {
+ this.isTouched = isTouched;
+ }
+ if (errors) {
+ this.errors = errors;
+ }
+
+ assert(
+ 'Validation requires an `onValidityChange` action or null for no action.',
+ onValidityChange !== undefined
+ );
+ this.#onValidityChange = onValidityChange;
+ }
+
+ /**
+ * didInsertNode provides a way to push in an input element to track.
+ *
+ * This allows the practitioner to construct the validator for availability on
+ * component creation, but where the node you need to track hasn't been
+ * inserted into the DOM yet.
+ *
+ * @param {HTMLInputElement} inputElement
+ */
+ didInsertNode(inputElement) {
+ this.#inputElement = inputElement;
+ }
+
+ /**
+ * true if validation errors have been found.
+ *
+ * @return {boolean}
+ */
+ get hasErrorMessages() {
+ return this.errorMessages.length > 0;
+ }
+
+ /**
+ * The result of isInvalid is appropriate for controlling the display of
+ * validation error messages. It also may be used to distinguish whether
+ * the input would be considered valid after it is touched.
+ *
+ * @public
+ * @return {boolean} Whether the input is or would be invalid.
+ * false: input is valid (touched or not), or is no longer rendered
+ * true: input has been touched and is invalid.
+ */
+ get isInvalid() {
+ return this.hasErrorMessages || this.isNativeInvalid;
+ }
+
+ /**
+ * returns true if the input element is considered valid.
+ *
+ * @return {boolean}
+ */
+ get isValid() {
+ return !this.isInvalid;
+ }
+
+ /**
+ * returns invalid based on the native input element's state of validity.
+ *
+ * @return {boolean}
+ */
+ get isNativeInvalid() {
+ let inputElement = this.#inputElement;
+ if (!inputElement) {
+ return false;
+ }
+
+ if (inputElement.type === 'date' && inputElement.value === '') {
+ // Chrome doesn't fire the onInput event when clearing the second and third date components.
+ // This means that we won't see another event when badInput becomes false if the user is clearing
+ // the date field. The reported value is empty, though, so we can already mark it as valid.
+ return false;
+ }
+
+ return (inputElement.validity && inputElement.validity.badInput) || false;
+ }
+
+ /**
+ * returns true if the input is considered invalid and has been touched.
+ *
+ * @return {boolean}
+ */
+ get isInvalidAndTouched() {
+ return this.isInvalid && this.isTouched;
+ }
+
+ /**
+ * validate executes configured validators and sets an array of errors and
+ * error messages for consumption.
+ *
+ * @param {Object} args - the values that are being supplied to the input.
+ */
+ validate(args) {
+ let messages = A();
+
+ let currentValue = this.value;
+ this.#validators.forEach((v) => {
+ assert(
+ 'validation must include a `validate(value)` function',
+ v && v.validate && typeof v.validate === 'function'
+ );
+ try {
+ let valParam = v.param;
+ let paramValue = valParam ? args[valParam] : undefined;
+ if (!v.validate(currentValue, paramValue)) {
+ let message = this.#errorMessageOverrides[valParam] || v.message;
+ messages.pushObject({
+ message: fmt(message.string || message, paramValue, currentValue),
+ });
+ }
+ } catch (error) {
+ warn(`Exception with validation: ${v} ${error}`, false);
+ }
+ });
+
+ // build the error messages array
+ let errors = this.errors || [];
+ assert('`errors` must be an array', isArray(errors));
+ messages.pushObjects(
+ errors.map((e) => {
+ return e.message ? e : { message: e };
+ })
+ );
+
+ this.errors = errors;
+ this.errorMessages = messages;
+ }
+
+ /**
+ * notifyOnChange will only notify if the value and errors have changed since
+ * last notification check.
+ *
+ * If values have changed, falls through to {@link notifyOnValidityChange}.
+ */
+ notifyOnChange() {
+ const previousErrors = this.#previousErrors;
+ const errors = this.errors;
+ const previousValue = this.#previousValue;
+ const value = this.value;
+ if (previousValue !== value || previousErrors !== errors) {
+ this.notifyOnValidityChange();
+
+ this.#previousErrors = errors;
+ this.#previousValue = value;
+ }
+ }
+
+ /**
+ * notifyOnValidityChange calls the provided onValidityChange callback with
+ * the value of valid and the id of the element to help track the validity of
+ * certain inputs.
+ */
+ notifyOnValidityChange() {
+ const prevIsValid = this.#previousIsValid;
+ const isValid = this.isValid;
+ const prevIsTouched = this.#previousIsTouched;
+ const isTouched = this.isTouched;
+ if (prevIsValid !== isValid || prevIsTouched !== isTouched) {
+ if (this.#onValidityChange) {
+ this.#onValidityChange(isValid, this.#id);
+ }
+
+ this.#previousIsValid = isValid;
+ this.#previousIsTouched = isTouched;
+ }
+ }
+}
+
+/**
+ * defaultValidations returns the default set of validations to run for an input.
+ *
+ * @return {validator[]}
+ */
+function defaultValidations() {
+ return A([
+ requiredValidator,
+ minValidator,
+ maxValidator,
+ minlengthValidator,
+ maxlengthValidator,
+ ]);
+}
+
+/**
+ * fmt provides an implementation of a format string that replaces %@ with the
+ * passed in arguments. It replaces usage of `loc` which was deprecated in
+ * `ember@v3.X`.
+ *
+ * @see https://github.com/emberjs/ember.js/blob/v3.22.2/packages/%40ember/string/index.ts
+ * @param {string} str - the format string.
+ * @param {string[]} formats - the ordered positional replacements.
+ * @returns {string}
+ */
+function fmt(str, formats) {
+ if (!Array.isArray(formats) || arguments.length > 2) {
+ formats = Array.prototype.slice.call(arguments, 1);
+ }
+
+ // first, replace any ORDERED replacements.
+ let idx = 0; // the current index for non-numerical replacements
+ return str.replace(/%@([0-9]+)?/g, (_s, argIndex) => {
+ let i = argIndex ? parseInt(argIndex, 10) - 1 : idx++;
+ let r = i < formats.length ? formats[i] : undefined;
+ return typeof r === 'string'
+ ? r
+ : r === null
+ ? '(null)'
+ : r === undefined
+ ? ''
+ : String(r);
+ });
+}