Allow non-property attributes to be set
This removes a limitation that prevented callers from passing attributes to a component that aren't backed by properties. The majority of the complication here is required to deal with the more sophisticated way that HTML attributes are represented in the Razor IR.
This commit is contained in:
parent
c05657c7f2
commit
0c162e8c5a
|
|
@ -413,7 +413,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// This rougly follows the design of the runtime writer for simplicity.
|
||||
if (node.AttributeStructure == AttributeStructure.Minimized)
|
||||
{
|
||||
// Do nothhing
|
||||
// Do nothing
|
||||
}
|
||||
else if (
|
||||
node.Children.Count != 1 ||
|
||||
|
|
@ -423,7 +423,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// We don't expect this to happen, we just want to know if it can.
|
||||
throw new InvalidOperationException("Attribute nodes should either be minimized or a single content node.");
|
||||
}
|
||||
else if (node.BoundAttribute.IsDelegateProperty())
|
||||
else if (node.BoundAttribute?.IsDelegateProperty() ?? false)
|
||||
{
|
||||
// See the runtime version of this code for a thorough description of what we're doing here
|
||||
if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
|
||||
|
|
|
|||
|
|
@ -69,23 +69,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
() => "Component attributes do not support complex content (mixed C# and markup). Attribute: '{0}', text '{1}'",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic Create_UnsupportedComplexContent(
|
||||
SourceSpan? source,
|
||||
TagHelperPropertyIntermediateNode node,
|
||||
IntermediateNodeCollection children)
|
||||
public static RazorDiagnostic Create_UnsupportedComplexContent(IntermediateNode node, string attributeName)
|
||||
{
|
||||
var content = string.Join("", children.OfType<IntermediateToken>().Select(t => t.Content));
|
||||
return RazorDiagnostic.Create(UnsupportedComplexContent, source ?? SourceSpan.Undefined, node.AttributeName, content);
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor UnboundComponentAttribute = new RazorDiagnosticDescriptor(
|
||||
"BL9987",
|
||||
() => "The component '{0}' does not have an attribute named '{1}'.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic Create_UnboundComponentAttribute(SourceSpan? source, string componentType, TagHelperHtmlAttributeIntermediateNode node)
|
||||
{
|
||||
return RazorDiagnostic.Create(UnboundComponentAttribute, source ?? SourceSpan.Undefined, componentType, node.AttributeName);
|
||||
var content = string.Join("", node.FindDescendantNodes<IntermediateToken>().Select(t => t.Content));
|
||||
return RazorDiagnostic.Create(UnsupportedComplexContent, node.Source ?? SourceSpan.Undefined, attributeName, content);
|
||||
}
|
||||
|
||||
private static SourceSpan? CalculateSourcePosition(
|
||||
|
|
|
|||
|
|
@ -470,7 +470,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// We don't expect this to happen, we just want to know if it can.
|
||||
throw new InvalidOperationException("Attribute nodes should either be minimized or a single content node.");
|
||||
}
|
||||
else if (node.BoundAttribute.IsDelegateProperty())
|
||||
else if (node.BoundAttribute?.IsDelegateProperty() ?? false)
|
||||
{
|
||||
// This is a UIEventHandler property. We do some special code generation for this
|
||||
// case so that it's easier to write for common cases.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,28 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
{
|
||||
}
|
||||
|
||||
public ComponentAttributeExtensionNode(TagHelperHtmlAttributeIntermediateNode attributeNode)
|
||||
{
|
||||
if (attributeNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attributeNode));
|
||||
}
|
||||
|
||||
AttributeName = attributeNode.AttributeName;
|
||||
AttributeStructure = attributeNode.AttributeStructure;
|
||||
Source = attributeNode.Source;
|
||||
|
||||
for (var i = 0; i < attributeNode.Children.Count; i++)
|
||||
{
|
||||
Children.Add(attributeNode.Children[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < attributeNode.Diagnostics.Count; i++)
|
||||
{
|
||||
Diagnostics.Add(attributeNode.Diagnostics[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public ComponentAttributeExtensionNode(TagHelperPropertyIntermediateNode propertyNode)
|
||||
{
|
||||
if (propertyNode == null)
|
||||
|
|
@ -24,7 +46,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
AttributeName = propertyNode.AttributeName;
|
||||
AttributeStructure = propertyNode.AttributeStructure;
|
||||
BoundAttribute = propertyNode.BoundAttribute;
|
||||
IsIndexerNameMatch = propertyNode.IsIndexerNameMatch;
|
||||
PropertyName = propertyNode.BoundAttribute.GetPropertyName();
|
||||
Source = propertyNode.Source;
|
||||
TagHelper = propertyNode.TagHelper;
|
||||
|
||||
|
|
@ -47,10 +69,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
public BoundAttributeDescriptor BoundAttribute { get; set; }
|
||||
|
||||
public string FieldName { get; set; }
|
||||
|
||||
public bool IsIndexerNameMatch { get; set; }
|
||||
|
||||
public string PropertyName { get; set; }
|
||||
|
||||
public TagHelperDescriptor TagHelper { get; set; }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
|
@ -65,7 +66,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
node.Children.Add(new ComponentCloseExtensionNode());
|
||||
|
||||
// Now we need to rewrite any set property nodes to call the appropriate AddAttribute api.
|
||||
// Now we need to rewrite any set property or HTML nodes to call the appropriate AddAttribute api.
|
||||
for (var i = node.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode &&
|
||||
|
|
@ -76,60 +77,99 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
//
|
||||
// This is where a lot of the complexity in the Razor/TagHelpers model creeps in and we
|
||||
// might be able to avoid it if these features aren't needed.
|
||||
if (propertyNode.Children.Count == 1 &&
|
||||
propertyNode.Children[0] is HtmlAttributeIntermediateNode htmlNode &&
|
||||
htmlNode.Children.Count > 1)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
|
||||
propertyNode.Source,
|
||||
propertyNode,
|
||||
htmlNode.Children));
|
||||
node.Children.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
if (propertyNode.Children.Count == 1 &&
|
||||
propertyNode.Children[0] is CSharpExpressionIntermediateNode cSharpNode &&
|
||||
cSharpNode.Children.Count > 1)
|
||||
{
|
||||
// This case can be hit when the attribute has an explicit @ inside, which
|
||||
// 'escapes' any special sugar we provide for codegen.
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
|
||||
propertyNode.Source,
|
||||
propertyNode,
|
||||
cSharpNode.Children));
|
||||
node.Children.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
else if (propertyNode.Children.Count > 1)
|
||||
if (HasComplexChildContent(propertyNode))
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
|
||||
propertyNode.Source,
|
||||
propertyNode,
|
||||
propertyNode.Children));
|
||||
propertyNode.AttributeName));
|
||||
node.Children.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
node.Children[i] = new ComponentAttributeExtensionNode(propertyNode)
|
||||
{
|
||||
PropertyName = propertyNode.BoundAttribute.GetPropertyName(),
|
||||
};
|
||||
node.Children[i] = new ComponentAttributeExtensionNode(propertyNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Add an error and remove any nodes that don't map to a component property.
|
||||
for (var i = node.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode attributeNode)
|
||||
else if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlNode)
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnboundComponentAttribute(
|
||||
attributeNode.Source,
|
||||
tagHelper.GetTypeName(),
|
||||
attributeNode));
|
||||
node.Children.RemoveAt(i);
|
||||
if (HasComplexChildContent(htmlNode))
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
|
||||
htmlNode,
|
||||
htmlNode.AttributeName));
|
||||
node.Children.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// For any nodes that don't map to a component property we won't have type information
|
||||
// but these should follow the same path through the runtime.
|
||||
var attributeNode = new ComponentAttributeExtensionNode(htmlNode);
|
||||
node.Children[i] = attributeNode;
|
||||
|
||||
// Since we don't support complex content, we can rewrite the inside of this
|
||||
// node to the rather simpler form that property nodes usually have.
|
||||
for (var j = 0; j < attributeNode.Children.Count; j++)
|
||||
{
|
||||
if (attributeNode.Children[j] is HtmlAttributeValueIntermediateNode htmlValue)
|
||||
{
|
||||
attributeNode.Children[j] = new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
htmlValue.Children.Single(),
|
||||
},
|
||||
Source = htmlValue.Source,
|
||||
};
|
||||
}
|
||||
else if (attributeNode.Children[j] is CSharpExpressionAttributeValueIntermediateNode expressionValue)
|
||||
{
|
||||
attributeNode.Children[j] = new CSharpExpressionIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
expressionValue.Children.Single(),
|
||||
},
|
||||
Source = expressionValue.Source,
|
||||
};
|
||||
}
|
||||
else if (attributeNode.Children[j] is CSharpCodeAttributeValueIntermediateNode codeValue)
|
||||
{
|
||||
attributeNode.Children[j] = new CSharpExpressionIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
codeValue.Children.Single(),
|
||||
},
|
||||
Source = codeValue.Source,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasComplexChildContent(IntermediateNode node)
|
||||
{
|
||||
if (node.Children.Count == 1 &&
|
||||
node.Children[0] is HtmlAttributeIntermediateNode htmlNode &&
|
||||
htmlNode.Children.Count > 1)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count == 1 &&
|
||||
node.Children[0] is CSharpExpressionIntermediateNode cSharpNode &&
|
||||
cSharpNode.Children.Count > 1)
|
||||
{
|
||||
// This case can be hit when the attribute has an explicit @ inside, which
|
||||
// 'escapes' any special sugar we provide for codegen.
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count > 1)
|
||||
{
|
||||
// This is the common case for 'mixed' content
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ namespace Test
|
|||
IntProperty=""123""
|
||||
BoolProperty=""true""
|
||||
StringProperty=""My string""
|
||||
ObjectProperty=""new SomeType()""/>");
|
||||
ObjectProperty=""new SomeType()"" />");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
|
@ -119,6 +119,40 @@ namespace Test
|
|||
frame => AssertFrame.Attribute(frame, "StringProperty", "42", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_ChildComponent_WithNonPropertyAttributes()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : BlazorComponent, IComponent
|
||||
{
|
||||
void IComponent.SetParameters(ParameterCollection parameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<MyComponent some-attribute=""foo"" another-attribute=""@(42.ToString())"" />");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Component(frame, "Test.MyComponent", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "some-attribute", "foo", 1),
|
||||
frame => AssertFrame.Attribute(frame, "another-attribute", "42", 2));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void Render_ChildComponent_WithLambdaEventHandler()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -164,6 +164,75 @@ global::System.Object __typeHelper = ""*, TestAssembly"";
|
|||
#line 2 ""x:\dir\subdir\Test\TestComponent.cshtml""
|
||||
42.ToString()
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
;
|
||||
builder.AddAttribute(-1, ""ChildContent"", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
|
||||
}
|
||||
));
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
||||
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CodeGeneration_ChildComponent_WithNonPropertyAttributes()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
// Act
|
||||
var generated = CompileToCSharp(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<MyComponent some-attribute=""foo"" another-attribute=""@(43.ToString())""/>");
|
||||
|
||||
// Assert
|
||||
CompileToAssembly(generated);
|
||||
|
||||
Assert.Equal(@"
|
||||
// <auto-generated/>
|
||||
#pragma warning disable 1591
|
||||
namespace Test
|
||||
{
|
||||
#line hidden
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
|
||||
{
|
||||
#pragma warning disable 219
|
||||
private void __RazorDirectiveTokenHelpers__() {
|
||||
((System.Action)(() => {
|
||||
global::System.Object __typeHelper = ""*, TestAssembly"";
|
||||
}
|
||||
))();
|
||||
}
|
||||
#pragma warning restore 219
|
||||
#pragma warning disable 0414
|
||||
private static System.Object __o = null;
|
||||
#pragma warning restore 0414
|
||||
#pragma warning disable 1998
|
||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
|
||||
__o =
|
||||
#line 2 ""x:\dir\subdir\Test\TestComponent.cshtml""
|
||||
43.ToString()
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
;
|
||||
|
|
|
|||
|
|
@ -175,6 +175,58 @@ namespace Test
|
|||
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CodeGeneration_ChildComponent_WithNonPropertyAttributes()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
// Act
|
||||
var generated = CompileToCSharp(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<MyComponent some-attribute=""foo"" another-attribute=""@(43.ToString())""/>");
|
||||
|
||||
// Assert
|
||||
CompileToAssembly(generated);
|
||||
|
||||
Assert.Equal(@"
|
||||
// <auto-generated/>
|
||||
#pragma warning disable 1591
|
||||
namespace Test
|
||||
{
|
||||
#line hidden
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
|
||||
{
|
||||
#pragma warning disable 1998
|
||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenComponent<Test.MyComponent>(0);
|
||||
builder.AddAttribute(1, ""some-attribute"", ""foo"");
|
||||
builder.AddAttribute(2, ""another-attribute"", 43.ToString());
|
||||
builder.CloseComponent();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
||||
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CodeGeneration_ChildComponent_WithLambdaEventHandler()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue