Skip to content

Commit

Permalink
NumericField: Resolve nullable issue introduced by MudBlazor#4971 (Mu…
Browse files Browse the repository at this point in the history
  • Loading branch information
iXyles authored and jammerware committed Sep 20, 2022
1 parent 66161dd commit dd0176b
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 70 deletions.
Expand Up @@ -39,25 +39,37 @@
Variant="Variant.Filled"
Margin="Margin.Dense"
Max="9.9"
Min="-5.0"
Min="-5.0"
Immediate="true"/>
<br/><br/>
<MudNumericField @bind-Value="_nullable"
Label="int? max 10"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Max="10" />

<br/>
<MudNumericField T="double?"
Label="double?"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<br/>
<MudNumericField T="decimal?"
Label="decimal?"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
Label="decimal?"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<br/>
<MudNumericField T="byte?"
Label="byte?"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<br/>
<MudNumericField T="int?"
Label="int? clearable with min/max"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true"
Min="1" Max="10" />

@code {
public static string __description__ = "The textfield should be changed by up/down arrows";
Expand Down
157 changes: 104 additions & 53 deletions src/MudBlazor.UnitTests/Components/NumericFieldTests.cs
Expand Up @@ -25,7 +25,7 @@ namespace MudBlazor.UnitTests.Components
public class NumericFieldTests : BunitTest
{
// TestCaseSource does not know about "Nullable<T>" so having values as Nullable<T> does not make sense here
static object[] TypeCases =
static object[] TypeCases =
{
new object[] { (byte)5 },
new object[] { (sbyte)5 },
Expand All @@ -50,7 +50,7 @@ public void NumericFieldLabelFor()
var label = comp.FindAll(".mud-input-label");
label[0].Attributes.GetNamedItem("for")?.Value.Should().Be("numericFieldLabelTest");
}

/// <summary>
/// Initial Text for double should be 0, with F1 format it should be 0.0
/// </summary>
Expand Down Expand Up @@ -93,10 +93,10 @@ public void NumericFieldTest2()
/// <summary>
/// Setting the value to null should not cause a validation error
/// </summary>
[Test]
public async Task IntNumericFieldWithNullableTypes()
[TestCaseSource(nameof(TypeCases))]
public async Task NumericField_WithNullableTypes_ShouldAllowNulls<T>(T value) where T : struct
{
var comp = Context.RenderComponent<MudNumericField<int?>>(ComponentParameter.CreateParameter("Value", 17));
var comp = Context.RenderComponent<MudNumericField<T?>>(ComponentParameter.CreateParameter("Value", value));
// print the generated html
//Console.WriteLine(comp.Markup);
comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null));
Expand All @@ -106,51 +106,6 @@ public async Task IntNumericFieldWithNullableTypes()
comp.Find("input").Blur();
comp.FindAll("div.mud-input-error").Count.Should().Be(0);
}

/// <summary>
/// Setting the value to null should not cause a validation error
/// </summary>
[Test]
public async Task DecimalNumericFieldWithNullableTypes()
{
var comp = Context.RenderComponent<MudNumericField<decimal?>>(ComponentParameter.CreateParameter("Value", 17M));
comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null));
comp.Find("input").Blur();
comp.FindAll("div.mud-input-error").Count.Should().Be(0);
comp.Find("input").Change("");
comp.Find("input").Blur();
comp.FindAll("div.mud-input-error").Count.Should().Be(0);
}

/// <summary>
/// Setting the value to null should not cause a validation error
/// </summary>
[Test]
public async Task Int64NumericFieldWithNullableTypes()
{
var comp = Context.RenderComponent<MudNumericField<long?>>(ComponentParameter.CreateParameter("Value", 17L));
comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null));
comp.Find("input").Blur();
comp.FindAll("div.mud-input-error").Count.Should().Be(0);
comp.Find("input").Change("");
comp.Find("input").Blur();
comp.FindAll("div.mud-input-error").Count.Should().Be(0);
}

/// <summary>
/// Setting the value to null should not cause a validation error
/// </summary>
[Test]
public async Task UInt64NumericFieldWithNullableTypes()
{
var comp = Context.RenderComponent<MudNumericField<ulong?>>(ComponentParameter.CreateParameter("Value", 17UL));
comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null));
comp.Find("input").Blur();
comp.FindAll("div.mud-input-error").Count.Should().Be(0);
comp.Find("input").Change("");
comp.Find("input").Blur();
comp.FindAll("div.mud-input-error").Count.Should().Be(0);
}

//This doesn't make any sense because you cannot set anything that's not a number
///// <summary>
Expand Down Expand Up @@ -280,7 +235,7 @@ public async Task NumericFieldFluentValidationTest1()
//Console.WriteLine("Error message: " + numericField.ErrorText);
numericField.ErrorText.Should().BeNullOrEmpty();
}

/// <summary>
/// Validate handling of decimal support & precision kept
/// </summary>
Expand Down Expand Up @@ -431,7 +386,7 @@ public async Task NumericFieldTest_KeyboardInput()
comp.Find("input").KeyUp(new KeyboardEventArgs() { Key = "9", Type = "keyup", });
comp.WaitForAssertion(() => numericField.Value.Should().Be(1234.56));
}

/// <summary>
/// Keydown disabled, should not do anything
/// </summary>
Expand All @@ -450,7 +405,7 @@ public async Task NumericFieldTest_KeyboardInput_Disabled()
comp.Find("input").KeyUp(new KeyboardEventArgs() { Key = "9", Type = "keyup", });
comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(1234.56));
}

/// <summary>
/// Keydown readonly, should not do anything
/// </summary>
Expand Down Expand Up @@ -676,6 +631,46 @@ public async Task NumericField_Validation<T>(T value)
numericField.Value.Should().Be(value);
}

[TestCaseSource(nameof(TypeCases))]
public async Task NumericFieldMinMax<T>(T value)
{
var min = (T)Convert.ChangeType(1, typeof(T));
var max = (T)Convert.ChangeType(10, typeof(T));
var comp = Context.RenderComponent<MudNumericField<T>>();
comp.SetParam(x => x.Min, min);
comp.SetParam(x => x.Max, max);

comp.Find("input").Change("15");
comp.Find("input").Blur();

comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(max));

comp.Find("input").Change("0");
comp.Find("input").Blur();

comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(min));
}

[TestCaseSource(nameof(TypeCases))]
public async Task NumericFieldMinMaxNullable<T>(T value) where T : struct
{
var min = (T)Convert.ChangeType(1, typeof(T));
var max = (T)Convert.ChangeType(10, typeof(T));
var comp = Context.RenderComponent<MudNumericField<T?>>();
comp.SetParam(x => x.Min, min);
comp.SetParam(x => x.Max, max);

comp.Find("input").Change("15");
comp.Find("input").Blur();

comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(max));

comp.Find("input").Change("0");
comp.Find("input").Blur();

comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(min));
}

[TestCaseSource(nameof(TypeCases))]
public async Task NumericField_Increment_Decrement<T>(T value)
{
Expand All @@ -697,6 +692,27 @@ public async Task NumericField_Increment_Decrement<T>(T value)
comp.Instance.Value.Should().Be(value);
}

[TestCaseSource(nameof(TypeCases))]
public async Task NumericFieldNullable_Increment_Decrement<T>(T value) where T : struct
{
var comp = Context.RenderComponent<MudNumericField<T?>>();
var max = Convert.ChangeType(10, typeof(T));
var min = Convert.ChangeType(0, typeof(T));
comp.SetParam(x => x.Max, max);
comp.SetParam(x => x.Min, min);
comp.SetParam(x => x.Step, value);
comp.SetParam(x => x.Value, value);
await comp.InvokeAsync(() => comp.Instance.Increment().Wait());
await comp.InvokeAsync(() => comp.Instance.Decrement().Wait());
comp.Instance.Value.Should().Be(value);
// setting min and max to value will cover the boundary checking code
comp.SetParam(x => x.Max, value);
comp.SetParam(x => x.Min, value);
await comp.InvokeAsync(() => comp.Instance.Increment().Wait());
await comp.InvokeAsync(() => comp.Instance.Decrement().Wait());
comp.Instance.Value.Should().Be(value);
}

[TestCaseSource(nameof(TypeCases))]
public async Task NumericField_Increment_Decrement_OverflowHandled<T>(T value)
{
Expand All @@ -714,6 +730,41 @@ public async Task NumericField_Increment_Decrement_OverflowHandled<T>(T value)
comp.Instance.Value.Should().Be(comp.Instance.Min);
}

[TestCaseSource(nameof(TypeCases))]
public async Task NumericFieldNullable_Increment_Decrement_OverflowHandled<T>(T value) where T : struct
{
var comp = Context.RenderComponent<MudNumericField<T?>>();
comp.SetParam(x => x.Step, value);

// test max overflow
comp.SetParam(x => x.Value, comp.Instance.Max);
await comp.InvokeAsync(() => comp.Instance.Increment().Wait());
comp.Instance.Value.Should().Be(comp.Instance.Max);

// test min overflow
comp.SetParam(x => x.Value, comp.Instance.Min);
await comp.InvokeAsync(() => comp.Instance.Decrement().Wait());
comp.Instance.Value.Should().Be(comp.Instance.Min);
}

/// <summary>
/// NumericField with min/max set and nullable int can be cleared
/// </summary>
[TestCase(10, 20, 15)]
[TestCase(-20, -10, -15)]
public async Task NumericFieldCanBeCleared(int min, int max, int value)
{
var comp = Context.RenderComponent<MudNumericField<int?>>();
comp.SetParam(x => x.Min, min);
comp.SetParam(x => x.Max, max);
comp.SetParam(x => x.Value, value);

comp.Find("input").Change("");
comp.Find("input").Blur();

comp.WaitForAssertion(() => comp.Instance.Value.Should().BeNull());
}

/// <summary>
/// Special format with currency format should not result in error
/// </summary>
Expand Down
32 changes: 20 additions & 12 deletions src/MudBlazor/Components/NumericField/MudNumericField.razor.cs
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
Expand All @@ -17,6 +18,7 @@ namespace MudBlazor
public partial class MudNumericField<T> : MudDebouncedInput<T>
{
private IKeyInterceptor _keyInterceptor;
private Comparer _comparer = new(CultureInfo.InvariantCulture);

public MudNumericField() : base()
{
Expand Down Expand Up @@ -180,14 +182,16 @@ private async Task Change(double factor = 1)
try
{
var nextValue = GetNextValue(factor);
if (nextValue is IComparable<T> comparable)

// validate that the data type is a value type before we compare them
if (typeof(T).IsValueType)
{
if (factor > 0 && comparable.CompareTo(Value) < 0)
if (factor > 0 && _comparer.Compare(nextValue, Value) < 0)
nextValue = Max;
else if (factor < 0 && comparable.CompareTo(Value) > 0)
else if (factor < 0 && (_comparer.Compare(nextValue, Value) > 0 || nextValue is null))
nextValue = Min;
}

await SetValueAsync(ConstrainBoundaries(nextValue).value);
_elementReference.SetText(Text).AndForget();
}
Expand Down Expand Up @@ -226,16 +230,20 @@ private T GetNextValue(double factor)
/// <returns>Returns a valid value and if it has been changed.</returns>
protected (T value, bool changed) ConstrainBoundaries(T value)
{
// check if Max/Min has value, if not use MaxValue/MinValue for that data type
if (value is IComparable<T> comparable)
if (value == null)
return (default(T), false);

// validate that the data type is a value type before we compare them
if (typeof(T).IsValueType)
{
if (comparable.CompareTo(Max) > 0)
// check if value is bigger than defined MAX, if so take the defined MAX value instead
if (_comparer.Compare(value, Max) > 0)
return (Max, true);
else if (comparable.CompareTo(Min) < 0)

// check if value is lower than defined MIN, if so take the defined MIN value instead
if (_comparer.Compare(value, Min) < 0)
return (Min, true);
}
else if (value == null)
return (default(T), true);
};

return (value, false);
}
Expand Down Expand Up @@ -419,7 +427,7 @@ private long FromInt64(T v)
=> Convert.ToInt64((long?)(object)v);
private ulong FromUInt64(T v)
=> Convert.ToUInt64((ulong?)(object)v);

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Expand Down

0 comments on commit dd0176b

Please sign in to comment.