Skip to content

Commit a729c42

Browse files
InputRadio component with form support (#23415)
* Started on InputRadio forms component. * Added E2E test for InputRadio. * Added docstring for InputRadio. * Changed value to be serialized using BindConverter. * Added InputChoice for choice-based inputs. InputChoice contains checks for valid choice types that used to exist in InputSelect. Both InputSelect and InputRadio now derive from InputChoice and thus also contain those checks. * Added InputRadioGroup. * Small fix. * Removed InputChoice, cleaned up. * Added internal access modifier to InputExtensions. * Small improvements. * Updated an outdated exception message. * Updated test to reflect updated exception message. * Improved API to enforce InputRadioGroup. * Added support for InputSelect int and Guid bindings. * Changed validation CSS classes to influence InputRadio components.
1 parent b7d9e8c commit a729c42

15 files changed

+629
-39
lines changed

src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ protected InputBase() { }
6161
protected TValue CurrentValue { get { throw null; } set { } }
6262
protected string? CurrentValueAsString { get { throw null; } set { } }
6363
protected Microsoft.AspNetCore.Components.Forms.EditContext EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
64-
protected Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
64+
protected internal Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
6565
[Microsoft.AspNetCore.Components.ParameterAttribute]
6666
[System.Diagnostics.CodeAnalysis.MaybeNullAttribute]
6767
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
@@ -71,7 +71,7 @@ protected InputBase() { }
7171
[Microsoft.AspNetCore.Components.ParameterAttribute]
7272
public System.Linq.Expressions.Expression<System.Func<TValue>>? ValueExpression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
7373
protected virtual void Dispose(bool disposing) { }
74-
protected virtual string? FormatValueAsString(TValue value) { throw null; }
74+
protected virtual string? FormatValueAsString([System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value) { throw null; }
7575
public override System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
7676
void System.IDisposable.Dispose() { }
7777
protected abstract bool TryParseValueFromString(string? value, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out TValue result, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] out string? validationErrorMessage);
@@ -88,7 +88,7 @@ public InputDate() { }
8888
[Microsoft.AspNetCore.Components.ParameterAttribute]
8989
public string ParsingErrorMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
9090
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
91-
protected override string FormatValueAsString(TValue value) { throw null; }
91+
protected override string FormatValueAsString([System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value) { throw null; }
9292
protected override bool TryParseValueFromString(string? value, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out TValue result, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] out string? validationErrorMessage) { throw null; }
9393
}
9494
public partial class InputNumber<TValue> : Microsoft.AspNetCore.Components.Forms.InputBase<TValue>
@@ -97,9 +97,34 @@ public InputNumber() { }
9797
[Microsoft.AspNetCore.Components.ParameterAttribute]
9898
public string ParsingErrorMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
9999
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
100-
protected override string? FormatValueAsString(TValue value) { throw null; }
100+
protected override string? FormatValueAsString([System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value) { throw null; }
101101
protected override bool TryParseValueFromString(string? value, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out TValue result, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] out string? validationErrorMessage) { throw null; }
102102
}
103+
public partial class InputRadioGroup<TValue> : Microsoft.AspNetCore.Components.Forms.InputBase<TValue>
104+
{
105+
public InputRadioGroup() { }
106+
[Microsoft.AspNetCore.Components.ParameterAttribute]
107+
public Microsoft.AspNetCore.Components.RenderFragment? ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
108+
[Microsoft.AspNetCore.Components.ParameterAttribute]
109+
public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
110+
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
111+
protected override void OnParametersSet() { }
112+
protected override bool TryParseValueFromString(string? value, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out TValue result, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] out string? validationErrorMessage) { throw null; }
113+
}
114+
public partial class InputRadio<TValue> : Microsoft.AspNetCore.Components.ComponentBase
115+
{
116+
public InputRadio() { }
117+
[Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
118+
public System.Collections.Generic.IReadOnlyDictionary<string, object>? AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
119+
[Microsoft.AspNetCore.Components.ParameterAttribute]
120+
public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
121+
[Microsoft.AspNetCore.Components.ParameterAttribute]
122+
[System.Diagnostics.CodeAnalysis.MaybeNullAttribute]
123+
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
124+
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
125+
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
126+
protected override void OnParametersSet() { }
127+
}
103128
public partial class InputSelect<TValue> : Microsoft.AspNetCore.Components.Forms.InputBase<TValue>
104129
{
105130
public InputSelect() { }

src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ protected InputBase() { }
5959
protected TValue CurrentValue { get { throw null; } set { } }
6060
protected string? CurrentValueAsString { get { throw null; } set { } }
6161
protected Microsoft.AspNetCore.Components.Forms.EditContext EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
62-
protected Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
62+
protected internal Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
6363
[Microsoft.AspNetCore.Components.ParameterAttribute]
6464
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
6565
[Microsoft.AspNetCore.Components.ParameterAttribute]
@@ -96,6 +96,29 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
9696
protected override string? FormatValueAsString(TValue value) { throw null; }
9797
protected override bool TryParseValueFromString(string? value, out TValue result, out string? validationErrorMessage) { throw null; }
9898
}
99+
public partial class InputRadioGroup<TValue> : Microsoft.AspNetCore.Components.Forms.InputBase<TValue>
100+
{
101+
public InputRadioGroup() { }
102+
[Microsoft.AspNetCore.Components.ParameterAttribute]
103+
public Microsoft.AspNetCore.Components.RenderFragment? ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
104+
[Microsoft.AspNetCore.Components.ParameterAttribute]
105+
public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
106+
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
107+
protected override void OnParametersSet() { }
108+
protected override bool TryParseValueFromString(string? value, out TValue result, out string? validationErrorMessage) { throw null; }
109+
}
110+
public partial class InputRadio<TValue> : Microsoft.AspNetCore.Components.ComponentBase
111+
{
112+
public InputRadio() { }
113+
[Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
114+
public System.Collections.Generic.IReadOnlyDictionary<string, object>? AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
115+
[Microsoft.AspNetCore.Components.ParameterAttribute]
116+
public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
117+
[Microsoft.AspNetCore.Components.ParameterAttribute]
118+
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
119+
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
120+
protected override void OnParametersSet() { }
121+
}
99122
public partial class InputSelect<TValue> : Microsoft.AspNetCore.Components.Forms.InputBase<TValue>
100123
{
101124
public InputSelect() { }

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public abstract class InputBase<TValue> : ComponentBase, IDisposable
5858
/// <summary>
5959
/// Gets the <see cref="FieldIdentifier"/> for the bound value.
6060
/// </summary>
61-
protected FieldIdentifier FieldIdentifier { get; set; }
61+
protected internal FieldIdentifier FieldIdentifier { get; set; }
6262

6363
/// <summary>
6464
/// Gets or sets the current value of the input.
@@ -142,7 +142,7 @@ protected InputBase()
142142
/// </summary>
143143
/// <param name="value">The value to format.</param>
144144
/// <returns>A string representation of the value.</returns>
145-
protected virtual string? FormatValueAsString(TValue value)
145+
protected virtual string? FormatValueAsString([AllowNull] TValue value)
146146
=> value?.ToString();
147147

148148
/// <summary>

src/Components/Web/src/Forms/InputDate.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
3434
}
3535

3636
/// <inheritdoc />
37-
protected override string FormatValueAsString(TValue value)
37+
protected override string FormatValueAsString([AllowNull] TValue value)
3838
{
3939
switch (value)
4040
{
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Globalization;
7+
8+
namespace Microsoft.AspNetCore.Components.Forms
9+
{
10+
internal static class InputExtensions
11+
{
12+
public static bool TryParseSelectableValueFromString<TValue>(this InputBase<TValue> input, string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
13+
{
14+
try
15+
{
16+
if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var parsedValue))
17+
{
18+
result = parsedValue;
19+
validationErrorMessage = null;
20+
return true;
21+
}
22+
else
23+
{
24+
result = default;
25+
validationErrorMessage = $"The {input.FieldIdentifier.FieldName} field is not valid.";
26+
return false;
27+
}
28+
}
29+
catch (InvalidOperationException ex)
30+
{
31+
throw new InvalidOperationException($"{input.GetType()} does not support the type '{typeof(TValue)}'.", ex);
32+
}
33+
}
34+
}
35+
}

src/Components/Web/src/Forms/InputNumber.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ protected override bool TryParseValueFromString(string? value, [MaybeNull] out T
7474
/// </summary>
7575
/// <param name="value">The value to format.</param>
7676
/// <returns>A string representation of the value.</returns>
77-
protected override string? FormatValueAsString(TValue value)
77+
protected override string? FormatValueAsString([AllowNull] TValue value)
7878
{
7979
// Avoiding a cast to IFormattable to avoid boxing.
8080
switch (value)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
8+
using Microsoft.AspNetCore.Components.Rendering;
9+
10+
namespace Microsoft.AspNetCore.Components.Forms
11+
{
12+
/// <summary>
13+
/// An input component used for selecting a value from a group of choices.
14+
/// </summary>
15+
public class InputRadio<TValue> : ComponentBase
16+
{
17+
/// <summary>
18+
/// Gets context for this <see cref="InputRadio{TValue}"/>.
19+
/// </summary>
20+
internal InputRadioContext? Context { get; private set; }
21+
22+
/// <summary>
23+
/// Gets or sets a collection of additional attributes that will be applied to the input element.
24+
/// </summary>
25+
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
26+
27+
/// <summary>
28+
/// Gets or sets the value of this input.
29+
/// </summary>
30+
[AllowNull]
31+
[MaybeNull]
32+
[Parameter]
33+
public TValue Value { get; set; } = default;
34+
35+
/// <summary>
36+
/// Gets or sets the name of the parent input radio group.
37+
/// </summary>
38+
[Parameter] public string? Name { get; set; }
39+
40+
[CascadingParameter] private InputRadioContext? CascadedContext { get; set; }
41+
42+
private string GetCssClass(string fieldClass)
43+
{
44+
if (AdditionalAttributes != null &&
45+
AdditionalAttributes.TryGetValue("class", out var @class) &&
46+
!string.IsNullOrEmpty(Convert.ToString(@class)))
47+
{
48+
return $"{@class} {fieldClass}";
49+
}
50+
51+
return fieldClass;
52+
}
53+
54+
/// <inheritdoc />
55+
protected override void OnParametersSet()
56+
{
57+
Context = string.IsNullOrEmpty(Name) ? CascadedContext : CascadedContext?.FindContextInAncestors(Name);
58+
59+
if (Context == null)
60+
{
61+
throw new InvalidOperationException($"{GetType()} must have an ancestor {typeof(InputRadioGroup<TValue>)} " +
62+
$"with a matching 'Name' property, if specified.");
63+
}
64+
}
65+
66+
/// <inheritdoc />
67+
protected override void BuildRenderTree(RenderTreeBuilder builder)
68+
{
69+
Debug.Assert(Context != null);
70+
71+
builder.OpenElement(0, "input");
72+
builder.AddMultipleAttributes(1, AdditionalAttributes);
73+
builder.AddAttribute(2, "class", GetCssClass(Context.FieldClass));
74+
builder.AddAttribute(3, "type", "radio");
75+
builder.AddAttribute(4, "name", Context.GroupName);
76+
builder.AddAttribute(5, "value", BindConverter.FormatValue(Value?.ToString()));
77+
builder.AddAttribute(6, "checked", Context.CurrentValue?.Equals(Value));
78+
builder.AddAttribute(7, "onchange", Context.ChangeEventCallback);
79+
builder.CloseElement();
80+
}
81+
}
82+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.Forms
5+
{
6+
/// <summary>
7+
/// Describes context for an <see cref="InputRadio{TValue}"/> component.
8+
/// </summary>
9+
internal class InputRadioContext
10+
{
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>
31+
public EventCallback<ChangeEventArgs> ChangeEventCallback { get; }
32+
33+
/// <summary>
34+
/// Instantiates a new <see cref="InputRadioContext" />.
35+
/// </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>
40+
/// <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)
47+
{
48+
_parentContext = parentContext;
49+
50+
GroupName = groupName;
51+
CurrentValue = currentValue;
52+
FieldClass = fieldClass;
53+
ChangeEventCallback = changeEventCallback;
54+
}
55+
56+
/// <summary>
57+
/// Finds an <see cref="InputRadioContext"/> in the context's ancestors with the matching <paramref name="groupName"/>.
58+
/// </summary>
59+
/// <param name="groupName">The group name of the ancestor <see cref="InputRadioContext"/>.</param>
60+
/// <returns>The <see cref="InputRadioContext"/>, or <c>null</c> if none was found.</returns>
61+
public InputRadioContext? FindContextInAncestors(string groupName)
62+
=> string.Equals(GroupName, groupName) ? this : _parentContext?.FindContextInAncestors(groupName);
63+
}
64+
}

0 commit comments

Comments
 (0)