diff --git a/src/Components/Components/src/BindConverter.cs b/src/Components/Components/src/BindConverter.cs index 06a03a05f390..5b83b2cb1017 100644 --- a/src/Components/Components/src/BindConverter.cs +++ b/src/Components/Components/src/BindConverter.cs @@ -24,7 +24,6 @@ public static class BindConverter private static readonly object BoxedFalse = false; private delegate object? BindFormatter(T value, CultureInfo? culture); - internal delegate bool BindParser(object? obj, CultureInfo? culture, [MaybeNullWhen(false)] out T value); internal delegate bool BindParserWithFormat(object? obj, CultureInfo? culture, string? format, [MaybeNullWhen(false)] out T value); @@ -212,7 +211,29 @@ private static string FormatShortValueCore(short value, CultureInfo? culture) /// /// The formatted value. [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string FormatValue(float value, CultureInfo? culture = null) => FormatFloatValueCore(value, culture); + public static string FormatValue(float value, CultureInfo? culture = null) => FormatFloatValueCore(value, culture, format: null); + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The format to use. Provided to . + /// The formatted value. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string FormatValue(float value, [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, CultureInfo? culture = null) => FormatFloatValueCore(value, culture, format); + + private static string FormatFloatValueCore(float value, CultureInfo? culture, string? format) + { + if (format != null) + { + return value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } private static string FormatFloatValueCore(float value, CultureInfo? culture) { @@ -228,7 +249,34 @@ private static string FormatFloatValueCore(float value, CultureInfo? culture) /// /// The formatted value. [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? FormatValue(float? value, CultureInfo? culture = null) => FormatNullableFloatValueCore(value, culture); + public static string? FormatValue(float? value, CultureInfo? culture = null) => FormatNullableFloatValueCore(value, culture, format: null); + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The format to use. Provided to . + /// The formatted value. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? FormatValue(float? value, [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, CultureInfo? culture = null) => FormatNullableFloatValueCore(value, culture, format); + + private static string? FormatNullableFloatValueCore(float? value, CultureInfo? culture, string? format) + { + if (value == null) + { + return null; + } + + if (format != null) + { + return value.Value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } private static string? FormatNullableFloatValueCore(float? value, CultureInfo? culture) { @@ -249,7 +297,29 @@ private static string FormatFloatValueCore(float value, CultureInfo? culture) /// /// The formatted value. [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? FormatValue(double value, CultureInfo? culture = null) => FormatDoubleValueCore(value, culture); + public static string? FormatValue(double value, CultureInfo? culture = null) => FormatDoubleValueCore(value, culture, format: null); + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The format to use. Provided to . + /// The formatted value. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? FormatValue(double value, [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, CultureInfo? culture = null) => FormatDoubleValueCore(value, culture, format); + + private static string FormatDoubleValueCore(double value, CultureInfo? culture, string? format) + { + if (format != null) + { + return value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } private static string FormatDoubleValueCore(double value, CultureInfo? culture) { @@ -265,7 +335,19 @@ private static string FormatDoubleValueCore(double value, CultureInfo? culture) /// /// The formatted value. [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? FormatValue(double? value, CultureInfo? culture = null) => FormatNullableDoubleValueCore(value, culture); + public static string? FormatValue(double? value, CultureInfo? culture = null) => FormatNullableDoubleValueCore(value, culture, format: null); + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The format to use. Provided to . + /// The formatted value. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? FormatValue(double? value, [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, CultureInfo? culture = null) => FormatNullableDoubleValueCore(value, culture, format); private static string? FormatNullableDoubleValueCore(double? value, CultureInfo? culture) { @@ -277,6 +359,21 @@ private static string FormatDoubleValueCore(double value, CultureInfo? culture) return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); } + private static string? FormatNullableDoubleValueCore(double? value, CultureInfo? culture, string? format) + { + if (value == null) + { + return null; + } + + if (format != null) + { + return value.Value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + /// /// Formats the provided for inclusion in an attribute. /// @@ -286,13 +383,46 @@ private static string FormatDoubleValueCore(double value, CultureInfo? culture) /// /// The formatted value. [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string FormatValue(decimal value, CultureInfo? culture = null) => FormatDecimalValueCore(value, culture); + public static string FormatValue(decimal value, CultureInfo? culture = null) => FormatDecimalValueCore(value, culture, format: null); + + /// + /// Formats the provided for inclusion in an attribute. + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The format to use. Provided to . + /// The formatted value. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string FormatValue(decimal value, [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, CultureInfo? culture = null) => FormatDecimalValueCore(value, culture, format); private static string FormatDecimalValueCore(decimal value, CultureInfo? culture) { return value.ToString(culture ?? CultureInfo.CurrentCulture); } + private static string FormatDecimalValueCore(decimal value, CultureInfo? culture, string? format) + { + if (format != null) + { + return value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.ToString(culture ?? CultureInfo.CurrentCulture); + } + + /// + /// Formats the provided as a . + /// + /// The value to format. + /// + /// The to use while formatting. Defaults to . + /// + /// The formatted value. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? FormatValue(decimal? value, CultureInfo? culture = null) => FormatNullableDecimalValueCore(value, culture, format: null); + /// /// Formats the provided as a . /// @@ -300,9 +430,10 @@ private static string FormatDecimalValueCore(decimal value, CultureInfo? culture /// /// The to use while formatting. Defaults to . /// + /// The format to use. Provided to . /// The formatted value. [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? FormatValue(decimal? value, CultureInfo? culture = null) => FormatNullableDecimalValueCore(value, culture); + public static string? FormatValue(decimal? value, [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, CultureInfo? culture = null) => FormatNullableDecimalValueCore(value, culture, format); private static string? FormatNullableDecimalValueCore(decimal? value, CultureInfo? culture) { @@ -314,6 +445,21 @@ private static string FormatDecimalValueCore(decimal value, CultureInfo? culture return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); } + private static string? FormatNullableDecimalValueCore(decimal? value, CultureInfo? culture, string? format) + { + if (value == null) + { + return null; + } + + if (format != null) + { + return value.Value.ToString(format, culture ?? CultureInfo.CurrentCulture); + } + + return value.Value.ToString(culture ?? CultureInfo.CurrentCulture); + } + /// /// Formats the provided as a . /// @@ -969,9 +1115,16 @@ public static bool TryConvertToNullableFloat(object? obj, CultureInfo? culture, } internal static BindParser ConvertToFloat = ConvertToFloatCore; + internal static BindParserWithFormat ConvertToFloatWithFormat = ConvertToFloatCore; internal static BindParser ConvertToNullableFloat = ConvertToNullableFloatCore; + internal static BindParserWithFormat ConvertToNullableFloatWithFormat = ConvertToNullableFloatCore; private static bool ConvertToFloatCore(object? obj, CultureInfo? culture, out float value) + { + return ConvertToFloatCore(obj, culture, format: null, out value); + } + + private static bool ConvertToFloatCore(object? obj, CultureInfo? culture, string? format, out float value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) @@ -997,6 +1150,11 @@ private static bool ConvertToFloatCore(object? obj, CultureInfo? culture, out fl } private static bool ConvertToNullableFloatCore(object? obj, CultureInfo? culture, out float? value) + { + return ConvertToNullableFloatCore(obj, culture, format: null, out value); + } + + private static bool ConvertToNullableFloatCore(object? obj, CultureInfo? culture, string? format, out float? value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) @@ -1046,9 +1204,16 @@ public static bool TryConvertToNullableDouble(object? obj, CultureInfo? culture, } internal static BindParser ConvertToDoubleDelegate = ConvertToDoubleCore; + internal static BindParserWithFormat ConvertToDoubleWithFormat = ConvertToDoubleCore; internal static BindParser ConvertToNullableDoubleDelegate = ConvertToNullableDoubleCore; + internal static BindParserWithFormat ConvertToNullableDoubleWithFormat = ConvertToNullableDoubleCore; private static bool ConvertToDoubleCore(object? obj, CultureInfo? culture, out double value) + { + return ConvertToDoubleCore(obj, culture, format: null, out value); + } + + private static bool ConvertToDoubleCore(object? obj, CultureInfo? culture, string? format, out double value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) @@ -1074,6 +1239,11 @@ private static bool ConvertToDoubleCore(object? obj, CultureInfo? culture, out d } private static bool ConvertToNullableDoubleCore(object? obj, CultureInfo? culture, out double? value) + { + return ConvertToNullableDoubleCore(obj, culture, format: null, out value); + } + + private static bool ConvertToNullableDoubleCore(object? obj, CultureInfo? culture, string? format, out double? value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) @@ -1123,9 +1293,16 @@ public static bool TryConvertToNullableDecimal(object? obj, CultureInfo? culture } internal static BindParser ConvertToDecimal = ConvertToDecimalCore; + internal static BindParserWithFormat ConvertToDecimalWithFormat = ConvertToDecimalCore; internal static BindParser ConvertToNullableDecimal = ConvertToNullableDecimalCore; + internal static BindParserWithFormat ConvertToNullableDecimalWithFormat = ConvertToNullableDecimalCore; private static bool ConvertToDecimalCore(object? obj, CultureInfo? culture, out decimal value) + { + return ConvertToDecimalCore(obj, culture, format: null, out value); + } + + private static bool ConvertToDecimalCore(object? obj, CultureInfo? culture, string? format, out decimal value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) @@ -1145,6 +1322,11 @@ private static bool ConvertToDecimalCore(object? obj, CultureInfo? culture, out } private static bool ConvertToNullableDecimalCore(object? obj, CultureInfo? culture, out decimal? value) + { + return ConvertToNullableDecimalCore(obj, culture, format: null, out value); + } + + private static bool ConvertToNullableDecimalCore(object? obj, CultureInfo? culture, string? format, out decimal? value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..b0f8a9e2c389 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +static Microsoft.AspNetCore.Components.BindConverter.FormatValue(decimal value, string? format, System.Globalization.CultureInfo? culture = null) -> string! +static Microsoft.AspNetCore.Components.BindConverter.FormatValue(decimal? value, string? format, System.Globalization.CultureInfo? culture = null) -> string? +static Microsoft.AspNetCore.Components.BindConverter.FormatValue(double value, string? format, System.Globalization.CultureInfo? culture = null) -> string? +static Microsoft.AspNetCore.Components.BindConverter.FormatValue(double? value, string? format, System.Globalization.CultureInfo? culture = null) -> string? +static Microsoft.AspNetCore.Components.BindConverter.FormatValue(float value, string? format, System.Globalization.CultureInfo? culture = null) -> string! +static Microsoft.AspNetCore.Components.BindConverter.FormatValue(float? value, string? format, System.Globalization.CultureInfo? culture = null) -> string? \ No newline at end of file diff --git a/src/Components/Components/test/BindConverterTest.cs b/src/Components/Components/test/BindConverterTest.cs index b18b71dc5684..ed10a4040278 100644 --- a/src/Components/Components/test/BindConverterTest.cs +++ b/src/Components/Components/test/BindConverterTest.cs @@ -369,6 +369,75 @@ public void TryConvertTo_NullableGuid__Invalid() Assert.Null(actual); } + [Theory] + [InlineData(1.1, "0.0#", "1.1")] // Single decimal place with optional second + [InlineData(1500, "0.00", "1500.00")] // Force two decimal places + [InlineData(1500, "0.##", "1500")] // Remove unnecessary decimals + [InlineData(0, "0.00", "0.00")] // Zero with fixed decimals + [InlineData(0, "0.##", "0")] // Zero with optional decimals + [InlineData(-1.1, "0.0#", "-1.1")] // Negative number with one decimal place + [InlineData(-1500, "0.00", "-1500.00")] // Negative number with two fixed decimals + [InlineData(1.999, "0.0", "2.0")] // Rounding up + [InlineData(1.111, "0.0", "1.1")] // Rounding down + [InlineData(1234567.89, "N2", "1,234,567.89")] // Large number with thousands separator + [InlineData(1234567.89, "#,##0.00", "1,234,567.89")] // Explicit thousands separator format + [InlineData(0.1234, "0.00%", "12.34%")] // Percentage formatting + [InlineData(0.12, "00.00", "00.12")] // Fixed zero's with fixed decimals + [InlineData(1234567.89, "0.00", "1234567.89")] // Fixed two decimals + public void FormatValue_Double_Format(double value, string format, string expected) + { + // Act + var actual = BindConverter.FormatValue(value, format, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1.1, "0.0#", "1.1")] // Single decimal place with optional second + [InlineData(1500, "0.00", "1500.00")] // Force two decimal places + [InlineData(1500, "0.##", "1500")] // Remove unnecessary decimals + [InlineData(0, "0.00", "0.00")] // Zero with fixed decimals + [InlineData(0, "0.##", "0")] // Zero with optional decimals + [InlineData(-1.1, "0.0#", "-1.1")] // Negative number with one decimal place + [InlineData(-1500, "0.00", "-1500.00")] // Negative number with two fixed decimals + [InlineData(1.999, "0.0", "2.0")] // Rounding up + [InlineData(1.111, "0.0", "1.1")] // Rounding down + [InlineData(1234567.89, "N2", "1,234,567.89")] // Large number with thousands separator + [InlineData(1234567.89, "#,##0.00", "1,234,567.89")] // Explicit thousands separator format + [InlineData(0.1234, "0.00%", "12.34%")] // Percentage formatting + [InlineData(0.12, "00.00", "00.12")] // Fixed zero's with fixed decimals + public void FormatValue_Decimal_Format(decimal value, string format, string expected) + { + // Act + var actual = BindConverter.FormatValue(value, format, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1.1, "0.0#", "1.1")] // Single decimal place with optional second + [InlineData(1500, "0.00", "1500.00")] // Force two decimal places + [InlineData(1500, "0.##", "1500")] // Remove unnecessary decimals + [InlineData(0, "0.00", "0.00")] // Zero with fixed decimals + [InlineData(0, "0.##", "0")] // Zero with optional decimals + [InlineData(-1.1, "0.0#", "-1.1")] // Negative number with one decimal place + [InlineData(-1500, "0.00", "-1500.00")] // Negative number with two fixed decimals + [InlineData(1.999, "0.0", "2.0")] // Rounding up + [InlineData(1.111, "0.0", "1.1")] // Rounding down + [InlineData(1234567.89, "N2", "1,234,567.88")] // Large number with thousands separator + [InlineData(0.1234, "0.00%", "12.34%")] // Percentage formatting + [InlineData(0.12, "00.00", "00.12")] // Fixed zero's with fixed decimals + public void FormatValue_Float_Format(float value, string format, string expected) + { + // Act + var actual = BindConverter.FormatValue(value, format, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal(expected, actual); + } + private enum SomeLetters { A, diff --git a/src/Components/Web/src/Forms/InputNumber.cs b/src/Components/Web/src/Forms/InputNumber.cs index b346b4b39772..194b7fdc078e 100644 --- a/src/Components/Web/src/Forms/InputNumber.cs +++ b/src/Components/Web/src/Forms/InputNumber.cs @@ -40,6 +40,11 @@ private static string GetStepAttributeValue() /// [Parameter] public string ParsingErrorMessage { get; set; } = "The {0} field must be a number."; + /// + /// Gets or sets the format to be used when displaying a number of types: , , . + /// + [Parameter] public string? Format { get; set; } + /// /// Gets or sets the associated . /// @@ -102,13 +107,13 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa return BindConverter.FormatValue(@short, CultureInfo.InvariantCulture); case float @float: - return BindConverter.FormatValue(@float, CultureInfo.InvariantCulture); + return BindConverter.FormatValue(@float, Format, CultureInfo.InvariantCulture); case double @double: - return BindConverter.FormatValue(@double, CultureInfo.InvariantCulture); + return BindConverter.FormatValue(@double, Format, CultureInfo.InvariantCulture); case decimal @decimal: - return BindConverter.FormatValue(@decimal, CultureInfo.InvariantCulture); + return BindConverter.FormatValue(@decimal, Format, CultureInfo.InvariantCulture); default: throw new InvalidOperationException($"Unsupported type {value.GetType()}"); diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..b77f05245715 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Components.Forms.InputNumber.Format.get -> string? +Microsoft.AspNetCore.Components.Forms.InputNumber.Format.set -> void diff --git a/src/Components/Web/test/Forms/InputNumberTest.cs b/src/Components/Web/test/Forms/InputNumberTest.cs index 12a7891b4bb4..ebdf9509482a 100644 --- a/src/Components/Web/test/Forms/InputNumberTest.cs +++ b/src/Components/Web/test/Forms/InputNumberTest.cs @@ -20,21 +20,59 @@ public InputNumberTest() _testRenderer = new TestRenderer(services.BuildServiceProvider()); } + [Theory] + [InlineData("1.1", "0.0#", "1.1")] // Single decimal place with optional second + [InlineData("1500", "0.00", "1500.00")] // Force two decimal places + [InlineData("1500", "0.0000", "1500.0000")] // Force four decimal places + [InlineData("1500", "0.##", "1500")] // Remove unnecessary decimals + [InlineData("0", "0.00", "0.00")] // Zero with fixed decimals + [InlineData("0", "0.##", "0")] // Zero with optional decimals + [InlineData("-1.1", "0.0#", "-1.1")] // Negative number with one decimal place + [InlineData("-1500", "0.00", "-1500.00")] // Negative number with two fixed decimals + [InlineData("1.999", "0.0", "2.0")] // Rounding up + [InlineData("1.111", "0.0", "1.1")] // Rounding down + [InlineData("1234567.89", "N2", "1,234,567.89")] // Large number with thousands separator + [InlineData("1234567.89", "#,##0.00", "1,234,567.89")] // Explicit thousands separator format + [InlineData("0.1234", "0.00%", "12.34%")] // Percentage formatting + [InlineData("0.12", "00.00", "00.12")] // Fixed zero's with fixed decimals + [InlineData("1234567.89", "0.00", "1234567.89")] // Fixed two decimals + public async Task FormatDoubles(string value, string format, string expected) + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputHostComponent> + { + EditContext = new EditContext(model), + ValueExpression = () => model.Double, + AdditionalAttributes = new Dictionary + { + { "Format", format } + } + }; + var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Act + inputComponent.CurrentValueAsString = value; + + // Assert + Assert.Equal(expected, inputComponent.CurrentValueAsString); + } + [Fact] public async Task ValidationErrorUsesDisplayAttributeName() { // Arrange var model = new TestModel(); - var rootComponent = new TestInputHostComponent + var rootComponent = new TestInputHostComponent> { EditContext = new EditContext(model), - ValueExpression = () => model.SomeNumber, + ValueExpression = () => model.Int, AdditionalAttributes = new Dictionary - { - { "DisplayName", "Some number" } - } + { + { "DisplayName", "Some number" } + } }; - var fieldIdentifier = FieldIdentifier.Create(() => model.SomeNumber); + var fieldIdentifier = FieldIdentifier.Create(() => model.Int); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent); // Act @@ -51,10 +89,10 @@ public async Task InputElementIsAssignedSuccessfully() { // Arrange var model = new TestModel(); - var rootComponent = new TestInputHostComponent + var rootComponent = new TestInputHostComponent> { EditContext = new EditContext(model), - ValueExpression = () => model.SomeNumber, + ValueExpression = () => model.Int, }; // Act @@ -69,10 +107,10 @@ public async Task UserDefinedTypeAttributeOverridesDefault() { // Arrange var model = new TestModel(); - var hostComponent = new TestInputHostComponent + var hostComponent = new TestInputHostComponent> { EditContext = new EditContext(model), - ValueExpression = () => model.SomeNumber, + ValueExpression = () => model.Int, AdditionalAttributes = new Dictionary { { "type", "range" } // User-defined 'type' attribute to override default @@ -93,21 +131,31 @@ public async Task UserDefinedTypeAttributeOverridesDefault() Assert.Equal("range", typeAttributeFrame.AttributeValue); } - private async Task RenderAndGetTestInputNumberComponentIdAsync(TestInputHostComponent hostComponent) + private async Task RenderAndGetTestInputNumberComponentIdAsync(TestInputHostComponent> hostComponent) { var hostComponentId = _testRenderer.AssignRootComponentId(hostComponent); await _testRenderer.RenderRootComponentAsync(hostComponentId); var batch = _testRenderer.Batches.Single(); - return batch.GetComponentFrames().Single().ComponentId; + return batch.GetComponentFrames>().Single().ComponentId; } private class TestModel { - public int SomeNumber { get; set; } + public int Int { get; set; } + public double Double { get; set; } + public float Float { get; set; } + public decimal Decimal { get; set; } } - private class TestInputNumberComponent : InputNumber + class TestInputNumberComponent : InputNumber { + public new TValue CurrentValue => base.CurrentValue; + + public new string CurrentValueAsString + { + get => base.CurrentValueAsString; + set => base.CurrentValueAsString = value; + } public async Task SetCurrentValueAsStringAsync(string value) { // This is equivalent to the subclass writing to CurrentValueAsString