In Razor compiler, support attributes with interpolated expressions

This commit is contained in:
Steve Sanderson 2018-01-16 15:24:34 +00:00
parent b690aeee28
commit 3a7b6b2178
4 changed files with 113 additions and 34 deletions

View File

@ -29,7 +29,7 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
StringComparer.OrdinalIgnoreCase);
private string _unconsumedHtml;
private string _currentAttributeName;
private IList<object> _currentAttributeValues;
private IDictionary<string, object> _currentElementAttributes = new Dictionary<string, object>();
public override void BeginWriterScope(CodeRenderingContext context, string writer)
@ -105,24 +105,40 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node)
{
if (string.IsNullOrEmpty(_currentAttributeName))
if (_currentAttributeValues == null)
{
throw new InvalidOperationException($"Invoked {nameof(WriteCSharpCodeAttributeValue)} while {nameof(_currentAttributeName)} was null or empty.");
throw new InvalidOperationException($"Invoked {nameof(WriteCSharpCodeAttributeValue)} while {nameof(_currentAttributeValues)} was null.");
}
_currentElementAttributes[_currentAttributeName] = node.Children.Single();
// In cases like "somestring @variable", Razor tokenizes it as:
// [0] HtmlContent="somestring"
// [1] CsharpContent="variable" Prefix=" "
// ... so to avoid losing whitespace, convert the prefix to a further token in the list
if (!string.IsNullOrEmpty(node.Prefix))
{
_currentAttributeValues.Add(node.Prefix);
}
_currentAttributeValues.Add((IntermediateToken)node.Children.Single());
}
public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
{
_currentAttributeName = node.AttributeName;
_currentAttributeValues = new List<object>();
context.RenderChildren(node);
_currentAttributeName = null;
_currentElementAttributes[node.AttributeName] = _currentAttributeValues;
_currentAttributeValues = null;
}
public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node)
{
throw new System.NotImplementedException(nameof(WriteHtmlAttributeValue));
if (_currentAttributeValues == null)
{
throw new InvalidOperationException($"Invoked {nameof(WriteHtmlAttributeValue)} while {nameof(_currentAttributeValues)} was null.");
}
var stringContent = ((IntermediateToken)node.Children.Single()).Content;
_currentAttributeValues.Add(node.Prefix + stringContent);
}
public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node)
@ -208,35 +224,11 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
private static void WriteAttribute(CodeWriter codeWriter, string key, object value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
codeWriter
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
.WriteStringLiteral(key)
.WriteParameterSeparator();
switch (value)
{
case IntermediateToken intermediateToken:
{
if (!intermediateToken.IsCSharp)
{
throw new ArgumentException($"Not yet supported: IntermediateToken where IsCSharp==false");
}
codeWriter.Write(intermediateToken.Content);
break;
}
case string valueString:
codeWriter.WriteStringLiteral(valueString);
break;
default:
throw new ArgumentException($"Unsupported attribute value type: {value.GetType().FullName}");
}
WriteAttributeValue(codeWriter, value);
codeWriter.WriteEndMethodInvocation();
}
@ -255,5 +247,52 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
}
return builder.ToString();
}
private static void WriteAttributeValue(CodeWriter writer, object value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
switch (value)
{
case string valueString:
writer.WriteStringLiteral(valueString);
break;
case IntermediateToken token:
{
if (token.IsCSharp)
{
writer.Write(token.Content);
}
else
{
writer.WriteStringLiteral(token.Content);
}
break;
}
case IEnumerable<object> concatenatedValues:
{
var first = true;
foreach (var concatenatedValue in concatenatedValues)
{
if (first)
{
first = false;
}
else
{
writer.Write(" + ");
}
WriteAttributeValue(writer, concatenatedValue);
}
break;
}
default:
throw new ArgumentException($"Unsupported attribute value type: {value.GetType().FullName}");
}
}
}
}

View File

@ -83,6 +83,18 @@ namespace Microsoft.Blazor.RenderTree
Append(RenderTreeNode.Attribute(name, value));
}
/// <summary>
/// Appends a node representing a string-valued attribute.
/// The attribute is associated with the most recently added element.
/// </summary>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(string name, object value)
{
AssertCanAddAttribute();
Append(RenderTreeNode.Attribute(name, value.ToString()));
}
/// <summary>
/// Appends a node representing a child component.
/// </summary>

View File

@ -184,6 +184,34 @@ namespace Microsoft.Blazor.Build.Test
node => AssertNode.Attribute(node, "attr", "My string"));
}
[Fact]
public void SupportsAttributesWithNonStringExpressionValues()
{
// Arrange/Act
var component = CompileToComponent(
"@{ var myValue = 123; }"
+ "<elem attr=@myValue />");
// Assert
Assert.Collection(GetRenderTree(component).Where(NotWhitespace),
node => AssertNode.Element(node, "elem", 1),
node => AssertNode.Attribute(node, "attr", "123"));
}
[Fact]
public void SupportsAttributesWithInterpolatedStringExpressionValues()
{
// Arrange/Act
var component = CompileToComponent(
"@{ var myValue = \"world\"; var myNum=123; }"
+ "<elem attr=\"Hello, @myValue.ToUpperInvariant() with number @(myNum*2)!\" />");
// Assert
Assert.Collection(GetRenderTree(component).Where(NotWhitespace),
node => AssertNode.Element(node, "elem", 1),
node => AssertNode.Attribute(node, "attr", "Hello, WORLD with number 246!"));
}
private static bool NotWhitespace(RenderTreeNode node)
=> node.NodeType != RenderTreeNodeType.Text
|| !string.IsNullOrWhiteSpace(node.TextContent);

View File

@ -153,7 +153,7 @@ namespace Microsoft.Blazor.Test
// Act
builder.OpenElement("myelement"); // 0: <myelement
builder.AddAttribute("attribute1", "value 1"); // 1: attribute1="value 1"
builder.AddAttribute("attribute2", "value 2"); // 2: attribute2="value 2">
builder.AddAttribute("attribute2", 123); // 2: attribute2=intExpression123>
builder.OpenElement("child"); // 3: <child
builder.AddAttribute("childevent", eventHandler); // 4: childevent=eventHandler>
builder.AddText("some text"); // 5: some text
@ -164,7 +164,7 @@ namespace Microsoft.Blazor.Test
Assert.Collection(builder.GetNodes(),
node => AssertNode.Element(node, "myelement", 5),
node => AssertNode.Attribute(node, "attribute1", "value 1"),
node => AssertNode.Attribute(node, "attribute2", "value 2"),
node => AssertNode.Attribute(node, "attribute2", "123"),
node => AssertNode.Element(node, "child", 5),
node => AssertNode.Attribute(node, "childevent", eventHandler),
node => AssertNode.Text(node, "some text"));