[Components] Create a renderer to render components into HTML (#4463)
* Adds an HtmlRenderer to Microsoft.AspNetCore.Components * It renders the component into a list of strings. * It only handles synchronous rendering.
This commit is contained in:
parent
a7f6154520
commit
436b5461ad
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 15.0.27130.2010
|
VisualStudioVersion = 16.0.28315.86
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
<InternalAspNetCoreSdkPackageVersion>3.0.0-alpha1-20181011.3</InternalAspNetCoreSdkPackageVersion>
|
<InternalAspNetCoreSdkPackageVersion>3.0.0-alpha1-20181011.3</InternalAspNetCoreSdkPackageVersion>
|
||||||
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview1-34576</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
|
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview1-34576</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
|
||||||
<MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreAppPackageVersion>
|
<MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreAppPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreHtmlAbstractionsPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreHtmlAbstractionsPackageVersion>
|
||||||
<MicrosoftAspNetCoreRazorDesignPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreRazorDesignPackageVersion>
|
<MicrosoftAspNetCoreRazorDesignPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreRazorDesignPackageVersion>
|
||||||
|
<MicrosoftExtensionsDependencyInjectionPackageVersion>3.0.0-alpha1-10605</MicrosoftExtensionsDependencyInjectionPackageVersion>
|
||||||
<MicrosoftNETCoreAppPackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreAppPackageVersion>
|
<MicrosoftNETCoreAppPackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreAppPackageVersion>
|
||||||
<SignalRPackageVersion>3.0.0-alpha1-10605</SignalRPackageVersion>
|
<SignalRPackageVersion>3.0.0-alpha1-10605</SignalRPackageVersion>
|
||||||
<TemplateBlazorPackageVersion>0.8.0-preview1-20181122.3</TemplateBlazorPackageVersion>
|
<TemplateBlazorPackageVersion>0.8.0-preview1-20181122.3</TemplateBlazorPackageVersion>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
public int ComponentId => _componentId;
|
public int ComponentId => _componentId;
|
||||||
public IComponent Component => _component;
|
public IComponent Component => _component;
|
||||||
public ComponentState ParentComponentState => _parentComponentState;
|
public ComponentState ParentComponentState => _parentComponentState;
|
||||||
|
public RenderTreeBuilder CurrrentRenderTree => _renderTreeBuilderCurrent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs an instance of <see cref="ComponentState"/>.
|
/// Constructs an instance of <see cref="ComponentState"/>.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="Renderer"/> that produces HTML.
|
||||||
|
/// </summary>
|
||||||
|
public class HtmlRenderer : Renderer
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> SelfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly Func<string, string> _htmlEncoder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="HtmlRenderer"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use to instantiate components.</param>
|
||||||
|
/// <param name="htmlEncoder">A <see cref="Func{T, TResult}"/> that will HTML encode the given string.</param>
|
||||||
|
public HtmlRenderer(IServiceProvider serviceProvider, Func<string, string> htmlEncoder) : base(serviceProvider)
|
||||||
|
{
|
||||||
|
_htmlEncoder = htmlEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
|
||||||
|
/// of the HTML produced by the component.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the <see cref="IComponent"/>.</typeparam>
|
||||||
|
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
|
||||||
|
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
|
||||||
|
public IEnumerable<string> RenderComponent<T>(ParameterCollection initialParameters) where T : IComponent
|
||||||
|
{
|
||||||
|
return RenderComponent(typeof(T), initialParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
|
||||||
|
/// of the HTML produced by the component.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
|
||||||
|
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
|
||||||
|
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
|
||||||
|
private IEnumerable<string> RenderComponent(Type componentType, ParameterCollection initialParameters)
|
||||||
|
{
|
||||||
|
var frames = CreateInitialRender(componentType, initialParameters);
|
||||||
|
|
||||||
|
if (frames.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
var newPosition = RenderFrames(result, frames, 0, frames.Count);
|
||||||
|
Debug.Assert(newPosition == frames.Count);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int RenderFrames(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
||||||
|
{
|
||||||
|
var nextPosition = position;
|
||||||
|
var endPosition = position + maxElements;
|
||||||
|
while (position < endPosition)
|
||||||
|
{
|
||||||
|
nextPosition = RenderCore(result, frames, position, maxElements);
|
||||||
|
if (position == nextPosition)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("We didn't consume any input.");
|
||||||
|
}
|
||||||
|
position = nextPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int RenderCore(
|
||||||
|
List<string> result,
|
||||||
|
ArrayRange<RenderTreeFrame> frames,
|
||||||
|
int position,
|
||||||
|
int length)
|
||||||
|
{
|
||||||
|
ref var frame = ref frames.Array[position];
|
||||||
|
switch (frame.FrameType)
|
||||||
|
{
|
||||||
|
case RenderTreeFrameType.Element:
|
||||||
|
return RenderElement(result, frames, position);
|
||||||
|
case RenderTreeFrameType.Attribute:
|
||||||
|
return RenderAttributes(result, frames, position, 1);
|
||||||
|
case RenderTreeFrameType.Text:
|
||||||
|
result.Add(_htmlEncoder(frame.TextContent));
|
||||||
|
return ++position;
|
||||||
|
case RenderTreeFrameType.Markup:
|
||||||
|
result.Add(frame.MarkupContent);
|
||||||
|
return ++position;
|
||||||
|
case RenderTreeFrameType.Component:
|
||||||
|
return RenderChildComponent(result, frames, position);
|
||||||
|
case RenderTreeFrameType.Region:
|
||||||
|
return RenderFrames(result, frames, position + 1, frame.RegionSubtreeLength - 1);
|
||||||
|
case RenderTreeFrameType.ElementReferenceCapture:
|
||||||
|
case RenderTreeFrameType.ComponentReferenceCapture:
|
||||||
|
return ++position;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Invalid element frame type '{frame.FrameType}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int RenderChildComponent(
|
||||||
|
List<string> result,
|
||||||
|
ArrayRange<RenderTreeFrame> frames,
|
||||||
|
int position)
|
||||||
|
{
|
||||||
|
ref var frame = ref frames.Array[position];
|
||||||
|
var childFrames = GetCurrentRenderTreeFrames(frame.ComponentId);
|
||||||
|
RenderFrames(result, childFrames, 0, childFrames.Count);
|
||||||
|
return position + frame.ComponentSubtreeLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int RenderElement(
|
||||||
|
List<string> result,
|
||||||
|
ArrayRange<RenderTreeFrame> frames,
|
||||||
|
int position)
|
||||||
|
{
|
||||||
|
ref var frame = ref frames.Array[position];
|
||||||
|
result.Add("<");
|
||||||
|
result.Add(frame.ElementName);
|
||||||
|
var afterAttributes = RenderAttributes(result, frames, position + 1, frame.ElementSubtreeLength - 1);
|
||||||
|
var remainingElements = frame.ElementSubtreeLength + position - afterAttributes;
|
||||||
|
if (remainingElements > 0)
|
||||||
|
{
|
||||||
|
result.Add(">");
|
||||||
|
var afterElement = RenderChildren(result, frames, afterAttributes, remainingElements);
|
||||||
|
result.Add("</");
|
||||||
|
result.Add(frame.ElementName);
|
||||||
|
result.Add(">");
|
||||||
|
Debug.Assert(afterElement == position + frame.ElementSubtreeLength);
|
||||||
|
return afterElement;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (SelfClosingElements.Contains(frame.ElementName))
|
||||||
|
{
|
||||||
|
result.Add(" />");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Add(">");
|
||||||
|
result.Add("</");
|
||||||
|
result.Add(frame.ElementName);
|
||||||
|
result.Add(">");
|
||||||
|
}
|
||||||
|
Debug.Assert(afterAttributes == position + frame.ElementSubtreeLength);
|
||||||
|
return afterAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int RenderChildren(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
||||||
|
{
|
||||||
|
if (maxElements == 0)
|
||||||
|
{
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RenderFrames(result, frames, position, maxElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int RenderAttributes(
|
||||||
|
List<string> result,
|
||||||
|
ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
||||||
|
{
|
||||||
|
if (maxElements == 0)
|
||||||
|
{
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < maxElements; i++)
|
||||||
|
{
|
||||||
|
var candidateIndex = position + i;
|
||||||
|
ref var frame = ref frames.Array[candidateIndex];
|
||||||
|
if (frame.FrameType != RenderTreeFrameType.Attribute)
|
||||||
|
{
|
||||||
|
return candidateIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (frame.AttributeValue)
|
||||||
|
{
|
||||||
|
case bool flag when flag:
|
||||||
|
result.Add(" ");
|
||||||
|
result.Add(frame.AttributeName);
|
||||||
|
break;
|
||||||
|
case string value:
|
||||||
|
result.Add(" ");
|
||||||
|
result.Add(frame.AttributeName);
|
||||||
|
result.Add("=");
|
||||||
|
result.Add("\"");
|
||||||
|
result.Add(_htmlEncoder(value));
|
||||||
|
result.Add("\"");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return position + maxElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayRange<RenderTreeFrame> CreateInitialRender(Type componentType, ParameterCollection initialParameters)
|
||||||
|
{
|
||||||
|
var component = InstantiateComponent(componentType);
|
||||||
|
var componentId = AssignRootComponentId(component);
|
||||||
|
|
||||||
|
RenderRootComponent(componentId, initialParameters);
|
||||||
|
|
||||||
|
return GetCurrentRenderTreeFrames(componentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +53,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
protected int AssignRootComponentId(IComponent component)
|
protected int AssignRootComponentId(IComponent component)
|
||||||
=> AttachAndInitComponent(component, -1).ComponentId;
|
=> AttachAndInitComponent(component, -1).ComponentId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current render tree for a given component.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentId">The id for the component.</param>
|
||||||
|
/// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns>
|
||||||
|
private protected ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrrentRenderTree.GetFrames();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs the first render for a root component. After this, the root component
|
/// Performs the first render for a root component. After this, the root component
|
||||||
/// makes its own decisions about when to re-render, so there is no need to call
|
/// makes its own decisions about when to re-render, so there is no need to call
|
||||||
|
|
@ -65,6 +72,19 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
.SetDirectParameters(ParameterCollection.Empty);
|
.SetDirectParameters(ParameterCollection.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs the first render for a root component. After this, the root component
|
||||||
|
/// makes its own decisions about when to re-render, so there is no need to call
|
||||||
|
/// this more than once.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
|
||||||
|
/// <param name="initialParameters">The <see cref="ParameterCollection"/>with the initial parameters to use for rendering.</param>
|
||||||
|
protected void RenderRootComponent(int componentId, ParameterCollection initialParameters)
|
||||||
|
{
|
||||||
|
GetRequiredComponentState(componentId)
|
||||||
|
.SetDirectParameters(initialParameters);
|
||||||
|
}
|
||||||
|
|
||||||
private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
|
private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
|
||||||
{
|
{
|
||||||
var componentId = _nextComponentId++;
|
var componentId = _nextComponentId++;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
<PackageReference Include="xunit" Version="2.3.1" />
|
<PackageReference Include="xunit" Version="2.3.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||||
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
|
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="$(MicrosoftAspNetCoreHtmlAbstractionsPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,447 @@
|
||||||
|
// 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.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
|
{
|
||||||
|
public class HtmlRendererTests
|
||||||
|
{
|
||||||
|
private static readonly Func<string, string> _encoder = (string t) => HtmlEncoder.Default.Encode(t);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanRenderEmptyElement()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "p", ">", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanRenderSimpleComponent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "p", ">", "Hello world!", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.AddContent(1, "Hello world!");
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_HtmlEncodesContent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "p", ">", "<Hello world!>", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.AddContent(1, "<Hello world!>");
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_DoesNotEncodeMarkup()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "p", ">", "<span>Hello world!</span>", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.AddMarkupContent(1, "<span>Hello world!</span>");
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanRenderWithAttributes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "lead", "\"", ">", "Hello world!", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.AddAttribute(1, "class", "lead");
|
||||||
|
rtb.AddContent(2, "Hello world!");
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_HtmlEncodesAttributeValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "<lead", "\"", ">", "Hello world!", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.AddAttribute(1, "class", "<lead");
|
||||||
|
rtb.AddContent(2, "Hello world!");
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanRenderBooleanAttributes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "input", " ", "disabled", " />" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "input");
|
||||||
|
rtb.AddAttribute(1, "disabled", true);
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_DoesNotRenderBooleanAttributesWhenValueIsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "input", " />" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "input");
|
||||||
|
rtb.AddAttribute(1, "disabled", false);
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanRenderWithChildren()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.OpenElement(1, "span");
|
||||||
|
rtb.AddContent(2, "Hello world!");
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanRenderWithMultipleChildren()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] { "<", "p", ">",
|
||||||
|
"<", "span", ">", "Hello world!", "</", "span", ">",
|
||||||
|
"<", "span", ">", "Bye Bye world!", "</", "span", ">",
|
||||||
|
"</", "p", ">"
|
||||||
|
};
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.OpenElement(1, "span");
|
||||||
|
rtb.AddContent(2, "Hello world!");
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.OpenElement(3, "span");
|
||||||
|
rtb.AddContent(4, "Bye Bye world!");
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanRenderComponentWithChildrenComponents()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] {
|
||||||
|
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">",
|
||||||
|
"<", "span", ">", "Child content!", "</", "span", ">"
|
||||||
|
};
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.OpenElement(1, "span");
|
||||||
|
rtb.AddContent(2, "Hello world!");
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.OpenComponent(3, typeof(ChildComponent));
|
||||||
|
rtb.AddAttribute(4, "Value", "Child content!");
|
||||||
|
rtb.CloseComponent();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_ComponentReferenceNoops()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] {
|
||||||
|
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">",
|
||||||
|
"<", "span", ">", "Child content!", "</", "span", ">"
|
||||||
|
};
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.OpenElement(1, "span");
|
||||||
|
rtb.AddContent(2, "Hello world!");
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.OpenComponent(3, typeof(ChildComponent));
|
||||||
|
rtb.AddAttribute(4, "Value", "Child content!");
|
||||||
|
rtb.AddComponentReferenceCapture(5, cr => { });
|
||||||
|
rtb.CloseComponent();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanPassParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] {
|
||||||
|
"<", "p", ">", "<", "input", " ", "value", "=", "\"", "5", "\"", " />", "</", "p", ">" };
|
||||||
|
|
||||||
|
RenderFragment Content(ParameterCollection pc) => new RenderFragment((RenderTreeBuilder rtb) =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.OpenElement(1, "input");
|
||||||
|
rtb.AddAttribute(2, "change", pc.GetValueOrDefault<Action<UIChangeEventArgs>>("update"));
|
||||||
|
rtb.AddAttribute(3, "value", pc.GetValueOrDefault<int>("value"));
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.CloseElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = new ServiceCollection()
|
||||||
|
.AddSingleton(new Func<ParameterCollection, RenderFragment>(Content))
|
||||||
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
Action<UIChangeEventArgs> change = (UIChangeEventArgs changeArgs) => throw new InvalidOperationException();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<ComponentWithParameters>(
|
||||||
|
new ParameterCollection(new[] {
|
||||||
|
RenderTreeFrame.Element(0,string.Empty),
|
||||||
|
RenderTreeFrame.Attribute(1,"update",change),
|
||||||
|
RenderTreeFrame.Attribute(2,"value",5)
|
||||||
|
}, 0));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_CanRenderComponentWithRenderFragmentContent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] {
|
||||||
|
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.OpenElement(1, "span");
|
||||||
|
rtb.AddContent(2,
|
||||||
|
// This internally creates a region frame.
|
||||||
|
rf => rf.AddContent(0, "Hello world!"));
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderComponent_ElementRefsNoops()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHtml = new[] {
|
||||||
|
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
|
||||||
|
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||||
|
{
|
||||||
|
rtb.OpenElement(0, "p");
|
||||||
|
rtb.AddElementReferenceCapture(1, er => { });
|
||||||
|
rtb.OpenElement(2, "span");
|
||||||
|
rtb.AddContent(3,
|
||||||
|
// This internally creates a region frame.
|
||||||
|
rf => rf.AddContent(0, "Hello world!"));
|
||||||
|
rtb.CloseElement();
|
||||||
|
rtb.CloseElement();
|
||||||
|
})).BuildServiceProvider();
|
||||||
|
|
||||||
|
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHtml, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ComponentWithParameters : IComponent
|
||||||
|
{
|
||||||
|
public RenderHandle RenderHandle { get; private set; }
|
||||||
|
|
||||||
|
public void Init(RenderHandle renderHandle)
|
||||||
|
{
|
||||||
|
RenderHandle = renderHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
Func<ParameterCollection, RenderFragment> CreateRenderFragment { get; set; }
|
||||||
|
|
||||||
|
public void SetParameters(ParameterCollection parameters)
|
||||||
|
{
|
||||||
|
RenderHandle.Render(CreateRenderFragment(parameters));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ChildComponent : IComponent
|
||||||
|
{
|
||||||
|
private RenderHandle _renderHandle;
|
||||||
|
|
||||||
|
public void Init(RenderHandle renderHandle)
|
||||||
|
{
|
||||||
|
_renderHandle = renderHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParameters(ParameterCollection parameters)
|
||||||
|
{
|
||||||
|
_renderHandle.Render(CreateRenderFragment(parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment CreateRenderFragment(ParameterCollection parameters)
|
||||||
|
{
|
||||||
|
return RenderFragment;
|
||||||
|
|
||||||
|
void RenderFragment(RenderTreeBuilder rtb)
|
||||||
|
{
|
||||||
|
rtb.OpenElement(1, "span");
|
||||||
|
rtb.AddContent(2, parameters.GetValueOrDefault<string>("Value"));
|
||||||
|
rtb.CloseElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestComponent : IComponent
|
||||||
|
{
|
||||||
|
private RenderHandle _renderHandle;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
public RenderFragment Fragment { get; set; }
|
||||||
|
|
||||||
|
public void Init(RenderHandle renderHandle)
|
||||||
|
{
|
||||||
|
_renderHandle = renderHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParameters(ParameterCollection parameters)
|
||||||
|
{
|
||||||
|
_renderHandle.Render(Fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue