Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Auto-select type="text" for DateTimeOffset values
Browse files Browse the repository at this point in the history
- cherry-picked from 7e4a8fe in dev
- #6648
- a different take on #4871
- `DateTime` can also round-trip `DateTimeKind.UTC` with `[DataType("datetimeoffset")]` or `[UIHint("datetimeoffset")]`
- since they're now handled differently by default, add more `DateTime` tests
- expand tests involving `Html5DateRenderingMode.CurrentCulture`

nits: make VS-suggested changes to files updated in this PR
  • Loading branch information
dougbu committed Sep 18, 2017
1 parent 9f5e4eb commit 6041c6b
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 48 deletions.
41 changes: 27 additions & 14 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class InputTagHelper : TagHelper
{ "Date", "date" },
{ "DateTime", "datetime-local" },
{ "DateTime-local", "datetime-local" },
{ nameof(DateTimeOffset), "text" },
{ "Time", "time" },
{ nameof(Byte), "number" },
{ nameof(SByte), "number" },
Expand Down Expand Up @@ -234,8 +235,7 @@ protected string GetInputType(ModelExplorer modelExplorer, out string inputTypeH
{
foreach (var hint in GetInputTypeHints(modelExplorer))
{
string inputType;
if (_defaultInputTypes.TryGetValue(hint, out inputType))
if (_defaultInputTypes.TryGetValue(hint, out var inputType))
{
inputTypeHint = hint;
return inputType;
Expand All @@ -252,8 +252,7 @@ private TagBuilder GenerateCheckBox(ModelExplorer modelExplorer, TagHelperOutput
{
if (modelExplorer.Model != null)
{
bool potentialBool;
if (!bool.TryParse(modelExplorer.Model.ToString(), out potentialBool))
if (!bool.TryParse(modelExplorer.Model.ToString(), out var potentialBool))
{
throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidStringResult(
ForAttributeName,
Expand Down Expand Up @@ -353,8 +352,7 @@ private TagBuilder GenerateTextBox(ModelExplorer modelExplorer, string inputType
private TagBuilder GenerateHidden(ModelExplorer modelExplorer)
{
var value = For.Model;
var byteArrayValue = value as byte[];
if (byteArrayValue != null)
if (value is byte[] byteArrayValue)
{
value = Convert.ToBase64String(byteArrayValue);
}
Expand All @@ -380,7 +378,6 @@ private TagBuilder GenerateHidden(ModelExplorer modelExplorer)
private string GetFormat(ModelExplorer modelExplorer, string inputTypeHint, string inputType)
{
string format;
string rfc3339Format;
if (string.Equals("decimal", inputTypeHint, StringComparison.OrdinalIgnoreCase) &&
string.Equals("text", inputType, StringComparison.Ordinal) &&
string.IsNullOrEmpty(modelExplorer.Metadata.EditFormatString))
Expand All @@ -389,14 +386,30 @@ private string GetFormat(ModelExplorer modelExplorer, string inputTypeHint, stri
// EditFormatString has precedence over this fall-back format.
format = "{0:0.00}";
}
else if (_rfc3339Formats.TryGetValue(inputType, out rfc3339Format) &&
ViewContext.Html5DateRenderingMode == Html5DateRenderingMode.Rfc3339 &&
else if (ViewContext.Html5DateRenderingMode == Html5DateRenderingMode.Rfc3339 &&
!modelExplorer.Metadata.HasNonDefaultEditFormat &&
(typeof(DateTime) == modelExplorer.Metadata.UnderlyingOrModelType || typeof(DateTimeOffset) == modelExplorer.Metadata.UnderlyingOrModelType))
(typeof(DateTime) == modelExplorer.Metadata.UnderlyingOrModelType ||
typeof(DateTimeOffset) == modelExplorer.Metadata.UnderlyingOrModelType))
{
// Rfc3339 mode _may_ override EditFormatString in a limited number of cases e.g. EditFormatString
// must be a default format (i.e. came from a built-in [DataType] attribute).
format = rfc3339Format;
// Rfc3339 mode _may_ override EditFormatString in a limited number of cases. Happens only when
// EditFormatString has a default format i.e. came from a [DataType] attribute.
if (string.Equals("text", inputType) &&
string.Equals(nameof(DateTimeOffset), inputTypeHint, StringComparison.OrdinalIgnoreCase))
{
// Auto-select a format that round-trips Offset and sub-Second values in a DateTimeOffset. Not
// done if user chose the "text" type in .cshtml file or with data annotations i.e. when
// inputTypeHint==null or "text".
format = _rfc3339Formats["datetime"];
}
else if (_rfc3339Formats.TryGetValue(inputType, out var rfc3339Format))
{
format = rfc3339Format;
}
else
{
// Otherwise use default EditFormatString.
format = modelExplorer.Metadata.EditFormatString;
}
}
else
{
Expand Down Expand Up @@ -428,7 +441,7 @@ private static IEnumerable<string> GetInputTypeHints(ModelExplorer modelExplorer
fieldType = modelExplorer.Metadata.UnderlyingOrModelType;
}

foreach (string typeName in TemplateRenderer.GetTypeNames(modelExplorer.Metadata, fieldType))
foreach (var typeName in TemplateRenderer.GetTypeNames(modelExplorer.Metadata, fieldType))
{
yield return typeName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,7 @@ private static IDictionary<string, object> MergeHtmlAttributes(
{
var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesObject);

object htmlClassObject;
if (htmlAttributes.TryGetValue("class", out htmlClassObject))
if (htmlAttributes.TryGetValue("class", out var htmlClassObject))
{
var htmlClassName = htmlClassObject + " " + className;
htmlAttributes["class"] = htmlClassName;
Expand Down Expand Up @@ -347,10 +346,10 @@ public static IHtmlContent EmailAddressInputTemplate(IHtmlHelper htmlHelper)
return GenerateTextBox(htmlHelper, inputType: "email");
}

public static IHtmlContent DateTimeInputTemplate(IHtmlHelper htmlHelper)
public static IHtmlContent DateTimeOffsetTemplate(IHtmlHelper htmlHelper)
{
ApplyRfc3339DateFormattingIfNeeded(htmlHelper, "{0:yyyy-MM-ddTHH:mm:ss.fffK}");
return GenerateTextBox(htmlHelper, inputType: "datetime");
return GenerateTextBox(htmlHelper, inputType: "text");
}

public static IHtmlContent DateTimeLocalInputTemplate(IHtmlHelper htmlHelper)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class TemplateRenderer
{ "Date", DefaultEditorTemplates.DateInputTemplate },
{ "DateTime", DefaultEditorTemplates.DateTimeLocalInputTemplate },
{ "DateTime-local", DefaultEditorTemplates.DateTimeLocalInputTemplate },
{ nameof(DateTimeOffset), DefaultEditorTemplates.DateTimeOffsetTemplate },
{ "Time", DefaultEditorTemplates.TimeInputTemplate },
{ typeof(byte).Name, DefaultEditorTemplates.NumberInputTemplate },
{ typeof(sbyte).Name, DefaultEditorTemplates.NumberInputTemplate },
Expand Down Expand Up @@ -115,7 +116,7 @@ public IHtmlContent Render()
var defaultActions = GetDefaultActions();
var modeViewPath = _readOnly ? DisplayTemplateViewPath : EditorTemplateViewPath;

foreach (string viewName in GetViewNames())
foreach (var viewName in GetViewNames())
{
var viewEngineResult = _viewEngine.GetView(_viewContext.ExecutingFilePath, viewName, isMainPage: false);
if (!viewEngineResult.Success)
Expand All @@ -141,8 +142,7 @@ public IHtmlContent Render()
}
}

Func<IHtmlHelper, IHtmlContent> defaultAction;
if (defaultActions.TryGetValue(viewName, out defaultAction))
if (defaultActions.TryGetValue(viewName, out var defaultAction))
{
return defaultAction(MakeHtmlHelper(_viewContext, _viewData));
}
Expand Down Expand Up @@ -255,8 +255,7 @@ private static IHtmlHelper MakeHtmlHelper(ViewContext viewContext, ViewDataDicti
{
var newHelper = viewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper>();

var contextable = newHelper as IViewContextAware;
if (contextable != null)
if (newHelper is IViewContextAware contextable)
{
var newViewContext = new ViewContext(viewContext, viewContext.View, viewData, viewContext.Writer);
contextable.Contextualize(newViewContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ public async Task AppWideDefaultsInViewAndPartialView()
<validationMessageElement class=""field-validation-error"">An error occurred.</validationMessageElement>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<div class=""editor-label""><label for=""MyDate"">MyDate</label></div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""datetime-local"" value=""2000-01-02T03:04:05.060"" /> </div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""text"" value=""2000-01-02T03:04:05.060&#x2B;00:00"" /> </div>
<div class=""validation-summary-errors""><validationSummaryElement>MySummary</validationSummaryElement>
<ul><li>A model error occurred.</li>
</ul></div>
<validationMessageElement class=""field-validation-error"">An error occurred.</validationMessageElement>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<div class=""editor-label""><label for=""MyDate"">MyDate</label></div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""datetime-local"" value=""2000-01-02T03:04:05.060"" /> </div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""text"" value=""2000-01-02T03:04:05.060&#x2B;00:00"" /> </div>
False";

Expand All @@ -59,7 +59,7 @@ public async Task OverrideAppWideDefaultsInViewAndPartialView()
<ValidationInView class=""field-validation-error"" data-valmsg-for=""Error"" data-valmsg-replace=""true"">An error occurred.</ValidationInView>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<div class=""editor-label""><label for=""MyDate"">MyDate</label></div>
<div class=""editor-field""><input class=""text-box single-line"" data-val=""true"" data-val-required=""The MyDate field is required."" id=""MyDate"" name=""MyDate"" type=""datetime-local"" value=""02/01/2000 03:04:05 &#x2B;00:00"" /> <ValidationInView class=""field-validation-valid"" data-valmsg-for=""MyDate"" data-valmsg-replace=""true""></ValidationInView></div>
<div class=""editor-field""><input class=""text-box single-line"" data-val=""true"" data-val-required=""The MyDate field is required."" id=""MyDate"" name=""MyDate"" type=""text"" value=""02/01/2000 03:04:05 &#x2B;00:00"" /> <ValidationInView class=""field-validation-valid"" data-valmsg-for=""MyDate"" data-valmsg-replace=""true""></ValidationInView></div>
True
<div class=""validation-summary-errors""><ValidationSummaryInPartialView>MySummary</ValidationSummaryInPartialView>
Expand All @@ -68,7 +68,7 @@ public async Task OverrideAppWideDefaultsInViewAndPartialView()
<ValidationInPartialView class=""field-validation-error"" data-valmsg-for=""Error"" data-valmsg-replace=""true"">An error occurred.</ValidationInPartialView>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<div class=""editor-label""><label for=""MyDate"">MyDate</label></div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""datetime-local"" value=""02/01/2000 03:04:05 &#x2B;00:00"" /> <ValidationInPartialView class=""field-validation-valid"" data-valmsg-for=""MyDate"" data-valmsg-replace=""true""></ValidationInPartialView></div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""text"" value=""02/01/2000 03:04:05 &#x2B;00:00"" /> <ValidationInPartialView class=""field-validation-valid"" data-valmsg-for=""MyDate"" data-valmsg-replace=""true""></ValidationInPartialView></div>
True";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ public async Task ProcessAsync_GeneratesExpectedOutput(

[Theory]
[InlineData("datetime", "datetime")]
[InlineData(null, "datetime-local")]
[InlineData(null, "text")]
[InlineData("hidden", "hidden")]
public void Process_GeneratesFormattedOutput(string specifiedType, string expectedType)
{
Expand Down Expand Up @@ -457,6 +457,77 @@ public void Process_GeneratesFormattedOutput(string specifiedType, string expect
Assert.Equal(expectedTagName, output.TagName);
}

[Theory]
[InlineData("datetime", "datetime")]
[InlineData(null, "datetime-local")]
[InlineData("hidden", "hidden")]
public void Process_GeneratesFormattedOutput_ForDateTime(string specifiedType, string expectedType)
{
// Arrange
var expectedAttributes = new TagHelperAttributeList
{
{ "type", expectedType },
{ "id", nameof(Model.DateTime) },
{ "name", nameof(Model.DateTime) },
{ "valid", "from validation attributes" },
{ "value", "datetime: 2011-08-31T05:30:45.0000000Z" },
};
var expectedTagName = "not-input";
var container = new Model
{
DateTime = new DateTime(2011, 8, 31, hour: 5, minute: 30, second: 45, kind: DateTimeKind.Utc),
};

var allAttributes = new TagHelperAttributeList
{
{ "type", specifiedType },
};
var context = new TagHelperContext(
tagName: "input",
allAttributes: allAttributes,
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) =>
{
throw new Exception("getChildContentAsync should not be called.");
})
{
TagMode = TagMode.StartTagOnly,
};

var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider())
{
ValidationAttributes =
{
{ "valid", "from validation attributes" },
}
};

var tagHelper = GetTagHelper(
htmlGenerator,
container,
typeof(Model),
model: container.DateTime,
propertyName: nameof(Model.DateTime),
expressionName: nameof(Model.DateTime));
tagHelper.Format = "datetime: {0:o}";
tagHelper.InputTypeName = specifiedType;

// Act
tagHelper.Process(context, output);

// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Empty(output.PreContent.GetContent());
Assert.Empty(output.Content.GetContent());
Assert.Empty(output.PostContent.GetContent());
Assert.Equal(TagMode.StartTagOnly, output.TagMode);
Assert.Equal(expectedTagName, output.TagName);
}

[Fact]
public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters()
{
Expand Down Expand Up @@ -966,6 +1037,7 @@ public static TheoryData<string, string, string> InputTypeData
{ "datetime", null, "datetime-local" },
{ "datetime-local", null, "datetime-local" },
{ "DATETIME-local", null, "datetime-local" },
{ "datetimeOffset", null, "text" },
{ "Decimal", "{0:0.00}", "text" },
{ "Double", null, "text" },
{ "Int16", null, "number" },
Expand Down Expand Up @@ -1139,15 +1211,15 @@ public async Task ProcessAsync_CallsGenerateTextBox_InputTypeDateTime_RendersAsD
[InlineData("Date", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")]
[InlineData("DateTime", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")]
[InlineData("DateTime", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")]
[InlineData("DateTimeOffset", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")]
[InlineData("DateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")]
[InlineData("DateTimeOffset", Html5DateRenderingMode.CurrentCulture, null, "text")]
[InlineData("DateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fffK}", "text")]
[InlineData("DateTimeLocal", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")]
[InlineData("DateTimeLocal", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")]
[InlineData("Time", Html5DateRenderingMode.CurrentCulture, "{0:t}", "time")] // Format from [DataType].
[InlineData("Time", Html5DateRenderingMode.Rfc3339, "{0:HH:mm:ss.fff}", "time")]
[InlineData("NullableDate", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")]
[InlineData("NullableDateTime", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")]
[InlineData("NullableDateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")]
[InlineData("NullableDateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fffK}", "text")]
public async Task ProcessAsync_CallsGenerateTextBox_AddsExpectedAttributesForRfc3339(
string propertyName,
Html5DateRenderingMode dateRenderingMode,
Expand Down
Loading

0 comments on commit 6041c6b

Please sign in to comment.