Search code examples
c#asp.net-corerazorasp.net-core-mvcrefactoring

Partial that expands into input tag helper


The book Pro ASP.NET Core 3 has the the following code in its example for chapter 27:

<div class="m-2">
    <h5 class="bg-primary text-white text-center p-2">HTML Form</h5>
    <form asp-page="FormHandler" method="post" id="htmlform">
        <div class="form-group">
            <label>Name</label>
            <input class="form-control" asp-for="Product.Name" />
        </div>
        <div class="form-group">
            <label>Price</label>
            <input class="form-control" asp-for="Product.Price" />
        </div>
        <div class="form-group">
            <label>Category</label>
            <input class="form-control" asp-for="Product.Category.Name" />
        </div>
        <div class="form-group">
            <label>Supplier</label>
            <input class="form-control" asp-for="Product.Supplier.Name" />
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <button form="htmlform" asp-page="FormHandler" class="btn btn-primary mt-2">
        Sumit (Outside Form)
    </button>
</div>

As you can see, the <div class="form-group>...</div> pattern is repeated four times. As a test of partials, I added the file Pages\_FormGroupPartial.cshtml:

@model FormGroupParams

<div class="form-group">
    <label>@Model.LabelContent</label>
    <input class="form-control" asp-for="@Model.ForString" />
</div>

In Pages\FormHandler.cshtml.cs I added the following class:

public class FormGroupParams
{        
    public string LabelContent;
    public string ForString;

    public FormGroupParams(string labelContent, string forString)
    {
        LabelContent = labelContent;
        ForString = forString;
    }
}

To test this out, I edited Pages\FormHandler.cshtml to use the new partial right after the original approach:

<div class="form-group">
    <label>Supplier</label>
    <input class="form-control" asp-for="Product.Supplier.Name" />
</div>

<partial name="_FormGroupPartial" model='new FormGroupParams("Supplier", "Product.Supplier.Name")' />

In the resulting page, we see that the partial did not quite generate the same code; note that the value is now Product.Supplier.Name instead of the expected Splash Dudes:

enter image description here

Looking at the source, we see that the original code:

<div class="form-group">
    <label>Supplier</label>
    <input class="form-control" asp-for="Product.Supplier.Name" />
</div>

expanded into:

<div class="form-group">
    <label>Supplier</label>
    <input class="form-control" type="text" id="Product_Supplier_Name" name="Product.Supplier.Name" value="Splash Dudes" />
</div>

Whereas our partial:

<partial name="_FormGroupPartial" model='new FormGroupParams("Supplier", "Product.Supplier.Name")' />

expanded into:

<div class="form-group">
    <label>Supplier</label>
    <input class="form-control" type="text" id="ForString" name="ForString" value="Product.Supplier.Name" />
</div>

Is there a good way to implement this partial so as to get the same output as the original code?


UPDATE 2020-09-30

PeterG suggests the following in his answer below:

<partial name="_FormGroupPartial" model='new FormGroupParams("Supplier", @Model.Product.Supplier.Name)' />

This expands into the following:

<div class="form-group">
    <label>Supplier</label>
    <input class="form-control" type="text" id="ForString" name="ForString" value="Splash Dudes" />
</div>

whereas the goal is the following:

<div class="form-group">
    <label>Supplier</label>
    <input class="form-control" type="text" id="Product_Supplier_Name" name="Product.Supplier.Name" value="Splash Dudes" />
</div>

So the value is now correct. But the id and name are still off.


Solution

  • The author of the book, Adam Freeman, has provided an example to me via email which demonstrates solving this using a tag helper.

    Add the file TagHelpers/FormGroupTagHelper.cs:

    using Microsoft.AspNetCore.Razor.TagHelpers;
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    
    namespace WebApp.TagHelpers
    {
        [HtmlTargetElement("form-group")]
        public class FormGroupTagHelper : TagHelper
        {
            public string Label { get; set; }
            public ModelExpression For { get; set; }
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                output.TagName = "div";
                output.TagMode = TagMode.StartTagAndEndTag;
                output.Attributes.SetAttribute("class", "form-group");
    
                var label = new TagBuilder("label");
                label.InnerHtml.Append(Label ?? For.Name);
                            
                var input = new TagBuilder("input");
                input.AddCssClass("form-control");
                input.Attributes["type"] = "text";
                input.Attributes["id"] = For.Name.Replace(".", "_");
                input.Attributes["name"] = For.Name;
                input.Attributes["value"] = For.Model.ToString();
                
                output.Content.AppendHtml(label);
                output.Content.AppendHtml(input);
            }
        }
    }
    

    Now, in Pages/FormHandler.cshtml instead of this:

    <div class="form-group">
        <label>Supplier</label>
        <input class="form-control" asp-for="Product.Supplier.Name" />
    </div>
    

    you can use the following:

    <form-group label="Supplier" for="Product.Supplier.Name"/>
    

    Thanks Adam!