From fdefe451d5c170d7b2dc6ee8fc562ecd91bd6f73 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 15 Mar 2022 12:21:22 +0100 Subject: [PATCH 1/3] Adds support for the formnovalidate attribute to skip validation on back buttons in multi-page forms. --- README.MD | 4 +-- package.json | 2 +- src/index.ts | 89 +++++++++++++++++++++++++++--------------------- types/index.d.ts | 5 +++ 4 files changed, 59 insertions(+), 41 deletions(-) diff --git a/README.MD b/README.MD index 0c77266..3d500bd 100644 --- a/README.MD +++ b/README.MD @@ -76,7 +76,7 @@ v.bootstrap(); ```powershell git clone https://github.com/haacked/aspnet-client-validation.git npm install -script/build # If using PowerShell: script/build.ps1 +script/scripts/build # If using PowerShell: script/scripts/build.ps1 ``` ## Adding Custom Validation @@ -134,7 +134,7 @@ v.addProvider('classicmovie', (value, element, params) => { // Let [Required] handle validation error for empty input... return true; } - + // Unlike the original, data-val-classicmovie-year is bound automatically to params['year'] as string! let year = parseInt(params.year); let date = new Date(value); diff --git a/package.json b/package.json index 6df79e1..7fa2f1a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "author": "Ryan Elian, Phil Haack", "license": "MIT", "scripts": { - "build": "mkdir -p dist && cp src/aspnet-validation.css dist && webpack && webpack --config webpack.config.min.js" + "build": "(if not exist dist mkdir dist) && cp src/aspnet-validation.css dist && webpack && webpack --config webpack.config.min.js" }, "devDependencies": { "ts-loader": "^5.3.1", diff --git a/src/index.ts b/src/index.ts index 43f0572..72e220b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,7 @@ type Validator = () => Promise; /** * Resolves and returns the element referred by original element using ASP.NET selector logic. - * @param elementName + * @param elementName */ function getRelativeFormElement(elementName: string, selector: string) { // example elementName: Form.PasswordConfirm, Form.Email @@ -235,13 +235,13 @@ export class MvcValidationProviders { if (!value) { return true; } - + let lowerCaseValue = value.toLowerCase(); // Match the logic in `UrlAttribute` return lowerCaseValue.indexOf('http://') > -1 || lowerCaseValue.indexOf('https://') > -1 - || lowerCaseValue.indexOf('ftp://') > -1; + || lowerCaseValue.indexOf('ftp://') > -1; } /** @@ -263,7 +263,7 @@ export class MvcValidationProviders { } /** - * Asynchronously validates the input value to a JSON GET API endpoint. + * Asynchronously validates the input value to a JSON GET API endpoint. */ remote: ValidationProvider = (value, element, params) => { if (!value) { @@ -342,7 +342,7 @@ export class MvcValidationProviders { */ export class ValidationService { /** - * A key-value collection of loaded validation plugins. + * A key-value collection of loaded validation plugins. */ private providers: { [name: string]: ValidationProvider } = {}; @@ -357,7 +357,7 @@ export class ValidationService { private elementUIDs: ElementUID[] = []; /** - * A key-value collection of UID to Element for quick lookup. + * A key-value collection of UID to Element for quick lookup. */ private elementByUID: { [uid: string]: Element } = {}; @@ -394,8 +394,8 @@ export class ValidationService { /** * Registers a new validation plugin of the given name, if not registered yet. * Registered plugin validates inputs with data-val-[name] attribute, used as error message. - * @param name - * @param callback + * @param name + * @param callback */ addProvider(name: string, callback: ValidationProvider) { if (this.providers[name]) { @@ -407,7 +407,7 @@ export class ValidationService { } /** - * Registers the default providers for enabling ASP.NET Core MVC client-side validation. + * Registers the default providers for enabling ASP.NET Core MVC client-side validation. */ private addMvcProviders() { let mvc = new MvcValidationProviders(); @@ -455,8 +455,8 @@ export class ValidationService { } /** - * Given attribute map for an HTML input, returns the validation directives to be executed. - * @param attributes + * Given attribute map for an HTML input, returns the validation directives to be executed. + * @param attributes */ parseDirectives(attributes: NamedNodeMap) { let directives: ValidationDirective = {}; @@ -513,7 +513,7 @@ export class ValidationService { /** * Gets a UID for an DOM element. - * @param node + * @param node */ private getElementUID(node: Element) { let x = this.elementUIDs.filter(e => { @@ -535,7 +535,7 @@ export class ValidationService { /** * Returns a Promise that returns validation result for each and every inputs within the form. - * @param formUID + * @param formUID */ private getFormValidationTask(formUID: string) { let formInputUIDs = this.formInputs[formUID]; @@ -556,12 +556,12 @@ export class ValidationService { /** * Fires off validation for elements within the provided form and then calls the callback - * @param form - * @param callback + * @param form + * @param callback */ validateForm = (form: HTMLFormElement, callback: Function) => { let formUID = this.getElementUID(form); - let formValidationEvent = this.elementEvents[formUID]; + let formValidationEvent = this.elementEvents[formUID]; if (formValidationEvent) { formValidationEvent(null, callback); } @@ -587,10 +587,10 @@ export class ValidationService { /** * Returns true if the provided form is valid, and then calls the callback. The form will be validated before checking, unless prevalidate is set to false - * @param form - * @param prevalidate - * @param callback - * @returns + * @param form + * @param prevalidate + * @param callback + * @returns */ isValid = (form: HTMLFormElement, prevalidate: boolean = true, callback: Function) => { if (prevalidate) { @@ -604,13 +604,13 @@ export class ValidationService { /** * Returns true if the provided field is valid, and then calls the callback. The form will be validated before checking, unless prevalidate is set to false - * @param form - * @param prevalidate - * @param callback - * @returns + * @param form + * @param prevalidate + * @param callback + * @returns */ isFieldValid = (field: HTMLElement, prevalidate: boolean = true, callback: Function) => { - + if (prevalidate) { let form = field.closest("form"); if (form != null) { @@ -622,10 +622,19 @@ export class ValidationService { return this.summary[fieldUID] != null; } + /** + * Returns true if the event triggering the form submission indicates we should validate the form. + * @param e + */ + private shouldValidate(e: Event) { + // Skip client-side validation if the form has been submitted via a button that has the "formnovalidate" attribute. + return !(e['submitter'] && e['submitter']['formNoValidate']); + } + /** * Tracks a
element as parent of an input UID. When the form is submitted, attempts to validate the said input asynchronously. - * @param form - * @param inputUID + * @param form + * @param inputUID */ private trackFormInput(form: HTMLFormElement, inputUID: string) { let formUID = this.getElementUID(form); @@ -642,6 +651,10 @@ export class ValidationService { } let cb = (e: Event, callback?: Function) => { + if (!this.shouldValidate(e)) { + return; + } + let validate = this.getFormValidationTask(formUID); if (!validate) { return; @@ -666,14 +679,14 @@ export class ValidationService { e.preventDefault(); e.stopImmediatePropagation(); } - + const validationEvent = new CustomEvent('validation', { detail: { valid: false } }); form.dispatchEvent(validationEvent); - - + + if (isProgrammaticValidate) { callback(false); } @@ -711,7 +724,7 @@ export class ValidationService { /** * Adds an input element to be managed and validated by the service. * Triggers a debounced live validation when input value changes. - * @param input + * @param input */ addInput(input: HTMLInputElement) { let uid = this.getElementUID(input); @@ -812,8 +825,8 @@ export class ValidationService { /** * Adds an error message to an input element, which also updates the validation message elements and validation summary elements. - * @param input - * @param message + * @param input + * @param message */ addError(input: HTMLInputElement, message: string) { let spans = this.messageFor[input.name]; @@ -834,7 +847,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 + * @param input */ removeError(input: HTMLInputElement) { let spans = this.messageFor[input.name]; @@ -854,9 +867,9 @@ export class ValidationService { } /** - * Returns a validation Promise factory for an input element, using given validation directives. - * @param input - * @param directives + * Returns a validation Promise factory for an input element, using given validation directives. + * @param input + * @param directives */ createValidator(input: HTMLInputElement, directives: ValidationDirective) { return async () => { @@ -908,7 +921,7 @@ export class ValidationService { /** * Checks if the provided input is hidden from the browser * @param input - * @returns + * @returns */ private isHidden(input: HTMLElement) { return !( input.offsetWidth || input.offsetHeight || input.getClientRects().length ); diff --git a/types/index.d.ts b/types/index.d.ts index fa72913..1b14c9a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -176,6 +176,11 @@ export declare class ValidationService { * @returns */ isFieldValid: (field: HTMLElement, prevalidate: boolean, callback: Function) => boolean; + /** + * Returns true if the event triggering the form submission indicates we should validate the form. + * @param e + */ + private shouldValidate; /** * Tracks a element as parent of an input UID. When the form is submitted, attempts to validate the said input asynchronously. * @param form From 6e2aa3d46ce94fdc8ff14ce8dc0914fe627e9d0f Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 15 Mar 2022 12:23:15 -0700 Subject: [PATCH 2/3] Fix build --- README.MD | 2 +- dist/aspnet-validation.js | 11 + dist/aspnet-validation.min.js | 2 +- dist/aspnet-validation.min.js.map | 2 +- package-lock.json | 4368 ++++++++++++++++++++++- package.json | 2 +- script/{scripts => }/build | 0 script/{scripts => }/build.ps1 | 0 yarn.lock | 5373 ++++++++++++++--------------- 9 files changed, 6942 insertions(+), 2818 deletions(-) rename script/{scripts => }/build (100%) rename script/{scripts => }/build.ps1 (100%) diff --git a/README.MD b/README.MD index 3d500bd..8ef5580 100644 --- a/README.MD +++ b/README.MD @@ -76,7 +76,7 @@ v.bootstrap(); ```powershell git clone https://github.com/haacked/aspnet-client-validation.git npm install -script/scripts/build # If using PowerShell: script/scripts/build.ps1 +script/build # If using PowerShell: script/build.ps1 ``` ## Adding Custom Validation diff --git a/dist/aspnet-validation.js b/dist/aspnet-validation.js index b148289..b4315c8 100644 --- a/dist/aspnet-validation.js +++ b/dist/aspnet-validation.js @@ -639,6 +639,14 @@ var ValidationService = /** @class */ (function () { var tasks = formValidators.map(function (factory) { return factory(); }); return Promise.all(tasks).then(function (result) { return result.every(function (e) { return e; }); }); }; + /** + * Returns true if the event triggering the form submission indicates we should validate the form. + * @param e + */ + ValidationService.prototype.shouldValidate = function (e) { + // Skip client-side validation if the form has been submitted via a button that has the "formnovalidate" attribute. + return !(e['submitter'] && e['submitter']['formNoValidate']); + }; /** * Tracks a element as parent of an input UID. When the form is submitted, attempts to validate the said input asynchronously. * @param form @@ -658,6 +666,9 @@ var ValidationService = /** @class */ (function () { return; } var cb = function (e, callback) { + if (!_this.shouldValidate(e)) { + return; + } var validate = _this.getFormValidationTask(formUID); if (!validate) { return; diff --git a/dist/aspnet-validation.min.js b/dist/aspnet-validation.min.js index 72a3cf9..7fcdcf3 100644 --- a/dist/aspnet-validation.min.js +++ b/dist/aspnet-validation.min.js @@ -1,2 +1,2 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.aspnetValidation=t():e.aspnetValidation=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(n,i,function(t){return e[t]}.bind(null,i));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";r.r(t),r.d(t,"MvcValidationProviders",(function(){return o})),r.d(t,"ValidationService",(function(){return s}));var n=function(e,t,r,n){return new(r||(r=Promise))((function(i,a){function o(e){try{u(n.next(e))}catch(e){a(e)}}function s(e){try{u(n.throw(e))}catch(e){a(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(o,s)}u((n=n.apply(e,t||[])).next())}))},i=function(e,t){var r,n,i,a,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(a){return function(s){return function(a){if(r)throw new TypeError("Generator is already executing.");for(;o;)try{if(r=1,n&&(i=2&a[0]?n.return:a[0]?n.throw||((i=n.return)&&i.call(n),0):n.next)&&!(i=i.call(n,a[1])).done)return i;switch(n=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,n=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!(i=o.trys,(i=i.length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]-1){var i=e.substr(0,n)+"."+r,a=document.getElementsByName(i)[0];if(a)return a}return document.getElementsByName(r)[0]}var o=function(){this.required=function(e,t,r){return Boolean(e)},this.stringLength=function(e,t,r){if(!e)return!0;if(r.min){var n=parseInt(r.min);if(e.lengthi)return!1}return!0},this.compare=function(e,t,r){if(!r.other)return!0;var n=a(t.name,r.other);return!n||n.value===e},this.range=function(e,t,r){if(!e)return!0;var n=parseFloat(e);return!isNaN(n)&&(!(r.min&&nparseFloat(r.max)))},this.regex=function(e,t,r){return!e||!r.pattern||new RegExp(r.pattern).test(e)},this.email=function(e,t,r){return!e||/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$/.test(e)},this.creditcard=function(e,t,r){if(!e)return!0;if(/[^0-9 \-]+/.test(e))return!1;var n,i,a=0,o=0,s=!1;if((e=e.replace(/\D/g,"")).length<13||e.length>19)return!1;for(n=e.length-1;n>=0;n--)i=e.charAt(n),o=parseInt(i,10),s&&(o*=2)>9&&(o-=9),a+=o,s=!s;return a%10==0},this.url=function(e,t,r){if(!e)return!0;var n=e.toLowerCase();return n.indexOf("http://")>-1||n.indexOf("https://")>-1||n.indexOf("ftp://")>-1},this.phone=function(e,t,r){return!e||!/[\+\-\s][\-\s]/g.test(e)&&/^\+?[0-9\-\s]+$/.test(e)},this.remote=function(e,t,r){if(!e)return!0;for(var n=r.additionalfields.split(","),i={},o=0,s=n;o=200&&n.status<300){var i=JSON.parse(n.responseText);e(i)}else t({status:n.status,statusText:n.statusText,data:n.responseText})},n.onerror=function(e){t({status:n.status,statusText:n.statusText,data:n.responseText})}}))}},s=function(){function e(){var e=this;this.providers={},this.messageFor={},this.elementUIDs=[],this.elementByUID={},this.formInputs={},this.validators={},this.elementEvents={},this.summary={},this.debounce=300,this.validateForm=function(t,r){var n=e.getElementUID(t),i=e.elementEvents[n];i&&i(null,r)},this.focusFirstInvalid=function(t){var r=e.getElementUID(t),n=e.formInputs[r].filter((function(t){return e.summary[t]}));if(n.length>0){var i=e.elementByUID[n[0]];i&&i.focus()}},this.isValid=function(t,r,n){void 0===r&&(r=!0),r&&e.validateForm(t,n);var i=e.getElementUID(t);return 0==e.formInputs[i].filter((function(t){return e.summary[t]})).length},this.isFieldValid=function(t,r,n){if(void 0===r&&(r=!0),r){var i=t.closest("form");null!=i&&e.validateForm(i,n)}var a=e.getElementUID(t);return null!=e.summary[a]}}return e.prototype.addProvider=function(e,t){this.providers[e]||(this.providers[e]=t)},e.prototype.addMvcProviders=function(){var e=new o;this.addProvider("required",e.required),this.addProvider("length",e.stringLength),this.addProvider("maxlength",e.stringLength),this.addProvider("minlength",e.stringLength),this.addProvider("equalto",e.compare),this.addProvider("range",e.range),this.addProvider("regex",e.regex),this.addProvider("creditcard",e.creditcard),this.addProvider("email",e.email),this.addProvider("url",e.url),this.addProvider("phone",e.phone),this.addProvider("remote",e.remote)},e.prototype.scanMessages=function(){for(var e=document.querySelectorAll("[data-valmsg-for]"),t=0;t0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]-1){var i=e.substr(0,n)+"."+r,a=document.getElementsByName(i)[0];if(a)return a}return document.getElementsByName(r)[0]}var o=function(){this.required=function(e,t,r){return Boolean(e)},this.stringLength=function(e,t,r){if(!e)return!0;if(r.min){var n=parseInt(r.min);if(e.lengthi)return!1}return!0},this.compare=function(e,t,r){if(!r.other)return!0;var n=a(t.name,r.other);return!n||n.value===e},this.range=function(e,t,r){if(!e)return!0;var n=parseFloat(e);return!isNaN(n)&&(!(r.min&&nparseFloat(r.max)))},this.regex=function(e,t,r){return!e||!r.pattern||new RegExp(r.pattern).test(e)},this.email=function(e,t,r){return!e||/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$/.test(e)},this.creditcard=function(e,t,r){if(!e)return!0;if(/[^0-9 \-]+/.test(e))return!1;var n,i,a=0,o=0,s=!1;if((e=e.replace(/\D/g,"")).length<13||e.length>19)return!1;for(n=e.length-1;n>=0;n--)i=e.charAt(n),o=parseInt(i,10),s&&(o*=2)>9&&(o-=9),a+=o,s=!s;return a%10==0},this.url=function(e,t,r){if(!e)return!0;var n=e.toLowerCase();return n.indexOf("http://")>-1||n.indexOf("https://")>-1||n.indexOf("ftp://")>-1},this.phone=function(e,t,r){return!e||!/[\+\-\s][\-\s]/g.test(e)&&/^\+?[0-9\-\s]+$/.test(e)},this.remote=function(e,t,r){if(!e)return!0;for(var n=r.additionalfields.split(","),i={},o=0,s=n;o=200&&n.status<300){var i=JSON.parse(n.responseText);e(i)}else t({status:n.status,statusText:n.statusText,data:n.responseText})},n.onerror=function(e){t({status:n.status,statusText:n.statusText,data:n.responseText})}}))}},s=function(){function e(){var e=this;this.providers={},this.messageFor={},this.elementUIDs=[],this.elementByUID={},this.formInputs={},this.validators={},this.elementEvents={},this.summary={},this.debounce=300,this.validateForm=function(t,r){var n=e.getElementUID(t),i=e.elementEvents[n];i&&i(null,r)},this.focusFirstInvalid=function(t){var r=e.getElementUID(t),n=e.formInputs[r].filter((function(t){return e.summary[t]}));if(n.length>0){var i=e.elementByUID[n[0]];i&&i.focus()}},this.isValid=function(t,r,n){void 0===r&&(r=!0),r&&e.validateForm(t,n);var i=e.getElementUID(t);return 0==e.formInputs[i].filter((function(t){return e.summary[t]})).length},this.isFieldValid=function(t,r,n){if(void 0===r&&(r=!0),r){var i=t.closest("form");null!=i&&e.validateForm(i,n)}var a=e.getElementUID(t);return null!=e.summary[a]}}return e.prototype.addProvider=function(e,t){this.providers[e]||(this.providers[e]=t)},e.prototype.addMvcProviders=function(){var e=new o;this.addProvider("required",e.required),this.addProvider("length",e.stringLength),this.addProvider("maxlength",e.stringLength),this.addProvider("minlength",e.stringLength),this.addProvider("equalto",e.compare),this.addProvider("range",e.range),this.addProvider("regex",e.regex),this.addProvider("creditcard",e.creditcard),this.addProvider("email",e.email),this.addProvider("url",e.url),this.addProvider("phone",e.phone),this.addProvider("remote",e.remote)},e.prototype.scanMessages=function(){for(var e=document.querySelectorAll("[data-valmsg-for]"),t=0;t\n */\nexport interface StringKeyValuePair {\n [key: string]: string\n}\n\n/**\n * A duplex key-value pair for an element, by GUID or its DOM object reference.\n */\ninterface ElementUID {\n node: Element,\n uid: string;\n}\n\n/**\n * Parameters passed into validation providers from the element attributes.\n * error property is read from data-val-[Provider Name] attribute.\n * params property is populated from data-val-[Provider Name]-[Parameter Name] attributes.\n */\nexport interface ValidationDirectiveBindings {\n error: string,\n params: StringKeyValuePair\n}\n\n/**\n * A key-value pair describing what validations to enforce to an input element, with respective parameters.\n */\nexport type ValidationDirective = {\n [key: string]: ValidationDirectiveBindings\n};\n\n/**\n * Validation plugin signature with multitype return.\n * Boolean return signifies the validation result, which uses the default validation error message read from the element attribute.\n * String return signifies failed validation, which then will be used as the validation error message.\n * Promise return signifies asynchronous plugin behavior, with same behavior as Boolean or String.\n */\nexport type ValidationProvider = (value: string, element: HTMLInputElement, params: StringKeyValuePair) => boolean | string | Promise;\n\n/**\n * A callback method signature that kickstarts a new validation task for an input element, as a Boolean Promise.\n */\ntype Validator = () => Promise;\n\n/**\n * Resolves and returns the element referred by original element using ASP.NET selector logic.\n * @param elementName \n */\nfunction getRelativeFormElement(elementName: string, selector: string) {\n // example elementName: Form.PasswordConfirm, Form.Email\n // example selector (dafuq): *.Password, *.__RequestVerificationToken\n // example result element name: Form.Password, __RequestVerificationToken\n\n let realSelector = selector.substr(2); // Password, __RequestVerificationToken\n let objectName = '';\n\n let dotLocation = elementName.lastIndexOf('.');\n if (dotLocation > -1) {\n // Form\n objectName = elementName.substr(0, dotLocation);\n\n // Form.Password\n let relativeElementName = objectName + '.' + realSelector;\n let relativeElement = document.getElementsByName(relativeElementName)[0];\n if (relativeElement) {\n return relativeElement;\n }\n }\n\n // __RequestVerificationToken\n return document.getElementsByName(realSelector)[0];\n}\n\n/**\n * Contains default implementations for ASP.NET Core MVC validation attributes.\n */\nexport class MvcValidationProviders {\n /**\n * Validates whether the input has a value.\n */\n required: ValidationProvider = (value, element, params) => {\n return Boolean(value);\n }\n\n /**\n * Validates whether the input value satisfies the length contstraint.\n */\n stringLength: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n if (params.min) {\n let min = parseInt(params.min);\n if (value.length < min) {\n return false;\n }\n }\n\n if (params.max) {\n let max = parseInt(params.max);\n if (value.length > max) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Validates whether the input value is equal to another input value.\n */\n compare: ValidationProvider = (value, element, params) => {\n if (!params.other) {\n return true;\n }\n\n let otherElement = getRelativeFormElement(element.name, params.other) as HTMLInputElement;\n if (!otherElement) {\n return true;\n }\n\n return (otherElement.value === value);\n }\n\n /**\n * Validates whether the input value is a number within a given range.\n */\n range: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n let val = parseFloat(value);\n if (isNaN(val)) {\n return false;\n }\n\n if (params.min) {\n let min = parseFloat(params.min);\n if (val < min) {\n return false;\n }\n }\n\n if (params.max) {\n let max = parseFloat(params.max);\n if (val > max) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Validates whether the input value satisfies a regular expression pattern.\n */\n regex: ValidationProvider = (value, element, params) => {\n if (!value || !params.pattern) {\n return true;\n }\n\n let r = new RegExp(params.pattern);\n return r.test(value);\n }\n\n /**\n * Validates whether the input value is an email in accordance to RFC822 specification, with a top level domain.\n */\n email: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n // RFC822 email address with .TLD validation\n // (c) Richard Willis, Chris Ferdinandi, MIT Licensed\n // https://gist.github.com/badsyntax/719800\n // https://gist.github.com/cferdinandi/d04aad4ce064b8da3edf21e26f8944c4\n\n let r = /^([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22))*\\x40([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d))*(\\.\\w{2,})+$/;\n return r.test(value);\n }\n\n /**\n * Validates whether the input value is a credit card number, with Luhn's Algorithm.\n */\n creditcard: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n // (c) jquery-validation, MIT Licensed\n // https://github.com/jquery-validation/jquery-validation/blob/master/src/additional/creditcard.js\n // based on https://en.wikipedia.org/wiki/Luhn_algorithm\n\n // Accept only spaces, digits and dashes\n if (/[^0-9 \\-]+/.test(value)) {\n return false;\n }\n\n var nCheck = 0,\n nDigit = 0,\n bEven = false,\n n, cDigit;\n\n value = value.replace(/\\D/g, \"\");\n\n // Basing min and max length on https://developer.ean.com/general_info/Valid_Credit_Card_Types\n if (value.length < 13 || value.length > 19) {\n return false;\n }\n\n for (n = value.length - 1; n >= 0; n--) {\n cDigit = value.charAt(n);\n nDigit = parseInt(cDigit, 10);\n if (bEven) {\n if ((nDigit *= 2) > 9) {\n nDigit -= 9;\n }\n }\n\n nCheck += nDigit;\n bEven = !bEven;\n }\n\n return (nCheck % 10) === 0;\n }\n\n /**\n * Validates whether the input value is a URL.\n */\n url: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n \n let lowerCaseValue = value.toLowerCase();\n\n // Match the logic in `UrlAttribute`\n return lowerCaseValue.indexOf('http://') > -1\n || lowerCaseValue.indexOf('https://') > -1\n || lowerCaseValue.indexOf('ftp://') > -1; \n }\n\n /**\n * Validates whether the input value is a phone number.\n */\n phone: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n // Allows whitespace or dash as number separator because some people like to do that...\n let consecutiveSeparator = /[\\+\\-\\s][\\-\\s]/g;\n if (consecutiveSeparator.test(value)) {\n return false;\n }\n\n let r = /^\\+?[0-9\\-\\s]+$/;\n return r.test(value);\n }\n\n /**\n * Asynchronously validates the input value to a JSON GET API endpoint. \n */\n remote: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n // params.additionalfields: *.Email,*.Username\n let fieldSelectors: string[] = (params.additionalfields as string).split(',');\n let fields: StringKeyValuePair = {};\n\n for (let fieldSelector of fieldSelectors) {\n let fieldName = fieldSelector.substr(2);\n let fieldElement = getRelativeFormElement(element.name, fieldSelector) as HTMLInputElement;\n\n let hasValue = Boolean(fieldElement && fieldElement.value);\n if (!hasValue) {\n continue;\n }\n\n fields[fieldName] = fieldElement.value;\n }\n\n let url: string = params['url'];\n // console.log(fields);\n\n let encodedParams: string[] = [];\n for (let fieldName in fields) {\n let encodedParam = encodeURIComponent(fieldName) + '=' + encodeURIComponent(fields[fieldName]);\n encodedParams.push(encodedParam);\n }\n let payload = encodedParams.join('&');\n // console.log(payload);\n\n return new Promise((ok, reject) => {\n let request = new XMLHttpRequest();\n\n if (params.type === 'Post') {\n let postData = new FormData();\n for (let fieldName in fields) {\n postData.append(fieldName, fields[fieldName]);\n }\n request.open('post', url);\n request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n request.send(payload);\n } else {\n request.open('get', url + '?' + payload);\n request.send();\n }\n\n request.onload = e => {\n if (request.status >= 200 && request.status < 300) {\n let data = JSON.parse(request.responseText);\n ok(data);\n } else {\n reject({\n status: request.status,\n statusText: request.statusText,\n data: request.responseText\n });\n }\n };\n\n request.onerror = e => {\n reject({\n status: request.status,\n statusText: request.statusText,\n data: request.responseText\n });\n };\n });\n }\n}\n\n/**\n * Responsibles for managing the DOM elements and running the validation providers.\n */\nexport class ValidationService {\n /**\n * A key-value collection of loaded validation plugins. \n */\n private providers: { [name: string]: ValidationProvider } = {};\n\n /**\n * A key-value collection of elements for displaying validation messages for an input (by DOM ID).\n */\n private messageFor: { [id: string]: Element[] } = {};\n\n /**\n * A list of managed elements, each having a randomly assigned unique identifier (UID).\n */\n private elementUIDs: ElementUID[] = [];\n\n /**\n * A key-value collection of UID to Element for quick lookup. \n */\n private elementByUID: { [uid: string]: Element } = {};\n\n /**\n * A key-value collection of input UIDs for a UID.\n */\n private formInputs: { [formUID: string]: string[] } = {};\n\n /**\n * A key-value map for input UID to its validator factory.\n */\n private validators: { [inputUID: string]: Validator } = {};\n\n /**\n * A key-value map for element UID to its trigger element (submit event for , input event for