3

I need to automatically display an asterisk for required fields, so I've managed to find some code online that does this. I've added a CSS Class named "required-label" to also turn it red. However, it only applies the CSS class to the asterisk and not the label too. Any ideas how to apply the CSS class to both? Here's the full snippet as requested.

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;

namespace App.TagHelpers
{
    [HtmlTargetElement("label", Attributes = ForAttributeName)]
    public class LabelRequiredTagHelper : LabelTagHelper
    {
        private const string ForAttributeName = "asp-for";

        public LabelRequiredTagHelper(IHtmlGenerator generator) : base(generator)
        {
        }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            await base.ProcessAsync(context, output);

            if (For.Metadata.IsRequired)
            {
                var sup = new TagBuilder("sup");
                sup.InnerHtml.Append("*");
                sup.AddCssClass("required-label");
                output.Content.AppendHtml(sup);
            }
        }
    }
}

Credit to Manoj Kulkarni for the code example.

2
  • What is the HTML output of the above code, can you post snippet of that? Commented Jun 6, 2018 at 19:58
  • Added the full code now Rikin Commented Jun 7, 2018 at 20:53

1 Answer 1

5

The tag helper in the OP works, but instead of appending a sup element to contain the asterisk, I think all you need to do is to add a css class to the label element itself and use CSS to style the label accordingly.

A little tweak on the tag-helper

[HtmlTargetElement("label", Attributes = ForAttributeName)]
public class LabelRequiredTagHelper : LabelTagHelper
{
    private const string ForAttributeName = "asp-for";
    private const string RequiredCssClass = "required";

    public LabelRequiredTagHelper(IHtmlGenerator generator) : base(generator)
    {
    }

    public override async Task ProcessAsync(TagHelperContext context, 
        TagHelperOutput output)
    {
        await base.ProcessAsync(context, output);

        if (For.Metadata.IsRequired)
        {
            output.Attributes.AddCssClass(RequiredCssClass);
        }
    }
}

AddCssClass extension

public static class TagHelperAttributeListExtensions
{
    public static void AddCssClass(this TagHelperAttributeList attributeList, 
        string cssClass)
    {
        var existingCssClassValue = attributeList
            .FirstOrDefault(x => x.Name == "class")?.Value.ToString();

        // If the class attribute doesn't exist, or the class attribute
        // value is empty, just add the CSS class
        if (String.IsNullOrEmpty(existingCssClassValue))
        {
            attributeList.SetAttribute("class", cssClass);
        }
        // Here I use Regular Expression to check if the existing css class
        // value has the css class already. If yes, you don't need to add
        // that css class again. Otherwise you just add the css class along
        // with the existing value.
        // \b indicates a word boundary, as you only want to check if
        // the css class exists as a whole word.  
        else if (!Regex.IsMatch(existingCssClassValue, $@"\b{ cssClass }\b",
            RegexOptions.IgnoreCase))
        {
            attributeList.SetAttribute("class", $"{ cssClass } { existingCssClassValue }");
        }
    }
}

The view model

A sample view model that contains required properties, annotated with [Required]. Also a boolean is required by default.

public class LoginViewModel
{
    [Required]
    public string Username { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "Remember my login?")]
    public bool RememberMe { get; set; }

    public string ReturnUrl { get; set; }
}

The view

For example, I have a login page that takes LoginViewModel.

@model LoginViewModel
@{
    ViewData["Title"] = "Login";
}

<form asp-area="" asp-controller="account" asp-action="login">
    <input type="hidden" asp-for="ReturnUrl" />

    <div asp-validation-summary="ModelOnly" class="text-danger"></div>

    <div class="form-group">
        <label asp-for="Email"></label>
        <input type="email" class="form-control" asp-for="Email" />
    </div>
    <div class="form-group">
        <label asp-for="Password"></label>
        <input type="password" class="form-control" asp-for="Password" />
    </div>
    <div class="form-group">
        <div class="custom-control custom-checkbox">
            <input type="checkbox" class="custom-control-input" asp-for="RememberMe" />
            <label asp-for="RememberMe" class="custom-control-label"></label>
        </div>
    </div>
    <button type="submit" class="btn btn-primary btn-block">Login</button>
</form>

Worth knowing that the label for checkbox RememberMe. On the view I have added additional css class custom-control-label, and the tag helper still manages to add the required css class required along with it.

Generated HTML

enter image description here

The style (in SASS)

You can tell I am using Bootstrap css framework and there are already styles for checkbox labels so I want to exclude those (denoted by custom-control-label css class).

label.required:not(.custom-control-label)::after {
    content: "*";
    padding-left: .3rem;
    color: theme-color('danger');      /* color: #dc3545; */
}

The result

enter image description here

If you want the required label to be red too, you can style it this way:

label.required:not(.custom-control-label) {
    color: theme-color('danger');      /* color: #dc3545; */

    &::after {
        content: "*";
        padding-left: .3rem;
    }
}
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks for the example David. For some previous MVC projects I used a Html Helper that automatically highlighted labels for Required fields. This code almost achieved the same for ASP Net Core so I was really trying to have the same functionality. I'm obviously getting lazy in my old age and want everything to be automatic without having to manually decorate with class="required".
Finally I see what you meant. I have updated OP. Please take a look.
Great answer! Can you explain how does this taghelper works (called) besides the built-in LabelTagHelper? I mean, it seems that we have two TagHelpers that target the label tag, so will both of them executed? in which order?
@S.Serpooshan: Good question! I think this custom tag helper and the built-in label tag helper will both run, since they target the same element label (the asp-for attribute is optional but I put it there to make sure this custom tag helper will only run on those with attributes asp-for). The trick here is: 1) the content for the same targeted element is shared/cache among all the tag helpers targeting the same element 2) (if you look at the source code of built-in label tag helper) one tag helper doesn't update/generate the content if another tag helper has already done so.
@S.Serpooshan: in my case, it doesn't matter which one runs first. If the built-in one runs first (I assume that's correct), when my custom one calls await base.ProcessAsync(), it processes the same logic that if the content has been modified, it won't regenerate the content. If my custom one runs first, it still calls base.ProcessAsync() to generate the content because mine inherits from the LabelTagHelper. The later when the default built-in label tag helper runs and sees the content has been modified, it won't regenerate the content anyway. Disclaim: I am not 100% sure though LOL
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.