diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs index 35128dafb2..106efb70cd 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs @@ -246,6 +246,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor .WriteEndMethodInvocation(); } + if (isComponent && nextTag.Attributes.Count > 0) + { + ThrowTemporaryComponentSyntaxError(node, nextTag, tagNameOriginalCase); + } + foreach (var attribute in nextTag.Attributes) { WriteAttribute(codeWriter, attribute.Key, attribute.Value); @@ -316,6 +321,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } + private void ThrowTemporaryComponentSyntaxError(HtmlContentIntermediateNode node, HtmlTagToken tag, string componentName) + => throw new RazorCompilerException( + $"Wrong syntax for '{tag.Attributes[0].Key}' on '{componentName}': As a temporary " + + $"limitation, component attributes must be expressed with C# syntax. For example, " + + $"SomeParam=@(\"Some value\") is allowed, but SomeParam=\"Some value\" is not.", + CalculateSourcePosition(node.Source, tag.Position)); + private SourceSpan? CalculateSourcePosition( SourceSpan? razorTokenPosition, TextPosition htmlNodePosition) diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs index ef5745aa00..f552281240 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs @@ -381,40 +381,71 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test // Arrange/Act var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.'); var testObjectTypeName = typeof(SomeType).FullName.Replace('+', '.'); + // TODO: Once we have the improved component tooling and can allow syntax + // like StringProperty="My string" or BoolProperty=true, update this + // test to use that syntax. var component = CompileToComponent($""); var frames = GetRenderTree(component); // Assert - // TODO: Fix this. - // * Currently the attribute names are lowercased if they were - // parsed by AngleSharp as HTML, and left in their original case if they - // were parsed by the Razor compiler as a C# expression. They should all - // retain their original case when the target element represents a component. - // * Similarly, unquoted values are interpreted as strings if they were parsed - // by AngleSharp (e.g., intproperty=123 passes a string). The values should - // always be treated as C# expressions if the target represents a component. - // This problem will probably go away on its own when we have new component - // tooling. Assert.Collection(frames, - frame => AssertFrame.Component(frame, 4, 0), - frame => AssertFrame.Attribute(frame, "intproperty", "123", 1), - frame => AssertFrame.Attribute(frame, "stringproperty", "My string", 2), + frame => AssertFrame.Component(frame, 5, 0), + frame => AssertFrame.Attribute(frame, "IntProperty", 123, 1), + frame => AssertFrame.Attribute(frame, "BoolProperty", true, 2), + frame => AssertFrame.Attribute(frame, "StringProperty", "My string", 3), frame => { - AssertFrame.Attribute(frame, "ObjectProperty"); + AssertFrame.Attribute(frame, "ObjectProperty", 4); Assert.IsType(frame.AttributeValue); }); } + [Fact] + public void TemporaryComponentSyntaxRejectsParametersExpressedAsPlainHtmlAttributes() + { + // This is a temporary syntax restriction. Currently you can write: + // + // ... but are *not* allowed to write: + // + // This is because until we get the improved taghelper-based tooling, + // we're using AngleSharp to parse the plain HTML attributes, and it + // suffers from limitations: + // * Loses the casing of attribute names (MyParam becomes myparam) + // * Doesn't recognize MyBool=true as an bool (becomes mybool="true"), + // plus equivalent for other primitives like enum values + // So to avoid people getting runtime errors, we're currently imposing + // the compile-time restriction that component params have to be given + // as C# expressions, e.g., MyBool=@true and MyString=@("Hello") + + // Arrange/Act + var result = CompileToCSharp( + $"Line 1\n" + + $"Some text "); + + // Assert + Assert.Collection(result.Diagnostics, + item => + { + Assert.Equal(RazorCompilerDiagnostic.DiagnosticType.Error, item.Type); + Assert.StartsWith($"Wrong syntax for 'myparam' on 'c:MyComponent': As a temporary " + + $"limitation, component attributes must be expressed with C# syntax. For " + + $"example, SomeParam=@(\"Some value\") is allowed, but SomeParam=\"Some value\" " + + $"is not.", item.Message); + Assert.Equal(2, item.Line); + Assert.Equal(11, item.Column); + }); + } + [Fact] public void CanIncludeChildrenInComponents() { // Arrange/Act var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.'); - var component = CompileToComponent($"" + + var component = CompileToComponent($"" + $"Some text" + $"Nested text" + $""); @@ -423,7 +454,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test // Assert: component frames are correct Assert.Collection(frames, frame => AssertFrame.Component(frame, 3, 0), - frame => AssertFrame.Attribute(frame, "attr", "abc", 1), + frame => AssertFrame.Attribute(frame, "MyAttr", "abc", 1), frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2)); // Assert: Captured ChildContent frames are correct