From 2107a1927f0fdd908dc3872cc17c8d6f2fddfc7b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 24 Jan 2018 10:43:23 -0800 Subject: [PATCH] In RazorCompiler, support temporary syntax --- .../Engine/BlazorIntermediateNodeWriter.cs | 56 +++++++++++++++++-- .../RenderTree/RenderTreeBuilder.cs | 14 ++++- .../RazorCompilerTest.cs | 27 ++++++++- .../RenderTreeBuilderTest.cs | 6 +- .../RenderTreeDiffComputerTest.cs | 6 +- test/Microsoft.Blazor.Test/RendererTest.cs | 6 +- .../BasicTestApp/ParentChildComponent.cs | 3 +- 7 files changed, 100 insertions(+), 18 deletions(-) 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(); }