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);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue