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); StringComparer.OrdinalIgnoreCase);
private string _unconsumedHtml; private string _unconsumedHtml;
private string _currentAttributeName; private IList<object> _currentAttributeValues;
private IDictionary<string, object> _currentElementAttributes = new Dictionary<string, object>(); private IDictionary<string, object> _currentElementAttributes = new Dictionary<string, object>();
public override void BeginWriterScope(CodeRenderingContext context, string writer) 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) 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) public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
{ {
_currentAttributeName = node.AttributeName; _currentAttributeValues = new List<object>();
context.RenderChildren(node); context.RenderChildren(node);
_currentAttributeName = null; _currentElementAttributes[node.AttributeName] = _currentAttributeValues;
_currentAttributeValues = null;
} }
public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node) 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) 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) private static void WriteAttribute(CodeWriter codeWriter, string key, object value)
{ {
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
codeWriter codeWriter
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}") .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
.WriteStringLiteral(key) .WriteStringLiteral(key)
.WriteParameterSeparator(); .WriteParameterSeparator();
WriteAttributeValue(codeWriter, value);
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}");
}
codeWriter.WriteEndMethodInvocation(); codeWriter.WriteEndMethodInvocation();
} }
@ -255,5 +247,52 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
} }
return builder.ToString(); 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)); 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> /// <summary>
/// Appends a node representing a child component. /// Appends a node representing a child component.
/// </summary> /// </summary>

View File

@ -184,6 +184,34 @@ namespace Microsoft.Blazor.Build.Test
node => AssertNode.Attribute(node, "attr", "My string")); 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) private static bool NotWhitespace(RenderTreeNode node)
=> node.NodeType != RenderTreeNodeType.Text => node.NodeType != RenderTreeNodeType.Text
|| !string.IsNullOrWhiteSpace(node.TextContent); || !string.IsNullOrWhiteSpace(node.TextContent);

View File

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