Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'm trying to simplify creation of long forms with the following custom tag helper in .net Core 2.0:

@model ObApp.Web.Models.ViewComponents.FormFieldViewModel

<span>Debug - Paramater value: @Model.FieldFor</span>
<div class="form-group">
    <label asp-for="@Model.FieldFor"></label>
    <input asp-for="@Model.FieldFor" class="form-control" />
</div>

It seems simple, but I'm getting an unexpected (to me) result when I use:

<vc:form-field field-for="PersonEmail"></vc:form-field>

Expected Result

<span>Debug - Paramater value: PersonEmail</span>
<div class="form-group">
    <label for="PersonEmail">Email</label>
    <input name="PersonEmail" class="form-control" id="PersonEmail" 
        type="text" value="PersonEmail">
</div>

Actual Result

<span>Debug - Paramater value: PersonEmail</span>
<div class="form-group">
    <label for="FieldFor">FieldFor</label>
    <input name="FieldFor" class="form-control" id="FieldFor" 
        type="text" value="PersonEmail">
</div>

I've tried removing the quotes from around @Model.FieldFor, as well as a few other syntactic changes.

Any suggestions?

Thank you!

like image 621
platypusjh Avatar asked Nov 29 '17 08:11

platypusjh


People also ask

What is the difference between tag helper vs HTML helper?

Tag Helpers are attached to HTML elements inside your Razor views and can help you write markup that is both cleaner and easier to read than the traditional HTML Helpers. HTML Helpers, on the other hand, are invoked as methods that are mixed with HTML inside your Razor views.

Which tag helper is used to generate HTML elements based on collection passed to it?

The Input Tag Helper. The Input Tag Helper binds an HTML <input> element to a model expression in your razor view. The Input Tag Helper: Generates the id and name HTML attributes for the expression name specified in the asp-for attribute.

Can we use tag helpers in MVC 5?

No, tag helpers are only supported in ASP.NET Core, not in the older ASP.NET (based on the classic . NET Framework, rather than the newer . NET Core). But instead you can use HTML Helpers which do a lot of the same things.

Which is the correct syntax of tag helper in form tag?

@addTagHelper makes Tag Helpers available The code above uses the wildcard syntax ("*") to specify that all Tag Helpers in the specified assembly (Microsoft. AspNetCore. Mvc. TagHelpers) will be available to every view file in the Views directory or subdirectory.


1 Answers

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.

like image 61
platypusjh Avatar answered Oct 05 '22 15:10

platypusjh