Capture component child content as a RenderFragment parameter.
This commit is contained in:
parent
bd455453d6
commit
1a31634b70
|
|
@ -19,8 +19,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
/// </summary>
|
||||
internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter
|
||||
{
|
||||
private const string builderVarName = "builder";
|
||||
|
||||
// Per the HTML spec, the following elements are inherently self-closing
|
||||
// For example, <img> is the same as <img /> (and therefore it cannot contain descendants)
|
||||
private static HashSet<string> htmlVoidElementsLookup
|
||||
|
|
@ -77,7 +75,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
{
|
||||
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
||||
{
|
||||
_scopeStack.IncrementCurrentScopeChildCount();
|
||||
_scopeStack.IncrementCurrentScopeChildCount(context);
|
||||
context.CodeWriter.Write(token.Content);
|
||||
}
|
||||
else
|
||||
|
|
@ -121,9 +119,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
// Since we're not in the middle of writing an element, this must evaluate as some
|
||||
// text to display
|
||||
_scopeStack.IncrementCurrentScopeChildCount();
|
||||
_scopeStack.IncrementCurrentScopeChildCount(context);
|
||||
context.CodeWriter
|
||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator();
|
||||
|
||||
|
|
@ -211,9 +209,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
case HtmlTokenType.Character:
|
||||
{
|
||||
// Text node
|
||||
_scopeStack.IncrementCurrentScopeChildCount();
|
||||
_scopeStack.IncrementCurrentScopeChildCount(context);
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator()
|
||||
.WriteStringLiteral(nextToken.Data)
|
||||
|
|
@ -230,18 +228,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
if (nextToken.Type == HtmlTokenType.StartTag)
|
||||
{
|
||||
_scopeStack.IncrementCurrentScopeChildCount();
|
||||
_scopeStack.IncrementCurrentScopeChildCount(context);
|
||||
if (isComponent)
|
||||
{
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenComponent)}<{componentTypeName}>")
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.OpenComponent)}<{componentTypeName}>")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteEndMethodInvocation();
|
||||
}
|
||||
else
|
||||
{
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator()
|
||||
.WriteStringLiteral(nextTag.Data)
|
||||
|
|
@ -267,7 +265,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
foreach (var token in _currentElementAttributeTokens)
|
||||
{
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator()
|
||||
.Write(token.AttributeValue.Content)
|
||||
|
|
@ -286,6 +284,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|| (!isComponent && htmlVoidElementsLookup.Contains(nextTag.Data)))
|
||||
{
|
||||
_scopeStack.CloseScope(
|
||||
context: context,
|
||||
tagName: isComponent ? tagNameOriginalCase : nextTag.Data,
|
||||
isComponent: isComponent,
|
||||
source: CalculateSourcePosition(node.Source, nextToken.Position));
|
||||
|
|
@ -293,7 +292,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
? nameof(RenderTreeBuilder.CloseComponent)
|
||||
: nameof(RenderTreeBuilder.CloseElement);
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{builderVarName}.{closeMethodName}")
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{closeMethodName}")
|
||||
.WriteEndMethodInvocation();
|
||||
}
|
||||
break;
|
||||
|
|
@ -372,15 +371,20 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
}
|
||||
|
||||
private void WriteAttribute(CodeWriter codeWriter, string key, object value)
|
||||
{
|
||||
BeginWriteAttribute(codeWriter, key);
|
||||
WriteAttributeValue(codeWriter, value);
|
||||
codeWriter.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
public void BeginWriteAttribute(CodeWriter codeWriter, string key)
|
||||
{
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator()
|
||||
.WriteStringLiteral(key)
|
||||
.WriteParameterSeparator();
|
||||
WriteAttributeValue(codeWriter, value);
|
||||
codeWriter.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
|
||||
|
|
|
|||
|
|
@ -22,5 +22,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
public static readonly string Clear = nameof(Clear);
|
||||
|
||||
public static readonly string GetFrames = nameof(GetFrames);
|
||||
|
||||
public static readonly string ChildContent = nameof(ChildContent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
|
@ -14,14 +15,19 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
/// </summary>
|
||||
internal class ScopeStack
|
||||
{
|
||||
private const string _renderFragmentTypeName = "Microsoft.AspNetCore.Blazor.RenderTree.RenderFragment";
|
||||
|
||||
private readonly Stack<ScopeEntry> _stack = new Stack<ScopeEntry>();
|
||||
private int _builderVarNumber = 1;
|
||||
|
||||
public string BuilderVarName { get; private set; } = "builder";
|
||||
|
||||
public void OpenScope(string tagName, bool isComponent)
|
||||
{
|
||||
_stack.Push(new ScopeEntry(tagName, isComponent));
|
||||
}
|
||||
|
||||
public void CloseScope(string tagName, bool isComponent, SourceSpan? source)
|
||||
public void CloseScope(CodeRenderingContext context, string tagName, bool isComponent, SourceSpan? source)
|
||||
{
|
||||
if (_stack.Count == 0)
|
||||
{
|
||||
|
|
@ -29,26 +35,59 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
$"Unexpected closing tag '{tagName}' with no matching start tag.", source);
|
||||
}
|
||||
|
||||
var expected = _stack.Pop();
|
||||
if (!tagName.Equals(expected.TagName, StringComparison.Ordinal))
|
||||
var currentScope = _stack.Pop();
|
||||
if (!tagName.Equals(currentScope.TagName, StringComparison.Ordinal))
|
||||
{
|
||||
throw new RazorCompilerException(
|
||||
$"Mismatching closing tag. Found '{tagName}' but expected '{expected.TagName}'.", source);
|
||||
$"Mismatching closing tag. Found '{tagName}' but expected '{currentScope.TagName}'.", source);
|
||||
}
|
||||
|
||||
// Note: there's no unit test to cover the following, because there's no known way of
|
||||
// triggering it from user code (i.e., Razor source code). But the test is here anyway
|
||||
// triggering it from user code (i.e., Razor source code). But the check is here anyway
|
||||
// just in case one day it turns out there is some way of causing this error.
|
||||
if (isComponent != expected.IsComponent)
|
||||
if (isComponent != currentScope.IsComponent)
|
||||
{
|
||||
throw new RazorCompilerException(
|
||||
$"Mismatching closing tag. Found '{tagName}' of type '{(isComponent ? "component" : "element")}' but expected type '{(expected.IsComponent ? "component" : "element")}'.");
|
||||
$"Mismatching closing tag. Found '{tagName}' of type '{(isComponent ? "component" : "element")}' but expected type '{(currentScope.IsComponent ? "component" : "element")}'.", source);
|
||||
}
|
||||
|
||||
// When closing the scope for a component with children, it's time to close the lambda
|
||||
if (currentScope.LambdaScope != null)
|
||||
{
|
||||
currentScope.LambdaScope.Dispose();
|
||||
context.CodeWriter.Write(")");
|
||||
context.CodeWriter.WriteEndMethodInvocation();
|
||||
OffsetBuilderVarNumber(-1);
|
||||
}
|
||||
}
|
||||
|
||||
public void IncrementCurrentScopeChildCount()
|
||||
public void IncrementCurrentScopeChildCount(CodeRenderingContext context)
|
||||
{
|
||||
if (_stack.Count > 0)
|
||||
{
|
||||
var currentScope = _stack.Peek();
|
||||
|
||||
if (currentScope.IsComponent && currentScope.ChildCount == 0)
|
||||
{
|
||||
// When we're about to insert the first child into a component,
|
||||
// it's time to open a new lambda
|
||||
var blazorNodeWriter = (BlazorIntermediateNodeWriter)context.NodeWriter;
|
||||
blazorNodeWriter.BeginWriteAttribute(context.CodeWriter, RenderTreeBuilder.ChildContent);
|
||||
OffsetBuilderVarNumber(1);
|
||||
context.CodeWriter.Write($"({_renderFragmentTypeName})(");
|
||||
currentScope.LambdaScope = context.CodeWriter.BuildLambda(BuilderVarName);
|
||||
}
|
||||
|
||||
currentScope.ChildCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private void OffsetBuilderVarNumber(int delta)
|
||||
{
|
||||
_builderVarNumber += delta;
|
||||
BuilderVarName = _builderVarNumber == 1
|
||||
? "builder"
|
||||
: $"builder{_builderVarNumber}";
|
||||
}
|
||||
|
||||
private class ScopeEntry
|
||||
|
|
@ -56,6 +95,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
public readonly string TagName;
|
||||
public readonly bool IsComponent;
|
||||
public int ChildCount;
|
||||
public IDisposable LambdaScope;
|
||||
|
||||
public ScopeEntry(string tagName, bool isComponent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
private readonly Stack<int> _openElementIndices = new Stack<int>();
|
||||
private RenderTreeFrameType? _lastNonAttributeFrameType;
|
||||
|
||||
/// <summary>
|
||||
/// The reserved parameter name used for supplying child content.
|
||||
/// </summary>
|
||||
public static string ChildContent = nameof(ChildContent);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="RenderTreeBuilder"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -420,13 +420,53 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
$"</c:{testComponentTypeName}>");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert: component frames are correct
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 6, 0),
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "attr", "abc", 1),
|
||||
frame => AssertFrame.Text(frame, "Some text", 2),
|
||||
frame => AssertFrame.Element(frame, "some-child", 3, 3),
|
||||
frame => AssertFrame.Attribute(frame, "a", "1", 4),
|
||||
frame => AssertFrame.Text(frame, "Nested text", 5));
|
||||
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2));
|
||||
|
||||
// Assert: Captured ChildContent frames are correct
|
||||
var childFrames = GetFrames((RenderFragment)frames[2].AttributeValue);
|
||||
Assert.Collection(childFrames,
|
||||
frame => AssertFrame.Text(frame, "Some text", 3),
|
||||
frame => AssertFrame.Element(frame, "some-child", 3, 4),
|
||||
frame => AssertFrame.Attribute(frame, "a", "1", 5),
|
||||
frame => AssertFrame.Text(frame, "Nested text", 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanNestComponentChildContent()
|
||||
{
|
||||
// Arrange/Act
|
||||
var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
|
||||
var component = CompileToComponent(
|
||||
$"<c:{testComponentTypeName}>" +
|
||||
$"<c:{testComponentTypeName}>" +
|
||||
$"Some text" +
|
||||
$"</c:{testComponentTypeName}>" +
|
||||
$"</c:{testComponentTypeName}>");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert: outer component frames are correct
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 1));
|
||||
|
||||
// Assert: first level of ChildContent is correct
|
||||
// Note that we don't really need the sequence numbers to continue on from the
|
||||
// sequence numbers at the parent level. All that really matters is that they are
|
||||
// correct relative to each other (i.e., incrementing) within the nesting level.
|
||||
// As an implementation detail, it happens that they do follow on from the parent
|
||||
// level, but we could change that part of the implementation if we wanted.
|
||||
var innerFrames = GetFrames((RenderFragment)frames[1].AttributeValue).ToArray();
|
||||
Assert.Collection(innerFrames,
|
||||
frame => AssertFrame.Component<TestComponent>(frame, 2, 2),
|
||||
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 3));
|
||||
|
||||
// Assert: second level of ChildContent is correct
|
||||
Assert.Collection(GetFrames((RenderFragment)innerFrames[1].AttributeValue),
|
||||
frame => AssertFrame.Text(frame, "Some text", 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -651,6 +691,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
}
|
||||
}
|
||||
|
||||
private ArrayRange<RenderTreeFrame> GetFrames(RenderFragment fragment)
|
||||
{
|
||||
var builder = new RenderTreeBuilder(new TestRenderer());
|
||||
fragment(builder);
|
||||
return builder.GetFrames();
|
||||
}
|
||||
|
||||
private class CompileToCSharpResult
|
||||
{
|
||||
public string Code { get; set; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue