In RazorCompiler, support temporary <c:MyComponent /> syntax

This commit is contained in:
Steve Sanderson 2018-01-24 10:43:23 -08:00
parent 04fa5f4b71
commit 2107a1927f
7 changed files with 100 additions and 18 deletions

View File

@ -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

View File

@ -130,8 +130,18 @@ namespace Microsoft.Blazor.RenderTree
/// </summary>
/// <typeparam name="TComponent">The type of the child component.</typeparam>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
public void AddComponent<TComponent>(int sequence) where TComponent: IComponent
=> Append(RenderTreeNode.ChildComponent<TComponent>(sequence));
public void AddComponentElement<TComponent>(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<TComponent>(sequence));
}
private void AssertCanAddAttribute()
{

View File

@ -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($"<c:{testComponentTypeName} />");
component.BuildRenderTree(treeBuilder);
// Assert
Assert.Collection(treeBuilder.GetNodes(),
node => AssertNode.Component<TestComponent>(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)}");
}
}
}
}

View File

@ -253,11 +253,13 @@ namespace Microsoft.Blazor.Test
// Act
builder.OpenElement(10, "parent"); // 0: <parent>
builder.AddComponent<TestComponent>(11); // 1: <testcomponent
builder.AddComponentElement<TestComponent>(11); // 1: <testcomponent
builder.AddAttribute(12, "child1attribute1", "A"); // 2: child1attribute1="A"
builder.AddAttribute(13, "child1attribute2", "B"); // 3: child1attribute2="B" />
builder.AddComponent<TestComponent>(14); // 4: <testcomponent
builder.CloseElement();
builder.AddComponentElement<TestComponent>(14); // 4: <testcomponent
builder.AddAttribute(15, "child2attribute", "C"); // 5: child2attribute="C" />
builder.CloseElement();
builder.CloseElement(); // </parent>
// Assert

View File

@ -42,7 +42,7 @@ namespace Microsoft.Blazor.Test
builder.AddAttribute(1, "My attribute", "My value");
builder.CloseElement();
},
builder => builder.AddComponent<FakeComponent>(0)
builder => builder.AddComponentElement<FakeComponent>(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<FakeComponent>(123);
newTree.AddComponent<FakeComponent2>(123);
oldTree.AddComponentElement<FakeComponent>(123);
newTree.AddComponentElement<FakeComponent2>(123);
// Act
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());

View File

@ -44,7 +44,7 @@ namespace Microsoft.Blazor.Test
var component = new TestComponent(builder =>
{
builder.AddText(0, "Hello");
builder.AddComponent<MessageComponent>(1);
builder.AddComponentElement<MessageComponent>(1);
});
// Act/Assert
@ -94,7 +94,7 @@ namespace Microsoft.Blazor.Test
var renderer = new TestRenderer();
var parentComponent = new TestComponent(builder =>
{
builder.AddComponent<MessageComponent>(0);
builder.AddComponentElement<MessageComponent>(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<EventComponent>(0);
builder.AddComponentElement<EventComponent>(0);
});
var parentComponentId = renderer.AssignComponentId(parentComponent);
renderer.RenderComponent(parentComponentId);

View File

@ -14,7 +14,8 @@ namespace BasicTestApp
builder.OpenElement(1, "legend");
builder.AddText(2, "Parent component");
builder.CloseElement();
builder.AddComponent<ChildComponent>(3);
builder.AddComponentElement<ChildComponent>(3);
builder.CloseElement();
builder.CloseElement();
}