From 138a24c79c211685d936a678b72a2e6beaf6aae0 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 7 Jul 2019 12:49:37 -0700 Subject: [PATCH] Add globalization support for forms Updates our form controls to behave correctly when used in non-en-US cultures. Adds E2E tests for the same --- .../src/Forms/InputComponents/InputDate.cs | 11 ++-- .../src/Forms/InputComponents/InputNumber.cs | 33 ++++++++++ .../ServerExecutionTests/GlobalizationTest.cs | 66 +++++++++++++++++++ .../BasicTestApp/GlobalizationBindCases.razor | 36 ++++++++++ 4 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/Components/Components/src/Forms/InputComponents/InputDate.cs b/src/Components/Components/src/Forms/InputComponents/InputDate.cs index add64bd6ff..0a79bbc15d 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputDate.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputDate.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Globalization; using Microsoft.AspNetCore.Components.RenderTree; namespace Microsoft.AspNetCore.Components.Forms @@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// public class InputDate : InputBase { - const string dateFormat = "yyyy-MM-dd"; // Compatible with HTML date inputs + private const string DateFormat = "yyyy-MM-dd"; // Compatible with HTML date inputs /// /// Gets or sets the error message used when displaying an a parsing error. @@ -37,9 +38,9 @@ namespace Microsoft.AspNetCore.Components.Forms switch (value) { case DateTime dateTimeValue: - return dateTimeValue.ToString(dateFormat); + return dateTimeValue.ToString(DateFormat, CultureInfo.InvariantCulture); case DateTimeOffset dateTimeOffsetValue: - return dateTimeOffsetValue.ToString(dateFormat); + return dateTimeOffsetValue.ToString(DateFormat, CultureInfo.InvariantCulture); default: return string.Empty; // Handles null for Nullable, etc. } @@ -80,7 +81,7 @@ namespace Microsoft.AspNetCore.Components.Forms static bool TryParseDateTime(string value, out T result) { - var success = DateTime.TryParse(value, out var parsedValue); + var success = DateTime.TryParseExact(value, DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedValue); if (success) { result = (T)(object)parsedValue; @@ -95,7 +96,7 @@ namespace Microsoft.AspNetCore.Components.Forms static bool TryParseDateTimeOffset(string value, out T result) { - var success = DateTimeOffset.TryParse(value, out var parsedValue); + var success = DateTimeOffset.TryParseExact(value, DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedValue); if (success) { result = (T)(object)parsedValue; diff --git a/src/Components/Components/src/Forms/InputComponents/InputNumber.cs b/src/Components/Components/src/Forms/InputComponents/InputNumber.cs index 2a65f863c8..044f4aac3d 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputNumber.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputNumber.cs @@ -86,6 +86,39 @@ namespace Microsoft.AspNetCore.Components.Forms } } + /// + /// Formats the value as a string. Derived classes can override this to determine the formating used for CurrentValueAsString. + /// + /// The value to format. + /// A string representation of the value. + protected override string FormatValueAsString(T value) + { + // Avoiding a cast to IFormattable to avoid boxing. + switch (value) + { + case null: + return null; + + case int @int: + return @int.ToString(CultureInfo.InvariantCulture); + + case long @long: + return @long.ToString(CultureInfo.InvariantCulture); + + case float @float: + return @float.ToString(CultureInfo.InvariantCulture); + + case double @double: + return @double.ToString(CultureInfo.InvariantCulture); + + case decimal @decimal: + return @decimal.ToString(CultureInfo.InvariantCulture); + + default: + throw new InvalidOperationException($"Unsupported type {value.GetType()}"); + } + } + static bool TryParseInt(string value, out T result) { var success = int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue); diff --git a/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs index bb3e633204..ae4c699153 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs @@ -173,6 +173,72 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value")); } + [Theory] + [InlineData("en-US")] + [InlineData("fr-FR")] + public void CanSetCultureAndParseCultureInvariantNumbersAndDatesWithFormComponents(string culture) + { + var cultureInfo = CultureInfo.GetCultureInfo(culture); + + var selector = new SelectElement(Browser.FindElement(By.Id("culture-selector"))); + selector.SelectByValue(culture); + + // That should have triggered a redirect, wait for the main test selector to come up. + MountTestComponent(); + WaitUntilExists(By.Id("globalization-cases")); + + var cultureDisplay = WaitUntilExists(By.Id("culture-name-display")); + Assert.Equal($"Culture is: {culture}", cultureDisplay.Text); + + // int + var input = Browser.FindElement(By.Id("inputnumber_int")); + var display = Browser.FindElement(By.Id("inputnumber_int_value")); + Browser.Equal(42.ToString(cultureInfo), () => display.Text); + Browser.Equal(42.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value")); + + input.Clear(); + input.SendKeys(9000.ToString(CultureInfo.InvariantCulture)); + input.SendKeys("\t"); + Browser.Equal(9000.ToString(cultureInfo), () => display.Text); + Browser.Equal(9000.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value")); + + // decimal + input = Browser.FindElement(By.Id("inputnumber_decimal")); + display = Browser.FindElement(By.Id("inputnumber_decimal_value")); + Browser.Equal(4.2m.ToString(cultureInfo), () => display.Text); + Browser.Equal(4.2m.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value")); + + input.Clear(); + input.SendKeys(9000.42m.ToString(CultureInfo.InvariantCulture)); + input.SendKeys("\t"); + Browser.Equal(9000.42m.ToString(cultureInfo), () => display.Text); + Browser.Equal(9000.42m.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value")); + + // datetime + input = Browser.FindElement(By.Id("inputdate_datetime")); + display = Browser.FindElement(By.Id("inputdate_datetime_value")); + var extraInput = Browser.FindElement(By.Id("inputdate_datetime_extrainput")); + Browser.Equal(new DateTime(1985, 3, 4).ToString(cultureInfo), () => display.Text); + Browser.Equal(new DateTime(1985, 3, 4).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value")); + + ReplaceText(extraInput, new DateTime(2000, 1, 2).ToString(cultureInfo)); + extraInput.SendKeys("\t"); + Browser.Equal(new DateTime(2000, 1, 2).ToString(cultureInfo), () => display.Text); + Browser.Equal(new DateTime(2000, 1, 2).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value")); + + // datetimeoffset + input = Browser.FindElement(By.Id("inputdate_datetimeoffset")); + display = Browser.FindElement(By.Id("inputdate_datetimeoffset_value")); + extraInput = Browser.FindElement(By.Id("inputdate_datetimeoffset_extrainput")); + Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString(cultureInfo), () => display.Text); + Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value")); + + ReplaceText(extraInput, new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo)); + extraInput.SendKeys("\t"); + Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo), () => display.Text); + Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value")); + } + // see: https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/214 // // Calling Clear() can trigger onchange, which will revert the value to its default. diff --git a/src/Components/test/testassets/BasicTestApp/GlobalizationBindCases.razor b/src/Components/test/testassets/BasicTestApp/GlobalizationBindCases.razor index beb6ae4688..41877e417d 100644 --- a/src/Components/test/testassets/BasicTestApp/GlobalizationBindCases.razor +++ b/src/Components/test/testassets/BasicTestApp/GlobalizationBindCases.razor @@ -53,6 +53,36 @@ +
+

Numbers with InputNumber

+ +
+ int: + @inputNumberInt +
+
+ decimal: + @inputNumberDecimal +
+
+
+ +
+

Dates with InputDate

+ +
+ DateTime: + + @inputDateDateTime +
+
+ DateTimeOffset: + + @inputDateDateTimeOffset +
+
+
+ @code { int inputTypeTextInt = 42; decimal inputTypeTextDecimal = 4.2m; @@ -65,4 +95,10 @@ DateTime inputTypeDateDateTime = new DateTime(1985, 3, 4); DateTimeOffset inputTypeDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4)); + + int inputNumberInt = 42; + decimal inputNumberDecimal = 4.2m; + + DateTime inputDateDateTime = new DateTime(1985, 3, 4); + DateTimeOffset inputDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4)); }