Skip to content

Commit

Permalink
Merge pull request #51 from haacked/haacked/48-required-validation-on…
Browse files Browse the repository at this point in the history
…-optional-checkboxes

Fix validation for checkboxes rendered with accompanying hidden input.
  • Loading branch information
haacked authored Jul 3, 2023
2 parents 3bdd41f + 11c70b6 commit 17f8a79
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 55 deletions.
63 changes: 63 additions & 0 deletions Pages/Demos/Checkboxes.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@page
@model DemoWeb.Pages.Demos.Checkboxes

@{
Layout = "Shared/_Layout";
}

<partial name="Shared/_StatusMessage" model="Model.StatusMessage"/>

<fieldset>
<legend>Required ASP.NET Checkboxes with hidden input</legend>

<form method="post">
<div class="form-field">
<p>
ASP.NET renders a checkbox and a hidden input for each boolean property.
The hidden input is used to ensure that a value is sent for the checkbox,
even if it is unchecked. Unchecking it should not fail validation.
</p>
<label>Is checked
<input asp-for="Input.IsChecked"/>
</label>
<span asp-validation-for="Input.IsChecked"></span>
@if (Model.Input.IsChecked) {
<em class="results">This is checked.</em>
} else {
<em class="results">This is not checked</em>
}
</div>

<div class="form-field">
<p>
However, if you manually render a checkbox, the checkbox is only submitted
when checked. So this allows cases where we require at least one checkbox
is selected.
</p>
@foreach (var animal in Model.Animals) {
<input name="SelectedAnimals"
type="checkbox"
value="@animal"
data-val="true"
data-val-required="Please select at least one animal"
data-rule-required="true"
data-msg-required="Please select at least one animal"
@if (Model.SelectedAnimals != null && Model.SelectedAnimals.Contains(animal)) {
<text>checked</text>
}/>
<label>@animal</label>
}
<span asp-validation-for="SelectedAnimals"></span>
@if (Model.SelectedAnimals.Any()) {
<em class="results">Selected animals: @string.Join(", ", Model.SelectedAnimals)</em>
}
</div>

<input type="submit" value="Submit"/>
</form>
</fieldset>

@if (Model.StatusMessage is not null) {
<a href="">Reset</a>
}

33 changes: 33 additions & 0 deletions Pages/Demos/Checkboxes.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace DemoWeb.Pages.Demos;

public class Checkboxes : PageModel
{
public string? StatusMessage { get; set; }

public IActionResult OnPost()
{
StatusMessage = "Form was submitted: " + (ModelState.IsValid
? "Model is valid"
: "Model is invalid");

return Page();
}

[BindProperty]
public InputModel Input { get; set; } = new();

[BindProperty]
[Required]
public List<string> SelectedAnimals { get; set; } = new();

public IReadOnlyList<string> Animals = new List<string> { "Dog", "Cat", "Fish" };

public class InputModel
{
public bool IsChecked { get; set; }
}
}
67 changes: 20 additions & 47 deletions Pages/Index.cshtml
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
@page
@model IndexModel
<!DOCTYPE html>
<html lang="en">
<head>
<title>ASP.NET Client Validation Demo Page</title>
<link rel="stylesheet" href="css/site.css">
<link rel="stylesheet" href="/dist/aspnet-validation.css" />
</head>
<body>

@if (Model.StatusMessage != null)
{
@{
Layout = "Shared/_Layout";
ViewData["Title"] = "ASP.NET Client Validation Demo Page";
}
<h1>Other Demos</h1>
<ul>
<li><a asp-page="Demos/Checkboxes">Checkboxes</a></li>
</ul>

@if (Model.StatusMessage != null) {
<h1 class="status-message">@Model.StatusMessage</h1>

<h2>Submitted Values</h2>
Expand All @@ -24,10 +23,6 @@
<th>Control: </th>
<td>@Model.Control</td>
</tr>
<tr>
<th>Selected Animals: </th>
<td>@(Model.SelectedAnimals != null ? string.Join(", ", Model.SelectedAnimals) : null)</td>
</tr>
<tr>
<th>Selected Color: </th>
<td>@Model.SelectedColor</td>
Expand Down Expand Up @@ -57,32 +52,17 @@
</div>

<div class="form-field">
@foreach (var animal in Model.Animals)
{
<input name="SelectedAnimals"
type="checkbox"
value="@animal"
data-val="true"
data-val-required="Please select at least one animal"
data-rule-required="true"
data-msg-required="Please select at least one animal"
@if (Model.SelectedAnimals != null && Model.SelectedAnimals.Contains(animal)) { <text>checked</text> } />
<label>@animal</label>
}
<span asp-validation-for="SelectedAnimals"></span>
</div>

<div class="form-field">
@foreach (var color in Model.Colors)
{
@foreach (var color in Model.Colors) {
<input name="SelectedColor"
type="radio"
value="@color"
data-val="true"
data-val-required="Please select a color"
data-rule-required="true"
data-msg-required="Please select a color"
@if (Model.SelectedColor == color) { <text>checked</text> } />
@if (Model.SelectedColor == color) {
<text>checked</text>
}/>
<label>@color</label>
}
<span asp-validation-for="SelectedColor"></span>
Expand Down Expand Up @@ -137,23 +117,16 @@

<fieldset>
<legend>External Form</legend>
<div class="form-field">
<label asp-for="Input.SomeRequiredField" form="external"></label>
<input asp-for="Input.SomeRequiredField" form="external"/>
<span asp-validation-for="Input.SomeRequiredField" form="external"></span>
</div>
<button type="submit" class="btn">Save</button>
<div class="form-field">
<label asp-for="Input.SomeRequiredField" form="external"></label>
<input asp-for="Input.SomeRequiredField" form="external"/>
<span asp-validation-for="Input.SomeRequiredField" form="external"></span>
</div>
<button type="submit" class="btn">Save</button>
</fieldset>


<form id="external">

</form>

<script src="/dist/aspnet-validation.js"></script>
<script>
const service = new aspnetValidation.ValidationService(console);
service.bootstrap();
</script>
</body>
</html>
6 changes: 0 additions & 6 deletions Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ public class IndexModel : PageModel
[Required]
public string? TextArea { get; set; }

[BindProperty]
[Required]
public List<string>? SelectedAnimals { get; set; }

public string[] Animals = new[] { "Dog", "Cat", "Fish" };

[BindProperty]
[Required]
public string? SelectedColor { get; set; }
Expand Down
19 changes: 19 additions & 0 deletions Pages/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"]</title>
<title>ASP.NET Client Validation Demo Page</title>
<link rel="stylesheet" href="/css/site.css">
<link rel="stylesheet" href="/dist/aspnet-validation.css" /></head>
<body>

@RenderBody()

<script src="/dist/aspnet-validation.js"></script>
<script>
const service = new aspnetValidation.ValidationService(console);
service.bootstrap();
</script>

</body>
</html>
7 changes: 7 additions & 0 deletions Pages/Shared/_StatusMessage.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@model string?

@if (Model is { Length: > 0 } statusMessage) {
<h1 class="status-message">
@statusMessage
</h1>
}
9 changes: 9 additions & 0 deletions dist/aspnet-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ var MvcValidationProviders = /** @class */ (function () {
return true;
}
}
// Checkboxes do not submit a value when unchecked. To work around this, platforms such as ASP.NET render a
// hidden input with the same name as the checkbox so that a value ("false") is still submitted even when
// the checkbox is not checked. We check this special case here.
if (elementType === "checkbox") {
var checkboxHiddenInput = element.form.querySelector("input[name='".concat(element.name, "'][type='hidden']"));
if (checkboxHiddenInput instanceof HTMLInputElement && checkboxHiddenInput.value === "false") {
return true;
}
}
return false;
}
// Default behavior otherwise.
Expand Down
2 changes: 1 addition & 1 deletion dist/aspnet-validation.min.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ export class MvcValidationProviders {
}
}

// Checkboxes do not submit a value when unchecked. To work around this, platforms such as ASP.NET render a
// hidden input with the same name as the checkbox so that a value ("false") is still submitted even when
// the checkbox is not checked. We check this special case here.
if (elementType === "checkbox") {
const checkboxHiddenInput = element.form.querySelector(`input[name='${element.name}'][type='hidden']`);
if (checkboxHiddenInput instanceof HTMLInputElement && checkboxHiddenInput.value === "false") {
return true;
}
}

return false;
}
// Default behavior otherwise.
Expand Down
5 changes: 5 additions & 0 deletions wwwroot/css/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ table.form-data {

table.form-data th {
text-align: left;
}

.results {
margin-left: 4px;
color: cadetblue;
}

0 comments on commit 17f8a79

Please sign in to comment.