In RazorCompiler, support temporary <c:MyComponent /> syntax
This commit is contained in:
parent
04fa5f4b71
commit
2107a1927f
|
|
@ -201,6 +201,10 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
|
||||||
HtmlEntityService.Resolver);
|
HtmlEntityService.Resolver);
|
||||||
var codeWriter = context.CodeWriter;
|
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;
|
HtmlToken nextToken;
|
||||||
while ((nextToken = tokenizer.Get()).Type != HtmlTokenType.EndOfFile)
|
while ((nextToken = tokenizer.Get()).Type != HtmlTokenType.EndOfFile)
|
||||||
{
|
{
|
||||||
|
|
@ -224,12 +228,23 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
|
||||||
var nextTag = nextToken.AsTag();
|
var nextTag = nextToken.AsTag();
|
||||||
if (nextToken.Type == HtmlTokenType.StartTag)
|
if (nextToken.Type == HtmlTokenType.StartTag)
|
||||||
{
|
{
|
||||||
codeWriter
|
var tagNameOriginalCase = GetTagNameWithOriginalCase(originalHtmlContent, nextTag);
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
|
if (TryGetComponentTypeNameFromTagName(tagNameOriginalCase, out var componentTypeName))
|
||||||
.Write((_sourceSequence++).ToString())
|
{
|
||||||
.WriteParameterSeparator()
|
codeWriter
|
||||||
.WriteStringLiteral(nextTag.Data)
|
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddComponentElement)}<{componentTypeName}>")
|
||||||
.WriteEndMethodInvocation();
|
.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)
|
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)
|
private static void WriteAttribute(CodeWriter codeWriter, int sourceSequence, string key, object value)
|
||||||
{
|
{
|
||||||
codeWriter
|
codeWriter
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,18 @@ namespace Microsoft.Blazor.RenderTree
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TComponent">The type of the child component.</typeparam>
|
/// <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>
|
/// <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
|
public void AddComponentElement<TComponent>(int sequence) where TComponent : IComponent
|
||||||
=> Append(RenderTreeNode.ChildComponent<TComponent>(sequence));
|
{
|
||||||
|
// 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()
|
private void AssertCanAddAttribute()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
private static bool NotWhitespace(RenderTreeNode node)
|
||||||
=> node.NodeType != RenderTreeNodeType.Text
|
=> node.NodeType != RenderTreeNodeType.Text
|
||||||
|| !string.IsNullOrWhiteSpace(node.TextContent);
|
|| !string.IsNullOrWhiteSpace(node.TextContent);
|
||||||
|
|
@ -370,7 +386,8 @@ namespace Microsoft.Blazor.Build.Test
|
||||||
var referenceAssembliesContainingTypes = new[]
|
var referenceAssembliesContainingTypes = new[]
|
||||||
{
|
{
|
||||||
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute), // System.Runtime
|
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
|
var references = referenceAssembliesContainingTypes
|
||||||
.SelectMany(type => type.Assembly.GetReferencedAssemblies().Concat(new[] { type.Assembly.GetName() }))
|
.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)
|
protected override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff)
|
||||||
=> throw new NotImplementedException();
|
=> throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TestComponent : IComponent
|
||||||
|
{
|
||||||
|
public void BuildRenderTree(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.AddText(0, $"Hello from {nameof(TestComponent)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -253,11 +253,13 @@ namespace Microsoft.Blazor.Test
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
builder.OpenElement(10, "parent"); // 0: <parent>
|
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(12, "child1attribute1", "A"); // 2: child1attribute1="A"
|
||||||
builder.AddAttribute(13, "child1attribute2", "B"); // 3: child1attribute2="B" />
|
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.AddAttribute(15, "child2attribute", "C"); // 5: child2attribute="C" />
|
||||||
|
builder.CloseElement();
|
||||||
builder.CloseElement(); // </parent>
|
builder.CloseElement(); // </parent>
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ namespace Microsoft.Blazor.Test
|
||||||
builder.AddAttribute(1, "My attribute", "My value");
|
builder.AddAttribute(1, "My attribute", "My value");
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
},
|
},
|
||||||
builder => builder.AddComponent<FakeComponent>(0)
|
builder => builder.AddComponentElement<FakeComponent>(0)
|
||||||
}.Select(x => new object[] { x });
|
}.Select(x => new object[] { x });
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -337,8 +337,8 @@ namespace Microsoft.Blazor.Test
|
||||||
var oldTree = new RenderTreeBuilder(new FakeRenderer());
|
var oldTree = new RenderTreeBuilder(new FakeRenderer());
|
||||||
var newTree = new RenderTreeBuilder(new FakeRenderer());
|
var newTree = new RenderTreeBuilder(new FakeRenderer());
|
||||||
var diff = new RenderTreeDiffComputer();
|
var diff = new RenderTreeDiffComputer();
|
||||||
oldTree.AddComponent<FakeComponent>(123);
|
oldTree.AddComponentElement<FakeComponent>(123);
|
||||||
newTree.AddComponent<FakeComponent2>(123);
|
newTree.AddComponentElement<FakeComponent2>(123);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());
|
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ namespace Microsoft.Blazor.Test
|
||||||
var component = new TestComponent(builder =>
|
var component = new TestComponent(builder =>
|
||||||
{
|
{
|
||||||
builder.AddText(0, "Hello");
|
builder.AddText(0, "Hello");
|
||||||
builder.AddComponent<MessageComponent>(1);
|
builder.AddComponentElement<MessageComponent>(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
|
|
@ -94,7 +94,7 @@ namespace Microsoft.Blazor.Test
|
||||||
var renderer = new TestRenderer();
|
var renderer = new TestRenderer();
|
||||||
var parentComponent = new TestComponent(builder =>
|
var parentComponent = new TestComponent(builder =>
|
||||||
{
|
{
|
||||||
builder.AddComponent<MessageComponent>(0);
|
builder.AddComponentElement<MessageComponent>(0);
|
||||||
});
|
});
|
||||||
var parentComponentId = renderer.AssignComponentId(parentComponent);
|
var parentComponentId = renderer.AssignComponentId(parentComponent);
|
||||||
renderer.RenderComponent(parentComponentId);
|
renderer.RenderComponent(parentComponentId);
|
||||||
|
|
@ -152,7 +152,7 @@ namespace Microsoft.Blazor.Test
|
||||||
var renderer = new TestRenderer();
|
var renderer = new TestRenderer();
|
||||||
var parentComponent = new TestComponent(builder =>
|
var parentComponent = new TestComponent(builder =>
|
||||||
{
|
{
|
||||||
builder.AddComponent<EventComponent>(0);
|
builder.AddComponentElement<EventComponent>(0);
|
||||||
});
|
});
|
||||||
var parentComponentId = renderer.AssignComponentId(parentComponent);
|
var parentComponentId = renderer.AssignComponentId(parentComponent);
|
||||||
renderer.RenderComponent(parentComponentId);
|
renderer.RenderComponent(parentComponentId);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ namespace BasicTestApp
|
||||||
builder.OpenElement(1, "legend");
|
builder.OpenElement(1, "legend");
|
||||||
builder.AddText(2, "Parent component");
|
builder.AddText(2, "Parent component");
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
builder.AddComponent<ChildComponent>(3);
|
builder.AddComponentElement<ChildComponent>(3);
|
||||||
|
builder.CloseElement();
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue