Skip to content

Commit 905557e

Browse files
Make InputRadioGroup preserve child elements (#40148)
1 parent a18aeb5 commit 905557e

File tree

5 files changed

+48
-63
lines changed

5 files changed

+48
-63
lines changed

src/Components/Web/src/Forms/AttributeUtilities.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Components.Forms;
77

88
internal static class AttributeUtilities
99
{
10-
public static string CombineClassNames(IReadOnlyDictionary<string, object>? additionalAttributes, string classNames)
10+
public static string? CombineClassNames(IReadOnlyDictionary<string, object>? additionalAttributes, string? classNames)
1111
{
1212
if (additionalAttributes is null || !additionalAttributes.TryGetValue("class", out var @class))
1313
{

src/Components/Web/src/Forms/InputBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ protected string CssClass
165165
{
166166
get
167167
{
168-
var fieldClass = EditContext?.FieldCssClass(FieldIdentifier) ?? string.Empty;
169-
return AttributeUtilities.CombineClassNames(AdditionalAttributes, fieldClass);
168+
var fieldClass = EditContext?.FieldCssClass(FieldIdentifier);
169+
return AttributeUtilities.CombineClassNames(AdditionalAttributes, fieldClass) ?? string.Empty;
170170
}
171171
}
172172

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

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,24 @@ namespace Microsoft.AspNetCore.Components.Forms;
66
/// <summary>
77
/// Describes context for an <see cref="InputRadio{TValue}"/> component.
88
/// </summary>
9-
internal class InputRadioContext
9+
internal sealed class InputRadioContext
1010
{
11-
private readonly InputRadioContext? _parentContext;
12-
13-
/// <summary>
14-
/// Gets the name of the input radio group.
15-
/// </summary>
16-
public string GroupName { get; }
17-
18-
/// <summary>
19-
/// Gets the current selected value in the input radio group.
20-
/// </summary>
21-
public object? CurrentValue { get; }
22-
23-
/// <summary>
24-
/// Gets a css class indicating the validation state of input radio elements.
25-
/// </summary>
26-
public string FieldClass { get; }
27-
28-
/// <summary>
29-
/// Gets the event callback to be invoked when the selected value is changed.
30-
/// </summary>
11+
public InputRadioContext? ParentContext { get; }
3112
public EventCallback<ChangeEventArgs> ChangeEventCallback { get; }
3213

14+
// Mutable properties that may change any time an InputRadioGroup is rendered
15+
public string? GroupName { get; set; }
16+
public object? CurrentValue { get; set; }
17+
public string? FieldClass { get; set; }
18+
3319
/// <summary>
3420
/// Instantiates a new <see cref="InputRadioContext" />.
3521
/// </summary>
36-
/// <param name="parentContext">The parent <see cref="InputRadioContext" />.</param>
37-
/// <param name="groupName">The name of the input radio group.</param>
38-
/// <param name="currentValue">The current selected value in the input radio group.</param>
39-
/// <param name="fieldClass">The css class indicating the validation state of input radio elements.</param>
22+
/// <param name="parentContext">The parent context, if any.</param>
4023
/// <param name="changeEventCallback">The event callback to be invoked when the selected value is changed.</param>
41-
public InputRadioContext(
42-
InputRadioContext? parentContext,
43-
string groupName,
44-
object? currentValue,
45-
string fieldClass,
46-
EventCallback<ChangeEventArgs> changeEventCallback)
24+
public InputRadioContext(InputRadioContext? parentContext, EventCallback<ChangeEventArgs> changeEventCallback)
4725
{
48-
_parentContext = parentContext;
49-
50-
GroupName = groupName;
51-
CurrentValue = currentValue;
52-
FieldClass = fieldClass;
26+
ParentContext = parentContext;
5327
ChangeEventCallback = changeEventCallback;
5428
}
5529

@@ -59,5 +33,5 @@ public InputRadioContext(
5933
/// <param name="groupName">The group name of the ancestor <see cref="InputRadioContext"/>.</param>
6034
/// <returns>The <see cref="InputRadioContext"/>, or <c>null</c> if none was found.</returns>
6135
public InputRadioContext? FindContextInAncestors(string groupName)
62-
=> string.Equals(GroupName, groupName) ? this : _parentContext?.FindContextInAncestors(groupName);
36+
=> string.Equals(GroupName, groupName) ? this : ParentContext?.FindContextInAncestors(groupName);
6337
}

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,33 @@ public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemb
3030
/// <inheritdoc />
3131
protected override void OnParametersSet()
3232
{
33-
var groupName = !string.IsNullOrEmpty(Name) ? Name : _defaultGroupName;
34-
var fieldClass = EditContext?.FieldCssClass(FieldIdentifier) ?? string.Empty;
35-
var changeEventCallback = EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString);
33+
// On the first render, we can instantiate the InputRadioContext
34+
if (_context is null)
35+
{
36+
var changeEventCallback = EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString);
37+
_context = new InputRadioContext(CascadedContext, changeEventCallback);
38+
}
39+
else if (_context.ParentContext != CascadedContext)
40+
{
41+
// This should never be possible in any known usage pattern, but if it happens, we want to know
42+
throw new InvalidOperationException("An InputRadioGroup cannot change context after creation");
43+
}
3644

37-
_context = new InputRadioContext(CascadedContext, groupName, CurrentValue, fieldClass, changeEventCallback);
45+
// Mutate the InputRadioContext instance in place. Since this is a non-fixed cascading parameter, the descendant
46+
// InputRadio/InputRadioGroup components will get notified to re-render and will see the new values.
47+
_context.GroupName = !string.IsNullOrEmpty(Name) ? Name : _defaultGroupName;
48+
_context.CurrentValue = CurrentValue;
49+
_context.FieldClass = EditContext?.FieldCssClass(FieldIdentifier);
3850
}
3951

4052
/// <inheritdoc />
4153
protected override void BuildRenderTree(RenderTreeBuilder builder)
4254
{
4355
Debug.Assert(_context != null);
4456

57+
// Note that we must not set IsFixed=true on the CascadingValue, because the mutations to _context
58+
// are what cause the descendant InputRadio components to re-render themselves
4559
builder.OpenComponent<CascadingValue<InputRadioContext>>(0);
46-
builder.SetKey(_context);
47-
builder.AddAttribute(1, "IsFixed", true);
4860
builder.AddAttribute(2, "Value", _context);
4961
builder.AddAttribute(3, "ChildContent", ChildContent);
5062
builder.CloseComponent();

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

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -360,37 +360,36 @@ public void InputRadioGroupWithoutNameInteractsWithEditContext()
360360
var appElement = MountTypicalValidationComponent();
361361
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
362362

363+
// By capturing the inputradio elements just once up front, we're implicitly showing
364+
// that they are retained as their values change
365+
var unknownAirlineInput = FindAirlineInputs().First(i => string.Equals("Unknown", i.GetAttribute("value")));
366+
var bestAirlineInput = FindAirlineInputs().First(i => string.Equals("BestAirline", i.GetAttribute("value")));
367+
363368
// Validate selected inputs
364-
Browser.True(() => FindUnknownAirlineInput().Selected);
365-
Browser.False(() => FindBestAirlineInput().Selected);
369+
Browser.True(() => unknownAirlineInput.Selected);
370+
Browser.False(() => bestAirlineInput.Selected);
366371

367372
// InputRadio emits additional attributes
368-
Browser.True(() => FindUnknownAirlineInput().GetAttribute("extra").Equals("additional"));
373+
Browser.True(() => unknownAirlineInput.GetAttribute("extra").Equals("additional"));
369374

370375
// Validates on edit
371-
Browser.Equal("valid", () => FindUnknownAirlineInput().GetAttribute("class"));
372-
Browser.Equal("valid", () => FindBestAirlineInput().GetAttribute("class"));
376+
Browser.Equal("valid", () => unknownAirlineInput.GetAttribute("class"));
377+
Browser.Equal("valid", () => bestAirlineInput.GetAttribute("class"));
373378

374-
FindBestAirlineInput().Click();
379+
bestAirlineInput.Click();
375380

376-
Browser.Equal("modified valid", () => FindUnknownAirlineInput().GetAttribute("class"));
377-
Browser.Equal("modified valid", () => FindBestAirlineInput().GetAttribute("class"));
381+
Browser.Equal("modified valid", () => unknownAirlineInput.GetAttribute("class"));
382+
Browser.Equal("modified valid", () => bestAirlineInput.GetAttribute("class"));
378383

379384
// Can become invalid
380-
FindUnknownAirlineInput().Click();
385+
unknownAirlineInput.Click();
381386

382-
Browser.Equal("modified invalid", () => FindUnknownAirlineInput().GetAttribute("class"));
383-
Browser.Equal("modified invalid", () => FindBestAirlineInput().GetAttribute("class"));
387+
Browser.Equal("modified invalid", () => unknownAirlineInput.GetAttribute("class"));
388+
Browser.Equal("modified invalid", () => bestAirlineInput.GetAttribute("class"));
384389
Browser.Equal(new[] { "Pick a valid airline." }, messagesAccessor);
385390

386391
IReadOnlyCollection<IWebElement> FindAirlineInputs()
387392
=> appElement.FindElement(By.ClassName("airline")).FindElements(By.TagName("input"));
388-
389-
IWebElement FindUnknownAirlineInput()
390-
=> FindAirlineInputs().First(i => string.Equals("Unknown", i.GetAttribute("value")));
391-
392-
IWebElement FindBestAirlineInput()
393-
=> FindAirlineInputs().First(i => string.Equals("BestAirline", i.GetAttribute("value")));
394393
}
395394

396395
[Fact]

0 commit comments

Comments
 (0)