Skip to content

Commit ef4efdd

Browse files
javiercnpranavkm
authored andcommitted
Introduce ComponentTagHelper
Fixes #13726
1 parent b14db57 commit ef4efdd

29 files changed

+689
-304
lines changed

src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
<link href="css/site.css" rel="stylesheet" />
1414
</head>
1515
<body>
16-
<app>
17-
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
18-
</app>
16+
<component type="typeof(App)" render-mode="ServerPrerendered" />
1917

2018
<script src="_framework/blazor.server.js"></script>
2119
</body>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Text.Json;
7+
using System.Text.RegularExpressions;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
10+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
11+
using Microsoft.AspNetCore.E2ETesting;
12+
using OpenQA.Selenium;
13+
using TestServer;
14+
using Xunit;
15+
using Xunit.Abstractions;
16+
17+
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
18+
{
19+
public class ComponentWithParametersTest : ServerTestBase<BasicTestAppServerSiteFixture<PrerenderedStartup>>
20+
{
21+
public ComponentWithParametersTest(
22+
BrowserFixture browserFixture,
23+
BasicTestAppServerSiteFixture<PrerenderedStartup> serverFixture,
24+
ITestOutputHelper output)
25+
: base(browserFixture, serverFixture, output)
26+
{
27+
}
28+
29+
public DateTime LastLogTimeStamp { get; set; } = DateTime.MinValue;
30+
31+
public override async Task InitializeAsync()
32+
{
33+
await base.InitializeAsync();
34+
35+
// Capture the last log timestamp so that we can filter logs when we
36+
// check for duplicate connections.
37+
var lastLog = Browser.Manage().Logs.GetLog(LogType.Browser).LastOrDefault();
38+
if (lastLog != null)
39+
{
40+
LastLogTimeStamp = lastLog.Timestamp;
41+
}
42+
}
43+
44+
[Fact]
45+
public void PassingParametersToComponentsWorks()
46+
{
47+
Navigate("/prerendered/componentwithparameters?QueryValue=testQueryValue");
48+
49+
BeginInteractivity();
50+
51+
Browser.Exists(By.CssSelector(".interactive"));
52+
53+
var parameter1 = Browser.FindElement(By.CssSelector(".Param1"));
54+
Assert.Equal(100, parameter1.FindElements(By.CssSelector("li")).Count);
55+
Assert.Equal("99 99", parameter1.FindElement(By.CssSelector("li:last-child")).Text);
56+
57+
// The assigned value is of a more derived type than the declared model type. This check
58+
// verifies we use the actual model type during round tripping.
59+
var parameter2 = Browser.FindElement(By.CssSelector(".Param2"));
60+
Assert.Equal("Value Derived-Value", parameter2.Text);
61+
62+
// This check verifies CaptureUnmatchedValues works
63+
var parameter3 = Browser.FindElements(By.CssSelector(".Param3 li"));
64+
Assert.Collection(
65+
parameter3,
66+
p => Assert.Equal("key1 testQueryValue", p.Text),
67+
p => Assert.Equal("key2 43", p.Text));
68+
}
69+
70+
private void BeginInteractivity()
71+
{
72+
Browser.FindElement(By.Id("load-boot-script")).Click();
73+
}
74+
}
75+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<h3 class="interactive">Component With Parameters</h3>
2+
3+
<ul class="Param1">
4+
@foreach (var value in Param1)
5+
{
6+
<li>@value.StringProperty @value.IntProperty</li>
7+
}
8+
</ul>
9+
10+
@* Making sure polymorphism works *@
11+
<div class="Param2">@DerivedParam2.StringProperty @DerivedParam2.DerivedProperty</div>
12+
13+
@* Making sure CaptureUnmatchedValues works *@
14+
15+
<ul class="Param3">
16+
@foreach (var value in Param3.OrderBy(kvp => kvp.Key))
17+
{
18+
<li>@value.Key @value.Value</li>
19+
}
20+
</ul>
21+
22+
@code
23+
{
24+
[Parameter] public List<TestModel> Param1 { get; set; }
25+
26+
[Parameter] public TestModel Param2 { get; set; }
27+
28+
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> Param3 { get; set; }
29+
30+
private DerivedModel DerivedParam2 => (DerivedModel)Param2;
31+
32+
public static List<TestModel> TestModelValues => Enumerable.Range(0, 100).Select(c => new TestModel { StringProperty = c.ToString(), IntProperty = c }).ToList();
33+
34+
public static DerivedModel DerivedModelValue = new DerivedModel { StringProperty = "Value", DerivedProperty = "Derived-Value" };
35+
36+
public class TestModel
37+
{
38+
39+
public string StringProperty { get; set; }
40+
41+
public int IntProperty { get; set; }
42+
}
43+
44+
public class DerivedModel : TestModel
45+
{
46+
public string DerivedProperty { get; set; }
47+
}
48+
}

src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<link href="css/site.css" rel="stylesheet" />
1313
</head>
1414
<body>
15-
<app>@(await Html.RenderComponentAsync<App>(RenderMode.Server))</app>
15+
<component type="typeof(App)" render-mode="Server" />
1616
<script src="_framework/blazor.server.js" autostart="false"></script>
1717
<script>
1818
Blazor.start({
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@page
2+
3+
<component type="typeof(ComponentWithParameters)"
4+
render-mode="ServerPrerendered"
5+
parameter-Param1="ComponentWithParameters.TestModelValues"
6+
parameter-Param2="ComponentWithParameters.DerivedModelValue"
7+
parameter-key1="QueryValue"
8+
parameter-key2="43" />
9+
10+
@*
11+
So that E2E tests can make assertions about both the prerendered and
12+
interactive states, we only load the .js file when told to.
13+
*@
14+
<hr />
15+
16+
<button id="load-boot-script" onclick="start()">Load boot script</button>
17+
18+
<script src="_framework/blazor.server.js" autostart="false"></script>
19+
<script>
20+
// Used by InteropOnInitializationComponent
21+
function setElementValue(element, newValue) {
22+
element.value = newValue;
23+
return element.value;
24+
}
25+
26+
function start() {
27+
Blazor.start({
28+
logLevel: 1 // LogLevel.Debug
29+
});
30+
}
31+
</script>
32+
33+
@functions
34+
{
35+
[BindProperty(SupportsGet = true)]
36+
public string QueryValue { get; set; }
37+
}

src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,26 @@
1010
</head>
1111
<body>
1212
<div id="test-container">
13-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
14-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
15-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Static, new { Name = "John" }))
16-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
13+
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered" />
14+
<component type="typeof(GreeterComponent)" render-mode="Server" />
15+
<component type="typeof(GreeterComponent)" render-mode="Static" parameter-name='"John"' />
16+
<component type="typeof(GreeterComponent)" render-mode="Server"/>
1717
<div id="container">
1818
<p>Some content before</p>
19-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
19+
<component type="typeof(GreeterComponent)" render-mode="Server"/>
2020
<p>Some content between</p>
21-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
21+
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
2222
<p>Some content after</p>
2323
<div id="nested-an-extra-level">
2424
<p>Some content before</p>
25-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
26-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
25+
<component type="typeof(GreeterComponent)" render-mode="Server"/>
26+
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
2727
<p>Some content after</p>
2828
</div>
2929
</div>
3030
<div id="container">
31-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" }))
32-
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" }))
31+
<component type="typeof(GreeterComponent)" render-mode="Server" parameter-name='"Albert"' />
32+
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered" parameter-name='"Abraham"' />
3333
</div>
3434
</div>
3535

src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
@page
22
@using BasicTestApp.RouterTest
3+
34
<!DOCTYPE html>
45
<html>
56
<head>
67
<title>Prerendering tests</title>
78
<base href="~/" />
89
</head>
910
<body>
10-
<app>@(await Html.RenderComponentAsync<TestRouter>(RenderMode.ServerPrerendered))</app>
11+
<app><component type="typeof(TestRouter)" render-mode="ServerPrerendered" /></app>
1112

1213
@*
1314
So that E2E tests can make assertions about both the prerendered and

src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@page ""
2+
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
23
<!DOCTYPE html>
34
<html>
45
<head>
@@ -11,7 +12,7 @@
1112
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
1213
</head>
1314
<body>
14-
<root>@(await Html.RenderComponentAsync<BasicTestApp.Index>(RenderMode.Server))</root>
15+
<root><component type="typeof(BasicTestApp.Index)" render-mode="Server" /></root>
1516

1617
<!-- Used for testing interop scenarios between JS and .NET -->
1718
<script src="js/jsinteroptests.js"></script>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
2+
@using BasicTestApp
3+

src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,22 @@ public partial class CacheTagHelperOptions
105105
public CacheTagHelperOptions() { }
106106
public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
107107
}
108+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("component", Attributes="type")]
109+
public partial class ComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
110+
{
111+
public ComponentTagHelper() { }
112+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("type")]
113+
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
114+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("parameters", DictionaryAttributePrefix="parameter-")]
115+
public System.Collections.Generic.IDictionary<string, object> Parameters { get { throw null; } set { } }
116+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")]
117+
public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
118+
[Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute]
119+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute]
120+
public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
121+
[System.Diagnostics.DebuggerStepThroughAttribute]
122+
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { throw null; }
123+
}
108124
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")]
109125
public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase
110126
{
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Mvc.Rendering;
8+
using Microsoft.AspNetCore.Mvc.ViewFeatures;
9+
using Microsoft.AspNetCore.Razor.TagHelpers;
10+
using Microsoft.Extensions.DependencyInjection;
11+
12+
namespace Microsoft.AspNetCore.Mvc.TagHelpers
13+
{
14+
/// <summary>
15+
/// A <see cref="TagHelper"/> that renders a Razor component.
16+
/// </summary>
17+
[HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
18+
public class ComponentTagHelper : TagHelper
19+
{
20+
private const string ComponentParameterName = "parameters";
21+
private const string ComponentParameterPrefix = "parameter-";
22+
private const string ComponentTypeName = "type";
23+
private const string RenderModeName = "render-mode";
24+
private IDictionary<string, object> _parameters;
25+
26+
/// <summary>
27+
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
28+
/// </summary>
29+
[HtmlAttributeNotBound]
30+
[ViewContext]
31+
public ViewContext ViewContext { get; set; }
32+
33+
/// <summary>
34+
/// Gets or sets values for component parameters.
35+
/// </summary>
36+
[HtmlAttributeName(ComponentParameterName, DictionaryAttributePrefix = ComponentParameterPrefix)]
37+
public IDictionary<string, object> Parameters
38+
{
39+
get
40+
{
41+
_parameters ??= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
42+
return _parameters;
43+
}
44+
set => _parameters = value;
45+
}
46+
47+
/// <summary>
48+
/// Gets or sets the component type. This value is required.
49+
/// </summary>
50+
[HtmlAttributeName(ComponentTypeName)]
51+
public Type ComponentType { get; set; }
52+
53+
/// <summary>
54+
/// Gets or sets the <see cref="Rendering.RenderMode"/>
55+
/// </summary>
56+
[HtmlAttributeName(RenderModeName)]
57+
public RenderMode RenderMode { get; set; }
58+
59+
/// <inheritdoc />
60+
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
61+
{
62+
if (context == null)
63+
{
64+
throw new ArgumentNullException(nameof(context));
65+
}
66+
67+
if (output == null)
68+
{
69+
throw new ArgumentNullException(nameof(output));
70+
}
71+
72+
var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
73+
var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters);
74+
75+
// Reset the TagName. We don't want `component` to render.
76+
output.TagName = null;
77+
output.Content.SetHtmlContent(result);
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)