diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs index 22695000a9..cd7e0135eb 100644 --- a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs +++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs @@ -31,6 +31,7 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine private string _unconsumedHtml; private IList _currentAttributeValues; private IDictionary _currentElementAttributes = new Dictionary(); + private IList _currentElementAttributeTokens = new List(); public override void BeginWriterScope(CodeRenderingContext context, string writer) { @@ -91,8 +92,19 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node) { - // Render as text node. Later, we'll need to add different handling for expressions - // that appear inside elements, e.g., + // To support syntax like (which in turn supports syntax + // like ), check whether we are currently in the middle of + // writing an element. If so, treat this C# expression as something that should evaluate + // as a RenderTreeNode of type Attribute. + if (_unconsumedHtml != null) + { + var token = (IntermediateToken)node.Children.Single(); + _currentElementAttributeTokens.Add(token); + return; + } + + // Since we're not in the middle of writing an element, this must evaluate as some + // text to display context.CodeWriter .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddText)}"); @@ -206,6 +218,18 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine _currentElementAttributes.Clear(); } + if (_currentElementAttributeTokens.Count > 0) + { + foreach (var token in _currentElementAttributeTokens) + { + codeWriter + .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}") + .Write(token.Content) + .WriteEndMethodInvocation(); + } + _currentElementAttributeTokens.Clear(); + } + if (nextToken.Type == HtmlTokenType.EndTag || nextTag.IsSelfClosing || htmlVoidElementsLookup.Contains(nextTag.Data)) diff --git a/src/Microsoft.Blazor/Components/BlazorComponent.cs b/src/Microsoft.Blazor/Components/BlazorComponent.cs index b8cf2b560f..5b9e3cab59 100644 --- a/src/Microsoft.Blazor/Components/BlazorComponent.cs +++ b/src/Microsoft.Blazor/Components/BlazorComponent.cs @@ -34,5 +34,13 @@ namespace Microsoft.Blazor.Components /// Always throws an exception. public virtual Task ExecuteAsync() => throw new NotImplementedException($"Blazor components do not implement {nameof(ExecuteAsync)}."); + + /// + /// Handles click events by invoking . + /// + /// The handler to be invoked when the event occurs. + /// A that represents the event handler. + protected RenderTreeNode onclick(Action handler) + => RenderTreeNode.Attribute("onclick", _ => handler()); } } diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs index 6b4d4db6dc..e7b2494fbd 100644 --- a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs +++ b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs @@ -102,6 +102,23 @@ namespace Microsoft.Blazor.RenderTree Append(RenderTreeNode.Attribute(name, value.ToString())); } + /// + /// Appends a node representing an attribute. + /// The attribute is associated with the most recently added element. + /// + /// The name of the attribute. + /// The value of the attribute. + public void AddAttribute(RenderTreeNode node) + { + if (node.NodeType != RenderTreeNodeType.Attribute) + { + throw new ArgumentException($"The {nameof(node.NodeType)} must be {RenderTreeNodeType.Attribute}."); + } + + AssertCanAddAttribute(); + Append(node); + } + /// /// 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 59d4d82e64..b8b5188f2c 100644 --- a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs @@ -303,6 +303,35 @@ namespace Microsoft.Blazor.Build.Test node => AssertNode.Text(node, typeof(List).FullName)); } + [Fact] + public void SupportsAttributeNodesEvaluatedInline() + { + // Arrange/Act + var component = CompileToComponent( + @" + @functions { + public bool DidInvokeCode { get; set; } = false; + void MyHandler() + { + DidInvokeCode = true; + } + }"); + var didInvokeCodeProperty = component.GetType().GetProperty("DidInvokeCode"); + + // Assert + Assert.False((bool)didInvokeCodeProperty.GetValue(component)); + Assert.Collection(GetRenderTree(component).Where(NotWhitespace), + node => AssertNode.Element(node, "elem", 1), + node => + { + Assert.Equal(RenderTreeNodeType.Attribute, node.NodeType); + Assert.NotNull(node.AttributeEventHandlerValue); + + node.AttributeEventHandlerValue(null); + Assert.True((bool)didInvokeCodeProperty.GetValue(component)); + }); + } + private static bool NotWhitespace(RenderTreeNode node) => node.NodeType != RenderTreeNodeType.Text || !string.IsNullOrWhiteSpace(node.TextContent); diff --git a/test/testapps/BasicTestApp/CounterComponent.cshtml b/test/testapps/BasicTestApp/CounterComponent.cshtml index 33802c3555..6c3d9c5d56 100644 --- a/test/testapps/BasicTestApp/CounterComponent.cshtml +++ b/test/testapps/BasicTestApp/CounterComponent.cshtml @@ -1,7 +1,12 @@ 

Counter

Current count: @currentCount

- + @functions { int currentCount = 0; + + void IncrementCount() + { + currentCount++; + } }