Skip to content

Add support for type = time and type = datetime-local for InputDate component #34624

Closed
@MarvinKlein1508

Description

@MarvinKlein1508

This request is related to #26324

I created a new request because the msfbot suggest to do so.

@SteveSandersonMS

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions