techgems / razor-component-tag-helpers

Server Components with no lifecycle for ASP.NET Core MVC and Razor Pages.
MIT License
16 stars 1 forks source link

Thank you for figuring this out...Feedback #1

Closed tomlm closed 1 year ago

tomlm commented 2 years ago

Thank you for figuring this out! I think your code can be simplified quite a bit, in fact all the way down to a single class which I call ViewTagHelper, (as I think it is a bit more aligned with the aspnetcore nomenclature.)

Here's the classs

    /// <summary>
    /// Implements a tag helper as a Razor view as the template
    /// </summary>
    /// <remarks>
    ///     uses convention that /TagHelpers/ has razor template based views for tags
    ///     For a folder /TagHelpers/Foo
    ///     * FooTagHelper.cs -> Defines the properties with HtmlAttribute on it (derived from ViewTagHelper)
    ///     * default.cshtml -> Defines the template with Model=>FooTagHelper
    /// </remarks>
    public class ViewTagHelper : TagHelper
    {
        private string _viewPath;

        public ViewTagHelper()
        {
            _viewPath = $"~/TagHelpers/{GetType().Namespace.Split('.').Last()}/Default.cshtml";
        }

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

        public TagHelperContent? ChildContent { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            throw new Exception("Use ProcessAsync()");
        }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (ViewContext is null)
            {
                throw new ArgumentNullException(nameof(ViewContext));
            }

            // get child content and capture it in our model so we can insert it in our output
            ChildContent = await output.GetChildContentAsync();

            IHtmlHelper? htmlHelper = ViewContext.HttpContext.RequestServices.GetService<IHtmlHelper>();
            ArgumentNullException.ThrowIfNull(htmlHelper);

            (htmlHelper as IViewContextAware)!.Contextualize(ViewContext);
            var content = await htmlHelper.PartialAsync(_viewPath, this);

            output.TagName = null;
            output.Content.SetHtmlContent(content);
        }
    }

You will see that:

A sample TagHelper then looks like this: ~/TagHelpers/Person/PersonTagHelper.cs

namespace OpBot.Cards.TagHelpers.Person
{
    [HtmlTargetElement("Person")]
    public class PersonTagHelper : ViewTagHelper
    {
        [HtmlAttributeName]
        public string? Name { get; set; } = string.Empty;

        [HtmlAttributeName]
        public string? ImageUrl { get; set; } = string.Empty;
    }
}

and template ~/TagHelpers/Person/default.cshtml

@using Project1.TagHelpers.Person
@model PersonTagHelper
<h1>@Model.Name</h1>
<table>
   <tr>
     <th colspan=2>@Model.Name</th>
   </tr>
  <tr>
    <td><img src="@Model.ImageUrl"/></td>
    <td>@Model.Content</td>
  </tr>
</table>

And of course can be used then in other templates:

<Person Name="Bob>
    The thing about bob is that <a href="...">link</a>
</Person>
techgems commented 2 years ago

Thanks! I plan to work on this a bit more and turn it into a nuget package with a companion VS Extension for avoiding boilerplate. The main thing I wanted to solve that I didn't find originally was how to not force the user to have to inject the IHtmlHelper and instead just have the dependency as part of the base class, which your sample covers and it's gonna be a huge improvement on this final product.

Ideally, I don't think something like this should be an 3rd party package, but something that the framework supports out of the box. But I understand the focus (and money) right now is in Blazor, not in adding better composition techniques inside plain old MVC. Though I would say that there are still use cases for a classic MVC approach in applications where you need to get going quickly and a rich web UI doesn't matter as much.

I do plan to add a feature request into the ASP.NET Core repo when I have finished this properly and leave this repo as a sample should this feature ever become part of the framework itself. Ideally this could be useful by allowing ways of using named slots into it (like Svelte).

But once again, thanks for the feedback, I never thought this would get any attention before me posting the related blog post talking about it.

techgems commented 1 year ago

Published the nuget package with the changes you suggested two days ago. Closing the issue. Working on a small documentation site.