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>
|
/// </summary>
|
||||||
internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter
|
internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter
|
||||||
{
|
{
|
||||||
private const string builderVarName = "builder";
|
|
||||||
|
|
||||||
// Per the HTML spec, the following elements are inherently self-closing
|
// 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)
|
// For example, <img> is the same as <img /> (and therefore it cannot contain descendants)
|
||||||
private static HashSet<string> htmlVoidElementsLookup
|
private static HashSet<string> htmlVoidElementsLookup
|
||||||
|
|
@ -77,7 +75,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
{
|
{
|
||||||
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
||||||
{
|
{
|
||||||
_scopeStack.IncrementCurrentScopeChildCount();
|
_scopeStack.IncrementCurrentScopeChildCount(context);
|
||||||
context.CodeWriter.Write(token.Content);
|
context.CodeWriter.Write(token.Content);
|
||||||
}
|
}
|
||||||
else
|
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
|
// Since we're not in the middle of writing an element, this must evaluate as some
|
||||||
// text to display
|
// text to display
|
||||||
_scopeStack.IncrementCurrentScopeChildCount();
|
_scopeStack.IncrementCurrentScopeChildCount(context);
|
||||||
context.CodeWriter
|
context.CodeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
||||||
.Write((_sourceSequence++).ToString())
|
.Write((_sourceSequence++).ToString())
|
||||||
.WriteParameterSeparator();
|
.WriteParameterSeparator();
|
||||||
|
|
||||||
|
|
@ -211,9 +209,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
case HtmlTokenType.Character:
|
case HtmlTokenType.Character:
|
||||||
{
|
{
|
||||||
// Text node
|
// Text node
|
||||||
_scopeStack.IncrementCurrentScopeChildCount();
|
_scopeStack.IncrementCurrentScopeChildCount(context);
|
||||||
codeWriter
|
codeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
||||||
.Write((_sourceSequence++).ToString())
|
.Write((_sourceSequence++).ToString())
|
||||||
.WriteParameterSeparator()
|
.WriteParameterSeparator()
|
||||||
.WriteStringLiteral(nextToken.Data)
|
.WriteStringLiteral(nextToken.Data)
|
||||||
|
|
@ -230,18 +228,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
|
|
||||||
if (nextToken.Type == HtmlTokenType.StartTag)
|
if (nextToken.Type == HtmlTokenType.StartTag)
|
||||||
{
|
{
|
||||||
_scopeStack.IncrementCurrentScopeChildCount();
|
_scopeStack.IncrementCurrentScopeChildCount(context);
|
||||||
if (isComponent)
|
if (isComponent)
|
||||||
{
|
{
|
||||||
codeWriter
|
codeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenComponent)}<{componentTypeName}>")
|
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.OpenComponent)}<{componentTypeName}>")
|
||||||
.Write((_sourceSequence++).ToString())
|
.Write((_sourceSequence++).ToString())
|
||||||
.WriteEndMethodInvocation();
|
.WriteEndMethodInvocation();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
codeWriter
|
codeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
|
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
|
||||||
.Write((_sourceSequence++).ToString())
|
.Write((_sourceSequence++).ToString())
|
||||||
.WriteParameterSeparator()
|
.WriteParameterSeparator()
|
||||||
.WriteStringLiteral(nextTag.Data)
|
.WriteStringLiteral(nextTag.Data)
|
||||||
|
|
@ -267,7 +265,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
foreach (var token in _currentElementAttributeTokens)
|
foreach (var token in _currentElementAttributeTokens)
|
||||||
{
|
{
|
||||||
codeWriter
|
codeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
||||||
.Write((_sourceSequence++).ToString())
|
.Write((_sourceSequence++).ToString())
|
||||||
.WriteParameterSeparator()
|
.WriteParameterSeparator()
|
||||||
.Write(token.AttributeValue.Content)
|
.Write(token.AttributeValue.Content)
|
||||||
|
|
@ -286,6 +284,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
|| (!isComponent && htmlVoidElementsLookup.Contains(nextTag.Data)))
|
|| (!isComponent && htmlVoidElementsLookup.Contains(nextTag.Data)))
|
||||||
{
|
{
|
||||||
_scopeStack.CloseScope(
|
_scopeStack.CloseScope(
|
||||||
|
context: context,
|
||||||
tagName: isComponent ? tagNameOriginalCase : nextTag.Data,
|
tagName: isComponent ? tagNameOriginalCase : nextTag.Data,
|
||||||
isComponent: isComponent,
|
isComponent: isComponent,
|
||||||
source: CalculateSourcePosition(node.Source, nextToken.Position));
|
source: CalculateSourcePosition(node.Source, nextToken.Position));
|
||||||
|
|
@ -293,7 +292,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
? nameof(RenderTreeBuilder.CloseComponent)
|
? nameof(RenderTreeBuilder.CloseComponent)
|
||||||
: nameof(RenderTreeBuilder.CloseElement);
|
: nameof(RenderTreeBuilder.CloseElement);
|
||||||
codeWriter
|
codeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{closeMethodName}")
|
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{closeMethodName}")
|
||||||
.WriteEndMethodInvocation();
|
.WriteEndMethodInvocation();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -372,15 +371,20 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteAttribute(CodeWriter codeWriter, string key, object value)
|
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
|
codeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
||||||
.Write((_sourceSequence++).ToString())
|
.Write((_sourceSequence++).ToString())
|
||||||
.WriteParameterSeparator()
|
.WriteParameterSeparator()
|
||||||
.WriteStringLiteral(key)
|
.WriteStringLiteral(key)
|
||||||
.WriteParameterSeparator();
|
.WriteParameterSeparator();
|
||||||
WriteAttributeValue(codeWriter, value);
|
|
||||||
codeWriter.WriteEndMethodInvocation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
|
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 Clear = nameof(Clear);
|
||||||
|
|
||||||
public static readonly string GetFrames = nameof(GetFrames);
|
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.
|
// 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;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
|
@ -14,14 +15,19 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ScopeStack
|
internal class ScopeStack
|
||||||
{
|
{
|
||||||
|
private const string _renderFragmentTypeName = "Microsoft.AspNetCore.Blazor.RenderTree.RenderFragment";
|
||||||
|
|
||||||
private readonly Stack<ScopeEntry> _stack = new Stack<ScopeEntry>();
|
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)
|
public void OpenScope(string tagName, bool isComponent)
|
||||||
{
|
{
|
||||||
_stack.Push(new ScopeEntry(tagName, 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)
|
if (_stack.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -29,26 +35,59 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
$"Unexpected closing tag '{tagName}' with no matching start tag.", source);
|
$"Unexpected closing tag '{tagName}' with no matching start tag.", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
var expected = _stack.Pop();
|
var currentScope = _stack.Pop();
|
||||||
if (!tagName.Equals(expected.TagName, StringComparison.Ordinal))
|
if (!tagName.Equals(currentScope.TagName, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
throw new RazorCompilerException(
|
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
|
// 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.
|
// 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(
|
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
|
private class ScopeEntry
|
||||||
|
|
@ -56,6 +95,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
public readonly string TagName;
|
public readonly string TagName;
|
||||||
public readonly bool IsComponent;
|
public readonly bool IsComponent;
|
||||||
public int ChildCount;
|
public int ChildCount;
|
||||||
|
public IDisposable LambdaScope;
|
||||||
|
|
||||||
public ScopeEntry(string tagName, bool isComponent)
|
public ScopeEntry(string tagName, bool isComponent)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
private readonly Stack<int> _openElementIndices = new Stack<int>();
|
private readonly Stack<int> _openElementIndices = new Stack<int>();
|
||||||
private RenderTreeFrameType? _lastNonAttributeFrameType;
|
private RenderTreeFrameType? _lastNonAttributeFrameType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The reserved parameter name used for supplying child content.
|
||||||
|
/// </summary>
|
||||||
|
public static string ChildContent = nameof(ChildContent);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs an instance of <see cref="RenderTreeBuilder"/>.
|
/// Constructs an instance of <see cref="RenderTreeBuilder"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -420,13 +420,53 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||||
$"</c:{testComponentTypeName}>");
|
$"</c:{testComponentTypeName}>");
|
||||||
var frames = GetRenderTree(component);
|
var frames = GetRenderTree(component);
|
||||||
|
|
||||||
|
// Assert: component frames are correct
|
||||||
Assert.Collection(frames,
|
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.Attribute(frame, "attr", "abc", 1),
|
||||||
frame => AssertFrame.Text(frame, "Some text", 2),
|
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2));
|
||||||
frame => AssertFrame.Element(frame, "some-child", 3, 3),
|
|
||||||
frame => AssertFrame.Attribute(frame, "a", "1", 4),
|
// Assert: Captured ChildContent frames are correct
|
||||||
frame => AssertFrame.Text(frame, "Nested text", 5));
|
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]
|
[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
|
private class CompileToCSharpResult
|
||||||
{
|
{
|
||||||
public string Code { get; set; }
|
public string Code { get; set; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue