Taghelpers Add Custom Class for Labeltaghelper Based on Validation Attribute [Required]

TagHelpers add custom class for LabelTagHelper based on validation attribute [Required]

Yup, you can extend this pretty easily by inheriting from the LabelTagHelper class and adding in your own class to the attribute list first.

[HtmlTargetElement("label", Attributes = "asp-for")]
public class RequiredLabelTagHelper : LabelTagHelper
{
public RequiredLabelTagHelper(IHtmlGenerator generator) : base(generator)
{
}

public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (For.Metadata.IsRequired)
{
CreateOrMergeAttribute("class", "required", output);
}

return base.ProcessAsync(context, output);
}

private void CreateOrMergeAttribute(string name, object content, TagHelperOutput output)
{
var currentAttribute = output.Attributes.FirstOrDefault(attribute => attribute.Name == name);
if (currentAttribute == null)
{
var attribute = new TagHelperAttribute(name, content);
output.Attributes.Add(attribute);
}
else
{
var newAttribute = new TagHelperAttribute(
name,
$"{currentAttribute.Value.ToString()} {content.ToString()}",
currentAttribute.ValueStyle);
output.Attributes.Remove(currentAttribute);
output.Attributes.Add(newAttribute);
}
}
}

How do I get HTML attribute's value by TagHelper?

Ok, I found what I was looking for. Solution based on >>THIS<< answer, includesalready taking color from DB:

[HtmlTargetElement(Attributes = "yellow")] //contains "yellow" attribute
public class YellowTagHelper : TagHelper
{
private ApplicationDbContext _context;

public YellowTagHelper(ApplicationDbContext ctx)
{
_context = ctx;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var colors = _context.ColorPanels.FirstOrDefault(); //context
var colorStyle = "color:" + colors.Element3BackgroundColor; //new color
if (!output.Attributes.ContainsName("style"))
{
output.Attributes.SetAttribute("style", colorStyle);
}
else
{
var currentAttribute = output.Attributes.FirstOrDefault(attribute => attribute.Name == "style"); //get value of 'style'
string newAttributeValue = $"{currentAttribute.Value.ToString() + "; " + colorStyle}"; //combine style values
output.Attributes.Remove(currentAttribute); //remove old attribute
output.Attributes.SetAttribute("style", newAttributeValue); //add merged attribute values
}
}
}

Combine TagHelper statements

This may work for you or at least get you in the right direction, I used a custom TagHelper that generates the other elements using their TagHelper implementation and appends all the outputs (may be there is a better / cleaner way of doing this, by the way).

[HtmlTargetElement("myfield")]
public class MyFieldTagHelper : TagHelper
{
private IHtmlGenerator _htmlGenerator;
public MyFieldTagHelper(IHtmlGenerator htmlGenerator)
{
_htmlGenerator = htmlGenerator;
}

public string LabelContent { get; set; }
public string ValidationContent { get; set; }

[HtmlAttributeName("asp-for")]
public ModelExpression For { get; set; }

[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)
{
var labelContext = CrateTagHelperContext();
var labelOutput = CrateTagHelperOutput("label");

labelOutput.Content.Append(LabelContent);

if (For != null)
{
labelOutput.Attributes.Add("for", For.Name);
}

var label = new LabelTagHelper(_htmlGenerator)
{
ViewContext = ViewContext
};

label.Process(labelContext, labelOutput);

var inputContext = CrateTagHelperContext();
var inputOutput = CrateTagHelperOutput("input");

inputOutput.Attributes.Add("class", "form-control");

var input = new InputTagHelper(_htmlGenerator)
{
For = For,
ViewContext = ViewContext
};

input.Process(inputContext, inputOutput);

var validationContext = CrateTagHelperContext();
var validationOutput = CrateTagHelperOutput("span");

validationOutput.Content.Append(ValidationContent);

validationOutput.Attributes.Add("class", "text-danger");

var validation = new ValidationMessageTagHelper(_htmlGenerator)
{
For = For,
ViewContext = ViewContext
};

validation.Process(validationContext, validationOutput);

output.TagName = "";
output.Content.AppendHtml(labelOutput);
output.Content.AppendHtml(inputOutput);
output.Content.AppendHtml(validationOutput);
}

private static TagHelperContext CrateTagHelperContext()
{
return new TagHelperContext(
new TagHelperAttributeList(),
new Dictionary<object, object>(),
Guid.NewGuid().ToString("N"));
}

private static TagHelperOutput CrateTagHelperOutput(string tagName)
{
return new TagHelperOutput(
tagName,
new TagHelperAttributeList(),
(a,b) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent(string.Empty);
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
}
}

Now in your view you should be able to do this:

 <myfield label-content="This is a label" validation-content="Validation content" asp-for="Age"></myfield> 

And it will generate the whole block for you.

Remember to register the assembly where you have your tag helpers, even if you have them in your Web application, you can do that by editing the file Views/_ViewImports.cshtml and adding the following line:

@addTagHelper *, YourAssemblyName

More info: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro

Tag Helper Embedded in Another Tag Helper's Code Doesn't Render

As was pointed out to me by others, it may not be possible to directly embed tag helpers the way I first desired when I posted this question. As a result I refactored the code to programmatically "new up" the desired tag helpers instead.

My final solution was significantly more work than I had expected but in the long run it will save lots of time developing the form-intensive applications I have planned.

The Objective

My goal is to speed-up creation of forms by using this custom tag helper, for example:

<formfield asp-for="OrganizationName"></formfield>

To generate these built-in Razor tag helpers:

<div class="form-group">
<div class="row">
<label class="col-md-3 col-form-label" for="OrganizationName">Company Name</label>
<div class="col-md-9">
<input name="OrganizationName" class="form-control" id="OrganizationName" type="text" value="" data-val-required="The Company Name field is required." data-val="true" data-val-maxlength-max="50" data-val-maxlength="Maximum company name length is 50 characters.">
<span class="field-validation-valid" data-valmsg-replace="true" data-valmsg-for="OrganizationName"></span>
</div>
</div>
</div>

My Initial Working Solution

This is the first-pass test solution for simple cases. I.e. for default hard coded classes and text-box input types.

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

namespace ObApp.Web.TagHelpers
{
// Builds form elements to generate the following (for example):
// <div class="form-group">
// <div class="row">
// <input ... >Email</input>
// <div>
// <input type="text" ... />
// <span class="field-validation-valid ... ></span>
// </div>
// </div>
// </div>

public class FormfieldTagHelper : TagHelper
{
private const string _forAttributeName = "asp-for";
private const string _defaultWraperDivClass = "form-group";
private const string _defaultRowDivClass = "row";
private const string _defaultLabelClass = "col-md-3 col-form-label";
private const string _defaultInputClass = "form-control";
private const string _defaultInnerDivClass = "col-md-9";
private const string _defaultValidationMessageClass = "";

public FormfieldTagHelper(IHtmlGenerator generator)
{
Generator = generator;
}

[HtmlAttributeName(_forAttributeName)]
public ModelExpression For { get; set; }

public IHtmlGenerator Generator { get; }

[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// Replace this parent tag helper with div tags wrapping the entire form block
output.TagName = "div";
output.Attributes.SetAttribute("class", _defaultWraperDivClass);

// Manually new-up each child asp form tag helper element
TagHelperOutput labelElement = await CreateLabelElement(context);
TagHelperOutput inputElement = await CreateInputElement(context);
TagHelperOutput validationMessageElement = await CreateValidationMessageElement(context);

// Wrap input and validation with column div
IHtmlContent innerDiv = WrapElementsWithDiv(
new List<IHtmlContent>()
{
inputElement,
validationMessageElement
},
_defaultInnerDivClass
);

// Wrap all elements with a row div
IHtmlContent rowDiv = WrapElementsWithDiv(
new List<IHtmlContent>()
{
labelElement,
innerDiv
},
_defaultRowDivClass
);

// Put everything into the innerHtml of this tag helper
output.Content.SetHtmlContent(rowDiv);
}

private async Task<TagHelperOutput> CreateLabelElement(TagHelperContext context)
{
LabelTagHelper labelTagHelper =
new LabelTagHelper(Generator)
{
For = this.For,
ViewContext = this.ViewContext
};

TagHelperOutput labelOutput = CreateTagHelperOutput("label");

await labelTagHelper.ProcessAsync(context, labelOutput);

labelOutput.Attributes.Add(
new TagHelperAttribute("class", _defaultLabelClass));

return labelOutput;
}

private async Task<TagHelperOutput> CreateInputElement(TagHelperContext context)
{
InputTagHelper inputTagHelper =
new InputTagHelper(Generator)
{
For = this.For,
ViewContext = this.ViewContext
};

TagHelperOutput inputOutput = CreateTagHelperOutput("input");

await inputTagHelper.ProcessAsync(context, inputOutput);

inputOutput.Attributes.Add(
new TagHelperAttribute("class", _defaultInputClass));

return inputOutput;
}

private async Task<TagHelperOutput> CreateValidationMessageElement(TagHelperContext context)
{
ValidationMessageTagHelper validationMessageTagHelper =
new ValidationMessageTagHelper(Generator)
{
For = this.For,
ViewContext = this.ViewContext
};

TagHelperOutput validationMessageOutput = CreateTagHelperOutput("span");

await validationMessageTagHelper.ProcessAsync(context, validationMessageOutput);

return validationMessageOutput;
}

private IHtmlContent WrapElementsWithDiv(List<IHtmlContent> elements, string classValue)
{
TagBuilder div = new TagBuilder("div");
div.AddCssClass(classValue);
foreach(IHtmlContent element in elements)
{
div.InnerHtml.AppendHtml(element);
}

return div;
}

private TagHelperOutput CreateTagHelperOutput(string tagName)
{
return new TagHelperOutput(
tagName: tagName,
attributes: new TagHelperAttributeList(),
getChildContentAsync: (s, t) =>
{
return Task.Factory.StartNew<TagHelperContent>(
() => new DefaultTagHelperContent());
}
);
}
}
}

Next Steps/Suggested Improvements

This is working well for text boxes without validation errors. After tweaking the default CSS the next steps I plan to take are:

  1. Display the correct CSS class attributes for formatting when there are validation errors. At this point the tag helper will be "working" for me.
  2. Move the hard coded CSS classes out to a site configuration file.
  3. Bind to HTML attributes in the view to allow non-default classes to be passed in. Another way to do this would be to pass in non-default form classes through the ViewModel.
  4. Detect non-textbox input types and format accordingly.

Thank you to @Chris Pratt for getting me started in the right direction on this.



Related Topics



Leave a reply



Submit