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
This commit is contained in:
Ryan Nowak 2019-07-07 12:49:37 -07:00 committed by Ryan Nowak
parent 80f71bcd1f
commit 138a24c79c
4 changed files with 141 additions and 5 deletions

View File

@ -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
/// </summary>
public class InputDate<T> : InputBase<T>
{
const string dateFormat = "yyyy-MM-dd"; // Compatible with HTML date inputs
private const string DateFormat = "yyyy-MM-dd"; // Compatible with HTML date inputs
/// <summary>
/// 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<DateTime>, 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;

View File

@ -86,6 +86,39 @@ namespace Microsoft.AspNetCore.Components.Forms
}
}
/// <summary>
/// Formats the value as a string. Derived classes can override this to determine the formating used for <c>CurrentValueAsString</c>.
/// </summary>
/// <param name="value">The value to format.</param>
/// <returns>A string representation of the value.</returns>
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);

View File

@ -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<GlobalizationBindCases>();
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.

View File

@ -53,6 +53,36 @@
</div>
</div>
<div>
<p>Numbers with InputNumber</p>
<EditForm Model="this">
<div>
int: <InputNumber id="inputnumber_int" @bind-Value="inputNumberInt" />
<span id="inputnumber_int_value">@inputNumberInt</span>
</div>
<div>
decimal: <InputNumber id="inputnumber_decimal" @bind-Value="inputNumberDecimal" />
<span id="inputnumber_decimal_value">@inputNumberDecimal</span>
</div>
</EditForm>
</div>
<div>
<p>Dates with InputDate</p>
<EditForm Model="this">
<div>
DateTime: <input type="text" id="inputdate_datetime_extrainput" @bind="inputDateDateTime" />
<InputDate id="inputdate_datetime" @bind-Value="inputDateDateTime" />
<span id="inputdate_datetime_value">@inputDateDateTime</span>
</div>
<div>
DateTimeOffset: <input type="text" id="inputdate_datetimeoffset_extrainput" @bind="inputDateDateTimeOffset" />
<InputDate id="inputdate_datetimeoffset" @bind-Value="inputDateDateTimeOffset" />
<span id="inputdate_datetimeoffset_value">@inputDateDateTimeOffset</span>
</div>
</EditForm>
</div>
@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));
}