Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I add a CSS class to an input element when my modelstate is invalid?

I'm new to .NET Core 2.0 Razor pages, and I'm finding it difficult to solve my problem in a clean way.

I create a simple login form in which the user must enter it's email and password. I use a model with the properties for the email and password with the corresponding data annotations.

public class AuthorizeOrganizationCommand
{
    [Required(ErrorMessage ="Please fill in your email")]
    public string Email { get; set; }

    [Required(ErrorMessage ="Please fill in your password")]
    public string Password { get; set; }
}

My pagemodel looks like this:

public class IndexModel : PageModel
{
    public IndexModel()
    {
    }
    
    [BindProperty]
    public AuthorizeOrganizationCommand Command { get; set; }
            
    public void OnGet()
    {
    }

    public IActionResult OnPost()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Code to validate the credentials

        return RedirectToPage("/Index");
    }
}

When my ModelState is invalid, I want to visualize an error message. This works just fine with the following code:

<div class="form-group">
    <label class="form-label" asp-for="Command.Email">Email</label>
    <input type="text" class="form-control" asp-for="Command.Email">
    <small class="invalid-feedback d-block">
        <span asp-validation-for="Command.Email"></span>
    </small>
</div>

Additionally I want to add the CSS class .is-invalid on my input element when my Modelstate is invalid for that specific property. This results in a red-edged input element (bootstrap 4). I made it work with the following code:

<input type="text" class="form-control @(ModelState["Command.Email"]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid ? "is-invalid": string.Empty)" asp-for="Command.Email">

To be honest, I don't like this solution.

The hard-coded "Command.Email" breaks the code when a rename is performed on the class instance name or property. After trying several things I didn't find a good and clean way to solve this.

like image 212
Kjell Derous Avatar asked Nov 27 '22 00:11

Kjell Derous


1 Answers

Better solution:

MVC InputTagHelper automatically adds a class to the input field depending on the model validation state. In case of validation error it adds the class input-validation-error so you can customize the input by defining css styles for this class e.g.

.input-validation-error {
    border: 1px solid red;
}

If instead you prefer to use your own css classes there is also a workaround to change the default classes that InputTagHelper adds to the input. You can find the code in this gist. To summarize you have to create a class that extends the DefaultHtmlGenerator class and replaces the default input validation css classes with whatever you like.

public class CustomHtmlGenerator : DefaultHtmlGenerator
{
    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)
    {
        var tagBuilder = base.GenerateInput(viewContext, inputType, modelExplorer, expression, value, useViewData, isChecked, setId, isExplicitValue, format, htmlAttributes);
        FixValidationCssClassNames(tagBuilder);

        return tagBuilder;
    }
    
    public override TagBuilder GenerateTextArea(ViewContext viewContext, ModelExplorer modelExplorer, string expression, int rows,
        int columns, object htmlAttributes)
    {
        var tagBuilder = base.GenerateTextArea(viewContext, modelExplorer, expression, rows, columns, htmlAttributes);
        FixValidationCssClassNames(tagBuilder);

        return tagBuilder;
    }

    private void FixValidationCssClassNames(TagBuilder tagBuilder)
    {
        tagBuilder.ReplaceCssClass(HtmlHelper.ValidationInputCssClassName, "is-invalid");
        tagBuilder.ReplaceCssClass(HtmlHelper.ValidationInputValidCssClassName, "is-valid");
    }
}

public static class TagBuilderHelpers
{
    public static void ReplaceCssClass(this TagBuilder tagBuilder, string old, string val)
    {
        if (!tagBuilder.Attributes.TryGetValue("class", out string str)) return;
        tagBuilder.Attributes["class"] = str.Replace(old, val);
    }
}

and then register it to the dependency injection

public void ConfigureServices(IServiceCollection services)
{
    // All your usual stuff
        
    // Configure custom html generator to override css classnames
    services.AddSingleton<IHtmlGenerator, CustomHtmlGenerator>();
}

Original answer:

You could define the css class as a property in your PageModel.

public class IndexModel : PageModel
{
    public IndexModel()
    {
    }
    
    [BindProperty]
    public AuthorizeOrganizationCommand Command { get; set; }

    public string InputClass { get; set; }
            
    public void OnGet()
    {
    }

    public IActionResult OnPost()
    {
        if (!ModelState.IsValid)
        {
            InputClass = "is-invalid";
            return Page();
        }

        // Code to validate the credentials

        return RedirectToPage("/Index");
    }
}

and then:

<input type="text" class="form-control @Model.InputClass" asp-for="Command.Email">
like image 65
Dimitris Maragkos Avatar answered Dec 05 '22 05:12

Dimitris Maragkos