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

View File

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

View File

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

View File

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

View File

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