Skip to content

Commit

Permalink
Merge pull request #45 from haacked/ParentNode
Browse files Browse the repository at this point in the history
Bootstrap/Scan/Watch `ParentNode`
  • Loading branch information
haacked authored Jun 21, 2023
2 parents f74388e + ea86e21 commit 3bdd41f
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 45 deletions.
6 changes: 6 additions & 0 deletions Pages/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
<span asp-validation-for="Control"></span>
</div>

<div class="form-field">
<label asp-for="TextArea"></label>
<textarea asp-for="TextArea"></textarea>
<span asp-validation-for="TextArea"></span>
</div>

<div class="form-field">
@foreach (var animal in Model.Animals)
{
Expand Down
4 changes: 4 additions & 0 deletions Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public class IndexModel : PageModel
[Required]
public string? Control { get; set; }

[BindProperty]
[Required]
public string? TextArea { get; set; }

[BindProperty]
[Required]
public List<string>? SelectedAnimals { get; set; }
Expand Down
36 changes: 28 additions & 8 deletions dist/aspnet-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ return /******/ (function(modules) { // webpackBootstrap
/*!**********************!*\
!*** ./src/index.ts ***!
\**********************/
/*! exports provided: MvcValidationProviders, ValidationService */
/*! exports provided: isValidatable, MvcValidationProviders, ValidationService */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isValidatable", function() { return isValidatable; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MvcValidationProviders", function() { return MvcValidationProviders; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ValidationService", function() { return ValidationService; });
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
Expand Down Expand Up @@ -155,6 +156,25 @@ var nullLogger = new (/** @class */ (function () {
};
return class_1;
}()))();
/**
* Checks if `element` is validatable (`input`, `select`, `textarea`).
* @param element The element to check.
* @returns `true` if validatable, otherwise `false`.
*/
var isValidatable = function (element) {
return element instanceof HTMLInputElement
|| element instanceof HTMLSelectElement
|| element instanceof HTMLTextAreaElement;
};
var 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.
*/
var validatableSelector = function (selector) {
return validatableElementTypes.map(function (t) { return "".concat(t).concat(selector || ''); }).join(',');
};
/**
* Resolves and returns the element referred by original element using ASP.NET selector logic.
* @param element - The input to validate
Expand All @@ -165,21 +185,21 @@ function getRelativeFormElement(element, selector) {
// example selector (dafuq): *.Password, *.__RequestVerificationToken
// example result element name: Form.Password, __RequestVerificationToken
var elementName = element.name;
var realSelector = selector.substring(2); // Password, __RequestVerificationToken
var selectedName = selector.substring(2); // Password, __RequestVerificationToken
var objectName = '';
var dotLocation = elementName.lastIndexOf('.');
if (dotLocation > -1) {
// Form
objectName = elementName.substring(0, dotLocation);
// Form.Password
var relativeElementName = objectName + '.' + realSelector;
var relativeElementName = objectName + '.' + selectedName;
var relativeElement = document.getElementsByName(relativeElementName)[0];
if (relativeElement) {
if (isValidatable(relativeElement)) {
return relativeElement;
}
}
// __RequestVerificationToken
return element.form.querySelector("[name=".concat(realSelector, "]"));
return element.form.querySelector(validatableSelector("[name=".concat(selectedName, "]")));
}
/**
* Contains default implementations for ASP.NET Core MVC validation attributes.
Expand Down Expand Up @@ -649,7 +669,7 @@ var ValidationService = /** @class */ (function () {
}
// Otherwise if a validation message span is inside a form, we group the span with the form it's inside.
var forms = Array.from(root.querySelectorAll('form'));
if (root.tagName === 'form') {
if (root instanceof HTMLFormElement) {
// 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.
forms.push(root);
Expand Down Expand Up @@ -893,10 +913,10 @@ var ValidationService = /** @class */ (function () {
* Scans the entire document for input elements to be validated.
*/
ValidationService.prototype.scanInputs = function (root) {
var inputs = Array.from(root.querySelectorAll('[data-val="true"]'));
var inputs = Array.from(root.querySelectorAll(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.getAttribute("data-val") === "true") {
if (isValidatable(root) && root.getAttribute("data-val") === "true") {
inputs.push(root);
}
for (var i = 0; i < inputs.length; i++) {
Expand Down
2 changes: 1 addition & 1 deletion dist/aspnet-validation.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/aspnet-validation.min.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aspnet-client-validation",
"version": "0.8.9",
"version": "0.8.10",
"description": "Enables ASP.NET MVC client-side validation, without jQuery!",
"main": "dist/aspnet-validation.js",
"style": "dist/aspnet-validation.css",
Expand Down
73 changes: 49 additions & 24 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,13 +91,13 @@ 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

let elementName = element.name;
let realSelector = selector.substring(2); // Password, __RequestVerificationToken
let selectedName = selector.substring(2); // Password, __RequestVerificationToken
let objectName = '';

let dotLocation = elementName.lastIndexOf('.');
Expand All @@ -81,15 +106,15 @@ function getRelativeFormElement(element: HTMLInputElement, selector: string) {
objectName = elementName.substring(0, dotLocation);

// Form.Password
let relativeElementName = objectName + '.' + realSelector;
let relativeElementName = objectName + '.' + selectedName;
let relativeElement = document.getElementsByName(relativeElementName)[0];
if (relativeElement) {
if (isValidatable(relativeElement)) {
return relativeElement;
}
}

// __RequestVerificationToken
return element.form.querySelector(`[name=${realSelector}]`);
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 @@ -483,7 +508,7 @@ export class ValidationService {
/**
* Scans document for all validation message <span> generated by ASP.NET Core MVC, then tracks them.
*/
private scanMessages(root: HTMLElement) {
private scanMessages(root: ParentNode) {
/* If a validation span explicitly declares a form, we group the span with that form. */
let validationMessageElements = Array.from(root.querySelectorAll<HTMLElement>('span[form]'));
for (let span of validationMessageElements) {
Expand All @@ -494,8 +519,8 @@ export class ValidationService {
}

// Otherwise if a validation message span is inside a form, we group the span with the form it's inside.
let forms = Array.from(root.querySelectorAll<HTMLElement>('form'));
if (root.tagName === 'form') {
let forms = Array.from(root.querySelectorAll<HTMLFormElement>('form'));
if (root instanceof HTMLFormElement) {
// 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.
forms.push(root);
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 @@ -884,17 +909,17 @@ export class ValidationService {
/**
* Scans the entire document for input elements to be validated.
*/
private scanInputs(root: HTMLElement) {
let inputs = Array.from(root.querySelectorAll<HTMLElement>('[data-val="true"]'));
private scanInputs(root: ParentNode) {
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.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 Expand Up @@ -1097,7 +1122,7 @@ export class ValidationService {
* Load default validation providers and scans the entire document when ready.
* @param options.watch If set to true, a MutationObserver will be used to continuously watch for new elements that provide validation directives.
*/
bootstrap(options?: { watch?: boolean, root?: HTMLElement }) {
bootstrap(options?: { watch?: boolean, root?: ParentNode }) {
options = options || {};

this.addMvcProviders();
Expand Down Expand Up @@ -1125,7 +1150,7 @@ export class ValidationService {
/**
* Scans the provided root element for any validation directives and attaches behavior to them.
*/
scan(root: HTMLElement) {
scan(root: ParentNode) {
this.logger.log('Scanning', root);
this.scanMessages(root);
this.scanInputs(root);
Expand All @@ -1135,7 +1160,7 @@ export class ValidationService {
* Watches the provided root element for mutations, and scans for new validation directives to attach behavior.
* @param root The root element to use, defaults to the document.documentElement.
*/
watch(root: HTMLElement) {
watch(root: ParentNode) {
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
this.observed(mutation);
Expand Down
Loading

0 comments on commit 3bdd41f

Please sign in to comment.