diff --git a/src/Components/Web/src/Forms/InputTime.cs b/src/Components/Web/src/Forms/InputTime.cs new file mode 100644 index 000000000000..9db0daf039db --- /dev/null +++ b/src/Components/Web/src/Forms/InputTime.cs @@ -0,0 +1,112 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.AspNetCore.Components.Rendering; + +namespace Microsoft.AspNetCore.Components.Forms +{ + /// + /// An input component for editing time values. + /// Supported types are and . + /// + public class InputTime : InputBase + { + private const string TimeFormat = "HH:mm:ss"; // Compatible with HTML time inputs + + /// + /// Gets or sets the error message used when displaying an a parsing error. + /// + [Parameter] public string ParsingErrorMessage { get; set; } = "The {0} field must be a time."; + + /// + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "input"); + builder.AddMultipleAttributes(1, AdditionalAttributes); + builder.AddAttribute(2, "type", "time"); + builder.AddAttribute(3, "class", CssClass); + builder.AddAttribute(4, "value", BindConverter.FormatValue(CurrentValueAsString)); + builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); + builder.CloseElement(); + } + + /// + protected override string FormatValueAsString(TValue? value) + { + switch (value) + { + case DateTime dateTimeValue: + return BindConverter.FormatValue(dateTimeValue, TimeFormat, CultureInfo.InvariantCulture); + case DateTimeOffset dateTimeOffsetValue: + return BindConverter.FormatValue(dateTimeOffsetValue, TimeFormat, CultureInfo.InvariantCulture); + default: + return string.Empty; // Handles null for Nullable, etc. + } + } + + /// + 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, out result); + } + else if (targetType == typeof(DateTimeOffset)) + { + success = TryParseDateTimeOffset(value, out result); + } + else + { + throw new InvalidOperationException($"The type '{targetType}' is not a supported time type."); + } + + if (success) + { + Debug.Assert(result != null); + validationErrorMessage = null; + return true; + } + else + { + validationErrorMessage = string.Format(ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName); + return false; + } + } + + private static bool TryParseDateTime(string? value, [MaybeNullWhen(false)] out TValue result) + { + var success = BindConverter.TryConvertToDateTime(value, CultureInfo.InvariantCulture, TimeFormat, out var parsedValue); + if (success) + { + result = (TValue)(object)parsedValue; + return true; + } + else + { + result = default; + return false; + } + } + + private static bool TryParseDateTimeOffset(string? value, [MaybeNullWhen(false)] out TValue result) + { + var success = BindConverter.TryConvertToDateTimeOffset(value, CultureInfo.InvariantCulture, TimeFormat, out var parsedValue); + if (success) + { + result = (TValue)(object)parsedValue; + return true; + } + else + { + result = default; + return false; + } + } + } +} \ No newline at end of file