-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Introduce ComponentTagHelper #14592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce ComponentTagHelper #14592
Conversation
src/Components/test/E2ETest/ServerExecutionTests/ComponentWithParametersTest.cs
Outdated
Show resolved
Hide resolved
<component type="typeof(ComponentWithParameters)" | ||
render-mode="ServerPrerendered" | ||
parameter-Param1="ComponentWithParameters.TestModelValues" | ||
parameter-Param2="ComponentWithParameters.DerivedModelValue" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kinda an interesting case. It's interesting to think about because the same is not true with Param1
. If you put an instance of DerivedModel
in that list, it wouldn't roundtrip.
I think is is probably the right thing to do but I'm sure it's going to come up. This use case for serialization is really different from our usage elsewhere. We'll have to make sure to write good docs for this - the main thing is that it's not really possible for people to marshal some kinds of data (like polymorphic collections) without doing a lot of S.T.J extensibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup. I wanted to at least capture some of the ordinary cases. The experience is generally going to be bad with polymorphism. Dictionary<string, object>
for instance are going to round trip as JsonElement
instances. I'll make sure to specifically say that the implementation uses S.T.J to marshal the values so we recommend simple values that are easy to round trip, don't exceed the size limits, etc
/// <summary> | ||
/// A <see cref="TagHelper"/> that renders a Blazor component. | ||
/// </summary> | ||
[HtmlTargetElement("component", Attributes = ComponentTypeName)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we enforce the style here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Razor component" vs. "Blazor component"?
Technically, as it is right now, they're Razor Components.
} | ||
|
||
var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>(); | ||
var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, Parameters); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to try and prevent the dictionary allocation here?
/// <summary> | ||
/// Extensions for rendering components. | ||
/// </summary> | ||
public static class HtmlHelperComponentExtensions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
um.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a folder move, ok.
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc.Rendering; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this new namespace? .ViewFeatures
is already kinda a junk namespace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we could clean it up. I thought everything else in that directory was intentionally under the namespace, but it's just this type: https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs#L18
|
||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents | ||
{ | ||
internal interface IComponentRenderer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do we get out of this? If we just called the HTML helper from the tag helper, what are the problmes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming we at least add the non-generic overload to the HTML helper's existing method right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having the things as extensions makes it really difficult to test. Also the extension method is just a place to dump code. Outside of using the ViewContext
, none of the IHtmlHelper
is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we at least get rid of the interface and use an abstract class instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat.
38c7eb7
to
15aabfe
Compare
e62dbbd
to
ccf5a3f
Compare
cf8ddd0
to
fe83fa0
Compare
02a7d8d
to
27556bb
Compare
var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>(); | ||
return componentRenderer.RenderComponentAsync(viewContext, typeof(TComponent), renderMode, parameters); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also add non-generic overloads. The HTML helper is a little too limiting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it, but in general we wouldn't encourage users to use the html helper if there's an equally capable tag helper available. Do you feel strongly about adding the additional overload?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really care that much about the TagHelper, but it should be uber cheap to add this, so I'm with @rynowak on this. There's not even need to add or change tests for them, simply chain the implementations. I trust you know what to do :)
/// A <see cref="TagHelper"/> that renders a Razor component. | ||
/// </summary> | ||
[HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)] | ||
public class ComponentTagHelper : TagHelper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we seal? Would this do something reasonable if someone inherited? I don't have a strong feeling either way.
throw new ArgumentNullException(nameof(output)); | ||
} | ||
|
||
var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a constructor parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want to make the utility type public as yet, at least until we got enough feedback about how users want to use it and how it fits for the client scenarios that were punted until 5.1. This forces the interface to not be a ctor parameter.
cb99d62
to
968d5e5
Compare
This is updated |
} | ||
|
||
[Fact] | ||
public void PassingParametersToComponentsWorks() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's already an E2E test for this. Could you rename it to make it clear that this is testing inheritance?
var parameter2 = Browser.FindElement(By.CssSelector(".Param2")); | ||
Assert.Equal("Value Derived-Value", parameter2.Text); | ||
|
||
// This check verifies CaptureUnmatchedValues works |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we validating this here, it feels completely unrelated to the feature or to the ability to pass parameters to the root component?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Steve noted in the other PR - #14465 (comment), that we want to ensure we have the ability to pass CaptureUnmatchedValues around. I wanted to make sure we covered that in the context of using the tag helper.
<p>Some content after</p> | ||
</div> | ||
</div> | ||
<div id="container"> | ||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" })) | ||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" })) | ||
<component type="typeof(GreeterComponent)" render-mode="Server" param-name='"Albert"' /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave some HtmlHelper instances around so that we can validate they keep working.
@@ -1,13 +1,14 @@ | |||
@page | |||
@using BasicTestApp.RouterTest | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: unnecessary white-space.
// | ||
services.TryAddScoped<IComponentRenderer, ComponentRenderer>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we get rid of the interface an use an internal abstract class with a public virtual method instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a particular benefit to changing this? Given it's internal, we should be free to modify the contract in the future if we need to.
|
||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures | ||
{ | ||
internal class ComponentRenderer : IComponentRenderer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any changes to what was there in the HtmlHelper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, this was pretty much copied from the old code as-is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a couple of comments, but otherwise looks solid. The main 3 things are:
- Get rid of IComponentRenderer.
- Add the non-generic HtmlHelper overloads.
- Leave around some HtmlHelper calls in the E2E tests to make sure they keep working E2E.
968d5e5
to
79a1769
Compare
Fixes #13726