Aspnetcore: Add <InputDateTime> blazor component

Created on 1 Jan 2020  路  8Comments  路  Source: dotnet/aspnetcore

Hello all,

I've been using blazor for a while and I really liked it.
However, there is one thing that I always need and it isn't present in Blazor.
It is DateTime input component. There is a InputDate component, so I can enter date, but there is no nice and convenient way to enter time and date in the same field (or in separate fields)

So it would be nice to add such components as InputTime, InputDateTime.

Thank you

affected-few area-blazor enhancement severity-nice-to-have

Most helpful comment

I took @BernhardNinaus' solution and created an override to the InputDate class. This makes a usable DateTime picker that binds and saves nicely with the model.

InputDateTime.cs

public class InputDateTime<TValue> : InputDate<TValue>
    {
        private const string DateFormat = "yyyy-MM-ddTHH:mm";

        /// <inheritdoc />
        protected override void BuildRenderTree ( RenderTreeBuilder builder )
        {
            builder.OpenElement( 0, "input" );
            builder.AddMultipleAttributes( 1, AdditionalAttributes );
            builder.AddAttribute( 2, "type", "datetime-local" );
            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.CloseElement();
        }

        /// <inheritdoc />
        protected override string FormatValueAsString ( TValue value )
        {
            switch ( value )
            {
                case DateTime dateTimeValue:
                    return BindConverter.FormatValue( dateTimeValue, DateFormat, CultureInfo.InvariantCulture );
                case DateTimeOffset dateTimeOffsetValue:
                    return BindConverter.FormatValue( dateTimeOffsetValue, DateFormat, CultureInfo.InvariantCulture );
                default:
                    return string.Empty; // Handles null for Nullable<DateTime>, etc.
            }
        }

        /// <inheritdoc />
        protected override bool TryParseValueFromString ( string value, out TValue result, 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, out result );
            }
            else if ( targetType == typeof( DateTimeOffset ) )
            {
                success = TryParseDateTimeOffset( value, out result );
            }
            else
            {
                throw new InvalidOperationException( $"The type '{targetType}' is not a supported date type." );
            }

            if ( success )
            {
                validationErrorMessage = null;
                return true;
            }
            else
            {
                validationErrorMessage = string.Format( ParsingErrorMessage, FieldIdentifier.FieldName );
                return false;
            }
        }

        static bool TryParseDateTime ( string value, out TValue result )
        {
            var success = BindConverter.TryConvertToDateTime( value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue );
            if ( success )
            {
                result = (TValue)(object)parsedValue;
                return true;
            }
            else
            {
                result = default;
                return false;
            }
        }

        static bool TryParseDateTimeOffset ( string value, out TValue result )
        {
            var success = BindConverter.TryConvertToDateTimeOffset( value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue );
            if ( success )
            {
                result = (TValue)(object)parsedValue;
                return true;
            }
            else
            {
                result = default;
                return false;
            }
        }
    }

Here's the usage in the .razor file.

<div class="form-group row">
    <label for="createdDate" class="col-sm-3">Created Date:</label>
    <InputDateTime id="createdDate" class="form-control col-sm-8" @bind-Value="SystemOutage.CreatedDate"></InputDateTime>
    <ValidationMessage class="offset-sm-3 col-sm-8" For="(()=>SystemOutage.CreatedDate)"></ValidationMessage>
</div>

Might be overkill, but I hope it helps someone.

All 8 comments

InputDate builds a HTML compliant Input element with the type attribute set to "date"

https://github.com/aspnet/AspNetCore/blob/8c02467b4a218df3b1b0a69bceb50f5b64f482b1/src/Components/Web/src/Forms/InputDate.cs

The w3c spec supports other values, which includes capturing date and time - eg.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local

But the UI will vary by browser so IME you'd be better off writing your own component for this.

Thanks for your issue report. We'll consider whether to address this during our next milestone planning.

Hey!
New here to .NET Core in general.

This works for me. I hope it's not complete wrong...

@inherits InputBase<DateTime?>

<input @bind=CurrentValue 
       @attributes=AdditionalAttributes 
       @bind:format="yyyy-MM-ddTHH:mm"
       class=@CssClass 
       type="datetime-local"/>

@code {
    protected override bool TryParseValueFromString(string value, out DateTime? result, out string validationErrorMessage)
    {
        result = CurrentValue;
        validationErrorMessage = "";
        return true;
    }
}

袩褉懈胁械褌,褋泻懈薪褜 褋胁芯泄 褌械谢械谐褉邪屑 懈谢懈 薪邪锌懈褕懈 屑薪械! @azeloneAnD

I've tried a few different ways of implementing this and there are some weird issues with data-binding.

I tried using the <input type="datetime-local"> that is referenced in the Mozilla documentation and sometimes the input would just completely crash.

It would be nice if there was a cleaner and more consistent way to do this.

I took @BernhardNinaus' solution and created an override to the InputDate class. This makes a usable DateTime picker that binds and saves nicely with the model.

InputDateTime.cs

public class InputDateTime<TValue> : InputDate<TValue>
    {
        private const string DateFormat = "yyyy-MM-ddTHH:mm";

        /// <inheritdoc />
        protected override void BuildRenderTree ( RenderTreeBuilder builder )
        {
            builder.OpenElement( 0, "input" );
            builder.AddMultipleAttributes( 1, AdditionalAttributes );
            builder.AddAttribute( 2, "type", "datetime-local" );
            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.CloseElement();
        }

        /// <inheritdoc />
        protected override string FormatValueAsString ( TValue value )
        {
            switch ( value )
            {
                case DateTime dateTimeValue:
                    return BindConverter.FormatValue( dateTimeValue, DateFormat, CultureInfo.InvariantCulture );
                case DateTimeOffset dateTimeOffsetValue:
                    return BindConverter.FormatValue( dateTimeOffsetValue, DateFormat, CultureInfo.InvariantCulture );
                default:
                    return string.Empty; // Handles null for Nullable<DateTime>, etc.
            }
        }

        /// <inheritdoc />
        protected override bool TryParseValueFromString ( string value, out TValue result, 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, out result );
            }
            else if ( targetType == typeof( DateTimeOffset ) )
            {
                success = TryParseDateTimeOffset( value, out result );
            }
            else
            {
                throw new InvalidOperationException( $"The type '{targetType}' is not a supported date type." );
            }

            if ( success )
            {
                validationErrorMessage = null;
                return true;
            }
            else
            {
                validationErrorMessage = string.Format( ParsingErrorMessage, FieldIdentifier.FieldName );
                return false;
            }
        }

        static bool TryParseDateTime ( string value, out TValue result )
        {
            var success = BindConverter.TryConvertToDateTime( value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue );
            if ( success )
            {
                result = (TValue)(object)parsedValue;
                return true;
            }
            else
            {
                result = default;
                return false;
            }
        }

        static bool TryParseDateTimeOffset ( string value, out TValue result )
        {
            var success = BindConverter.TryConvertToDateTimeOffset( value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue );
            if ( success )
            {
                result = (TValue)(object)parsedValue;
                return true;
            }
            else
            {
                result = default;
                return false;
            }
        }
    }

Here's the usage in the .razor file.

<div class="form-group row">
    <label for="createdDate" class="col-sm-3">Created Date:</label>
    <InputDateTime id="createdDate" class="form-control col-sm-8" @bind-Value="SystemOutage.CreatedDate"></InputDateTime>
    <ValidationMessage class="offset-sm-3 col-sm-8" For="(()=>SystemOutage.CreatedDate)"></ValidationMessage>
</div>

Might be overkill, but I hope it helps someone.

@IanKnighton

Your solution helped me alot !

Thanks !

@IanKnighton
Nice and elegant solution. I used it with success! Thanks for the code.

Was this page helpful?
0 / 5 - 0 ratings