├── .gitattributes ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.MD ├── dist ├── aspnet-validation.css ├── aspnet-validation.js ├── aspnet-validation.min.js └── aspnet-validation.min.js.map ├── docs └── DEVELOPMENT.md ├── package-lock.json ├── package.json ├── sample ├── AspNetClientValidation.sln ├── Controllers │ └── Validations.cs ├── DemoWeb.csproj ├── Pages │ ├── Demos │ │ ├── Checkboxes.cshtml │ │ ├── Checkboxes.cshtml.cs │ │ ├── DisabledInputsNoWatch.cshtml │ │ ├── DisabledInputsNoWatch.cshtml.cs │ │ ├── DisabledInputsWithWatch.cshtml │ │ ├── DisabledInputsWithWatch.cshtml.cs │ │ ├── FormAction.cshtml │ │ ├── FormAction.cshtml.cs │ │ ├── ImmediateValidation.cshtml │ │ ├── ImmediateValidation.cshtml.cs │ │ ├── RemovedInputs.cshtml │ │ ├── RemovedInputs.cshtml.cs │ │ ├── SelectInput.cshtml │ │ ├── SelectInput.cshtml.cs │ │ ├── SubmitButton.cshtml │ │ ├── SubmitButton.cshtml.cs │ │ ├── SubmitButtonDownload.cshtml │ │ └── SubmitButtonDownload.cshtml.cs │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Index.cshtml │ ├── Index.cshtml.cs │ ├── Shared │ │ ├── _Layout.cshtml │ │ └── _StatusMessage.cshtml │ └── _ViewImports.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── appsettings.json ├── package-lock.json └── wwwroot │ ├── css │ └── site.css │ ├── dist │ ├── aspnet-validation.css │ ├── aspnet-validation.js │ ├── aspnet-validation.min.js │ └── aspnet-validation.min.js.map │ ├── favicon.ico │ └── lib │ ├── bootstrap │ └── LICENSE │ ├── jquery-validation-unobtrusive │ ├── LICENSE.txt │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ └── LICENSE.md │ └── jquery │ └── LICENSE.txt ├── script ├── build ├── build.ps1 └── prebuild.js ├── src ├── aspnet-validation.css └── index.ts ├── tsconfig.json ├── types └── index.d.ts ├── webpack.config.js └── webpack.config.min.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.gif binary 4 | *.jpg binary 5 | *.eot binary 6 | *.woff binary 7 | *.svg binary 8 | *.ttf binary 9 | *.png binary 10 | *.otf binary 11 | *.tif binary -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build aspnet-validation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: '16.x' 20 | - run: npm install 21 | - run: npm run build --if-present 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: '16.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: npm install 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio and Visual Studio Code temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | .vscode/ 7 | !.vscode/settings.json 8 | !.vscode/tasks.json 9 | !.vscode/launch.json 10 | !.vscode/extensions.json 11 | 12 | ## Ignore Rider / JetBrains config files 13 | .idea/ 14 | 15 | # Logal Blob Storage For Testing 16 | 17 | blob/ 18 | 19 | # User-specific files 20 | *.rsuser 21 | *.suo 22 | *.user 23 | *.userosscache 24 | *.sln.docstates 25 | 26 | # User-specific files (MonoDevelop/Xamarin Studio) 27 | *.userprefs 28 | 29 | # Mono auto generated files 30 | mono_crash.* 31 | 32 | # Build results 33 | wwwroot/dist 34 | [Dd]ebug/ 35 | [Dd]ebugPublic/ 36 | [Rr]elease/ 37 | [Rr]eleases/ 38 | x64/ 39 | x86/ 40 | [Aa][Rr][Mm]/ 41 | [Aa][Rr][Mm]64/ 42 | bld/ 43 | [Bb]in/ 44 | [Oo]bj/ 45 | [Ll]og/ 46 | 47 | # Visual Studio 2015/2017 cache/options directory 48 | .vs/ 49 | # Uncomment if you have tasks that create the project's static files in wwwroot 50 | #wwwroot/ 51 | 52 | # Visual Studio 2017 auto generated files 53 | Generated\ Files/ 54 | 55 | # MSTest test Results 56 | [Tt]est[Rr]esult*/ 57 | [Bb]uild[Ll]og.* 58 | 59 | # NUnit 60 | *.VisualState.xml 61 | TestResult.xml 62 | nunit-*.xml 63 | 64 | # Build Results of an ATL Project 65 | [Dd]ebugPS/ 66 | [Rr]eleasePS/ 67 | dlldata.c 68 | 69 | # Benchmark Results 70 | BenchmarkDotNet.Artifacts/ 71 | 72 | # .NET Core 73 | project.lock.json 74 | project.fragment.lock.json 75 | artifacts/ 76 | output/ 77 | 78 | # StyleCop 79 | StyleCopReport.xml 80 | 81 | # Files built by Visual Studio 82 | *_i.c 83 | *_p.c 84 | *_h.h 85 | *.ilk 86 | *.meta 87 | *.obj 88 | *.iobj 89 | *.pch 90 | *.pdb 91 | *.ipdb 92 | *.pgc 93 | *.pgd 94 | *.rsp 95 | *.sbr 96 | *.tlb 97 | *.tli 98 | *.tlh 99 | *.tmp 100 | *.tmp_proj 101 | *_wpftmp.csproj 102 | *.log 103 | *.vspscc 104 | *.vssscc 105 | .builds 106 | *.pidb 107 | *.svclog 108 | *.scc 109 | 110 | # Chutzpah Test files 111 | _Chutzpah* 112 | 113 | # Visual C++ cache files 114 | ipch/ 115 | *.aps 116 | *.ncb 117 | *.opendb 118 | *.opensdf 119 | *.sdf 120 | *.cachefile 121 | *.VC.db 122 | *.VC.VC.opendb 123 | 124 | # Visual Studio profiler 125 | *.psess 126 | *.vsp 127 | *.vspx 128 | *.sap 129 | 130 | # Visual Studio Trace Files 131 | *.e2e 132 | 133 | # TFS 2012 Local Workspace 134 | $tf/ 135 | 136 | # Guidance Automation Toolkit 137 | *.gpState 138 | 139 | # ReSharper is a .NET coding add-in 140 | _ReSharper*/ 141 | *.[Rr]e[Ss]harper 142 | *.DotSettings.user 143 | 144 | # JustCode is a .NET coding add-in 145 | .JustCode 146 | 147 | # TeamCity is a build add-in 148 | _TeamCity* 149 | 150 | # DotCover is a Code Coverage Tool 151 | *.dotCover 152 | 153 | # AxoCover is a Code Coverage Tool 154 | .axoCover/* 155 | !.axoCover/settings.json 156 | 157 | # Visual Studio code coverage results 158 | *.coverage 159 | *.coveragexml 160 | 161 | # NCrunch 162 | _NCrunch_* 163 | .*crunch*.local.xml 164 | nCrunchTemp_* 165 | 166 | # MightyMoose 167 | *.mm.* 168 | AutoTest.Net/ 169 | 170 | # Web workbench (sass) 171 | .sass-cache/ 172 | 173 | # Installshield output folder 174 | [Ee]xpress/ 175 | 176 | # DocProject is a documentation generator add-in 177 | DocProject/buildhelp/ 178 | DocProject/Help/*.HxT 179 | DocProject/Help/*.HxC 180 | DocProject/Help/*.hhc 181 | DocProject/Help/*.hhk 182 | DocProject/Help/*.hhp 183 | DocProject/Help/Html2 184 | DocProject/Help/html 185 | 186 | # Click-Once directory 187 | publish/ 188 | 189 | # Publish Web Output 190 | *.[Pp]ublish.xml 191 | *.azurePubxml 192 | # Note: Comment the next line if you want to checkin your web deploy settings, 193 | # but database connection strings (with potential passwords) will be unencrypted 194 | *.pubxml 195 | *.publishproj 196 | 197 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 198 | # checkin your Azure Web App publish settings, but sensitive information contained 199 | # in these scripts will be unencrypted 200 | PublishScripts/ 201 | 202 | # NuGet Packages 203 | *.nupkg 204 | # NuGet Symbol Packages 205 | *.snupkg 206 | # The packages folder can be ignored because of Package Restore 207 | **/[Pp]ackages/* 208 | # except build/, which is used as an MSBuild target. 209 | !**/[Pp]ackages/build/ 210 | # except our Packages folder which we intend to keep. 211 | !src/product/Abbot.Web/Pages/**/Packages/** 212 | # Uncomment if necessary however generally it will be regenerated when needed 213 | #!**/[Pp]ackages/repositories.config 214 | # NuGet v3's project.json files produces more ignorable files 215 | *.nuget.props 216 | *.nuget.targets 217 | 218 | # Microsoft Azure Build Output 219 | csx/ 220 | *.build.csdef 221 | 222 | # Microsoft Azure Emulator 223 | ecf/ 224 | rcf/ 225 | 226 | # Windows Store app package directories and files 227 | AppPackages/ 228 | BundleArtifacts/ 229 | Package.StoreAssociation.xml 230 | _pkginfo.txt 231 | *.appx 232 | *.appxbundle 233 | *.appxupload 234 | 235 | # Visual Studio cache files 236 | # files ending in .cache can be ignored 237 | *.[Cc]ache 238 | # but keep track of directories ending in .cache 239 | !?*.[Cc]ache/ 240 | 241 | # Others 242 | ClientBin/ 243 | ~$* 244 | *~ 245 | *.dbmdl 246 | *.dbproj.schemaview 247 | *.jfm 248 | *.pfx 249 | *.publishsettings 250 | orleans.codegen.cs 251 | 252 | # Including strong name files can present a security risk 253 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 254 | #*.snk 255 | 256 | # Since there are multiple workflows, uncomment next line to ignore bower_components 257 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 258 | #bower_components/ 259 | 260 | # RIA/Silverlight projects 261 | Generated_Code/ 262 | 263 | # Backup & report files from converting an old project file 264 | # to a newer Visual Studio version. Backup files are not needed, 265 | # because we have git ;-) 266 | _UpgradeReport_Files/ 267 | Backup*/ 268 | UpgradeLog*.XML 269 | UpgradeLog*.htm 270 | ServiceFabricBackup/ 271 | *.rptproj.bak 272 | 273 | # SQL Server files 274 | *.mdf 275 | *.ldf 276 | *.ndf 277 | 278 | # Business Intelligence projects 279 | *.rdl.data 280 | *.bim.layout 281 | *.bim_*.settings 282 | *.rptproj.rsuser 283 | *- [Bb]ackup.rdl 284 | *- [Bb]ackup ([0-9]).rdl 285 | *- [Bb]ackup ([0-9][0-9]).rdl 286 | 287 | # Microsoft Fakes 288 | FakesAssemblies/ 289 | 290 | # GhostDoc plugin setting file 291 | *.GhostDoc.xml 292 | 293 | # Node.js Tools for Visual Studio 294 | .ntvs_analysis.dat 295 | node_modules/ 296 | 297 | # Visual Studio 6 build log 298 | *.plg 299 | 300 | # Visual Studio 6 workspace options file 301 | *.opt 302 | 303 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 304 | *.vbw 305 | 306 | # Visual Studio LightSwitch build output 307 | **/*.HTMLClient/GeneratedArtifacts 308 | **/*.DesktopClient/GeneratedArtifacts 309 | **/*.DesktopClient/ModelManifest.xml 310 | **/*.Server/GeneratedArtifacts 311 | **/*.Server/ModelManifest.xml 312 | _Pvt_Extensions 313 | 314 | # Paket dependency manager 315 | .paket/paket.exe 316 | paket-files/ 317 | 318 | # FAKE - F# Make 319 | .fake/ 320 | 321 | # CodeRush personal settings 322 | .cr/personal 323 | 324 | # Python Tools for Visual Studio (PTVS) 325 | __pycache__/ 326 | *.pyc 327 | 328 | # Cake - Uncomment if you are using it 329 | # tools/** 330 | # !tools/packages.config 331 | 332 | # Tabs Studio 333 | *.tss 334 | 335 | # Telerik's JustMock configuration file 336 | *.jmconfig 337 | 338 | # BizTalk build output 339 | *.btp.cs 340 | *.btm.cs 341 | *.odx.cs 342 | *.xsd.cs 343 | 344 | # OpenCover UI analysis results 345 | OpenCover/ 346 | 347 | # Azure Stream Analytics local run output 348 | ASALocalRun/ 349 | 350 | # MSBuild Binary and Structured Log 351 | *.binlog 352 | 353 | # NVidia Nsight GPU debugger configuration file 354 | *.nvuser 355 | 356 | # MFractors (Xamarin productivity tool) working folder 357 | .mfractor/ 358 | 359 | # Local History for Visual Studio 360 | .localhistory/ 361 | 362 | # BeatPulse healthcheck temp database 363 | healthchecksdb 364 | 365 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 366 | MigrationBackup/ 367 | *.db 368 | 369 | # CMake 370 | cmake-build-*/ 371 | 372 | # File-based project format 373 | *.iws 374 | 375 | # IntelliJ 376 | out/ 377 | 378 | # mpeltonen/sbt-idea plugin 379 | 380 | # JIRA plugin 381 | atlassian-ide-plugin.xml 382 | 383 | # Crashlytics plugin (for Android Studio and IntelliJ) 384 | com_crashlytics_export_strings.xml 385 | crashlytics.properties 386 | crashlytics-build.properties 387 | fabric.properties 388 | 389 | .DS_Store 390 | 391 | .store 392 | tools/ 393 | *.pkg 394 | *.dmg 395 | build/ 396 | *.exe 397 | 398 | bin/ 399 | obj/ 400 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | sample/ 3 | src/ 4 | .gitattributes 5 | .travis.yml 6 | build.ps1 7 | tsconfig.json 8 | webpack.config.js 9 | webpack.config.min.js 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Ryan Elian (Changes 2020 and after Copyright Phil Haack) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # ASP.NET Client Validation 2 | 3 | Enables ASP.NET MVC client-side validation without jQuery! Originally forked from https://github.com/ryanelian/aspnet-validation. This library replaces the need for `jquery.validate.min.js`, `jquery.validate.unobtrusive.min.js`. It's a nearly drop-in replacement. The only difference is you need to initialize the library as shown in the [Quickstart Guide](#quick-start-guide). 4 | 5 | [![npm](https://img.shields.io/npm/v/aspnet-client-validation.svg)](https://www.npmjs.com/package/aspnet-client-validation) 6 | [![Build Status](https://github.com/haacked/aspnet-client-validation/workflows/Build%20aspnet-validation/badge.svg)](https://github.com/haacked/aspnet-client-validation/actions) 7 | 8 | ## Install 9 | 10 | ``` 11 | npm install aspnet-client-validation 12 | ``` 13 | 14 | or 15 | 16 | ``` 17 | yarn add aspnet-client-validation 18 | ``` 19 | 20 | Alternatively, extract these files from the [dist.zip folder of the latest release](https://github.com/haacked/aspnet-client-validation/releases/): 21 | 22 | * aspnet-validation.min.js 23 | * aspnet-validation.min.js.map 24 | 25 | 26 | > aspnet-client-validation uses [Promise API](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise), which is not supported in Internet Explorer. **It is recommended to use [promise-polyfill](https://github.com/taylorhakes/promise-polyfill) or [ts-polyfill](https://github.com/ryanelian/ts-polyfill) or [core-js](https://github.com/zloirock/core-js) to resolve this issue...** 27 | 28 | > If you are also using Bootstrap, you may [un-jQuery](http://youmightnotneedjquery.com/) the application by using https://github.com/thednp/bootstrap.native 29 | 30 | ## Quick Start Guide 31 | 32 | ### Via \ 36 | 37 | ``` 38 | 39 | ```js 40 | // Exposes window['aspnetValidation'] 41 | var v = new aspnetValidation.ValidationService(); 42 | v.bootstrap(); 43 | ``` 44 | 45 | ### Via CommonJS / Browserify 46 | 47 | ```js 48 | require('core-js'); 49 | const aspnetValidation = require('aspnet-client-validation'); 50 | 51 | let v = new aspnetValidation.ValidationService(); 52 | v.bootstrap(); 53 | ``` 54 | 55 | ### Via TypeScript / ES Modules 56 | 57 | ```ts 58 | import 'ts-polyfill'; 59 | import { ValidationService } from 'aspnet-client-validation'; 60 | 61 | let v = new ValidationService(); 62 | v.bootstrap(); 63 | ``` 64 | 65 | ## Why? 66 | 67 | **jquery-3.3.2.min.js** + **jquery.validate.min.js** + **jquery.validate.unobtrusive.min.js** = 112 KB 68 | 69 | **aspnet-validation.min.js:** 10.6 KB **(9.46%, ~4 KB GZIP)** 70 | - **promise-polyfill**: +3.06 KB (< 1 KB GZIP) 71 | 72 | Also, `jquery.validate.unobtrusive.min.js` has not been meaningfully updated in over a decade, and is hard to configure. 73 | 74 | ## Building the Source Code 75 | 76 | ```powershell 77 | git clone https://github.com/haacked/aspnet-client-validation.git 78 | npm install 79 | script/build # If using PowerShell: script/build.ps1 80 | ``` 81 | 82 | ## Adding Custom Validation 83 | 84 | Example stolen from https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation 85 | 86 | ### Server Code (C#) 87 | 88 | ```cs 89 | public class ClassicMovieAttribute : ValidationAttribute, IClientModelValidator 90 | { 91 | private int _year; 92 | 93 | public ClassicMovieAttribute(int Year) 94 | { 95 | _year = Year; 96 | } 97 | 98 | protected override ValidationResult IsValid(object value, ValidationContext validationContext) 99 | { 100 | Movie movie = (Movie)validationContext.ObjectInstance; 101 | 102 | if (movie.Genre == Genre.Classic && movie.ReleaseDate.Year > _year) 103 | { 104 | return new ValidationResult(GetErrorMessage()); 105 | } 106 | 107 | return ValidationResult.Success; 108 | } 109 | 110 | public void AddValidation(ClientModelValidationContext context) 111 | { 112 | if (context == null) 113 | { 114 | throw new ArgumentNullException(nameof(context)); 115 | } 116 | 117 | MergeAttribute(context.Attributes, "data-val", "true"); 118 | MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage()); 119 | 120 | var year = _year.ToString(CultureInfo.InvariantCulture); 121 | MergeAttribute(context.Attributes, "data-val-classicmovie-year", year); 122 | } 123 | 124 | private static bool MergeAttribute(IDictionary attributes, string key, string value) { 125 | if (attributes.ContainsKey(key)) { 126 | return false; 127 | } 128 | 129 | attributes.Add(key, value); 130 | return true; 131 | } 132 | 133 | } 134 | ``` 135 | 136 | ### Client Code 137 | 138 | ```ts 139 | import { ValidationService } from 'aspnet-client-validation'; 140 | let v = new ValidationService(); 141 | 142 | v.addProvider('classicmovie', (value, element, params) => { 143 | if (!value) { 144 | // Let [Required] handle validation error for empty input... 145 | return true; 146 | } 147 | 148 | // Unlike the original, data-val-classicmovie-year is bound automatically to params['year'] as string! 149 | let year = parseInt(params.year); 150 | let date = new Date(value); 151 | let genre = (document.getElementById('Genre') as HTMLSelectElement).value; 152 | 153 | if (genre && genre === '0') { 154 | return date.getFullYear() <= year; 155 | } 156 | 157 | return true; 158 | }); 159 | 160 | v.bootstrap(); 161 | ``` 162 | 163 | ## Adding Custom Asynchronous Validation 164 | 165 | Other than `boolean` and `string`, `addProvider` callback accepts `Promise` as return value: 166 | 167 | ```ts 168 | v.addProvider('io', (value, element, params) => { 169 | if (!value) { 170 | return true; 171 | } 172 | 173 | return async () => { 174 | let result: number = await Some_IO_Operation(value); 175 | return result > 0; 176 | }; 177 | }); 178 | ``` 179 | 180 | ## Controlling when validation occurs 181 | 182 | ### Events 183 | 184 | By default, validation occurs immediately upon changes to form fields: on `input` for inputs and textareas, and on `change` for selects. 185 | 186 | One can change to a different event by setting a field's `data-val-event` attribute. For example, one can use `data-val-event="blur"` to validate that field on the `blur` event. 187 | 188 | ### Timing 189 | 190 | To prevent unnecessary validation, a debounce of 300ms is used. This ensures validation does not occur for every keystroke, which is especially important during remote validation. 191 | 192 | In some cases it may be unnecessary, for example when performing local validation on blur (rather than on change). To change the default: 193 | 194 | ```ts 195 | v.debounce = 0; 196 | ``` 197 | 198 | ## Subscribing to Client Form Validation Event 199 | 200 | ```ts 201 | const form = document.getElementById('form'); 202 | form.addEventListener('validation', function (e) { 203 | /* Check if form is valid here. */ 204 | }); 205 | ``` 206 | 207 | ## Programatically validate a form 208 | 209 | ```ts 210 | v.validateForm(document.getElementById('form')); 211 | ``` 212 | 213 | ## Checking form validity 214 | 215 | ```ts 216 | v.isValid(document.getElementById('form')) 217 | ``` 218 | 219 | By default it will try to validate the form, before returning whether the form is valid. This can be disabled by setting the `prevalidate` parameter like so: 220 | 221 | ```ts 222 | v.isValid(document.getElementById('form'), false) 223 | ``` 224 | 225 | You can also supply a callback function to be run after the check. 226 | 227 | ```ts 228 | v.isValid(document.getElementById('form'), true, myCallbackFn) 229 | ``` 230 | 231 | ## Checking field validity 232 | 233 | Similar to checking a forms validity, you can check individual fields too. 234 | 235 | ```ts 236 | v.isFieldValid(document.getElementById('field')) 237 | ``` 238 | 239 | By default it will try to validate the field, before returning whether the field is valid. This can be disabled by setting the `prevalidate` parameter like so: 240 | 241 | ```ts 242 | v.isFieldValid(document.getElementById('field'), false) 243 | ``` 244 | 245 | You can also supply a callback function to be run after the check. 246 | 247 | ```ts 248 | v.isFieldValid(document.getElementById('field'), true, myCallbackFn) 249 | ``` 250 | 251 | ## Hidden fields validation 252 | 253 | By default validation is skipped for hidden fields. To enable validation for hidden fields validation use: 254 | ```ts 255 | v.allowHiddenFields = true; 256 | ``` 257 | 258 | ## Monitoring the DOM for changes 259 | 260 | If configured, aspnet-client-validation can monitor the DOM for changes using [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver), if the browser supports it. 261 | As new elements are added/modified, the library will automatically wire up validation directives found. 262 | This can be very useful when using frameworks that modify the DOM, such as [Turbo](http://turbo.hotwired.dev). 263 | To configure this, set the `watch` option to `true` when calling `bootstrap`: 264 | 265 | ```js 266 | let v = new aspnetValidation.ValidationService(); 267 | v.bootstrap({ watch: true }); 268 | ``` 269 | 270 | Alternatively, to update the validation service for a specific form (which was created or modified dynamically) without using a `MutationObserver`: 271 | ```js 272 | let form = document.getElementById('my-form'); 273 | v.scan(form); 274 | ``` 275 | 276 | ## Customizing CSS for use with other libraries 277 | 278 | One can customize the CSS classes so the generated markup is compatible with various frameworks. For example, to integrate with [Bootstrap](https://getbootstrap.com/docs/5.3/forms/validation) (v5+): 279 | 280 | ```js 281 | var v = new aspnetValidation.ValidationService(); 282 | v.ValidationInputCssClassName = 'is-invalid'; // change from default of 'input-validation-error' 283 | v.ValidationInputValidCssClassName = 'is-valid'; // change from default of 'input-validation-valid' 284 | v.ValidationMessageCssClassName = 'invalid-feedback'; // change from default of 'field-validation-error' 285 | v.ValidationMessageValidCssClassName = 'valid-feedback'; // change from default of 'field-validation-valid' 286 | //v.ValidationSummaryCssClassName = 'validation-summary-errors'; // unnecessary: bootstrap lacks validation summary component 287 | //v.ValidationSummaryValidCssClassName = 'validation-summary-valid'; // " 288 | v.bootstrap(); 289 | ``` 290 | 291 | ## Customizing highlight and unhighlight functions 292 | 293 | ```js 294 | var v = new aspnetValidation.ValidationService(); 295 | v.highlight = function (input, errorClass, validClass) { ... }; 296 | v.unhighlight = function (input, errorClass, validClass) { ... }; 297 | v.bootstrap(); 298 | ``` 299 | 300 | ## Logging 301 | 302 | There is a rudimentary logging infrastructure in place if you want to get more insight into what the library is doing. 303 | To enable logging, pass an object that implements the `Logger` interface (see below) in to the `ValidationService` constructor. 304 | The `window.console` object satisfies this interface automatically: 305 | 306 | ```typescript 307 | // The Logger interface, for reference 308 | export interface Logger { 309 | log(message: string, ...args: any[]): void; 310 | warn(message: string, ...args: any[]): void; 311 | } 312 | 313 | let v = new aspnetValidation.ValidationService(console); 314 | v.bootstrap(); 315 | ``` 316 | -------------------------------------------------------------------------------- /dist/aspnet-validation.css: -------------------------------------------------------------------------------- 1 | .field-validation-error { 2 | color: red; 3 | font-style: italic; 4 | } 5 | 6 | .input-validation-error { 7 | border-color: red; 8 | outline-color: red; 9 | } 10 | 11 | .input-validation-valid { 12 | border-color: green; 13 | outline-color: green; 14 | } 15 | 16 | .validation-summary-errors { 17 | color: red; 18 | font-style: italic; 19 | } 20 | 21 | .validation-summary-valid span { 22 | display: none; 23 | } 24 | -------------------------------------------------------------------------------- /dist/aspnet-validation.min.js: -------------------------------------------------------------------------------- 1 | !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()}(self,(()=>(()=>{"use strict";var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{MvcValidationProviders:()=>u,ValidationService:()=>d,isValidatable:()=>a});var n=function(e,t,n,r){return new(n||(n=Promise))((function(i,a){function s(e){try{l(r.next(e))}catch(e){a(e)}}function o(e){try{l(r.throw(e))}catch(e){a(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(s,o)}l((r=r.apply(e,t||[])).next())}))},r=function(e,t){var n,r,i,a,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return a={next:o(0),throw:o(1),return:o(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function o(o){return function(l){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,o[0]&&(s=0)),s;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!((i=(i=s.trys).length>0&&i[i.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]-1){var s=n.substring(0,i)+"."+r,l=document.getElementsByName(s)[0];if(a(l))return l}return e.form.querySelector(o("[name=".concat(r,"]")))}var u=function(){this.required=function(e,t,n){var r=t.type.toLowerCase();if("checkbox"===r||"radio"===r){for(var i=0,a=Array.from(t.form.querySelectorAll(o("[name='".concat(t.name,"'][type='").concat(r,"']"))));ii)return!1}return!0},this.compare=function(e,t,n){if(!n.other)return!0;var r=l(t,n.other);return!r||r.value===e},this.range=function(e,t,n){if(!e)return!0;var r=parseFloat(e);return!(isNaN(r)||n.min&&rparseFloat(n.max))},this.regex=function(e,t,n){return!e||!n.pattern||new RegExp(n.pattern).test(e)},this.email=function(e,t,n){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,n){if(!e)return!0;if(/[^0-9 \-]+/.test(e))return!1;var r,i,a=0,s=0,o=!1;if((e=e.replace(/\D/g,"")).length<13||e.length>19)return!1;for(r=e.length-1;r>=0;r--)i=e.charAt(r),s=parseInt(i,10),o&&(s*=2)>9&&(s-=9),a+=s,o=!o;return a%10==0},this.url=function(e,t,n){if(!e)return!0;var r=e.toLowerCase();return r.indexOf("http://")>-1||r.indexOf("https://")>-1||r.indexOf("ftp://")>-1},this.phone=function(e,t,n){return!e||!/[\+\-\s][\-\s]/g.test(e)&&/^\+?[0-9\-\s]+$/.test(e)},this.remote=function(e,t,n){if(!e)return!0;for(var r=n.additionalfields.split(","),i={},a=0,s=r;a=200&&r.status<300){var i=JSON.parse(r.responseText);e(i)}else t({status:r.status,statusText:r.statusText,data:r.responseText})},r.onerror=function(e){t({status:r.status,statusText:r.statusText,data:r.responseText})}}))}},d=function(){function e(e){var t=this;this.providers={},this.messageFor={},this.elementUIDs=[],this.elementByUID={},this.formInputs={},this.validators={},this.formEvents={},this.inputEvents={},this.summary={},this.debounce=300,this.allowHiddenFields=!1,this.validateForm=function(e,i){return n(t,void 0,void 0,(function(){var t,n,a;return r(this,(function(r){switch(r.label){case 0:if(!(e instanceof HTMLFormElement))throw new Error("validateForm() can only be called on
elements");return t=this.getElementUID(e),n=this.formEvents[t],(a=!n)?[3,2]:[4,n(void 0,i)];case 1:a=r.sent(),r.label=2;case 2:return[2,a]}}))}))},this.validateField=function(e,i){return n(t,void 0,void 0,(function(){var t,n,a;return r(this,(function(r){switch(r.label){case 0:return t=this.getElementUID(e),n=this.inputEvents[t],(a=!n)?[3,2]:[4,n(void 0,i)];case 1:a=r.sent(),r.label=2;case 2:return[2,a]}}))}))},this.preValidate=function(e){e.preventDefault(),e.stopImmediatePropagation()},this.handleValidated=function(e,n,r){if(!(e instanceof HTMLFormElement))throw new Error("handleValidated() can only be called on elements");n?r&&t.submitValidForm(e,r):t.focusFirstInvalid(e)},this.submitValidForm=function(e,t){if(!(e instanceof HTMLFormElement))throw new Error("submitValidForm() can only be called on elements");var n=new SubmitEvent("submit",t);if(e.dispatchEvent(n)){var r=t.submitter,i=null,a=e.action;if(r){var s=r.getAttribute("name");s&&((i=document.createElement("input")).type="hidden",i.name=s,i.value=r.getAttribute("value"),e.appendChild(i));var o=r.getAttribute("formaction");o&&(e.action=o)}try{e.submit()}finally{i&&e.removeChild(i),e.action=a}}},this.focusFirstInvalid=function(e){if(!(e instanceof HTMLFormElement))throw new Error("focusFirstInvalid() can only be called on elements");var n=t.getElementUID(e),r=t.formInputs[n],i=null==r?void 0:r.find((function(e){return t.summary[e]}));if(i){var a=t.elementByUID[i];a instanceof HTMLElement&&a.focus()}},this.isValid=function(e,n,r){if(void 0===n&&(n=!0),!(e instanceof HTMLFormElement))throw new Error("isValid() can only be called on elements");n&&t.validateForm(e,r);var i=t.getElementUID(e),a=t.formInputs[i];return!(!0===(null==a?void 0:a.some((function(e){return t.summary[e]}))))},this.isFieldValid=function(e,n,r){void 0===n&&(n=!0),n&&t.validateField(e,r);var i=t.getElementUID(e);return void 0===t.summary[i]},this.options={root:document.body,watch:!1,addNoValidate:!0},this.ValidationInputCssClassName="input-validation-error",this.ValidationInputValidCssClassName="input-validation-valid",this.ValidationMessageCssClassName="field-validation-error",this.ValidationMessageValidCssClassName="field-validation-valid",this.ValidationSummaryCssClassName="validation-summary-errors",this.ValidationSummaryValidCssClassName="validation-summary-valid",this.logger=e||i}return e.prototype.addProvider=function(e,t){this.providers[e]||(this.logger.log("Registered provider: %s",e),this.providers[e]=t)},e.prototype.addMvcProviders=function(){var e=new u;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(e,t){for(var n=0,r=Array.from(e.querySelectorAll("span[form]"));n=0?a.splice(s,1):this.logger.log("Validation element for '%s' was already removed",name,t)}}}},e.prototype.parseDirectives=function(e){for(var t={},n={},r=0;r=0?(i.splice(a,1),i.length||(null===(n=this.formEvents[r])||void 0===n||n.remove(),delete this.formEvents[r],delete this.formInputs[r],delete this.messageFor[r])):this.logger.log("Form input for UID '%s' was already removed",t)}},e.prototype.addInput=function(e){var t,i=this,a=this.getElementUID(e),s=this.parseDirectives(e.attributes);if(this.validators[a]=this.createValidator(e,s),e.form&&this.trackFormInput(e.form,a),!this.inputEvents[a]){var o=function(t,s){return n(i,void 0,void 0,(function(){var n,i,o;return r(this,(function(r){switch(r.label){case 0:if(!(n=this.validators[a]))return[2,!0];if(!e.dataset.valEvent&&t&&"input"===t.type&&!e.classList.contains(this.ValidationInputCssClassName))return[2,!0];this.logger.log("Validating",{event:t}),r.label=1;case 1:return r.trys.push([1,3,,4]),[4,n()];case 2:return i=r.sent(),s(i),[2,i];case 3:return o=r.sent(),this.logger.log("Validation error",o),[2,!1];case 4:return[2]}}))}))},l=null;o.debounced=function(e,t){null!==l&&clearTimeout(l),l=setTimeout((function(){o(e,t)}),i.debounce)};var u=e instanceof HTMLSelectElement?"change":"input change",d=(null!==(t=e.dataset.valEvent)&&void 0!==t?t:u).split(" ");d.forEach((function(t){e.addEventListener(t,o.debounced)})),o.remove=function(){d.forEach((function(t){e.removeEventListener(t,o.debounced)}))},this.inputEvents[a]=o}},e.prototype.removeInput=function(e){var t=this.getElementUID(e),n=this.inputEvents[t];(null==n?void 0:n.remove)&&(n.remove(),delete n.remove),delete this.summary[t],delete this.inputEvents[t],delete this.validators[t],e.form&&this.untrackFormInput(e.form,t)},e.prototype.scanInputs=function(e,t){var n=Array.from(e.querySelectorAll(o('[data-val="true"]')));a(e)&&"true"===e.getAttribute("data-val")&&n.push(e);for(var r=0;r-1)){var i=document.createElement("li");i.innerHTML=this.summary[n],t.appendChild(i),e.push(this.summary[n])}}return t},e.prototype.renderSummary=function(){var e=document.querySelectorAll('[data-valmsg-summary="true"]');if(e.length){var t=JSON.stringify(this.summary,Object.keys(this.summary).sort());if(t!==this.renderedSummaryJSON){this.renderedSummaryJSON=t;for(var n=this.createSummaryDOM(),r=0;r 2 | 3 | 4 | net8.0 5 | enable 6 | true 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /sample/Pages/Demos/Checkboxes.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.Checkboxes 3 | 4 | @{ 5 | Layout = "Shared/_Layout"; 6 | } 7 | 8 | 9 | 10 |
11 | Please correct the following errors 12 |
13 | 14 |
15 | Required ASP.NET Checkboxes (with hidden input) and Radio buttons 16 | 17 | 18 | 19 |
20 |

21 | ASP.NET renders a checkbox and a hidden input for each boolean property. 22 | The hidden input is used to ensure that a value is sent for the checkbox, 23 | even if it is unchecked. Unchecking it should not fail validation. 24 |

25 | 28 | 29 | @if (Model.Input.IsChecked) { 30 | This is checked. 31 | } else { 32 | This is not checked 33 | } 34 |
35 | 36 |
37 |

38 | One can also use the [Remote] validation attribute with checkboxes. 39 | This checkbox's remote validator returns valid only when it matches the one above. 40 | Note: Changes to 41 | additional fields 42 | do not automatically revalidate. 43 |

44 | 47 | 48 |
49 | 50 |
51 |

52 | However, if you manually render a checkbox, the checkbox is only submitted 53 | when checked. So this allows cases where we require at least one checkbox 54 | is selected. 55 |

56 | @foreach (var animal in Model.Animals) { 57 | checked 66 | }/> 67 | 68 | } 69 | 70 | @if (Model.SelectedAnimals!.Any()) { 71 | Selected animals: @string.Join(", ", Model.SelectedAnimals!) 72 | } 73 |
74 | 75 |
76 |

77 | Similarly, with a required radio button list, one element should be checked. 78 |

79 | @foreach (var fruit in Model.Fruits) { 80 | checked 89 | }/> 90 | 91 | } 92 | 93 | @if (Model.SelectedFruit != string.Empty) { 94 | Selected fruit: @Model.SelectedFruit 95 | } 96 |
97 | 98 |
99 |

100 | It's also common to bind a boolean property from a list to checkboxes. 101 |

102 | @for (var i = 0; i 106 | 107 | @number.Name 108 | 109 | @* The asp-for above appends an input with value="false" to the form. *@ 110 | @* An explicit hidden generates input with value="False": *@ 111 | @* *@ 112 | 113 | } 114 | 115 | Selected numbers: @string.Join(", ", Model.Numbers.Where(n => n.IsSelected).Select(n => n.Name)) 116 |
117 | 118 | 119 | 120 | 121 |
122 | 123 | @if (Model.StatusMessage is not null) { 124 | Reset 125 | } 126 | 127 | @section Scripts { 128 | 132 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/Checkboxes.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace DemoWeb.Pages.Demos; 6 | 7 | public class Checkboxes : PageModel 8 | { 9 | public string? StatusMessage { get; set; } 10 | 11 | public void OnGet() 12 | { 13 | Numbers.Add(new() { Name = "One" }); 14 | Numbers.Add(new() { Name = "Two" }); 15 | Numbers.Add(new() { Name = "Three" }); 16 | } 17 | 18 | public IActionResult OnPost() 19 | { 20 | StatusMessage = "Form was submitted: " + (ModelState.IsValid 21 | ? "Model is valid" 22 | : "Model is invalid"); 23 | 24 | return Page(); 25 | } 26 | 27 | [BindProperty] 28 | public InputModel Input { get; set; } = new(); 29 | 30 | [BindProperty] 31 | [Required] 32 | public List SelectedAnimals { get; set; } = new(); 33 | 34 | public IReadOnlyList Animals = new List { "Dog", "Cat", "Fish" }; 35 | 36 | [BindProperty] 37 | [Required] 38 | public string SelectedFruit { get; set; } = string.Empty; 39 | 40 | public IReadOnlyList Fruits = new List { "Apple", "Banana", "Strawberry" }; 41 | 42 | public class InputModel 43 | { 44 | public bool IsChecked { get; set; } 45 | 46 | [Remote("CheckboxRemote", "Validations", HttpMethod = "Post", 47 | ErrorMessage = "Must match other checkbox.", 48 | AdditionalFields = $"{nameof(IsChecked)}" 49 | )] 50 | public bool IsCheckedToo { get; set; } 51 | } 52 | 53 | [BindProperty] 54 | public List Numbers { get; } = new(); 55 | 56 | public class Selectable 57 | { 58 | public required string Name { get; set; } 59 | 60 | public bool IsSelected { get; set; } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sample/Pages/Demos/DisabledInputsNoWatch.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.DisabledInputsNoWatchPageModel 3 | 4 | @{ 5 | Layout = "Shared/_Layout"; 6 | } 7 | 8 | 9 | 10 |
11 | Disabled inputs 12 | 13 |
14 |
15 |

16 | This simple test demonstrates that we don't validate disabled inputs. 17 |

18 | 19 |
20 | 25 |
26 | 27 |
28 | 33 |
34 | 35 |
36 | 39 | 40 |
41 | 42 | 43 |
44 |
45 |
46 | 47 |
48 | Disabled fieldset 49 | 50 |
51 |
52 |

53 | This simple test demonstrates that we don't validate disabled fieldsets. 54 |

55 | 56 |
57 | 61 |
62 | 63 |
64 | 68 |
69 | 70 |
71 | 74 | 75 |
76 | 77 | 78 |
79 |
80 |
81 | 82 | @section Scripts { 83 | 112 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/DisabledInputsNoWatch.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace DemoWeb.Pages.Demos; 6 | 7 | public class DisabledInputsNoWatchPageModel : PageModel 8 | { 9 | [TempData] 10 | public string? StatusMessage { get; set; } 11 | 12 | [BindProperty] 13 | [Required] 14 | public string? Value1 { get; set; } 15 | 16 | [BindProperty] 17 | [Required] 18 | public string? Value2 { get; set; } 19 | 20 | public bool IsChecked { get; set; } 21 | 22 | [Remote("CheckboxRemote", "Validations", HttpMethod = "Post", 23 | ErrorMessage = "Must match other checkbox.", 24 | AdditionalFields = $"{nameof(IsChecked)}" 25 | )] 26 | public bool IsCheckedToo { get; set; } 27 | 28 | public IActionResult OnPost() 29 | { 30 | StatusMessage = "Form was submitted to server. Any validation errors that may be present are due to server side validation, not client."; 31 | 32 | return RedirectToPage(); 33 | } 34 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/DisabledInputsWithWatch.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.DisabledInputsWithWatchPageModel 3 | 4 | @{ 5 | Layout = "Shared/_Layout"; 6 | } 7 | 8 | 9 | 10 |
11 | Disabled inputs 12 | 13 |
14 |
15 |

16 | This simple test demonstrates that we don't validate disabled inputs. 17 |

18 | 19 |
20 | 25 |
26 | 27 |
28 | 33 |
34 | 35 |
36 | 39 | 40 |
41 | 42 | 43 |
44 |
45 |
46 | 47 |
48 | Disabled fieldset 49 | 50 |
51 |
52 |

53 | This simple test demonstrates that we don't validate disabled fieldsets. 54 |

55 | 56 |
57 | 61 |
62 | 63 |
64 | 68 |
69 | 70 |
71 | 74 | 75 |
76 | 77 | 78 |
79 |
80 |
81 | 82 | @section Scripts { 83 | 104 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/DisabledInputsWithWatch.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace DemoWeb.Pages.Demos; 6 | 7 | public class DisabledInputsWithWatchPageModel : PageModel 8 | { 9 | [TempData] 10 | public string? StatusMessage { get; set; } 11 | 12 | [BindProperty] 13 | [Required] 14 | public string? Value1 { get; set; } 15 | 16 | [BindProperty] 17 | [Required] 18 | public string? Value2 { get; set; } 19 | 20 | public bool IsChecked { get; set; } 21 | 22 | [Remote("CheckboxRemote", "Validations", HttpMethod = "Post", 23 | ErrorMessage = "Must match other checkbox.", 24 | AdditionalFields = $"{nameof(IsChecked)}" 25 | )] 26 | public bool IsCheckedToo { get; set; } 27 | 28 | public IActionResult OnPost() 29 | { 30 | StatusMessage = "Form was submitted to server. Any validation errors that may be present are due to server side validation, not client."; 31 | 32 | return RedirectToPage(); 33 | } 34 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/FormAction.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.FormAction 3 | 4 | @{ 5 | Layout = "Shared/_Layout"; 6 | } 7 | 8 | 9 | 10 |
11 |
The following problems occurred when submitting the form:
12 |
13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | @section Scripts { 23 | 27 | } 28 | -------------------------------------------------------------------------------- /sample/Pages/Demos/FormAction.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace DemoWeb.Pages.Demos; 6 | 7 | public class FormAction : PageModel 8 | { 9 | [TempData] 10 | public string? StatusMessage { get; set; } 11 | 12 | [BindProperty] 13 | public InputModel Input { get; set; } = new(); 14 | 15 | public class InputModel 16 | { 17 | [Display(Name = "Name")] 18 | [Required, StringLength(50)] 19 | public string? Name { get; set; } 20 | } 21 | 22 | public IActionResult OnPostSubmitAsync() 23 | { 24 | StatusMessage = "Submit button clicked"; 25 | 26 | return RedirectToPage(); 27 | } 28 | 29 | public IActionResult OnPostSave() 30 | { 31 | StatusMessage = "Save button clicked"; 32 | 33 | return RedirectToPage(); 34 | } 35 | 36 | public IActionResult OnPost() 37 | { 38 | StatusMessage = "The button with no formaction was clicked"; 39 | 40 | return RedirectToPage(); 41 | } 42 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/ImmediateValidation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.ImmediateValidation 3 | @{ 4 | Layout = "Shared/_Layout"; 5 | } 6 | 7 |
8 | Please correct the following errors 9 |
10 | 11 |
12 | Required Email input with data-val-event specified for immediate validation as you type. 13 |
14 | Input with immediate validation (data-val-event="input") 15 |
16 | 17 | 18 | 19 |
20 | Input with default behavior 21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 | 30 | @section Scripts { 31 | 35 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/ImmediateValidation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace DemoWeb.Pages.Demos; 6 | 7 | public class ImmediateValidation : PageModel 8 | { 9 | [BindProperty] 10 | [Required] 11 | [EmailAddress] 12 | public string? EmailAddress { get; set; } 13 | 14 | [BindProperty] 15 | [Required] 16 | [EmailAddress] 17 | public string? AnotherEmailAddress { get; set; } 18 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/RemovedInputs.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.RemovedInputs 3 | 4 | @{ 5 | Layout = "Shared/_Layout"; 6 | } 7 | 8 | 9 | 10 |
11 | Required ASP.NET Checkboxes with hidden input 12 | 13 |
14 |
15 |

16 | This simple test demonstrates that we don't validate removed inputs. 17 |

18 | 19 |
20 | 24 |
25 | 26 |
27 | 31 |
32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | 40 | @section Scripts { 41 | 45 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/RemovedInputs.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace DemoWeb.Pages.Demos; 6 | 7 | public class RemovedInputs : PageModel 8 | { 9 | [TempData] 10 | public string? StatusMessage { get; set; } 11 | 12 | [BindProperty] 13 | [Required] 14 | public string? Value1 { get; set; } 15 | 16 | [BindProperty] 17 | [Required] 18 | public string? Value2 { get; set; } 19 | 20 | public IActionResult OnPost() 21 | { 22 | StatusMessage = "Form was submitted"; 23 | 24 | return RedirectToPage(); 25 | } 26 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/SelectInput.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.SelectInput 3 | @{ 4 | Layout = "Shared/_Layout"; 5 | } 6 | 7 |
8 | Please correct the following errors 9 |
10 | 11 |
12 | Required Select input with validation summary. 13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 | 28 | @section Scripts { 29 | 33 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/SelectInput.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using Microsoft.AspNetCore.Mvc.Rendering; 5 | 6 | namespace DemoWeb.Pages.Demos; 7 | 8 | public class SelectInput : PageModel 9 | { 10 | [BindProperty] 11 | [Required] 12 | public string? Animal { get; set; } 13 | 14 | [BindProperty] 15 | [Required] 16 | public string? AnotherRequiredField { get; set; } 17 | 18 | public IReadOnlyList Animals => new List 19 | { 20 | new("None", ""), 21 | new("Dog", "Dog"), 22 | new("Cat", "Cat"), 23 | new("Fish", "Fish"), 24 | }; 25 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/SubmitButton.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.SubmitButton 3 | 4 | @{ 5 | Layout = "Shared/_Layout"; 6 | } 7 | 8 |
9 | Required ASP.NET Checkboxes with hidden input 10 | 11 |
12 |
13 |

14 | This simple test demonstrates that we ensure that submit button values are submitted. 15 |

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | @if (Model.SubmitButtonValue is not null) { 26 | 27 | The submitted button value is "@Model.SubmitButtonValue". 28 | 29 | 30 |

The submitted form collection:

31 | 32 | @foreach (var item in Request.Form) { 33 | 34 | 35 | 36 | 37 | } 38 |
@item.Key@item.Value
39 | } 40 | 41 | 42 |
43 |
44 |
45 | 46 | @section Scripts { 47 | 51 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/SubmitButton.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace DemoWeb.Pages.Demos; 6 | 7 | public class SubmitButton : PageModel 8 | { 9 | [BindProperty] 10 | [Required] 11 | public string? SubmitButtonValue { get; set; } 12 | 13 | public void OnPost() 14 | { 15 | SubmitButtonValue ??= "Form submitted, but button value not submitted"; 16 | } 17 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/SubmitButtonDownload.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.Demos.SubmitButtonDownload 3 | 4 | @{ 5 | Layout = "Shared/_Layout"; 6 | } 7 | 8 |
9 | Required ASP.NET Checkboxes with hidden input 10 | 11 | @DateTime.UtcNow 12 | 13 |
14 |
15 |

16 | This simple test demonstrates that we ensure that submit button values are submitted 17 | correctly even when the form is posting to a download (and the page is not refreshed). 18 |

19 | 20 | Required: 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | @section Scripts { 29 | 33 | } -------------------------------------------------------------------------------- /sample/Pages/Demos/SubmitButtonDownload.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace DemoWeb.Pages.Demos; 6 | 7 | public class SubmitButtonDownload : PageModel 8 | { 9 | [BindProperty] 10 | [Required] 11 | public string? SubmitButtonValue { get; set; } 12 | 13 | [BindProperty] 14 | [Required] 15 | public string? SomeRequiredValue { get; set; } 16 | 17 | public IActionResult OnPost() 18 | { 19 | return File([0], "application/octet-stream", $"DEMO-{SubmitButtonValue}.txt"); 20 | } 21 | } -------------------------------------------------------------------------------- /sample/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DemoWeb.Pages.ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |

21 |

22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |

27 | -------------------------------------------------------------------------------- /sample/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace DemoWeb.Pages; 6 | 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /sample/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IndexModel 3 | @{ 4 | Layout = "Shared/_Layout"; 5 | ViewData["Title"] = "ASP.NET Client Validation Demo Page"; 6 | } 7 |

Other Demos

8 | 19 | 20 | @if (Model.StatusMessage != null) { 21 |

@Model.StatusMessage

22 | 23 |

Submitted Values

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
Id: @Model.Id
Control: @Model.Control
Selected Color: @Model.SelectedColor
Button Value: @Model.SubmitButton
43 | } 44 | 45 |
46 | Form 1 47 |
48 |
49 | 50 | 51 | 52 |
53 | 54 |
55 | 56 | 57 | 58 |
59 | 60 |
61 | 62 | 63 | 64 |
65 | 66 |
67 | @foreach (var color in Model.Colors) { 68 | checked 77 | }/> 78 | 79 | } 80 | 81 |
82 | 83 | 84 | 85 |
86 |
87 | 88 |
89 | Form 2 90 |
91 |
92 | 93 | 94 | 95 |
96 | 97 |
98 | 99 | 100 | 101 |
102 | 103 | 104 | 105 |
106 |
107 | 108 |
109 | Form 3 110 |
111 |
112 | 113 | 114 | 115 |
116 | 117 | 118 |
119 |
120 | 121 | @section scripts { 122 | 149 | } 150 | 151 |
152 | Form 4 (preventDefault()) 153 |
154 |
155 | 156 | 157 | 158 |
159 | 160 |
161 |
162 | 163 |
164 | External Form 165 |
166 | 167 | 168 | 169 |
170 | 171 |
172 | 173 | 174 |
175 | 176 |
177 | 178 | -------------------------------------------------------------------------------- /sample/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace DemoWeb.Pages; 6 | 7 | public class IndexModel : PageModel 8 | { 9 | [TempData] 10 | public string? StatusMessage { get; set; } 11 | 12 | [BindProperty] 13 | [Display(Name = "Id (42)")] 14 | [Required] 15 | [Remote("CheckRemote", "Validations", HttpMethod = "Post")] 16 | public string? Id { get; set; } 17 | 18 | [BindProperty] 19 | [Required] 20 | public string? Control { get; set; } 21 | 22 | [BindProperty] 23 | [Required] 24 | public string? TextArea { get; set; } 25 | 26 | [BindProperty] 27 | [Required] 28 | public string? SelectedColor { get; set; } 29 | 30 | [BindProperty] 31 | public string? SubmitButton { get; set; } 32 | 33 | public string[] Colors = new[] { "Red", "Green", "Blue" }; 34 | 35 | public IActionResult OnPost() 36 | { 37 | if (!ModelState.IsValid) 38 | { 39 | StatusMessage = "It failed"; 40 | return Page(); 41 | } 42 | 43 | StatusMessage = "Form submission successful"; 44 | return Page(); 45 | } 46 | 47 | [BindProperty] 48 | public InputModel Input { get; set; } = new(); 49 | 50 | public class InputModel 51 | { 52 | [Required] 53 | public string? SomeRequiredField { get; set; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /sample/Pages/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @ViewData["Title"] 5 | ASP.NET Client Validation Demo Page 6 | 7 | 8 | 9 | 10 | @RenderBody() 11 | 12 | 13 | @RenderSection("scripts", required: false) 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/Pages/Shared/_StatusMessage.cshtml: -------------------------------------------------------------------------------- 1 | @model string? 2 | 3 | @if (Model is { Length: > 0 } statusMessage) { 4 |

5 | @statusMessage 6 |

7 | } -------------------------------------------------------------------------------- /sample/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using DemoWeb.Pages 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /sample/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | // Add services to the container. 4 | builder.Services.AddRazorPages(); 5 | 6 | var app = builder.Build(); 7 | 8 | // Configure the HTTP request pipeline. 9 | if (!app.Environment.IsDevelopment()) 10 | { 11 | app.UseExceptionHandler("/Error"); 12 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 13 | app.UseHsts(); 14 | } 15 | 16 | app.UseHttpsRedirection(); 17 | app.UseStaticFiles(); 18 | 19 | app.UseRouting(); 20 | app.UseAuthorization(); 21 | 22 | app.MapRazorPages(); 23 | 24 | app.MapControllerRoute( 25 | name: "default", 26 | pattern: "{controller=Home}/{action=Index}/{id?}"); 27 | 28 | app.Run(); 29 | -------------------------------------------------------------------------------- /sample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:19376", 7 | "sslPort": 44333 8 | } 9 | }, 10 | "profiles": { 11 | "http": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "http://localhost:5045", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "https": { 21 | "commandName": "Project", 22 | "dotnetRunMessages": true, 23 | "launchBrowser": true, 24 | "applicationUrl": "https://localhost:7109;http://localhost:5045", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | }, 29 | "IIS Express": { 30 | "commandName": "IISExpress", 31 | "launchBrowser": true, 32 | "environmentVariables": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sample/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /sample/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 14px; 3 | } 4 | 5 | @media (min-width: 768px) { 6 | html { 7 | font-size: 16px; 8 | } 9 | } 10 | 11 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 12 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 13 | } 14 | 15 | html { 16 | position: relative; 17 | min-height: 100%; 18 | } 19 | 20 | body { 21 | margin-bottom: 60px; 22 | } 23 | 24 | .form-field { 25 | margin-bottom: 1em; 26 | border: solid 1px #eee; 27 | padding: 4px; 28 | } 29 | 30 | .status-message { 31 | font-size: 2em; 32 | font-weight: bold; 33 | } 34 | 35 | .input-validation-error + label { 36 | color: red; 37 | } 38 | 39 | .input-validation-valid + label { 40 | color: green; 41 | } 42 | 43 | table.form-data { 44 | margin-bottom: 10px; 45 | } 46 | 47 | table.form-data th { 48 | text-align: left; 49 | } 50 | 51 | .results { 52 | margin-left: 4px; 53 | color: cadetblue; 54 | } -------------------------------------------------------------------------------- /sample/wwwroot/dist/aspnet-validation.css: -------------------------------------------------------------------------------- 1 | .field-validation-error { 2 | color: red; 3 | font-style: italic; 4 | } 5 | 6 | .input-validation-error { 7 | border-color: red; 8 | outline-color: red; 9 | } 10 | 11 | .input-validation-valid { 12 | border-color: green; 13 | outline-color: green; 14 | } 15 | 16 | .validation-summary-errors { 17 | color: red; 18 | font-style: italic; 19 | } 20 | 21 | .validation-summary-valid span { 22 | display: none; 23 | } 24 | -------------------------------------------------------------------------------- /sample/wwwroot/dist/aspnet-validation.min.js: -------------------------------------------------------------------------------- 1 | !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()}(self,(()=>(()=>{"use strict";var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{MvcValidationProviders:()=>u,ValidationService:()=>d,isValidatable:()=>a});var n=function(e,t,n,r){return new(n||(n=Promise))((function(i,a){function s(e){try{l(r.next(e))}catch(e){a(e)}}function o(e){try{l(r.throw(e))}catch(e){a(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(s,o)}l((r=r.apply(e,t||[])).next())}))},r=function(e,t){var n,r,i,a,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return a={next:o(0),throw:o(1),return:o(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function o(o){return function(l){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,o[0]&&(s=0)),s;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!((i=(i=s.trys).length>0&&i[i.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]-1){var s=n.substring(0,i)+"."+r,l=document.getElementsByName(s)[0];if(a(l))return l}return e.form.querySelector(o("[name=".concat(r,"]")))}var u=function(){this.required=function(e,t,n){var r=t.type.toLowerCase();if("checkbox"===r||"radio"===r){for(var i=0,a=Array.from(t.form.querySelectorAll(o("[name='".concat(t.name,"'][type='").concat(r,"']"))));ii)return!1}return!0},this.compare=function(e,t,n){if(!n.other)return!0;var r=l(t,n.other);return!r||r.value===e},this.range=function(e,t,n){if(!e)return!0;var r=parseFloat(e);return!(isNaN(r)||n.min&&rparseFloat(n.max))},this.regex=function(e,t,n){return!e||!n.pattern||new RegExp(n.pattern).test(e)},this.email=function(e,t,n){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,n){if(!e)return!0;if(/[^0-9 \-]+/.test(e))return!1;var r,i,a=0,s=0,o=!1;if((e=e.replace(/\D/g,"")).length<13||e.length>19)return!1;for(r=e.length-1;r>=0;r--)i=e.charAt(r),s=parseInt(i,10),o&&(s*=2)>9&&(s-=9),a+=s,o=!o;return a%10==0},this.url=function(e,t,n){if(!e)return!0;var r=e.toLowerCase();return r.indexOf("http://")>-1||r.indexOf("https://")>-1||r.indexOf("ftp://")>-1},this.phone=function(e,t,n){return!e||!/[\+\-\s][\-\s]/g.test(e)&&/^\+?[0-9\-\s]+$/.test(e)},this.remote=function(e,t,n){if(!e)return!0;for(var r=n.additionalfields.split(","),i={},a=0,s=r;a=200&&r.status<300){var i=JSON.parse(r.responseText);e(i)}else t({status:r.status,statusText:r.statusText,data:r.responseText})},r.onerror=function(e){t({status:r.status,statusText:r.statusText,data:r.responseText})}}))}},d=function(){function e(e){var t=this;this.providers={},this.messageFor={},this.elementUIDs=[],this.elementByUID={},this.formInputs={},this.validators={},this.formEvents={},this.inputEvents={},this.summary={},this.debounce=300,this.allowHiddenFields=!1,this.validateForm=function(e,i){return n(t,void 0,void 0,(function(){var t,n,a;return r(this,(function(r){switch(r.label){case 0:if(!(e instanceof HTMLFormElement))throw new Error("validateForm() can only be called on
elements");return t=this.getElementUID(e),n=this.formEvents[t],(a=!n)?[3,2]:[4,n(void 0,i)];case 1:a=r.sent(),r.label=2;case 2:return[2,a]}}))}))},this.validateField=function(e,i){return n(t,void 0,void 0,(function(){var t,n,a;return r(this,(function(r){switch(r.label){case 0:return t=this.getElementUID(e),n=this.inputEvents[t],(a=!n)?[3,2]:[4,n(void 0,i)];case 1:a=r.sent(),r.label=2;case 2:return[2,a]}}))}))},this.preValidate=function(e){e.preventDefault(),e.stopImmediatePropagation()},this.handleValidated=function(e,n,r){if(!(e instanceof HTMLFormElement))throw new Error("handleValidated() can only be called on elements");n?r&&t.submitValidForm(e,r):t.focusFirstInvalid(e)},this.submitValidForm=function(e,t){if(!(e instanceof HTMLFormElement))throw new Error("submitValidForm() can only be called on elements");var n=new SubmitEvent("submit",t);if(e.dispatchEvent(n)){var r=t.submitter,i=null,a=e.action;if(r){var s=r.getAttribute("name");s&&((i=document.createElement("input")).type="hidden",i.name=s,i.value=r.getAttribute("value"),e.appendChild(i));var o=r.getAttribute("formaction");o&&(e.action=o)}try{e.submit()}finally{i&&e.removeChild(i),e.action=a}}},this.focusFirstInvalid=function(e){if(!(e instanceof HTMLFormElement))throw new Error("focusFirstInvalid() can only be called on elements");var n=t.getElementUID(e),r=t.formInputs[n],i=null==r?void 0:r.find((function(e){return t.summary[e]}));if(i){var a=t.elementByUID[i];a instanceof HTMLElement&&a.focus()}},this.isValid=function(e,n,r){if(void 0===n&&(n=!0),!(e instanceof HTMLFormElement))throw new Error("isValid() can only be called on elements");n&&t.validateForm(e,r);var i=t.getElementUID(e),a=t.formInputs[i];return!(!0===(null==a?void 0:a.some((function(e){return t.summary[e]}))))},this.isFieldValid=function(e,n,r){void 0===n&&(n=!0),n&&t.validateField(e,r);var i=t.getElementUID(e);return void 0===t.summary[i]},this.options={root:document.body,watch:!1,addNoValidate:!0},this.ValidationInputCssClassName="input-validation-error",this.ValidationInputValidCssClassName="input-validation-valid",this.ValidationMessageCssClassName="field-validation-error",this.ValidationMessageValidCssClassName="field-validation-valid",this.ValidationSummaryCssClassName="validation-summary-errors",this.ValidationSummaryValidCssClassName="validation-summary-valid",this.logger=e||i}return e.prototype.addProvider=function(e,t){this.providers[e]||(this.logger.log("Registered provider: %s",e),this.providers[e]=t)},e.prototype.addMvcProviders=function(){var e=new u;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(e,t){for(var n=0,r=Array.from(e.querySelectorAll("span[form]"));n=0?a.splice(s,1):this.logger.log("Validation element for '%s' was already removed",name,t)}}}},e.prototype.parseDirectives=function(e){for(var t={},n={},r=0;r=0?(i.splice(a,1),i.length||(null===(n=this.formEvents[r])||void 0===n||n.remove(),delete this.formEvents[r],delete this.formInputs[r],delete this.messageFor[r])):this.logger.log("Form input for UID '%s' was already removed",t)}},e.prototype.addInput=function(e){var t,i=this,a=this.getElementUID(e),s=this.parseDirectives(e.attributes);if(this.validators[a]=this.createValidator(e,s),e.form&&this.trackFormInput(e.form,a),!this.inputEvents[a]){var o=function(t,s){return n(i,void 0,void 0,(function(){var n,i,o;return r(this,(function(r){switch(r.label){case 0:if(!(n=this.validators[a]))return[2,!0];if(!e.dataset.valEvent&&t&&"input"===t.type&&!e.classList.contains(this.ValidationInputCssClassName))return[2,!0];this.logger.log("Validating",{event:t}),r.label=1;case 1:return r.trys.push([1,3,,4]),[4,n()];case 2:return i=r.sent(),s(i),[2,i];case 3:return o=r.sent(),this.logger.log("Validation error",o),[2,!1];case 4:return[2]}}))}))},l=null;o.debounced=function(e,t){null!==l&&clearTimeout(l),l=setTimeout((function(){o(e,t)}),i.debounce)};var u=e instanceof HTMLSelectElement?"change":"input change",d=(null!==(t=e.dataset.valEvent)&&void 0!==t?t:u).split(" ");d.forEach((function(t){e.addEventListener(t,o.debounced)})),o.remove=function(){d.forEach((function(t){e.removeEventListener(t,o.debounced)}))},this.inputEvents[a]=o}},e.prototype.removeInput=function(e){var t=this.getElementUID(e),n=this.inputEvents[t];(null==n?void 0:n.remove)&&(n.remove(),delete n.remove),delete this.summary[t],delete this.inputEvents[t],delete this.validators[t],e.form&&this.untrackFormInput(e.form,t)},e.prototype.scanInputs=function(e,t){var n=Array.from(e.querySelectorAll(o('[data-val="true"]')));a(e)&&"true"===e.getAttribute("data-val")&&n.push(e);for(var r=0;r-1)){var i=document.createElement("li");i.innerHTML=this.summary[n],t.appendChild(i),e.push(this.summary[n])}}return t},e.prototype.renderSummary=function(){var e=document.querySelectorAll('[data-valmsg-summary="true"]');if(e.length){var t=JSON.stringify(this.summary,Object.keys(this.summary).sort());if(t!==this.renderedSummaryJSON){this.renderedSummaryJSON=t;for(var n=this.createSummaryDOM(),r=0;r?@\[\\\]^`{|}~])/g, "\\$1"); 42 | } 43 | 44 | function getModelPrefix(fieldName) { 45 | return fieldName.substr(0, fieldName.lastIndexOf(".") + 1); 46 | } 47 | 48 | function appendModelPrefix(value, prefix) { 49 | if (value.indexOf("*.") === 0) { 50 | value = value.replace("*.", prefix); 51 | } 52 | return value; 53 | } 54 | 55 | function onError(error, inputElement) { // 'this' is the form element 56 | var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"), 57 | replaceAttrValue = container.attr("data-valmsg-replace"), 58 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null; 59 | 60 | container.removeClass("field-validation-valid").addClass("field-validation-error"); 61 | error.data("unobtrusiveContainer", container); 62 | 63 | if (replace) { 64 | container.empty(); 65 | error.removeClass("input-validation-error").appendTo(container); 66 | } 67 | else { 68 | error.hide(); 69 | } 70 | } 71 | 72 | function onErrors(event, validator) { // 'this' is the form element 73 | var container = $(this).find("[data-valmsg-summary=true]"), 74 | list = container.find("ul"); 75 | 76 | if (list && list.length && validator.errorList.length) { 77 | list.empty(); 78 | container.addClass("validation-summary-errors").removeClass("validation-summary-valid"); 79 | 80 | $.each(validator.errorList, function () { 81 | $("
  • ").html(this.message).appendTo(list); 82 | }); 83 | } 84 | } 85 | 86 | function onSuccess(error) { // 'this' is the form element 87 | var container = error.data("unobtrusiveContainer"); 88 | 89 | if (container) { 90 | var replaceAttrValue = container.attr("data-valmsg-replace"), 91 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null; 92 | 93 | container.addClass("field-validation-valid").removeClass("field-validation-error"); 94 | error.removeData("unobtrusiveContainer"); 95 | 96 | if (replace) { 97 | container.empty(); 98 | } 99 | } 100 | } 101 | 102 | function onReset(event) { // 'this' is the form element 103 | var $form = $(this), 104 | key = '__jquery_unobtrusive_validation_form_reset'; 105 | if ($form.data(key)) { 106 | return; 107 | } 108 | // Set a flag that indicates we're currently resetting the form. 109 | $form.data(key, true); 110 | try { 111 | $form.data("validator").resetForm(); 112 | } finally { 113 | $form.removeData(key); 114 | } 115 | 116 | $form.find(".validation-summary-errors") 117 | .addClass("validation-summary-valid") 118 | .removeClass("validation-summary-errors"); 119 | $form.find(".field-validation-error") 120 | .addClass("field-validation-valid") 121 | .removeClass("field-validation-error") 122 | .removeData("unobtrusiveContainer") 123 | .find(">*") // If we were using valmsg-replace, get the underlying error 124 | .removeData("unobtrusiveContainer"); 125 | } 126 | 127 | function validationInfo(form) { 128 | var $form = $(form), 129 | result = $form.data(data_validation), 130 | onResetProxy = $.proxy(onReset, form), 131 | defaultOptions = $jQval.unobtrusive.options || {}, 132 | execInContext = function (name, args) { 133 | var func = defaultOptions[name]; 134 | func && $.isFunction(func) && func.apply(form, args); 135 | }; 136 | 137 | if (!result) { 138 | result = { 139 | options: { // options structure passed to jQuery Validate's validate() method 140 | errorClass: defaultOptions.errorClass || "input-validation-error", 141 | errorElement: defaultOptions.errorElement || "span", 142 | errorPlacement: function () { 143 | onError.apply(form, arguments); 144 | execInContext("errorPlacement", arguments); 145 | }, 146 | invalidHandler: function () { 147 | onErrors.apply(form, arguments); 148 | execInContext("invalidHandler", arguments); 149 | }, 150 | messages: {}, 151 | rules: {}, 152 | success: function () { 153 | onSuccess.apply(form, arguments); 154 | execInContext("success", arguments); 155 | } 156 | }, 157 | attachValidation: function () { 158 | $form 159 | .off("reset." + data_validation, onResetProxy) 160 | .on("reset." + data_validation, onResetProxy) 161 | .validate(this.options); 162 | }, 163 | validate: function () { // a validation function that is called by unobtrusive Ajax 164 | $form.validate(); 165 | return $form.valid(); 166 | } 167 | }; 168 | $form.data(data_validation, result); 169 | } 170 | 171 | return result; 172 | } 173 | 174 | $jQval.unobtrusive = { 175 | adapters: [], 176 | 177 | parseElement: function (element, skipAttach) { 178 | /// 179 | /// Parses a single HTML element for unobtrusive validation attributes. 180 | /// 181 | /// The HTML element to be parsed. 182 | /// [Optional] true to skip attaching the 183 | /// validation to the form. If parsing just this single element, you should specify true. 184 | /// If parsing several elements, you should specify false, and manually attach the validation 185 | /// to the form when you are finished. The default is false. 186 | var $element = $(element), 187 | form = $element.parents("form")[0], 188 | valInfo, rules, messages; 189 | 190 | if (!form) { // Cannot do client-side validation without a form 191 | return; 192 | } 193 | 194 | valInfo = validationInfo(form); 195 | valInfo.options.rules[element.name] = rules = {}; 196 | valInfo.options.messages[element.name] = messages = {}; 197 | 198 | $.each(this.adapters, function () { 199 | var prefix = "data-val-" + this.name, 200 | message = $element.attr(prefix), 201 | paramValues = {}; 202 | 203 | if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy) 204 | prefix += "-"; 205 | 206 | $.each(this.params, function () { 207 | paramValues[this] = $element.attr(prefix + this); 208 | }); 209 | 210 | this.adapt({ 211 | element: element, 212 | form: form, 213 | message: message, 214 | params: paramValues, 215 | rules: rules, 216 | messages: messages 217 | }); 218 | } 219 | }); 220 | 221 | $.extend(rules, { "__dummy__": true }); 222 | 223 | if (!skipAttach) { 224 | valInfo.attachValidation(); 225 | } 226 | }, 227 | 228 | parse: function (selector) { 229 | /// 230 | /// Parses all the HTML elements in the specified selector. It looks for input elements decorated 231 | /// with the [data-val=true] attribute value and enables validation according to the data-val-* 232 | /// attribute values. 233 | /// 234 | /// Any valid jQuery selector. 235 | 236 | // $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one 237 | // element with data-val=true 238 | var $selector = $(selector), 239 | $forms = $selector.parents() 240 | .addBack() 241 | .filter("form") 242 | .add($selector.find("form")) 243 | .has("[data-val=true]"); 244 | 245 | $selector.find("[data-val=true]").each(function () { 246 | $jQval.unobtrusive.parseElement(this, true); 247 | }); 248 | 249 | $forms.each(function () { 250 | var info = validationInfo(this); 251 | if (info) { 252 | info.attachValidation(); 253 | } 254 | }); 255 | } 256 | }; 257 | 258 | adapters = $jQval.unobtrusive.adapters; 259 | 260 | adapters.add = function (adapterName, params, fn) { 261 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation. 262 | /// The name of the adapter to be added. This matches the name used 263 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 264 | /// [Optional] An array of parameter names (strings) that will 265 | /// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and 266 | /// mmmm is the parameter name). 267 | /// The function to call, which adapts the values from the HTML 268 | /// attributes into jQuery Validate rules and/or messages. 269 | /// 270 | if (!fn) { // Called with no params, just a function 271 | fn = params; 272 | params = []; 273 | } 274 | this.push({ name: adapterName, params: params, adapt: fn }); 275 | return this; 276 | }; 277 | 278 | adapters.addBool = function (adapterName, ruleName) { 279 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 280 | /// the jQuery Validate validation rule has no parameter values. 281 | /// The name of the adapter to be added. This matches the name used 282 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 283 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 284 | /// of adapterName will be used instead. 285 | /// 286 | return this.add(adapterName, function (options) { 287 | setValidationValues(options, ruleName || adapterName, true); 288 | }); 289 | }; 290 | 291 | adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) { 292 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 293 | /// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and 294 | /// one for min-and-max). The HTML parameters are expected to be named -min and -max. 295 | /// The name of the adapter to be added. This matches the name used 296 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 297 | /// The name of the jQuery Validate rule to be used when you only 298 | /// have a minimum value. 299 | /// The name of the jQuery Validate rule to be used when you only 300 | /// have a maximum value. 301 | /// The name of the jQuery Validate rule to be used when you 302 | /// have both a minimum and maximum value. 303 | /// [Optional] The name of the HTML attribute that 304 | /// contains the minimum value. The default is "min". 305 | /// [Optional] The name of the HTML attribute that 306 | /// contains the maximum value. The default is "max". 307 | /// 308 | return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) { 309 | var min = options.params.min, 310 | max = options.params.max; 311 | 312 | if (min && max) { 313 | setValidationValues(options, minMaxRuleName, [min, max]); 314 | } 315 | else if (min) { 316 | setValidationValues(options, minRuleName, min); 317 | } 318 | else if (max) { 319 | setValidationValues(options, maxRuleName, max); 320 | } 321 | }); 322 | }; 323 | 324 | adapters.addSingleVal = function (adapterName, attribute, ruleName) { 325 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 326 | /// the jQuery Validate validation rule has a single value. 327 | /// The name of the adapter to be added. This matches the name used 328 | /// in the data-val-nnnn HTML attribute(where nnnn is the adapter name). 329 | /// [Optional] The name of the HTML attribute that contains the value. 330 | /// The default is "val". 331 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 332 | /// of adapterName will be used instead. 333 | /// 334 | return this.add(adapterName, [attribute || "val"], function (options) { 335 | setValidationValues(options, ruleName || adapterName, options.params[attribute]); 336 | }); 337 | }; 338 | 339 | $jQval.addMethod("__dummy__", function (value, element, params) { 340 | return true; 341 | }); 342 | 343 | $jQval.addMethod("regex", function (value, element, params) { 344 | var match; 345 | if (this.optional(element)) { 346 | return true; 347 | } 348 | 349 | match = new RegExp(params).exec(value); 350 | return (match && (match.index === 0) && (match[0].length === value.length)); 351 | }); 352 | 353 | $jQval.addMethod("nonalphamin", function (value, element, nonalphamin) { 354 | var match; 355 | if (nonalphamin) { 356 | match = value.match(/\W/g); 357 | match = match && match.length >= nonalphamin; 358 | } 359 | return match; 360 | }); 361 | 362 | if ($jQval.methods.extension) { 363 | adapters.addSingleVal("accept", "mimtype"); 364 | adapters.addSingleVal("extension", "extension"); 365 | } else { 366 | // for backward compatibility, when the 'extension' validation method does not exist, such as with versions 367 | // of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for 368 | // validating the extension, and ignore mime-type validations as they are not supported. 369 | adapters.addSingleVal("extension", "extension", "accept"); 370 | } 371 | 372 | adapters.addSingleVal("regex", "pattern"); 373 | adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"); 374 | adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range"); 375 | adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength"); 376 | adapters.add("equalto", ["other"], function (options) { 377 | var prefix = getModelPrefix(options.element.name), 378 | other = options.params.other, 379 | fullOtherName = appendModelPrefix(other, prefix), 380 | element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0]; 381 | 382 | setValidationValues(options, "equalTo", element); 383 | }); 384 | adapters.add("required", function (options) { 385 | // jQuery Validate equates "required" with "mandatory" for checkbox elements 386 | if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") { 387 | setValidationValues(options, "required", true); 388 | } 389 | }); 390 | adapters.add("remote", ["url", "type", "additionalfields"], function (options) { 391 | var value = { 392 | url: options.params.url, 393 | type: options.params.type || "GET", 394 | data: {} 395 | }, 396 | prefix = getModelPrefix(options.element.name); 397 | 398 | $.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) { 399 | var paramName = appendModelPrefix(fieldName, prefix); 400 | value.data[paramName] = function () { 401 | var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']"); 402 | // For checkboxes and radio buttons, only pick up values from checked fields. 403 | if (field.is(":checkbox")) { 404 | return field.filter(":checked").val() || field.filter(":hidden").val() || ''; 405 | } 406 | else if (field.is(":radio")) { 407 | return field.filter(":checked").val() || ''; 408 | } 409 | return field.val(); 410 | }; 411 | }); 412 | 413 | setValidationValues(options, "remote", value); 414 | }); 415 | adapters.add("password", ["min", "nonalphamin", "regex"], function (options) { 416 | if (options.params.min) { 417 | setValidationValues(options, "minlength", options.params.min); 418 | } 419 | if (options.params.nonalphamin) { 420 | setValidationValues(options, "nonalphamin", options.params.nonalphamin); 421 | } 422 | if (options.params.regex) { 423 | setValidationValues(options, "regex", options.params.regex); 424 | } 425 | }); 426 | adapters.add("fileextensions", ["extensions"], function (options) { 427 | setValidationValues(options, "extension", options.params.extensions); 428 | }); 429 | 430 | $(function () { 431 | $jQval.unobtrusive.parse(document); 432 | }); 433 | 434 | return $jQval.unobtrusive; 435 | })); 436 | -------------------------------------------------------------------------------- /sample/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Unobtrusive validation support library for jQuery and jQuery Validate 4 | * Copyright (c) .NET Foundation. All rights reserved. 5 | * Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 6 | * @version v4.0.0 7 | */ 8 | !function(a){"function"==typeof define&&define.amd?define("jquery.validate.unobtrusive",["jquery-validation"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery-validation")):jQuery.validator.unobtrusive=a(jQuery)}(function(s){var a,o=s.validator,d="unobtrusiveValidation";function l(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function u(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function n(a){return a.substr(0,a.lastIndexOf(".")+1)}function m(a,e){return a=0===a.indexOf("*.")?a.replace("*.",e):a}function f(a){var e=s(this),n="__jquery_unobtrusive_validation_form_reset";if(!e.data(n)){e.data(n,!0);try{e.data("validator").resetForm()}finally{e.removeData(n)}e.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),e.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function p(n){function a(a,e){(a=r[a])&&s.isFunction(a)&&a.apply(n,e)}var e=s(n),t=e.data(d),i=s.proxy(f,n),r=o.unobtrusive.options||{};return t||(t={options:{errorClass:r.errorClass||"input-validation-error",errorElement:r.errorElement||"span",errorPlacement:function(){!function(a,e){var e=s(this).find("[data-valmsg-for='"+u(e[0].name)+"']"),n=(n=e.attr("data-valmsg-replace"))?!1!==s.parseJSON(n):null;e.removeClass("field-validation-valid").addClass("field-validation-error"),a.data("unobtrusiveContainer",e),n?(e.empty(),a.removeClass("input-validation-error").appendTo(e)):a.hide()}.apply(n,arguments),a("errorPlacement",arguments)},invalidHandler:function(){!function(a,e){var n=s(this).find("[data-valmsg-summary=true]"),t=n.find("ul");t&&t.length&&e.errorList.length&&(t.empty(),n.addClass("validation-summary-errors").removeClass("validation-summary-valid"),s.each(e.errorList,function(){s("
  • ").html(this.message).appendTo(t)}))}.apply(n,arguments),a("invalidHandler",arguments)},messages:{},rules:{},success:function(){!function(a){var e,n=a.data("unobtrusiveContainer");n&&(e=(e=n.attr("data-valmsg-replace"))?s.parseJSON(e):null,n.addClass("field-validation-valid").removeClass("field-validation-error"),a.removeData("unobtrusiveContainer"),e&&n.empty())}.apply(n,arguments),a("success",arguments)}},attachValidation:function(){e.off("reset."+d,i).on("reset."+d,i).validate(this.options)},validate:function(){return e.validate(),e.valid()}},e.data(d,t)),t}return o.unobtrusive={adapters:[],parseElement:function(t,a){var e,i,r,o=s(t),d=o.parents("form")[0];d&&((e=p(d)).options.rules[t.name]=i={},e.options.messages[t.name]=r={},s.each(this.adapters,function(){var a="data-val-"+this.name,e=o.attr(a),n={};void 0!==e&&(a+="-",s.each(this.params,function(){n[this]=o.attr(a+this)}),this.adapt({element:t,form:d,message:e,params:n,rules:i,messages:r}))}),s.extend(i,{__dummy__:!0}),a||e.attachValidation())},parse:function(a){var a=s(a),e=a.parents().addBack().filter("form").add(a.find("form")).has("[data-val=true]");a.find("[data-val=true]").each(function(){o.unobtrusive.parseElement(this,!0)}),e.each(function(){var a=p(this);a&&a.attachValidation()})}},(a=o.unobtrusive.adapters).add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},a.addBool=function(e,n){return this.add(e,function(a){l(a,n||e,!0)})},a.addMinMax=function(a,t,i,r,e,n){return this.add(a,[e||"min",n||"max"],function(a){var e=a.params.min,n=a.params.max;e&&n?l(a,r,[e,n]):e?l(a,t,e):n&&l(a,i,n)})},a.addSingleVal=function(e,n,t){return this.add(e,[n||"val"],function(a){l(a,t||e,a.params[n])})},o.addMethod("__dummy__",function(a,e,n){return!0}),o.addMethod("regex",function(a,e,n){return!!this.optional(e)||(e=new RegExp(n).exec(a))&&0===e.index&&e[0].length===a.length}),o.addMethod("nonalphamin",function(a,e,n){var t;return t=n?(t=a.match(/\W/g))&&t.length>=n:t}),o.methods.extension?(a.addSingleVal("accept","mimtype"),a.addSingleVal("extension","extension")):a.addSingleVal("extension","extension","accept"),a.addSingleVal("regex","pattern"),a.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),a.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),a.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),a.add("equalto",["other"],function(a){var e=n(a.element.name),e=m(a.params.other,e);l(a,"equalTo",s(a.form).find(":input").filter("[name='"+u(e)+"']")[0])}),a.add("required",function(a){"INPUT"===a.element.tagName.toUpperCase()&&"CHECKBOX"===a.element.type.toUpperCase()||l(a,"required",!0)}),a.add("remote",["url","type","additionalfields"],function(t){var i={url:t.params.url,type:t.params.type||"GET",data:{}},r=n(t.element.name);s.each((t.params.additionalfields||t.element.name).replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g),function(a,e){var n=m(e,r);i.data[n]=function(){var a=s(t.form).find(":input").filter("[name='"+u(n)+"']");return a.is(":checkbox")?a.filter(":checked").val()||a.filter(":hidden").val()||"":a.is(":radio")?a.filter(":checked").val()||"":a.val()}}),l(t,"remote",i)}),a.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&l(a,"minlength",a.params.min),a.params.nonalphamin&&l(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&l(a,"regex",a.params.regex)}),a.add("fileextensions",["extensions"],function(a){l(a,"extension",a.params.extensions)}),s(function(){o.unobtrusive.parse(document)}),o.unobtrusive}); -------------------------------------------------------------------------------- /sample/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /sample/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | npm install 4 | npm run-script build -------------------------------------------------------------------------------- /script/build.ps1: -------------------------------------------------------------------------------- 1 | npm run build 2 | -------------------------------------------------------------------------------- /script/prebuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const dist = path.join(__dirname, '../dist'); 5 | try { 6 | fs.mkdirSync(dist, { recursive: true }); 7 | } catch (e) { 8 | if (e.code !== 'EEXIST') throw e; 9 | } 10 | 11 | ['aspnet-validation.css'].forEach(file => { 12 | console.log(`Copying src/${file} to ${dist}`); 13 | fs.copyFileSync( 14 | path.join(__dirname, `../src/${file}`), 15 | path.join(dist, file)); 16 | }) -------------------------------------------------------------------------------- /src/aspnet-validation.css: -------------------------------------------------------------------------------- 1 | .field-validation-error { 2 | color: red; 3 | font-style: italic; 4 | } 5 | 6 | .input-validation-error { 7 | border-color: red; 8 | outline-color: red; 9 | } 10 | 11 | .input-validation-valid { 12 | border-color: green; 13 | outline-color: green; 14 | } 15 | 16 | .validation-summary-errors { 17 | color: red; 18 | font-style: italic; 19 | } 20 | 21 | .validation-summary-valid span { 22 | display: none; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "noImplicitThis": true, 5 | "alwaysStrict": true, 6 | "strictNullChecks": false, 7 | "strictFunctionTypes": true, 8 | "strictPropertyInitialization": false, 9 | "skipLibCheck": true, 10 | 11 | "noUnusedLocals": false, 12 | "noUnusedParameters": false, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | 16 | "experimentalDecorators": true, 17 | "allowSyntheticDefaultImports": true, 18 | 19 | "noEmit": true, 20 | "target": "es5", 21 | "module": "es2015", 22 | "moduleResolution": "node", 23 | "importHelpers": false, 24 | "jsx": "react", 25 | 26 | "lib": [ 27 | "dom", 28 | "es5", 29 | "es2015", 30 | "es2018.promise" 31 | ] 32 | }, 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple IDictionary 3 | */ 4 | export interface StringKeyValuePair { 5 | [key: string]: string; 6 | } 7 | /** 8 | * A simple logging interface that mirrors the Console object. 9 | */ 10 | export interface Logger { 11 | log(message: string, ...args: any[]): void; 12 | warn(message: string, ...args: any[]): void; 13 | } 14 | /** 15 | * An `HTMLElement` that can be validated (`input`, `select`, `textarea`). 16 | */ 17 | export type ValidatableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; 18 | /** 19 | * Checks if `element` is validatable (`input`, `select`, `textarea`). 20 | * @param element The element to check. 21 | * @returns `true` if validatable, otherwise `false`. 22 | */ 23 | export declare const isValidatable: (element: Node) => element is ValidatableElement; 24 | /** 25 | * Parameters passed into validation providers from the element attributes. 26 | * error property is read from data-val-[Provider Name] attribute. 27 | * params property is populated from data-val-[Provider Name]-[Parameter Name] attributes. 28 | */ 29 | export interface ValidationDirectiveBindings { 30 | error: string; 31 | params: StringKeyValuePair; 32 | } 33 | /** 34 | * A key-value pair describing what validations to enforce to an input element, with respective parameters. 35 | */ 36 | export type ValidationDirective = { 37 | [key: string]: ValidationDirectiveBindings; 38 | }; 39 | /** 40 | * Validation plugin signature with multitype return. 41 | * Boolean return signifies the validation result, which uses the default validation error message read from the element attribute. 42 | * String return signifies failed validation, which then will be used as the validation error message. 43 | * Promise return signifies asynchronous plugin behavior, with same behavior as Boolean or String. 44 | */ 45 | export type ValidationProvider = (value: string, element: ValidatableElement, params: StringKeyValuePair) => boolean | string | Promise; 46 | /** 47 | * Callback to receive the result of validating a form. 48 | */ 49 | export type ValidatedCallback = (success: boolean) => void; 50 | /** 51 | * Contains default implementations for ASP.NET Core MVC validation attributes. 52 | */ 53 | export declare class MvcValidationProviders { 54 | /** 55 | * Validates whether the input has a value. 56 | */ 57 | required: ValidationProvider; 58 | /** 59 | * Validates whether the input value satisfies the length contstraint. 60 | */ 61 | stringLength: ValidationProvider; 62 | /** 63 | * Validates whether the input value is equal to another input value. 64 | */ 65 | compare: ValidationProvider; 66 | /** 67 | * Validates whether the input value is a number within a given range. 68 | */ 69 | range: ValidationProvider; 70 | /** 71 | * Validates whether the input value satisfies a regular expression pattern. 72 | */ 73 | regex: ValidationProvider; 74 | /** 75 | * Validates whether the input value is an email in accordance to RFC822 specification, with a top level domain. 76 | */ 77 | email: ValidationProvider; 78 | /** 79 | * Validates whether the input value is a credit card number, with Luhn's Algorithm. 80 | */ 81 | creditcard: ValidationProvider; 82 | /** 83 | * Validates whether the input value is a URL. 84 | */ 85 | url: ValidationProvider; 86 | /** 87 | * Validates whether the input value is a phone number. 88 | */ 89 | phone: ValidationProvider; 90 | /** 91 | * Asynchronously validates the input value to a JSON GET API endpoint. 92 | */ 93 | remote: ValidationProvider; 94 | } 95 | /** 96 | * Configuration for @type {ValidationService}. 97 | */ 98 | export interface ValidationServiceOptions { 99 | watch: boolean; 100 | root: ParentNode; 101 | addNoValidate: boolean; 102 | } 103 | /** 104 | * Responsible for managing the DOM elements and running the validation providers. 105 | */ 106 | export declare class ValidationService { 107 | /** 108 | * A key-value collection of loaded validation plugins. 109 | */ 110 | private providers; 111 | /** 112 | * A key-value collection of form UIDs and their elements for displaying validation messages for an input (by DOM name). 113 | */ 114 | private messageFor; 115 | /** 116 | * A list of managed elements, each having a randomly assigned unique identifier (UID). 117 | */ 118 | private elementUIDs; 119 | /** 120 | * A key-value collection of UID to Element for quick lookup. 121 | */ 122 | private elementByUID; 123 | /** 124 | * A key-value collection of input UIDs for a UID. 125 | */ 126 | private formInputs; 127 | /** 128 | * A key-value map for input UID to its validator factory. 129 | */ 130 | private validators; 131 | /** 132 | * A key-value map for form UID to its trigger element (submit event for ). 133 | */ 134 | private formEvents; 135 | /** 136 | * A key-value map for element UID to its trigger element (input event for