Description
This request is related to #26324
I created a new request because the msfbot suggest to do so.
I came across the need of this component again recently. So I gave it a new try. Unfortunately I don't get the aspnetcore repo to compile with my settings so I created the component within a custom razor class library. I came across this solution:
/// <summary>
/// An input component for editing date values.
/// Supported types are <see cref="DateTime"/> and <see cref="DateTimeOffset"/>.
/// </summary>
public class InputDate<TValue> : InputBase<TValue>
{
/// <summary>
/// Gets or sets the error message used when displaying an a parsing error.
/// </summary>
[Parameter] public string ParsingErrorMessage { get; set; } = "The {0} field must be a date.";
/// <summary>
/// Gets or sets the <see cref="InputDateFormat"/> for the input.
/// </summary>
[Parameter] public InputDateFormat Format { get; set; } = InputDateFormat.Date;
/// <summary>
/// Gets or sets the associated <see cref="ElementReference"/>.
/// <para>
/// May be <see langword="null"/> if accessed before the component is rendered.
/// </para>
/// </summary>
[DisallowNull] public ElementReference? Element { get; protected set; }
/// <summary>
/// Gets the corresponding input type for the provided <see cref="Format"/>
/// </summary>
protected string InputType => Format switch
{
InputDateFormat.Date => "date",
InputDateFormat.DateTimeLocal => "datetime-local",
InputDateFormat.Time => "time",
_ => throw new InvalidOperationException($"The format '{Format} is not supported'")
};
/// <summary>
/// Gets the corresponding input format for the provided <see cref="Format"/>
/// </summary>
protected string InputFormat => Format switch
{
InputDateFormat.Date => "yyyy-MM-dd",
InputDateFormat.DateTimeLocal => "yyyy-MM-ddThh:mm",
InputDateFormat.Time => "hh:mm:ss",
_ => throw new InvalidOperationException($"The format '{Format} is not supported'")
};
/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "type", InputType);
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
builder.AddElementReferenceCapture(6, __inputReference => Element = __inputReference);
builder.CloseElement();
}
/// <inheritdoc />
protected override string FormatValueAsString(TValue? value)
{
switch (value)
{
case DateTime dateTimeValue:
return BindConverter.FormatValue(dateTimeValue, InputFormat, CultureInfo.InvariantCulture);
case DateTimeOffset dateTimeOffsetValue:
return BindConverter.FormatValue(dateTimeOffsetValue, InputFormat, CultureInfo.InvariantCulture);
default:
return string.Empty; // Handles null for Nullable<DateTime>, etc.
}
}
/// <inheritdoc />
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
// Unwrap nullable types. We don't have to deal with receiving empty values for nullable
// types here, because the underlying InputBase already covers that.
var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue);
bool success;
if (targetType == typeof(DateTime))
{
success = TryParseDateTime(value, InputFormat, out result);
}
else if (targetType == typeof(DateTimeOffset))
{
success = TryParseDateTimeOffset(value, InputFormat, out result);
}
else
{
throw new InvalidOperationException($"The type '{targetType}' is not a supported date type.");
}
if (success)
{
Debug.Assert(result != null);
validationErrorMessage = null;
return true;
}
else
{
validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
return false;
}
}
private static bool TryParseDateTime(string? value, string format, [MaybeNullWhen(false)] out TValue result)
{
var success = BindConverter.TryConvertToDateTime(value, CultureInfo.InvariantCulture, format, out var parsedValue);
if (success)
{
result = (TValue)(object)parsedValue;
return true;
}
else
{
result = default;
return false;
}
}
private static bool TryParseDateTimeOffset(string? value, string format, [MaybeNullWhen(false)] out TValue result)
{
var success = BindConverter.TryConvertToDateTimeOffset(value, CultureInfo.InvariantCulture, format, out var parsedValue);
if (success)
{
result = (TValue)(object)parsedValue;
return true;
}
else
{
result = default;
return false;
}
}
}
As you can see I've added two get only properties:
InputType
and InputFormat
and one new property as parameter called Format
. Format
takes an enum of type InputDateFormat
which is defined as:
/// <summary>
/// Defines the types of an <see cref="InputDate{TValue}"/> component.
/// </summary>
public enum InputDateFormat
{
Date,
DateTimeLocal,
Time
}
I also created a short test project to see if everything is working. It turns out that type date
works fine. datetime-local
and time
doesn't work fully yet because they need to get another CultureInfo
.
For example: the validation of time works until 12 hours and 59 minutes. In Europe we enter the time until 23 hours and 59 minutes. But I am not sure how this is done correctly (I haven't worked as much with CultureInfo because I only needed to optimize software for our own purposes yet). But I am pretty sure you will know how to change this.