Repository navigation
[Input] Required message refactoring - Part 1 #5399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
008d111
1784120
0f3ebd0
86f173a
63268d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| // ------------------------------------------------------------------------ | ||
| // This file is licensed to you under the MIT License. | ||
| // ------------------------------------------------------------------------ | ||
|
|
||
| using System.ComponentModel.DataAnnotations; | ||
| using System.Linq.Expressions; | ||
| using System.Reflection; | ||
| using Microsoft.AspNetCore.Components.Forms; | ||
|
|
||
| namespace Microsoft.FluentUI.AspNetCore.Components; | ||
|
|
||
| public abstract partial class FluentInputBase<TValue> | ||
| { | ||
| /// <summary> | ||
| /// Determines whether a required-field message condition is met. | ||
| /// </summary> | ||
| /// <param name="field">The field whose focus state may be used.</param> | ||
| /// <param name="isEmpty">Determines whether the current value is empty.</param> | ||
| /// <param name="useFieldFocusLost">Whether to use the field's focus state instead of this component's.</param> | ||
| /// <param name="fieldIdentifier"> | ||
| /// The field identifier to use for validation messages, if different from this component's. | ||
| /// </param> | ||
| protected bool IsRequiredMessageConditionMet(IFluentField field, Func<bool> isEmpty, bool useFieldFocusLost = false, FieldIdentifier? fieldIdentifier = null) | ||
| { | ||
| return EditContext?.GetValidationMessages(fieldIdentifier ?? FieldIdentifier).Any() != true && | ||
| (useFieldFocusLost ? field.FocusLost : FocusLost) && | ||
| (Required ?? false) && | ||
| !(Disabled ?? false) && | ||
| !ReadOnly && | ||
| isEmpty(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets the default required-field message on the supplied field. | ||
| /// </summary> | ||
| /// <param name="field">The field receiving the message.</param> | ||
| /// <param name="fieldIdentifier"> | ||
| /// The field identifier whose RequiredAttribute should supply the message, if different from this component's. | ||
| /// </param> | ||
| protected void SetRequiredErrorMessage(IFluentField field, FieldIdentifier? fieldIdentifier = null) | ||
| { | ||
| field.MessageIcon = FluentStatus.ErrorIcon; | ||
| field.Message = GetRequiredErrorMessage(fieldIdentifier ?? FieldIdentifier); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates the default required-field message condition for an input component. | ||
| /// </summary> | ||
| /// <param name="isEmpty">Determines whether the current value is empty.</param> | ||
| /// <param name="useFieldFocusLost">Whether to use the field's focus state instead of this component's.</param> | ||
| /// <param name="fieldIdentifierProvider"> | ||
| /// Provides the field identifier when it differs from this component's value expression. | ||
| /// </param> | ||
| protected Func<IFluentField, bool> CreateRequiredMessageCondition(Func<bool> isEmpty, bool useFieldFocusLost = false, Func<FieldIdentifier>? fieldIdentifierProvider = null) | ||
| { | ||
| return field => | ||
| { | ||
| var fieldIdentifier = fieldIdentifierProvider?.Invoke() ?? FieldIdentifier; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Required metadata and validation-message lookup can target different fields
Suggested fix: derive one |
||
| if (EditContext?.GetValidationMessages(fieldIdentifier).Any() == true) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| SetRequiredErrorMessage(field, fieldIdentifier); | ||
| return IsRequiredMessageConditionMet(field, isEmpty, useFieldFocusLost, fieldIdentifier); | ||
| }; | ||
| } | ||
|
|
||
| private string GetRequiredErrorMessage(FieldIdentifier fieldIdentifier) | ||
| { | ||
| var property = FindValidationProperty(fieldIdentifier); | ||
|
|
||
| var requiredAttribute = | ||
| property?.GetCustomAttribute<RequiredAttribute>(); | ||
|
|
||
| if (requiredAttribute is null || | ||
| (requiredAttribute.ErrorMessage is null && | ||
| requiredAttribute.ErrorMessageResourceName is null)) | ||
| { | ||
| return Localizer[Localization.LanguageResource.FluentInputBase_RequiredMessage]; | ||
| } | ||
|
|
||
| var displayName = | ||
| property?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? | ||
| fieldIdentifier.FieldName; | ||
|
|
||
| return requiredAttribute.FormatErrorMessage(displayName); | ||
| } | ||
|
|
||
| private PropertyInfo? FindValidationProperty(FieldIdentifier fieldIdentifier) | ||
| { | ||
| var property = GetProperty(ValidationFieldExpression); | ||
|
|
||
| return property is not null && | ||
| string.Equals(property.Name, fieldIdentifier.FieldName, StringComparison.Ordinal) | ||
| ? property | ||
| : null; | ||
| } | ||
|
|
||
| private static PropertyInfo? GetProperty(LambdaExpression? expression) | ||
| { | ||
| if (expression is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var body = expression.Body; | ||
|
|
||
| while (body is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked, } conversion) | ||
| { | ||
| body = conversion.Operand; | ||
| } | ||
|
|
||
| return body is MemberExpression { Member: PropertyInfo property, } ? property : null; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the expression identifying the model field used for validation. | ||
| /// </summary> | ||
| protected virtual LambdaExpression? ValidationFieldExpression => ValidationFieldFor ?? ValueExpression; | ||
|
|
||
| /// <summary> | ||
| /// Gets a value indicating whether the value expression was supplied for a bound model field. | ||
| /// </summary> | ||
| protected bool HasExplicitValueExpression | ||
| => GetProperty(ValueExpression) is { } property && | ||
| (property.DeclaringType != typeof(FluentInputBase<TValue>) || | ||
| !string.Equals(property.Name, nameof(CurrentValueOrDefault), StringComparison.Ordinal)); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,8 @@ public partial class FluentCheckbox : FluentInputBase<bool>, IFluentComponentEle | |
| public FluentCheckbox(LibraryConfiguration configuration) : base(configuration) | ||
| { | ||
| LabelPosition = Components.LabelPosition.After; | ||
|
|
||
| MessageCondition = CreateRequiredMessageCondition(() => !Value); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High — Default required conditions hide caller-supplied messages on Checkbox, Switch, and RadioGroup The new constructor defaults in Consequently, a consumer-provided Suggested fix: preserve explicit message behavior. Compute the fallback required message only after the required predicate succeeds, and do not mutate a caller-supplied |
||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,23 +75,23 @@ else | |
| <FluentValidationMessage TValue="object" | ||
| Field="@_fieldIdentifier" /> | ||
| } | ||
| else { | ||
| if (!HasValidationMessages && HasMessageOrCondition && Parameters.MessageCondition?.Invoke(InputComponent ?? this) == true) | ||
|
vnbaaij marked this conversation as resolved.
|
||
| { | ||
| <FluentText slot="@FluentSlot.FieldMessage" Size="@TextSize.Size200" Style="@(Parameters.MessageIcon == FluentStatus.ErrorIcon ? "color: var(--error);" : "")"> | ||
|
|
||
| @if (HasMessageOrCondition && Parameters.MessageCondition?.Invoke(InputComponent ?? this) == true) | ||
| { | ||
| <FluentText slot="@FluentSlot.FieldMessage" | ||
| size="@TextSize.Size200"> | ||
|
|
||
| @CreateIcon(Parameters.MessageIcon) | ||
| @CreateIcon(Parameters.MessageIcon) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — FluentField now suppresses all custom messages when validation errors exist The new Suggested fix: keep custom message rendering independent. Suppress only the generated required fallback when an |
||
|
|
||
| @if (Parameters.MessageTemplate is not null) | ||
| { | ||
| @Parameters.MessageTemplate | ||
| } | ||
| else | ||
| { | ||
| @Parameters.Message | ||
| } | ||
| </FluentText> | ||
| @if (Parameters.MessageTemplate is not null) | ||
| { | ||
| @Parameters.MessageTemplate | ||
| } | ||
| else | ||
| { | ||
| @Parameters.Message | ||
| } | ||
| </FluentText> | ||
| } | ||
| } | ||
|
|
||
| </fluent-field> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium — The new boolean Required annotations do not validate
falseThe added/updated
[Required]attributes on non-nullableboolproperties inStarship.csalways pass because a non-nullable Boolean is never null. The adjacent[Range(typeof(bool), "true", "true")]attributes perform the actual “must be true” validation, so the newRequiredAttribute.ErrorMessagecannot demonstrate the behavior this PR intends to showcase.Suggested fix: remove
[Required]from these Boolean properties and keep the enforcement/message on[Range], or use a nullable Boolean if null is the state being validated.