diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs index 602e78f8cb..5d951af204 100644 --- a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs +++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs @@ -29,7 +29,7 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine StringComparer.OrdinalIgnoreCase); private string _unconsumedHtml; - private string _currentAttributeName; + private IList _currentAttributeValues; private IDictionary _currentElementAttributes = new Dictionary(); 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(); 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 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}"); + } + } } } diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs index 70df197b7b..e04b9e2bd4 100644 --- a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs +++ b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs @@ -83,6 +83,18 @@ namespace Microsoft.Blazor.RenderTree Append(RenderTreeNode.Attribute(name, value)); } + /// + /// Appends a node representing a string-valued attribute. + /// The attribute is associated with the most recently added element. + /// + /// The name of the attribute. + /// The value of the attribute. + public void AddAttribute(string name, object value) + { + AssertCanAddAttribute(); + Append(RenderTreeNode.Attribute(name, value.ToString())); + } + /// /// Appends a node representing a child component. /// diff --git a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs index cc619e2e74..902efb6747 100644 --- a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs @@ -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; }" + + ""); + + // 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; }" + + ""); + + // 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); diff --git a/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs index 6102a4ff31..ab9fe21bec 100644 --- a/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs +++ b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs @@ -153,7 +153,7 @@ namespace Microsoft.Blazor.Test // Act builder.OpenElement("myelement"); // 0: + builder.AddAttribute("attribute2", 123); // 2: attribute2=intExpression123> builder.OpenElement("child"); // 3: 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"));