I have a form with client side validation, and when an error input
is detected, a class attribute is changed of an input field; It is changed to include "input-validation-error" class.
I want to change this class, in order not to use it but instead use Bootstraps class "is-invalid".
I tried using ASP.NET Core's TagHelpers, but this has no effect;
I believe that this will not work as the helpers will only work if the "whole page" is loaded, it does not help with client side validation.
When I search in the .NET project one finds the css class defined in,
the "Unobtrusive validation support library for jQuery".
What is the best way to change this class?
Could CSS help by changing a class from one to the other? (overriding the original class, not sure if this is possible)
Or should one use JavaScript to reconfigure JQuery?
Here is my TagHelper, adding the helpers: validation-for,validation-error-class,validation-valid-class
The Form/Html...
<input type="email" asp-for="Email" id="inputEmail" class="form-control" placeholder="Email address" required
validation-for="Email" validation-error-class="is-invalid" validation-valid-class="is-valid"/>
<span class="small" asp-validation-for="Email"></span>
Here is a snippet of the code for my TagHelper.
[HtmlTargetElement("input", Attributes = "validation-for,validation-error-class,validation-valid-class")]
public class ValidationErrorClassTagHelper : TagHelper
{
[HtmlAttributeName("validation-for")]
public ModelExpression For { get; set; }
[HtmlAttributeName("validation-error-class")]
public string ErrorClass { get; set; }
[HtmlAttributeName("validation-valid-class")]
public string ValidClass { get; set; }
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.RemoveClass(ErrorClass,HtmlEncoder.Default);
output.RemoveClass(ValidClass,HtmlEncoder.Default);
if (ViewContext.ViewData.ModelState.IsValid) {
output.AddClass(ValidClass,HtmlEncoder.Default);
} else
{
output.AddClass(ErrorClass,HtmlEncoder.Default);
}
}
}
New Approach not working 100%.
I have tried an alternative approach, by modifying the jQuery defaultOptions, changing the errorClass and the validClass.
Snippet of the Code found here on [https://github.com/brecons/jquery-validation-unobtrusive-bootstrap][gitHub]
function ($) {
if($.validator && $.validator.unobtrusive){
var defaultOptions = {
validClass: 'is-valid',
errorClass: 'is-invalid',
This works for the errorClass, but for me the validClass remains unchanged,
it remains to be named valid.
I have encountered this issue too but I don't really like the idea of fixing it at client-side as the jQuery-trick works only when JavaScript is enabled in the browser. Because of this, I think the problem should be addressed at server-side.
Unfortunately, the framework doesn't provide a way to configure the validation-related css classes, these strings are simply hard-coded.
However, these fields are not constants but declared as static, so we may change their value at run-time via reflection (somehow like presented here). But these kinds of things are dirty hacks which should be our last resort.
OP's idea of a custom tag helper looks much better. But it has a shortcoming: it only fixes markup generated by tag helpers. The classic, Html.TextBox(...)-like approach would be still broken.
So, can we do any better? Luckily, yes!
Both Html and tag helper implementations use the IHtmlGenerator service under the hood to generate their markup. Thanks to the modular architecture of ASP.NET Core, we can provide a customized version of this service. (What's more, we can even do this without copying a bunch of code as the default implementation declares the relevant methods as virtual.)
Thus, I could come up with this solution:
public sealed class CustomHtmlGenerator : DefaultHtmlGenerator
{
private static IHtmlHelper GetHtmlHelperFor(ViewContext viewContext)
{
const string htmlHelperViewDataKey = nameof(CustomHtmlGenerator) + "_" + nameof(IHtmlHelper);
if (!viewContext.ViewData.TryGetValue(htmlHelperViewDataKey, out var htmlHelperObj) || !(htmlHelperObj is IHtmlHelper htmlHelper))
viewContext.ViewData[htmlHelperViewDataKey] = htmlHelper = GetViewHtmlHelper(viewContext) ?? CreateHtmlHelper(viewContext);
return htmlHelper;
static IHtmlHelper GetViewHtmlHelper(ViewContext viewContext)
{
if (!(viewContext.View is RazorView razorView))
return null;
dynamic razorPage = razorView.RazorPage;
try { return (IHtmlHelper)razorPage.Html; }
catch { return null; }
}
static IHtmlHelper CreateHtmlHelper(ViewContext viewContext)
{
var htmlHelper = viewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper>();
(htmlHelper as IViewContextAware)?.Contextualize(viewContext);
return htmlHelper;
}
}
private static TagBuilder AddBootstrapValidationCssClasses(ViewContext viewContext, string expression, TagBuilder tagBuilder)
{
// we need to get the model property key from the expression, which functionality is buried in an internal class unfortunately
// (https://github.com/dotnet/aspnetcore/blob/v3.1.6/src/Mvc/Mvc.ViewFeatures/src/NameAndIdProvider.cs#L147)
// however, this internal API is exposed via the IHtmlHelper.Name method:
// (https://github.com/dotnet/aspnetcore/blob/v3.1.6/src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs#L451)
var htmlHelper = GetHtmlHelperFor(viewContext);
var fullName = htmlHelper.Name(expression);
if (viewContext.ModelState.TryGetValue(fullName, out var entry))
{
if (entry.ValidationState == ModelValidationState.Invalid)
tagBuilder.AddCssClass("is-invalid");
else if (entry.ValidationState == ModelValidationState.Valid)
tagBuilder.AddCssClass("is-valid");
}
return tagBuilder;
}
public CustomHtmlGenerator(IAntiforgery antiforgery, IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, ValidationHtmlAttributeProvider validationAttributeProvider)
: base(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder, validationAttributeProvider) { }
protected override TagBuilder GenerateInput(ViewContext viewContext, InputType inputType, ModelExplorer modelExplorer, string expression, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes) =>
AddBootstrapValidationCssClasses(viewContext, expression, base.GenerateInput(viewContext, inputType, modelExplorer, expression, value, useViewData, isChecked, setId, isExplicitValue, format, htmlAttributes));
public override TagBuilder GenerateSelect(ViewContext viewContext, ModelExplorer modelExplorer, string optionLabel, string expression, IEnumerable<SelectListItem> selectList, ICollection<string> currentValues, bool allowMultiple, object htmlAttributes) =>
AddBootstrapValidationCssClasses(viewContext, expression, base.GenerateSelect(viewContext, modelExplorer, optionLabel, expression, selectList, currentValues, allowMultiple, htmlAttributes));
public override TagBuilder GenerateTextArea(ViewContext viewContext, ModelExplorer modelExplorer, string expression, int rows, int columns, object htmlAttributes) =>
AddBootstrapValidationCssClasses(viewContext, expression, base.GenerateTextArea(viewContext, modelExplorer, expression, rows, columns, htmlAttributes));
}
All that is left is to configure DI to resolve this custom implementation by adding the following at the end of the Startup.ConfigureService method:
services.Replace(ServiceDescriptor.Singleton<IHtmlGenerator, CustomHtmlGenerator>());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With