| File: Forms\InputRadio.cs | Web Access |
| Project: src\aspnetcore\src\Components\Web\src\Microsoft.AspNetCore.Components.Web.csproj (Microsoft.AspNetCore.Components.Web) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms; /// <summary> /// An input component used for selecting a value from a group of choices. /// </summary> public class InputRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : ComponentBase { bool _trueValueToggle; /// <summary> /// Gets context for this <see cref="InputRadio{TValue}"/>. /// </summary> internal InputRadioContext? Context { get; private set; } /// <summary> /// Gets or sets a collection of additional attributes that will be applied to the input element. /// </summary> [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; } /// <summary> /// Gets or sets the value of this input. /// </summary> [Parameter] public TValue? Value { get; set; } /// <summary> /// Gets or sets the name of the parent input radio group. /// </summary> [Parameter] public string? Name { get; set; } /// <summary> /// Gets or sets the associated <see cref="ElementReference"/>. /// <para> /// May be <see langword="null"/> if accessed before the component is rendered. /// </para> /// </summary> [DisallowNull] public ElementReference? Element { get; protected set; } [CascadingParameter] private InputRadioContext? CascadedContext { get; set; } /// <inheritdoc /> protected override void OnParametersSet() { Context = string.IsNullOrEmpty(Name) ? CascadedContext : CascadedContext?.FindContextInAncestors(Name); if (Context == null) { throw new InvalidOperationException($"{GetType()} must have an ancestor {typeof(InputRadioGroup<TValue>)} " + $"with a matching 'Name' property, if specified."); } } /// <inheritdoc /> protected override void BuildRenderTree(RenderTreeBuilder builder) { Debug.Assert(Context != null); builder.OpenElement(0, "input"); builder.AddMultipleAttributes(1, AdditionalAttributes); builder.AddAttributeIfNotNullOrEmpty(2, "class", AttributeUtilities.CombineClassNames(AdditionalAttributes, Context.FieldClass)); builder.AddAttribute(3, "type", "radio"); builder.AddAttribute(4, "name", Context.GroupName); builder.AddAttribute(5, "value", BindConverter.FormatValue(Value?.ToString())); builder.AddAttribute(6, "checked", Context.CurrentValue?.Equals(Value) == true ? GetToggledTrueValue() : null); builder.AddAttribute(7, "onchange", Context.ChangeEventCallback); builder.SetUpdatesAttributeName("checked"); builder.AddElementReferenceCapture(8, __inputReference => Element = __inputReference); builder.CloseElement(); } // This is an unfortunate hack, but is needed for the scenario described by test InputRadioGroupWorksWithMutatingSetter. // Radio groups are special in that modifying one <input type=radio> instantly and implicitly also modifies the previously // selected one in the same group. As such, our SetUpdatesAttributeName mechanism isn't sufficient to stay in sync with the // DOM, because the 'change' event will fire on the new <input type=radio> you just selected, not the previously-selected // one, and so the previously-selected one doesn't get notified to update its state in the old rendertree. So, if the setter // reverts the incoming value, the previously-selected one would produce an empty diff (because its .NET value hasn't changed) // and hence it would be left unselected in the DOM. If you don't understand why this is a problem, try commenting out the // line that toggles _trueValueToggle and see the E2E test fail. // // This hack works around that by causing InputRadio *always* to force its own 'checked' state to be true in the DOM if it's // true in .NET, whether or not it was true before, by continally changing the value that represents 'true'. This doesn't // really cause any significant increase in traffic because if we're rendering this InputRadio at all, sending one more small // attribute value is inconsequential. // // Ultimately, a better solution would be to make SetUpdatesAttributeName smarter still so that it knows about the special // semantics of radio buttons so that, when one <input type="radio"> changes, it treats any previously-selected sibling // as needing DOM sync as well. That's a more sophisticated change and might not even be useful if the radio buttons // aren't truly siblings and are in different DOM subtrees (and especially if they were rendered by different components!) private string GetToggledTrueValue() { _trueValueToggle = !_trueValueToggle; return _trueValueToggle ? "a" : "b"; } }