Skip to content

Commit 6cfc445

Browse files
authored
[Blazor] Blazor InputRadio is broken when parent component implements IHandleEvent (#53245)
The CurrentValue property in the context was being initialized the first time but was not being updated afterwards even though it changed on the InputRadioGroup. That meant that InputRadio components would not see the value immediately as part of the first render. This was working on other scenarios because the default implementation in ComponentBase will trigger a re-render of the parent component, which in turn will trigger a re-render of the InputGroup component (since it receives a render fragment) and at that point, the component would update the parameter and the re-render of the individual InputRadio elements would work. When someone implements IHandleEvent, that doesn't necessarily happen, hence the reason for the bug. The fix is to avoid updating the value in the context as a separate step and instead making the context reflect the value from the input right away.
1 parent fbe2294 commit 6cfc445

File tree

6 files changed

+58
-10
lines changed

6 files changed

+58
-10
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
internal interface IInputRadioValueProvider
7+
{
8+
public object? CurrentValue { get; }
9+
}

src/Components/Web/src/Forms/InputRadioContext.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,19 @@ namespace Microsoft.AspNetCore.Components.Forms;
88
/// </summary>
99
internal sealed class InputRadioContext
1010
{
11+
private readonly IInputRadioValueProvider _valueProvider;
12+
1113
public InputRadioContext? ParentContext { get; }
1214
public EventCallback<ChangeEventArgs> ChangeEventCallback { get; }
15+
public object? CurrentValue => _valueProvider.CurrentValue;
1316

1417
// Mutable properties that may change any time an InputRadioGroup is rendered
1518
public string? GroupName { get; set; }
16-
public object? CurrentValue { get; set; }
1719
public string? FieldClass { get; set; }
1820

19-
/// <summary>
20-
/// Instantiates a new <see cref="InputRadioContext" />.
21-
/// </summary>
22-
/// <param name="parentContext">The parent context, if any.</param>
23-
/// <param name="changeEventCallback">The event callback to be invoked when the selected value is changed.</param>
24-
public InputRadioContext(InputRadioContext? parentContext, EventCallback<ChangeEventArgs> changeEventCallback)
21+
public InputRadioContext(IInputRadioValueProvider valueProvider, InputRadioContext? parentContext, EventCallback<ChangeEventArgs> changeEventCallback)
2522
{
23+
_valueProvider = valueProvider;
2624
ParentContext = parentContext;
2725
ChangeEventCallback = changeEventCallback;
2826
}

src/Components/Web/src/Forms/InputRadioGroup.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components.Forms;
1010
/// <summary>
1111
/// Groups child <see cref="InputRadio{TValue}"/> components.
1212
/// </summary>
13-
public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : InputBase<TValue>
13+
public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : InputBase<TValue>, IInputRadioValueProvider
1414
{
1515
private readonly string _defaultGroupName = Guid.NewGuid().ToString("N");
1616
private InputRadioContext? _context;
@@ -27,14 +27,16 @@ public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemb
2727

2828
[CascadingParameter] private InputRadioContext? CascadedContext { get; set; }
2929

30+
object? IInputRadioValueProvider.CurrentValue => CurrentValue;
31+
3032
/// <inheritdoc />
3133
protected override void OnParametersSet()
3234
{
3335
// On the first render, we can instantiate the InputRadioContext
3436
if (_context is null)
3537
{
3638
var changeEventCallback = EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString);
37-
_context = new InputRadioContext(CascadedContext, changeEventCallback);
39+
_context = new InputRadioContext(this, CascadedContext, changeEventCallback);
3840
}
3941
else if (_context.ParentContext != CascadedContext)
4042
{
@@ -59,7 +61,7 @@ protected override void OnParametersSet()
5961
// Otherwise, just use a GUID to disambiguate this group's radio inputs from any others on the page.
6062
_context.GroupName = _defaultGroupName;
6163
}
62-
_context.CurrentValue = CurrentValue;
64+
6365
_context.FieldClass = EditContext?.FieldCssClass(FieldIdentifier);
6466
}
6567

src/Components/test/E2ETest/Tests/FormsTest.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,30 @@ public void InputRadioGroupWorksWithMutatingSetter()
844844
Browser.Equal("False", () => tuesday.GetDomProperty("checked"));
845845
}
846846

847+
[Theory]
848+
[InlineData(0)]
849+
[InlineData(2)]
850+
public void InputRadioGroupWorksWithParentImplementingIHandleEvent(int n)
851+
{
852+
Browser.Url = new UriBuilder(Browser.Url) { Query = ($"?n={n}") }.ToString();
853+
var appElement = Browser.MountTestComponent<InputRadioParentImplementsIHandleEvent>();
854+
var zero = appElement.FindElement(By.Id("inputradiogroup-parent-ihandle-event-0"));
855+
var one = appElement.FindElement(By.Id("inputradiogroup-parent-ihandle-event-1"));
856+
857+
Browser.Equal(n == 0 ? "True" : "False", () => zero.GetDomProperty("checked"));
858+
Browser.Equal("False", () => one.GetDomProperty("checked"));
859+
860+
// Observe the changes after a click
861+
one.Click();
862+
Browser.Equal("False", () => zero.GetDomProperty("checked"));
863+
Browser.Equal("True", () => one.GetDomProperty("checked"));
864+
865+
// Ensure other options can be selected
866+
zero.Click();
867+
Browser.Equal("False", () => one.GetDomProperty("checked"));
868+
Browser.Equal("True", () => zero.GetDomProperty("checked"));
869+
}
870+
847871
[Fact]
848872
public void InputSelectWorksWithMutatingSetter()
849873
{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@using Microsoft.AspNetCore.Components.Forms
2+
@implements IHandleEvent
3+
4+
<InputRadioGroup @bind-Value="N">
5+
<InputRadio id="inputradiogroup-parent-ihandle-event-0" Value="0" />
6+
<InputRadio id="inputradiogroup-parent-ihandle-event-1" Value="1" />
7+
</InputRadioGroup>
8+
9+
@code {
10+
11+
[SupplyParameterFromQuery(Name = "n")] int? N { get; set; }
12+
13+
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg) => callback.InvokeAsync(arg);
14+
}

src/Components/test/testassets/BasicTestApp/Index.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<option value="BasicTestApp.FormsTest.InputRangeComponent">Input range</option>
5050
<option value="BasicTestApp.FormsTest.InputsWithoutEditForm">Inputs without EditForm</option>
5151
<option value="BasicTestApp.FormsTest.InputsWithMutatingSetters">Inputs with mutating setters</option>
52+
<option value="BasicTestApp.FormsTest.InputRadioParentImplementsIHandleEvent">Input Radio Parent Implements IHandleEvent</option>
5253
<option value="BasicTestApp.NavigateOnSubmit">Navigate to submit</option>
5354
<option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
5455
<option value="BasicTestApp.GracefulTermination">Graceful Termination</option>

0 commit comments

Comments
 (0)