Skip to content

Commit

Permalink
ValidatableElement = input|select|textarea
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlbyk committed Jun 21, 2023
1 parent 0fd1700 commit 1394fe2
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 20 deletions.
55 changes: 40 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ const nullLogger = new (class implements Logger {
warn = globalThis.console.warn;
})();

/**
* An `HTMLElement` that can be validated (`input`, `select`, `textarea`).
*/
export type ValidatableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;

/**
* Checks if `element` is validatable (`input`, `select`, `textarea`).
* @param element The element to check.
* @returns `true` if validatable, otherwise `false`.
*/
export const isValidatable = (element: Node): element is ValidatableElement =>
element instanceof HTMLInputElement
|| element instanceof HTMLSelectElement
|| element instanceof HTMLTextAreaElement;

const validatableElementTypes = ['input', 'select', 'textarea'];

/**
* Generates a selector to match validatable elements (`input`, `select`, `textarea`).
* @param selector An optional selector to apply to the valid input types, e.g. `[data-val="true"]`.
* @returns The validatable elements.
*/
const validatableSelector = (selector?: string) =>
validatableElementTypes.map(t => `${t}${selector || ''}`).join(',');

/**
* Parameters passed into validation providers from the element attributes.
* error property is read from data-val-[Provider Name] attribute.
Expand All @@ -49,7 +74,7 @@ export type ValidationDirective = {
* String return signifies failed validation, which then will be used as the validation error message.
* Promise return signifies asynchronous plugin behavior, with same behavior as Boolean or String.
*/
export type ValidationProvider = (value: string, element: HTMLInputElement, params: StringKeyValuePair) => boolean | string | Promise<boolean | string>;
export type ValidationProvider = (value: string, element: ValidatableElement, params: StringKeyValuePair) => boolean | string | Promise<boolean | string>;

/**
* Callback to receive the result of validating a form.
Expand All @@ -66,7 +91,7 @@ type Validator = () => Promise<boolean>;
* @param element - The input to validate
* @param selector - Used to find the field. Ex. *.Password where * replaces whatever prefixes asp.net might add.
*/
function getRelativeFormElement(element: HTMLInputElement, selector: string) {
function getRelativeFormElement(element: ValidatableElement, selector: string): ValidatableElement {
// example elementName: Form.PasswordConfirm, Form.Email
// example selector (dafuq): *.Password, *.__RequestVerificationToken
// example result element name: Form.Password, __RequestVerificationToken
Expand All @@ -83,13 +108,13 @@ function getRelativeFormElement(element: HTMLInputElement, selector: string) {
// Form.Password
let relativeElementName = objectName + '.' + selectedName;
let relativeElement = document.getElementsByName(relativeElementName)[0];
if (relativeElement) {
if (isValidatable(relativeElement)) {
return relativeElement;
}
}

// __RequestVerificationToken
return element.form.querySelector(`[name=${selectedName}]`);
return element.form.querySelector(validatableSelector(`[name=${selectedName}]`));
}

/**
Expand Down Expand Up @@ -149,7 +174,7 @@ export class MvcValidationProviders {
return true;
}

let otherElement = getRelativeFormElement(element, params.other) as HTMLInputElement;
let otherElement = getRelativeFormElement(element, params.other);
if (!otherElement) {
return true;
}
Expand Down Expand Up @@ -309,7 +334,7 @@ export class MvcValidationProviders {

for (let fieldSelector of fieldSelectors) {
let fieldName = fieldSelector.substr(2);
let fieldElement = getRelativeFormElement(element, fieldSelector) as HTMLInputElement;
let fieldElement = getRelativeFormElement(element, fieldSelector);

let hasValue = Boolean(fieldElement && fieldElement.value);
if (!hasValue) {
Expand Down Expand Up @@ -622,7 +647,7 @@ export class ValidationService {
}

// Retrieves the validation span for the input.
private getMessageFor(input: HTMLInputElement) {
private getMessageFor(input: ValidatableElement) {
let formId = this.getElementUID(input.form);
let name = `${formId}:${input.name}`;
return this.messageFor[name];
Expand Down Expand Up @@ -819,7 +844,7 @@ export class ValidationService {
let uids = this.formInputs[formUID];

for (let uid of uids) {
let input = this.elementByUID[uid] as HTMLInputElement;
let input = this.elementByUID[uid] as ValidatableElement;
if (input.classList.contains(this.ValidationInputCssClassName)) {
input.classList.remove(this.ValidationInputCssClassName);
}
Expand All @@ -846,7 +871,7 @@ export class ValidationService {
* Triggers a debounced live validation when input value changes.
* @param input
*/
addInput(input: HTMLInputElement) {
addInput(input: ValidatableElement) {
let uid = this.getElementUID(input);

let directives = this.parseDirectives(input.attributes);
Expand Down Expand Up @@ -885,16 +910,16 @@ export class ValidationService {
* Scans the entire document for input elements to be validated.
*/
private scanInputs(root: ParentNode) {
let inputs = Array.from(root.querySelectorAll<HTMLElement>('[data-val="true"]'));
let inputs = Array.from(root.querySelectorAll<ValidatableElement>(validatableSelector('[data-val="true"]')));

// querySelectorAll does not include the root element itself.
// we could use 'matches', but that's newer than querySelectorAll so we'll keep it simple and compatible.
if (root instanceof HTMLElement && root.getAttribute("data-val") === "true") {
if (isValidatable(root) && root.getAttribute("data-val") === "true") {
inputs.push(root);
}

for (let i = 0; i < inputs.length; i++) {
let input = inputs[i] as HTMLInputElement;
let input = inputs[i];
this.addInput(input);
}
}
Expand Down Expand Up @@ -956,7 +981,7 @@ export class ValidationService {
* @param input
* @param message
*/
addError(input: HTMLInputElement, message: string) {
addError(input: ValidatableElement, message: string) {
let spans = this.getMessageFor(input);
if (spans) {
for (let i = 0; i < spans.length; i++) {
Expand Down Expand Up @@ -989,7 +1014,7 @@ export class ValidationService {
* Removes an error message from an input element, which also updates the validation message elements and validation summary elements.
* @param input
*/
removeError(input: HTMLInputElement) {
removeError(input: ValidatableElement) {
let spans = this.getMessageFor(input);
if (spans) {
for (let i = 0; i < spans.length; i++) {
Expand Down Expand Up @@ -1022,7 +1047,7 @@ export class ValidationService {
* @param input
* @param directives
*/
createValidator(input: HTMLInputElement, directives: ValidationDirective) {
createValidator(input: ValidatableElement, directives: ValidationDirective) {
return async () => {

// only validate visible fields
Expand Down
20 changes: 15 additions & 5 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ export interface Logger {
log(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
}
/**
* An `HTMLElement` that can be validated (`input`, `select`, `textarea`).
*/
export type ValidatableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
/**
* Checks if `element` is validatable (`input`, `select`, `textarea`).
* @param element The element to check.
* @returns `true` if validatable, otherwise `false`.
*/
export declare const isValidatable: (element: Node) => element is ValidatableElement;
/**
* Parameters passed into validation providers from the element attributes.
* error property is read from data-val-[Provider Name] attribute.
Expand All @@ -32,7 +42,7 @@ export type ValidationDirective = {
* String return signifies failed validation, which then will be used as the validation error message.
* Promise return signifies asynchronous plugin behavior, with same behavior as Boolean or String.
*/
export type ValidationProvider = (value: string, element: HTMLInputElement, params: StringKeyValuePair) => boolean | string | Promise<boolean | string>;
export type ValidationProvider = (value: string, element: ValidatableElement, params: StringKeyValuePair) => boolean | string | Promise<boolean | string>;
/**
* Callback to receive the result of validating a form.
*/
Expand Down Expand Up @@ -237,7 +247,7 @@ export declare class ValidationService {
* Triggers a debounced live validation when input value changes.
* @param input
*/
addInput(input: HTMLInputElement): void;
addInput(input: ValidatableElement): void;
/**
* Scans the entire document for input elements to be validated.
*/
Expand All @@ -255,18 +265,18 @@ export declare class ValidationService {
* @param input
* @param message
*/
addError(input: HTMLInputElement, message: string): void;
addError(input: ValidatableElement, message: string): void;
/**
* Removes an error message from an input element, which also updates the validation message elements and validation summary elements.
* @param input
*/
removeError(input: HTMLInputElement): void;
removeError(input: ValidatableElement): void;
/**
* Returns a validation Promise factory for an input element, using given validation directives.
* @param input
* @param directives
*/
createValidator(input: HTMLInputElement, directives: ValidationDirective): () => Promise<boolean>;
createValidator(input: ValidatableElement, directives: ValidationDirective): () => Promise<boolean>;
/**
* Checks if the provided input is hidden from the browser
* @param input
Expand Down

0 comments on commit 1394fe2

Please sign in to comment.