Capture component child content as a RenderFragment parameter.

This commit is contained in:
Steve Sanderson 2018-02-22 10:15:49 +00:00
parent bd455453d6
commit 1a31634b70
5 changed files with 126 additions and 28 deletions

View File

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

View File

@ -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);
} }
} }

View File

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

View File

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

View File

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