diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs
index 7d8c09cb92..1a44498c0f 100644
--- a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs
+++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs
@@ -201,6 +201,10 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
HtmlEntityService.Resolver);
var codeWriter = context.CodeWriter;
+ // TODO: As an optimization, identify static subtrees (i.e., HTML elements in the Razor source
+ // that contain no C#) and represent them as a new RenderTreeNodeType called StaticElement or
+ // similar. This means you can have arbitrarily deep static subtrees without paying any per-
+ // node cost during rendering or diffing.
HtmlToken nextToken;
while ((nextToken = tokenizer.Get()).Type != HtmlTokenType.EndOfFile)
{
@@ -224,12 +228,23 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
var nextTag = nextToken.AsTag();
if (nextToken.Type == HtmlTokenType.StartTag)
{
- codeWriter
- .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
- .Write((_sourceSequence++).ToString())
- .WriteParameterSeparator()
- .WriteStringLiteral(nextTag.Data)
- .WriteEndMethodInvocation();
+ var tagNameOriginalCase = GetTagNameWithOriginalCase(originalHtmlContent, nextTag);
+ if (TryGetComponentTypeNameFromTagName(tagNameOriginalCase, out var componentTypeName))
+ {
+ codeWriter
+ .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddComponentElement)}<{componentTypeName}>")
+ .Write((_sourceSequence++).ToString())
+ .WriteEndMethodInvocation();
+ }
+ else
+ {
+ codeWriter
+ .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
+ .Write((_sourceSequence++).ToString())
+ .WriteParameterSeparator()
+ .WriteStringLiteral(nextTag.Data)
+ .WriteEndMethodInvocation();
+ }
}
foreach (var attribute in nextTag.Attributes)
@@ -286,6 +301,35 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
}
}
+ private static string GetTagNameWithOriginalCase(string document, HtmlTagToken tagToken)
+ => document.Substring(tagToken.Position.Position, tagToken.Name.Length);
+
+ private bool TryGetComponentTypeNameFromTagName(string tagName, out string componentTypeName)
+ {
+ // Determine whether 'tagName' represents a Blazor component, and if so, return the
+ // name of the component's .NET type. The type name doesn't have to be fully-qualified,
+ // because it's up to the developer to put in whatever @using statements are required.
+
+ // TODO: Remove this temporary syntax and make the compiler smart enough to infer it
+ // directly. This could either work by having a configurable list of non-component tag names
+ // (which would default to all standard HTML elements, plus anything that contains a '-'
+ // character, since those are mandatory for custom HTML elements and prohibited for .NET
+ // type names), or better, could somehow know what .NET types are in scope at this point
+ // in the compilation and treat everything else as a non-component element.
+
+ const string temporaryPrefix = "c:";
+ if (tagName.StartsWith(temporaryPrefix, StringComparison.Ordinal))
+ {
+ componentTypeName = tagName.Substring(temporaryPrefix.Length);
+ return true;
+ }
+ else
+ {
+ componentTypeName = null;
+ return false;
+ }
+ }
+
private static void WriteAttribute(CodeWriter codeWriter, int sourceSequence, string key, object value)
{
codeWriter
diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs
index e823fae230..0c094249bd 100644
--- a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs
+++ b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs
@@ -130,8 +130,18 @@ namespace Microsoft.Blazor.RenderTree
///
/// The type of the child component.
/// An integer that represents the position of the instruction in the source code.
- public void AddComponent(int sequence) where TComponent: IComponent
- => Append(RenderTreeNode.ChildComponent(sequence));
+ public void AddComponentElement(int sequence) where TComponent : IComponent
+ {
+ // Currently, child components can't have further grandchildren of their own, so it would
+ // technically be possible to skip their CloseElement calls and not track them in _openElementIndices.
+ // However at some point we might want to have the grandchildren nodes available at runtime
+ // (rather than being parsed as attributes at compile time) so that we could have APIs for
+ // components to query the complete hierarchy of transcluded nodes instead of forcing the
+ // transcluded subtree to be in a particular shape such as representing key/value pairs.
+ // So it's more flexible if we track open/close nodes for components explicitly.
+ _openElementIndices.Push(_entries.Count);
+ Append(RenderTreeNode.ChildComponent(sequence));
+ }
private void AssertCanAddAttribute()
{
diff --git a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs
index 81b6353dcf..52a816f720 100644
--- a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs
+++ b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs
@@ -332,6 +332,22 @@ namespace Microsoft.Blazor.Build.Test
});
}
+ [Fact]
+ public void SupportsChildComponentsViaTemporarySyntax()
+ {
+ // Arrange
+ var treeBuilder = new RenderTreeBuilder(new TestRenderer());
+
+ // Arrange/Act
+ var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
+ var component = CompileToComponent($"");
+ component.BuildRenderTree(treeBuilder);
+
+ // Assert
+ Assert.Collection(treeBuilder.GetNodes(),
+ node => AssertNode.Component(node));
+ }
+
private static bool NotWhitespace(RenderTreeNode node)
=> node.NodeType != RenderTreeNodeType.Text
|| !string.IsNullOrWhiteSpace(node.TextContent);
@@ -370,7 +386,8 @@ namespace Microsoft.Blazor.Build.Test
var referenceAssembliesContainingTypes = new[]
{
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute), // System.Runtime
- typeof(BlazorComponent)
+ typeof(BlazorComponent),
+ typeof(RazorCompilerTest), // Reference this assembly, so that we can refer to test component types
};
var references = referenceAssembliesContainingTypes
.SelectMany(type => type.Assembly.GetReferencedAssemblies().Concat(new[] { type.Assembly.GetName() }))
@@ -445,5 +462,13 @@ namespace Microsoft.Blazor.Build.Test
protected override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff)
=> throw new NotImplementedException();
}
+
+ public class TestComponent : IComponent
+ {
+ public void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.AddText(0, $"Hello from {nameof(TestComponent)}");
+ }
+ }
}
}
diff --git a/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs
index e2fa28cea3..a2bac87d8f 100644
--- a/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs
+++ b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs
@@ -253,11 +253,13 @@ namespace Microsoft.Blazor.Test
// Act
builder.OpenElement(10, "parent"); // 0:
- builder.AddComponent(11); // 1: (11); // 1:
- builder.AddComponent(14); // 4: (14); // 4:
+ builder.CloseElement();
builder.CloseElement(); //
// Assert
diff --git a/test/Microsoft.Blazor.Test/RenderTreeDiffComputerTest.cs b/test/Microsoft.Blazor.Test/RenderTreeDiffComputerTest.cs
index 556155fe9f..e7fdb922f9 100644
--- a/test/Microsoft.Blazor.Test/RenderTreeDiffComputerTest.cs
+++ b/test/Microsoft.Blazor.Test/RenderTreeDiffComputerTest.cs
@@ -42,7 +42,7 @@ namespace Microsoft.Blazor.Test
builder.AddAttribute(1, "My attribute", "My value");
builder.CloseElement();
},
- builder => builder.AddComponent(0)
+ builder => builder.AddComponentElement(0)
}.Select(x => new object[] { x });
[Fact]
@@ -337,8 +337,8 @@ namespace Microsoft.Blazor.Test
var oldTree = new RenderTreeBuilder(new FakeRenderer());
var newTree = new RenderTreeBuilder(new FakeRenderer());
var diff = new RenderTreeDiffComputer();
- oldTree.AddComponent(123);
- newTree.AddComponent(123);
+ oldTree.AddComponentElement(123);
+ newTree.AddComponentElement(123);
// Act
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());
diff --git a/test/Microsoft.Blazor.Test/RendererTest.cs b/test/Microsoft.Blazor.Test/RendererTest.cs
index caf12e43a1..5fc6ff1256 100644
--- a/test/Microsoft.Blazor.Test/RendererTest.cs
+++ b/test/Microsoft.Blazor.Test/RendererTest.cs
@@ -44,7 +44,7 @@ namespace Microsoft.Blazor.Test
var component = new TestComponent(builder =>
{
builder.AddText(0, "Hello");
- builder.AddComponent(1);
+ builder.AddComponentElement(1);
});
// Act/Assert
@@ -94,7 +94,7 @@ namespace Microsoft.Blazor.Test
var renderer = new TestRenderer();
var parentComponent = new TestComponent(builder =>
{
- builder.AddComponent(0);
+ builder.AddComponentElement(0);
});
var parentComponentId = renderer.AssignComponentId(parentComponent);
renderer.RenderComponent(parentComponentId);
@@ -152,7 +152,7 @@ namespace Microsoft.Blazor.Test
var renderer = new TestRenderer();
var parentComponent = new TestComponent(builder =>
{
- builder.AddComponent(0);
+ builder.AddComponentElement(0);
});
var parentComponentId = renderer.AssignComponentId(parentComponent);
renderer.RenderComponent(parentComponentId);
diff --git a/test/testapps/BasicTestApp/ParentChildComponent.cs b/test/testapps/BasicTestApp/ParentChildComponent.cs
index 58220f7890..10b8be8aed 100644
--- a/test/testapps/BasicTestApp/ParentChildComponent.cs
+++ b/test/testapps/BasicTestApp/ParentChildComponent.cs
@@ -14,7 +14,8 @@ namespace BasicTestApp
builder.OpenElement(1, "legend");
builder.AddText(2, "Parent component");
builder.CloseElement();
- builder.AddComponent(3);
+ builder.AddComponentElement(3);
+ builder.CloseElement();
builder.CloseElement();
}