From 73e8fc10e4df4e30e2e4ff5b3ae338d4d09add9d Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Tue, 7 Apr 2015 11:10:16 -0700 Subject: [PATCH] Automatically use `type="number"` for expressions of more types - #2261 - include `short`, `ushort`, `float`, `double` - correct typo in `` tag helper; ignored calculated format - only one test for ``'s calculated format :frowning: - fill some `Editor*()` and `` tag helper test gaps nit: clean up some trailing whitespace --- .../Rendering/Html/TemplateRenderer.cs | 4 + .../InputTagHelper.cs | 8 +- .../Rendering/DefaultEditorTemplatesTest.cs | 116 ++++++----- ...elpersWebSite.Employee.Create.Invalid.html | 2 +- .../InputTagHelperTest.cs | 182 +++++++++++++++++- 5 files changed, 254 insertions(+), 58 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs index b43b2d199d..76c273d117 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs @@ -50,10 +50,14 @@ namespace Microsoft.AspNet.Mvc.Rendering { "Time", DefaultEditorTemplates.TimeInputTemplate }, { typeof(byte).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(sbyte).Name, DefaultEditorTemplates.NumberInputTemplate }, + { typeof(short).Name, DefaultEditorTemplates.NumberInputTemplate }, + { typeof(ushort).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(int).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(uint).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(long).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(ulong).Name, DefaultEditorTemplates.NumberInputTemplate }, + { typeof(float).Name, DefaultEditorTemplates.NumberInputTemplate }, + { typeof(double).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(bool).Name, DefaultEditorTemplates.BooleanTemplate }, { typeof(decimal).Name, DefaultEditorTemplates.DecimalTemplate }, { typeof(string).Name, DefaultEditorTemplates.StringTemplate }, diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs index 78818cb5dc..2bf9e59d65 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs @@ -36,10 +36,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { "Time", "time" }, { nameof(Byte), "number" }, { nameof(SByte), "number" }, + { nameof(Int16), "number" }, + { nameof(UInt16), "number" }, { nameof(Int32), "number" }, { nameof(UInt32), "number" }, { nameof(Int64), "number" }, { nameof(UInt64), "number" }, + { nameof(Single), "number" }, + { nameof(Double), "number" }, { nameof(Boolean), InputType.CheckBox.ToString().ToLowerInvariant() }, { nameof(Decimal), InputType.Text.ToString().ToLowerInvariant() }, { nameof(String), InputType.Text.ToString().ToLowerInvariant() }, @@ -191,7 +195,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers if (tagBuilder != null) { // This TagBuilder contains the one element of interest. Since this is not the "checkbox" - // special-case, output is a self-closing element no longer guarunteed. + // special-case, output is a self-closing element no longer guaranteed. output.MergeAttributes(tagBuilder); output.Content.Append(tagBuilder.InnerHtml); } @@ -272,7 +276,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers modelExplorer, For.Name, value: modelExplorer.Model, - format: Format, + format: format, htmlAttributes: null); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTest.cs index 7c27d4cfea..d195231ce2 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTest.cs @@ -28,48 +28,56 @@ namespace Microsoft.AspNet.Mvc.Core { return new TheoryData { - { null, "__TextBox__" }, - { string.Empty, "__TextBox__" }, - { "EmailAddress", "__TextBox__" }, - { "emailaddress", "__TextBox__" }, + { null, "__TextBox__ class='text-box single-line'" }, + { string.Empty, "__TextBox__ class='text-box single-line'" }, + { "EmailAddress", "__TextBox__ class='text-box single-line' type='email'" }, + { "emailaddress", "__TextBox__ class='text-box single-line' type='email'" }, { "HiddenInput", "True__Hidden__" }, // Hidden also generates value by default. { "HIDDENINPUT", "True__Hidden__" }, - { "MultilineText", "__TextArea__" }, - { "multilinetext", "__TextArea__" }, - { "Password", "__Password__" }, - { "PASSWORD", "__Password__" }, - { "PhoneNumber", "__TextBox__" }, - { "phonenumber", "__TextBox__" }, - { "Text", "__TextBox__" }, - { "TEXT", "__TextBox__" }, - { "Url", "__TextBox__" }, - { "url", "__TextBox__" }, - { "Date", "__TextBox__" }, - { "DATE", "__TextBox__" }, - { "DateTime", "__TextBox__" }, - { "datetime", "__TextBox__" }, - { "DateTime-local", "__TextBox__" }, - { "DATETIME-LOCAL", "__TextBox__" }, - { "Time", "__TextBox__" }, - { "time", "__TextBox__" }, - { "Byte", "__TextBox__" }, - { "BYTE", "__TextBox__" }, - { "SByte", "__TextBox__" }, - { "sbyte", "__TextBox__" }, - { "Int32", "__TextBox__" }, - { "INT32", "__TextBox__" }, - { "UInt32", "__TextBox__" }, - { "uint32", "__TextBox__" }, - { "Int64", "__TextBox__" }, - { "INT64", "__TextBox__" }, - { "UInt64", "__TextBox__" }, - { "uint64", "__TextBox__" }, - { "Boolean", "__CheckBox__" }, // String is not a Nullable type. - { "BOOLEAN", "__CheckBox__" }, - { "Decimal", "__TextBox__" }, - { "decimal", "__TextBox__" }, - { "String", "__TextBox__" }, - { "STRING", "__TextBox__" }, + { "MultilineText", "__TextArea__ class='text-box multi-line'" }, + { "multilinetext", "__TextArea__ class='text-box multi-line'" }, + { "Password", "__Password__ class='text-box single-line password'" }, + { "PASSWORD", "__Password__ class='text-box single-line password'" }, + { "PhoneNumber", "__TextBox__ class='text-box single-line' type='tel'" }, + { "phonenumber", "__TextBox__ class='text-box single-line' type='tel'" }, + { "Text", "__TextBox__ class='text-box single-line'" }, + { "TEXT", "__TextBox__ class='text-box single-line'" }, + { "Url", "__TextBox__ class='text-box single-line' type='url'" }, + { "url", "__TextBox__ class='text-box single-line' type='url'" }, + { "Date", "__TextBox__ class='text-box single-line' type='date'" }, + { "DATE", "__TextBox__ class='text-box single-line' type='date'" }, + { "DateTime", "__TextBox__ class='text-box single-line' type='datetime'" }, + { "datetime", "__TextBox__ class='text-box single-line' type='datetime'" }, + { "DateTime-local", "__TextBox__ class='text-box single-line' type='datetime-local'" }, + { "DATETIME-LOCAL", "__TextBox__ class='text-box single-line' type='datetime-local'" }, + { "Time", "__TextBox__ class='text-box single-line' type='time'" }, + { "time", "__TextBox__ class='text-box single-line' type='time'" }, + { "Byte", "__TextBox__ class='text-box single-line' type='number'" }, + { "BYTE", "__TextBox__ class='text-box single-line' type='number'" }, + { "SByte", "__TextBox__ class='text-box single-line' type='number'" }, + { "sbyte", "__TextBox__ class='text-box single-line' type='number'" }, + { "Int16", "__TextBox__ class='text-box single-line' type='number'" }, + { "INT16", "__TextBox__ class='text-box single-line' type='number'" }, + { "UInt16", "__TextBox__ class='text-box single-line' type='number'" }, + { "uint16", "__TextBox__ class='text-box single-line' type='number'" }, + { "Int32", "__TextBox__ class='text-box single-line' type='number'" }, + { "INT32", "__TextBox__ class='text-box single-line' type='number'" }, + { "UInt32", "__TextBox__ class='text-box single-line' type='number'" }, + { "uint32", "__TextBox__ class='text-box single-line' type='number'" }, + { "Int64", "__TextBox__ class='text-box single-line' type='number'" }, + { "INT64", "__TextBox__ class='text-box single-line' type='number'" }, + { "UInt64", "__TextBox__ class='text-box single-line' type='number'" }, + { "uint64", "__TextBox__ class='text-box single-line' type='number'" }, + { "Single", "__TextBox__ class='text-box single-line' type='number'" }, + { "SINGLE", "__TextBox__ class='text-box single-line' type='number'" }, + { "Double", "__TextBox__ class='text-box single-line' type='number'" }, + { "double", "__TextBox__ class='text-box single-line' type='number'" }, + { "Boolean", "__CheckBox__ class='check-box'" }, // Not tri-state b/c string is not a Nullable type. + { "BOOLEAN", "__CheckBox__ class='check-box'" }, + { "Decimal", "__TextBox__ class='text-box single-line'" }, + { "decimal", "__TextBox__ class='text-box single-line'" }, + { "String", "__TextBox__ class='text-box single-line'" }, + { "STRING", "__TextBox__ class='text-box single-line'" }, }; } } @@ -946,7 +954,7 @@ Environment.NewLine; public HtmlString CheckBox(string name, bool? isChecked, object htmlAttributes) { - return new HtmlString("__CheckBox__"); + return HelperName("__CheckBox__", htmlAttributes); } public HtmlString Display( @@ -974,7 +982,7 @@ Environment.NewLine; string optionLabel, object htmlAttributes) { - return new HtmlString("__DropDownList__"); + return HelperName("__DropDownList__", htmlAttributes); } public HtmlString Editor( @@ -1030,7 +1038,7 @@ Environment.NewLine; public HtmlString Hidden(string name, object value, object htmlAttributes) { - return new HtmlString("__Hidden__"); + return HelperName("__Hidden__", htmlAttributes); } public string Id(string name) @@ -1040,7 +1048,7 @@ Environment.NewLine; public HtmlString Label(string expression, string labelText, object htmlAttributes) { - return new HtmlString("__Label__"); + return HelperName("__Label__", htmlAttributes); } public HtmlString ListBox(string name, IEnumerable selectList, object htmlAttributes) @@ -1063,12 +1071,12 @@ Environment.NewLine; public HtmlString Password(string name, object value, object htmlAttributes) { - return new HtmlString("__Password__"); + return HelperName("__Password__", htmlAttributes); } public HtmlString RadioButton(string name, object value, bool? isChecked, object htmlAttributes) { - return new HtmlString("__RadioButton__"); + return HelperName("__RadioButton__", htmlAttributes); } public HtmlString Raw(object value) @@ -1100,17 +1108,17 @@ Environment.NewLine; public HtmlString TextArea(string name, string value, int rows, int columns, object htmlAttributes) { - return new HtmlString("__TextArea__"); + return HelperName("__TextArea__", htmlAttributes); } public HtmlString TextBox(string name, object value, string format, object htmlAttributes) { - return new HtmlString("__TextBox__"); + return HelperName("__TextBox__", htmlAttributes); } public HtmlString ValidationMessage(string modelName, string message, object htmlAttributes, string tag) { - return new HtmlString("__ValidationMessage__"); + return HelperName("__ValidationMessage__", htmlAttributes); } public HtmlString ValidationSummary( @@ -1126,6 +1134,16 @@ Environment.NewLine; { throw new NotImplementedException(); } + + private HtmlString HelperName(string name, object htmlAttributes) + { + var htmlAttributesDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); + var htmlAttributesString = + string.Join(" ", htmlAttributesDictionary.Select(entry => $"{ entry.Key }='{ entry.Value }'")); + var helperName = $"{ name } { htmlAttributesString }"; + + return new HtmlString(helperName.TrimEnd()); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html index 44f32743b6..99144d46c1 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html @@ -55,7 +55,7 @@
- + The JoinDate field is required.
diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/InputTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/InputTagHelperTest.cs index 806abfc296..087865ee98 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/InputTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/InputTagHelperTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering; @@ -378,8 +379,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var htmlGenerator = new Mock(MockBehavior.Strict); var tagHelper = GetTagHelper( - htmlGenerator.Object, - model, + htmlGenerator.Object, + model, nameof(Model.Text), metadataProvider: metadataProvider); tagHelper.InputTypeName = inputTypeName; @@ -579,8 +580,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var htmlGenerator = new Mock(MockBehavior.Strict); var tagHelper = GetTagHelper( - htmlGenerator.Object, - model, + htmlGenerator.Object, + model, nameof(Model.Text), metadataProvider: metadataProvider); tagHelper.InputTypeName = inputTypeName; @@ -617,9 +618,165 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.Equal(expectedTagName, output.TagName); } + [Theory] + [InlineData(null, null, "text")] + [InlineData("Byte", null, "number")] + [InlineData("custom-datatype", null, "text")] + [InlineData("Custom-Datatype", null, "text")] + [InlineData("date", null, "date")] // No date/time special cases since ModelType is string. + [InlineData("datetime", null, "datetime")] + [InlineData("datetime-local", null, "datetime-local")] + [InlineData("DATETIME-local", null, "datetime-local")] + [InlineData("Decimal", "{0:0.00}", "text")] + [InlineData("Double", null, "number")] + [InlineData("Int16", null, "number")] + [InlineData("Int32", null, "number")] + [InlineData("int32", null, "number")] + [InlineData("Int64", null, "number")] + [InlineData("SByte", null, "number")] + [InlineData("Single", null, "number")] + [InlineData("SINGLE", null, "number")] + [InlineData("string", null, "text")] + [InlineData("STRING", null, "text")] + [InlineData("text", null, "text")] + [InlineData("TEXT", null, "text")] + [InlineData("time", null, "time")] + [InlineData("UInt16", null, "number")] + [InlineData("uint16", null, "number")] + [InlineData("UInt32", null, "number")] + [InlineData("UInt64", null, "number")] + public async Task ProcessAsync_CallsGenerateTextBox_AddsExpectedAttributes( + string dataTypeName, + string expectedFormat, + string expectedType) + { + // Arrange + var expectedAttributes = new Dictionary + { + { "type", expectedType }, // Calculated; not passed to HtmlGenerator. + }; + var expectedTagName = "not-input"; + + var context = new TagHelperContext( + allAttributes: new Dictionary(), + items: new Dictionary(), + uniqueId: "test", + getChildContentAsync: () => Task.FromResult(new DefaultTagHelperContent())); + + var output = new TagHelperOutput(expectedTagName, attributes: new Dictionary()) + { + SelfClosing = true, + }; + + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider.ForProperty("Text").DisplayDetails(dd => dd.DataTypeName = dataTypeName); + + var htmlGenerator = new Mock(MockBehavior.Strict); + var tagHelper = GetTagHelper( + htmlGenerator.Object, + model: null, + propertyName: nameof(Model.Text), + metadataProvider: metadataProvider); + + var tagBuilder = new TagBuilder("input", new HtmlEncoder()); + htmlGenerator + .Setup(mock => mock.GenerateTextBox( + tagHelper.ViewContext, + tagHelper.For.ModelExplorer, + tagHelper.For.Name, + null, // value + expectedFormat, + null)) // htmlAttributes + .Returns(tagBuilder) + .Verifiable(); + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + htmlGenerator.Verify(); + + Assert.True(output.SelfClosing); + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Empty(output.PreContent); + Assert.Equal(new[] { string.Empty }, output.Content); + Assert.Empty(output.PostContent); + Assert.Equal(expectedTagName, output.TagName); + } + + [Theory] + [InlineData("Date", Html5DateRenderingMode.CurrentCulture, "{0:d}", "date")] // Format from [DataType]. + [InlineData("Date", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")] + [InlineData("DateTime", Html5DateRenderingMode.CurrentCulture, null, "datetime")] + [InlineData("DateTime", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fffK}", "datetime")] + [InlineData("DateTimeOffset", Html5DateRenderingMode.CurrentCulture, null, "datetime")] + [InlineData("DateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fffK}", "datetime")] + [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")] + public async Task ProcessAsync_CallsGenerateTextBox_AddsExpectedAttributesForRfc3339( + string propertyName, + Html5DateRenderingMode dateRenderingMode, + string expectedFormat, + string expectedType) + { + // Arrange + var expectedAttributes = new Dictionary + { + { "type", expectedType }, // Calculated; not passed to HtmlGenerator. + }; + var expectedTagName = "not-input"; + + var context = new TagHelperContext( + allAttributes: new Dictionary(), + items: new Dictionary(), + uniqueId: "test", + getChildContentAsync: () => Task.FromResult(new DefaultTagHelperContent())); + + var output = new TagHelperOutput(expectedTagName, attributes: new Dictionary()) + { + SelfClosing = true, + }; + + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var htmlGenerator = new Mock(MockBehavior.Strict); + var tagHelper = GetTagHelper( + htmlGenerator.Object, + model: null, + propertyName: propertyName, + metadataProvider: metadataProvider); + tagHelper.ViewContext.Html5DateRenderingMode = dateRenderingMode; + + var tagBuilder = new TagBuilder("input", new HtmlEncoder()); + htmlGenerator + .Setup(mock => mock.GenerateTextBox( + tagHelper.ViewContext, + tagHelper.For.ModelExplorer, + tagHelper.For.Name, + null, // value + expectedFormat, + null)) // htmlAttributes + .Returns(tagBuilder) + .Verifiable(); + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + htmlGenerator.Verify(); + + Assert.True(output.SelfClosing); + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Empty(output.PreContent); + Assert.Equal(new[] { string.Empty }, output.Content); + Assert.Empty(output.PostContent); + Assert.Equal(expectedTagName, output.TagName); + } + private static InputTagHelper GetTagHelper( - IHtmlGenerator htmlGenerator, - object model, + IHtmlGenerator htmlGenerator, + object model, string propertyName, IModelMetadataProvider metadataProvider = null) { @@ -685,6 +842,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers public NestedModel NestedModel { get; set; } public bool IsACar { get; set; } + + [DataType(DataType.Date)] + public DateTime Date { get; set; } + + public DateTime DateTime { get; set; } + + public DateTimeOffset DateTimeOffset { get; set; } + + [DataType("datetime-local")] + public DateTime DateTimeLocal { get; set; } + + [DataType(DataType.Time)] + public DateTimeOffset Time { get; set; } } private class NestedModel