Skip to content

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

Merged
merged 7 commits into from
Oct 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<component type="typeof(App)" render-mode="ServerPrerendered" />

<script src="_framework/blazor.server.js"></script>
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
public class ComponentWithParametersTest : ServerTestBase<BasicTestAppServerSiteFixture<PrerenderedStartup>>
{
public ComponentWithParametersTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<PrerenderedStartup> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}

[Fact]
public void PassingParametersToComponentsFromThePageWorks()
{
Navigate("/prerendered/componentwithparameters?QueryValue=testQueryValue");

BeginInteractivity();

Browser.Exists(By.CssSelector(".interactive"));

var parameter1 = Browser.FindElement(By.CssSelector(".Param1"));
Assert.Equal(100, parameter1.FindElements(By.CssSelector("li")).Count);
Assert.Equal("99 99", parameter1.FindElement(By.CssSelector("li:last-child")).Text);

// The assigned value is of a more derived type than the declared model type. This check
// verifies we use the actual model type during round tripping.
var parameter2 = Browser.FindElement(By.CssSelector(".Param2"));
Assert.Equal("Value Derived-Value", parameter2.Text);

// This check verifies CaptureUnmatchedValues works
Copy link
Member

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?

Copy link
Contributor Author

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.

var parameter3 = Browser.FindElements(By.CssSelector(".Param3 li"));
Assert.Collection(
parameter3,
p => Assert.Equal("key1 testQueryValue", p.Text),
p => Assert.Equal("key2 43", p.Text));
}

private void BeginInteractivity()
{
Browser.FindElement(By.Id("load-boot-script")).Click();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<h3 class="interactive">Component With Parameters</h3>

<ul class="Param1">
@foreach (var value in Param1)
{
<li>@value.StringProperty @value.IntProperty</li>
}
</ul>

@* Making sure polymorphism works *@
<div class="Param2">@DerivedParam2.StringProperty @DerivedParam2.DerivedProperty</div>

@* Making sure CaptureUnmatchedValues works *@

<ul class="Param3">
@foreach (var value in Param3.OrderBy(kvp => kvp.Key))
{
<li>@value.Key @value.Value</li>
}
</ul>

@code
{
[Parameter] public List<TestModel> Param1 { get; set; }

[Parameter] public TestModel Param2 { get; set; }

[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> Param3 { get; set; }

private DerivedModel DerivedParam2 => (DerivedModel)Param2;

public static List<TestModel> TestModelValues => Enumerable.Range(0, 100).Select(c => new TestModel { StringProperty = c.ToString(), IntProperty = c }).ToList();

public static DerivedModel DerivedModelValue = new DerivedModel { StringProperty = "Value", DerivedProperty = "Derived-Value" };

public class TestModel
{

public string StringProperty { get; set; }

public int IntProperty { get; set; }
}

public class DerivedModel : TestModel
{
public string DerivedProperty { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<App>(RenderMode.Server))</app>
<component type="typeof(App)" render-mode="Server" />
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
Blazor.start({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@page

<component type="typeof(ComponentWithParameters)"
render-mode="ServerPrerendered"
param-Param1="ComponentWithParameters.TestModelValues"
param-Param2="ComponentWithParameters.DerivedModelValue"
param-key1="QueryValue"
param-key2="43" />

@*
So that E2E tests can make assertions about both the prerendered and
interactive states, we only load the .js file when told to.
*@
<hr />

<button id="load-boot-script" onclick="start()">Load boot script</button>

<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
// Used by InteropOnInitializationComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}

function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
}
</script>

@functions
{
[BindProperty(SupportsGet = true)]
public string QueryValue { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@
<div id="test-container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Static, new { Name = "John" }))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
<component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<div id="container">
<p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<p>Some content between</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p>
<div id="nested-an-extra-level">
<p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<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"' />
Copy link
Member

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.

<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered" param-name='"Abraham"' />
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
@page
@using BasicTestApp.RouterTest

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unnecessary white-space.

<!DOCTYPE html>
<html>
<head>
<title>Prerendering tests</title>
<base href="~/" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<TestRouter>(RenderMode.ServerPrerendered))</app>
<app><component type="typeof(TestRouter)" render-mode="ServerPrerendered" /></app>

@*
So that E2E tests can make assertions about both the prerendered and
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@page ""
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
<!DOCTYPE html>
<html>
<head>
Expand All @@ -11,7 +12,7 @@
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
</head>
<body>
<root>@(await Html.RenderComponentAsync<BasicTestApp.Index>(RenderMode.Server))</root>
<root><component type="typeof(BasicTestApp.Index)" render-mode="Server" /></root>

<!-- Used for testing interop scenarios between JS and .NET -->
<script src="js/jsinteroptests.js"></script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using BasicTestApp

Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ public partial class CacheTagHelperOptions
public CacheTagHelperOptions() { }
public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("component", Attributes="type", TagStructure=Microsoft.AspNetCore.Razor.TagHelpers.TagStructure.WithoutEndTag)]
public sealed partial class ComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
public ComponentTagHelper() { }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("type")]
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("params", DictionaryAttributePrefix="param-")]
public System.Collections.Generic.IDictionary<string, object> Parameters { get { throw null; } set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")]
public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute]
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute]
public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[System.Diagnostics.DebuggerStepThroughAttribute]
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { throw null; }
}
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")]
public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase
{
Expand Down
80 changes: 80 additions & 0 deletions src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// A <see cref="TagHelper"/> that renders a Razor component.
/// </summary>
[HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
public sealed class ComponentTagHelper : TagHelper
{
private const string ComponentParameterName = "params";
private const string ComponentParameterPrefix = "param-";
private const string ComponentTypeName = "type";
private const string RenderModeName = "render-mode";
private IDictionary<string, object> _parameters;

/// <summary>
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }

/// <summary>
/// Gets or sets values for component parameters.
/// </summary>
[HtmlAttributeName(ComponentParameterName, DictionaryAttributePrefix = ComponentParameterPrefix)]
public IDictionary<string, object> Parameters
{
get
{
_parameters ??= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
return _parameters;
}
set => _parameters = value;
}

/// <summary>
/// Gets or sets the component type. This value is required.
/// </summary>
[HtmlAttributeName(ComponentTypeName)]
public Type ComponentType { get; set; }

/// <summary>
/// Gets or sets the <see cref="Rendering.RenderMode"/>
/// </summary>
[HtmlAttributeName(RenderModeName)]
public RenderMode RenderMode { get; set; }

/// <inheritdoc />
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (output == null)
{
throw new ArgumentNullException(nameof(output));
}

var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
Copy link
Member

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?

Copy link
Contributor Author

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.

var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters);

// Reset the TagName. We don't want `component` to render.
output.TagName = null;
output.Content.SetHtmlContent(result);
}
}
}
Loading