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:
parent
80f71bcd1f
commit
138a24c79c
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue