Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,12 @@
Gets the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentInputBase`1.FieldIdentifier"/> for the bound value.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentInputBase`1.FieldDisplayName">
<summary>
Gets the display name of the field, using the specified display name if set; otherwise, uses the field
identifier's name if the field is bound.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentInputBase`1.CurrentValue">
<summary>
Gets or sets the current value of the input.
Expand Down Expand Up @@ -767,6 +773,16 @@
<param name="e"></param>
<returns></returns>
</member>
<member name="T:Microsoft.FluentUI.AspNetCore.Components.ICultureSensitiveComponent">
<summary>
Defines an interface for components with values that are sensitive to culture settings, eg : parsing to string.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.ICultureSensitiveComponent.Culture">
<summary>
Gets or sets the culture of the component.
</summary>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.InputHelpers`1.GetMaxValue">
<summary>
Because of the limitation of the web component, the maximum value is set to 9999999999 for really large numbers.
Expand All @@ -779,6 +795,11 @@
</summary>
<returns>The minimum value for the underlying type</returns>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.IStringParsableComponent.ParsingErrorMessage">
<summary>
Gets or sets the error message to show when the field can not be parsed.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentBodyContent.ChildContent">
<summary>
Gets or sets the content to be rendered inside the component.
Expand Down Expand Up @@ -2999,6 +3020,9 @@
By default <see cref="P:System.Globalization.CultureInfo.CurrentCulture"/> to display using the OS culture.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentCalendarBase.ParsingErrorMessage">
<inheritdoc/>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentCalendarBase.DisabledDateFunc">
<summary>
Function to know if a specific day must be disabled.
Expand Down Expand Up @@ -6703,6 +6727,9 @@
⚠️ Only available when Multiple = true.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.ListComponentBase`1.ParsingErrorMessage">
<inheritdoc/>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.ListComponentBase`1.#ctor">
<summary />
</member>
Expand Down Expand Up @@ -8127,11 +8154,6 @@
An Id value must be set to use this property.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentNumberField`1.ParsingErrorMessage">
<summary>
Gets or sets the error message to show when the field can not be parsed.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentNumberField`1.ChildContent">
<summary>
Gets or sets the content to be rendered inside the component.
Expand All @@ -8143,6 +8165,9 @@
unless an explicit value for Min or Max is provided.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentNumberField`1.ParsingErrorMessage">
<inheritdoc/>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentNumberField`1.FormatValueAsString(`0)">
<summary>
Formats the value as a string. Derived classes can override this to determine the formatting used for <c>CurrentValueAsString</c>.
Expand Down Expand Up @@ -9036,6 +9061,9 @@
Gets or sets the child content to be rendering inside the <see cref="T:Microsoft.FluentUI.AspNetCore.Components.FluentRadioGroup`1"/>.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentRadioGroup`1.ParsingErrorMessage">
<inheritdoc/>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentRadioGroup`1.OnParametersSet">
<inheritdoc />
</member>
Expand Down Expand Up @@ -9091,6 +9119,9 @@
Fires when hovered value changes. Value will be null if no rating item is hovered.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentRating.ParsingErrorMessage">
<inheritdoc/>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentRating.GroupName">
<summary />
</member>
Expand Down
6 changes: 6 additions & 0 deletions src/Core/Components/Base/FluentInputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ public abstract partial class FluentInputBase<TValue> : FluentComponentBase, IDi

internal virtual bool FieldBound => Field is not null || ValueExpression is not null || ValueChanged.HasDelegate;

/// <summary>
/// Gets the display name of the field, using the specified display name if set; otherwise, uses the field
/// identifier's name if the field is bound.
/// </summary>
internal string FieldDisplayName => DisplayName ?? (FieldBound ? FieldIdentifier.FieldName : UnknownBoundField);

protected async Task SetCurrentValueAsync(TValue? value)
{
var hasChanged = !EqualityComparer<TValue>.Default.Equals(value, Value);
Expand Down
18 changes: 18 additions & 0 deletions src/Core/Components/Base/ICultureSensitiveComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// ------------------------------------------------------------------------
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

using System.Globalization;

namespace Microsoft.FluentUI.AspNetCore.Components;

/// <summary>
/// Defines an interface for components with values that are sensitive to culture settings, eg : parsing to string.
/// </summary>
public interface ICultureSensitiveComponent
{
/// <summary>
/// Gets or sets the culture of the component.
/// </summary>
public CultureInfo Culture { get; set; }
}
16 changes: 16 additions & 0 deletions src/Core/Components/Base/IStringParsableComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// ------------------------------------------------------------------------
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

namespace Microsoft.FluentUI.AspNetCore.Components;

/// <summary>
/// Defines an interface for components with values that can be parsed from a string.
/// </summary>
public interface IStringParsableComponent
{
/// <summary>
/// Gets or sets the error message to show when the field can not be parsed.
/// </summary>
public string ParsingErrorMessage { get; set; }
}
6 changes: 3 additions & 3 deletions src/Core/Components/DateTime/FluentCalendar.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,9 @@ private async Task PickerYearSelectAsync(DateTime? year)
/// <summary />
protected override bool TryParseValueFromString(string? value, out DateTime? result, [NotNullWhen(false)] out string? validationErrorMessage)
{
BindConverter.TryConvertTo(value, Culture, out result);
validationErrorMessage = null;
return true;
bool success = BindConverter.TryConvertTo(value, Culture, out result);
validationErrorMessage = success ? null : string.Format(ParsingErrorMessage, FieldDisplayName);
return success;
}

/// <summary />
Expand Down
6 changes: 5 additions & 1 deletion src/Core/Components/DateTime/FluentCalendarBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Microsoft.FluentUI.AspNetCore.Components;

public abstract class FluentCalendarBase : FluentInputBase<DateTime?>
public abstract class FluentCalendarBase : FluentInputBase<DateTime?> , ICultureSensitiveComponent, IStringParsableComponent
{
/// <summary>
/// Gets or sets the culture of the component.
Expand All @@ -16,6 +16,10 @@ public abstract class FluentCalendarBase : FluentInputBase<DateTime?>
[Parameter]
public virtual CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture;

/// <inheritdoc/>
[Parameter]
public virtual string ParsingErrorMessage { get; set; } = "The {0} field must have a valid format.";

/// <summary>
/// Function to know if a specific day must be disabled.
/// </summary>
Expand Down
7 changes: 3 additions & 4 deletions src/Core/Components/DateTime/FluentDatePicker.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,9 @@ protected override bool TryParseValueFromString(string? value, out DateTime? res
value = new DateTime(year, 1, 1).ToString(Culture.DateTimeFormat.ShortDatePattern);
}

BindConverter.TryConvertTo(value, Culture, out result);

validationErrorMessage = null;
return true;
bool success = BindConverter.TryConvertTo(value, Culture, out result);
validationErrorMessage = success ? null : string.Format(ParsingErrorMessage, FieldDisplayName);
return success;
}

private string PlaceholderAccordingToView()
Expand Down
6 changes: 5 additions & 1 deletion src/Core/Components/List/ListComponentBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
/// Component that provides a list of options.
/// </summary>
/// <typeparam name="TOption"></typeparam>
public abstract partial class ListComponentBase<TOption> : FluentInputBase<string?>, IAsyncDisposable where TOption : notnull
public abstract partial class ListComponentBase<TOption> : FluentInputBase<string?>, IAsyncDisposable, IStringParsableComponent where TOption : notnull
{
private const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/List/ListComponentBase.razor.js";

Expand Down Expand Up @@ -212,6 +212,10 @@ protected string? InternalValue
[Parameter]
public Expression<Func<IEnumerable<TOption>>>? SelectedOptionsExpression { get; set; }

/// <inheritdoc/>
[Parameter]
public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number.";

/// <summary />
protected ListComponentBase()
{
Expand Down
12 changes: 5 additions & 7 deletions src/Core/Components/NumberField/FluentNumberField.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Microsoft.FluentUI.AspNetCore.Components;

public partial class FluentNumberField<TValue> : FluentInputBase<TValue>, IAsyncDisposable
public partial class FluentNumberField<TValue> : FluentInputBase<TValue>, IAsyncDisposable, IStringParsableComponent
{
private const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/TextField/FluentTextField.razor.js";

Expand Down Expand Up @@ -86,12 +86,6 @@ public partial class FluentNumberField<TValue> : FluentInputBase<TValue>, IAsync
[Parameter]
public string? AutoComplete { get; set; }

/// <summary>
/// Gets or sets the error message to show when the field can not be parsed.
/// </summary>
[Parameter]
public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number.";

/// <summary>
/// Gets or sets the content to be rendered inside the component.
/// </summary>
Expand All @@ -105,6 +99,10 @@ public partial class FluentNumberField<TValue> : FluentInputBase<TValue>, IAsync
[Parameter]
public bool UseTypeConstraints { get; set; }

/// <inheritdoc/>
[Parameter]
public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number.";

private static readonly string _stepAttributeValue = GetStepAttributeValue();

// If type constraints is true and min is null, set min to the minimum value of TValue.
Expand Down
6 changes: 5 additions & 1 deletion src/Core/Components/Radio/FluentRadioGroup.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
/// </summary>

[CascadingTypeParameter(nameof(TValue))]
public partial class FluentRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase<TValue>
public partial class FluentRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase<TValue>, IStringParsableComponent
{
private readonly string _defaultGroupName = Identifier.NewId();
private FluentRadioContext? _context;
Expand All @@ -40,6 +40,10 @@ public FluentRadioGroup()
[CascadingParameter]
private FluentRadioContext? CascadedContext { get; set; }

/// <inheritdoc/>
[Parameter]
public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number.";

/// <inheritdoc />
protected override void OnParametersSet()
{
Expand Down
9 changes: 6 additions & 3 deletions src/Core/Components/Rating/FluentRating.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Microsoft.FluentUI.AspNetCore.Components;

public partial class FluentRating : FluentInputBase<int>
public partial class FluentRating : FluentInputBase<int>, IStringParsableComponent
{
private bool _updatingCurrentValue = false;
private int? _hoverValue = null;
Expand Down Expand Up @@ -74,6 +74,10 @@ public partial class FluentRating : FluentInputBase<int>
[Parameter]
public EventCallback<int?> OnHoverValueChanged { get; set; }

/// <inheritdoc/>
[Parameter]
public string ParsingErrorMessage { get; set; } = "The {0} field must be a number.";

/// <summary />
private string GroupName => Id ?? $"rating-{Id}";

Expand All @@ -90,8 +94,7 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa
}
else
{
validationErrorMessage = string.Format(CultureInfo.InvariantCulture,
"The {0} field must be a number.",
validationErrorMessage = string.Format(ParsingErrorMessage,
FieldBound ? FieldIdentifier.FieldName : UnknownBoundField);
return false;
}
Expand Down
12 changes: 7 additions & 5 deletions src/Core/Extensions/FluentInputExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Extensions;
internal static class FluentInputExtensions
{

public static bool TryParseSelectableValueFromString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue>(
this FluentInputBase<TValue> input, string? value,
public static bool TryParseSelectableValueFromString<TInput, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue>(
this TInput input, string? value,
[MaybeNullWhen(false)] out TValue result,
[NotNullWhen(false)] out string? validationErrorMessage)
[NotNullWhen(false)] out string? validationErrorMessage) where TInput : FluentInputBase<TValue>, IStringParsableComponent
{
try
{
var culture = (input as ICultureSensitiveComponent)?.Culture ?? CultureInfo.CurrentCulture;

// We special-case bool values because BindConverter reserves bool conversion for conditional attributes.
if (typeof(TValue) == typeof(bool))
{
Expand All @@ -34,15 +36,15 @@ internal static class FluentInputExtensions
return true;
}
}
else if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var parsedValue))
else if (BindConverter.TryConvertTo<TValue>(value, culture, out var parsedValue))
{
result = parsedValue;
validationErrorMessage = null;
return true;
}

result = default;
validationErrorMessage = $"The {input.DisplayName ?? (input.FieldBound ? input.FieldIdentifier.FieldName : input.UnknownBoundField)} field is not valid.";
validationErrorMessage = string.Format(input.ParsingErrorMessage, input.FieldDisplayName);
return false;
}
catch (InvalidOperationException ex)
Expand Down
39 changes: 39 additions & 0 deletions tests/Core/DateTime/FluentDatePickerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Bunit;
using Microsoft.AspNetCore.Components;
Expand Down Expand Up @@ -292,4 +293,42 @@ public void FluentDatePicker_OnDoubleClickEventTriggers()
// Assert
Assert.Equal(expected, actual);
}

[Theory]
[InlineData("02-22-2000", "en-GB", "02/22/2000")]
[InlineData("02-22-2000", "nl-BE", null)]
[InlineData("22-12-2000", "nl-BE", "22/12/2000")]
[InlineData("02/22/2000", "en-GB", "02/22/2000")]
[InlineData("abc", "en-GB", null)]
[InlineData("02022000", "en-GB", null)]
public void FluentDatePicker_TryParseValueFromString(string? value, string? cultureName, string? expectedValue)
{
// Arrange
var picker = new TestDatePicker();
var culture = cultureName != null ? CultureInfo.GetCultureInfo(cultureName) : CultureInfo.InvariantCulture;

// Act
picker.Culture = culture;
var successfullParse = picker.CallTryParseValueFromString(value, out var resultDate, out var validationErrorMessage);

// Assert
if (successfullParse)
{
Assert.Equal(expectedValue, resultDate?.ToString(culture.DateTimeFormat.ShortDatePattern, culture));
}
else
{
Assert.Null(resultDate);
Assert.NotNull(validationErrorMessage);
}
}

// Temporary class to expose protected method
private class TestDatePicker : FluentDatePicker
{
public bool CallTryParseValueFromString(string? value, out System.DateTime? result, [NotNullWhen(false)] out string? validationErrorMessage)
{
return base.TryParseValueFromString(value, out result, out validationErrorMessage);
}
}
}
Loading