diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs index 654d4668c8..8ea3effdd4 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs @@ -113,6 +113,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlAttributeName("type")] public string InputTypeName { get; set; } + /// + /// The name of the <input> element. + /// + /// + /// Passed through to the generated HTML in all cases. Also used to determine whether is + /// valid with an empty . + /// + public string Name { get; set; } + /// /// The value of the <input> element. /// @@ -146,6 +155,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers output.CopyHtmlAttribute("type", context); } + if (Name != null) + { + output.CopyHtmlAttribute(nameof(Name), context); + } + if (Value != null) { output.CopyHtmlAttribute(nameof(Value), context); @@ -183,15 +197,27 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers output.Attributes.SetAttribute("type", inputType); } + // Ensure Generator does not throw due to empty "fullName" if user provided a name attribute. + IDictionary htmlAttributes = null; + if (string.IsNullOrEmpty(For.Name) && + string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix) && + !string.IsNullOrEmpty(Name)) + { + htmlAttributes = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "name", Name }, + }; + } + TagBuilder tagBuilder; switch (inputType) { case "hidden": - tagBuilder = GenerateHidden(modelExplorer); + tagBuilder = GenerateHidden(modelExplorer, htmlAttributes); break; case "checkbox": - tagBuilder = GenerateCheckBox(modelExplorer, output); + tagBuilder = GenerateCheckBox(modelExplorer, output, htmlAttributes); break; case "password": @@ -200,15 +226,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers modelExplorer, For.Name, value: null, - htmlAttributes: null); + htmlAttributes: htmlAttributes); break; case "radio": - tagBuilder = GenerateRadio(modelExplorer); + tagBuilder = GenerateRadio(modelExplorer, htmlAttributes); break; default: - tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType); + tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType, htmlAttributes); break; } @@ -248,7 +274,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers return inputTypeHint; } - private TagBuilder GenerateCheckBox(ModelExplorer modelExplorer, TagHelperOutput output) + private TagBuilder GenerateCheckBox( + ModelExplorer modelExplorer, + TagHelperOutput output, + IDictionary htmlAttributes) { if (modelExplorer.ModelType == typeof(string)) { @@ -282,6 +311,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var renderingMode = output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag; hiddenForCheckboxTag.TagRenderMode = renderingMode; + if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") && + !string.IsNullOrEmpty(Name)) + { + // The checkbox and hidden elements should have the same name attribute value. Attributes will + // match if both are present because both have a generated value. Reach here in the special case + // where user provided a non-empty fallback name. + hiddenForCheckboxTag.MergeAttribute("name", Name); + } if (ViewContext.FormContext.CanRenderAtEndOfForm) { @@ -298,10 +335,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers modelExplorer, For.Name, isChecked: null, - htmlAttributes: null); + htmlAttributes: htmlAttributes); } - private TagBuilder GenerateRadio(ModelExplorer modelExplorer) + private TagBuilder GenerateRadio(ModelExplorer modelExplorer, IDictionary htmlAttributes) { // Note empty string is allowed. if (Value == null) @@ -319,10 +356,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers For.Name, Value, isChecked: null, - htmlAttributes: null); + htmlAttributes: htmlAttributes); } - private TagBuilder GenerateTextBox(ModelExplorer modelExplorer, string inputTypeHint, string inputType) + private TagBuilder GenerateTextBox( + ModelExplorer modelExplorer, + string inputTypeHint, + string inputType, + IDictionary htmlAttributes) { var format = Format; if (string.IsNullOrEmpty(format)) @@ -338,12 +379,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers format = GetFormat(modelExplorer, inputTypeHint, inputType); } } - var htmlAttributes = new Dictionary - { - { "type", inputType } - }; - if (string.Equals(inputType, "file") && string.Equals(inputTypeHint, TemplateRenderer.IEnumerableOfIFormFileName)) + if (htmlAttributes == null) + { + htmlAttributes = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + htmlAttributes["type"] = inputType; + if (string.Equals(inputType, "file") && + string.Equals( + inputTypeHint, + TemplateRenderer.IEnumerableOfIFormFileName, + StringComparison.OrdinalIgnoreCase)) { htmlAttributes["multiple"] = "multiple"; } @@ -352,14 +399,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers ViewContext, modelExplorer, For.Name, - value: modelExplorer.Model, - format: format, - htmlAttributes: htmlAttributes); + modelExplorer.Model, + format, + htmlAttributes); } // Imitate Generator.GenerateHidden() using Generator.GenerateTextBox(). This adds support for asp-format that // is not available in Generator.GenerateHidden(). - private TagBuilder GenerateHidden(ModelExplorer modelExplorer) + private TagBuilder GenerateHidden(ModelExplorer modelExplorer, IDictionary htmlAttributes) { var value = For.Model; if (value is byte[] byteArrayValue) @@ -367,21 +414,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers value = Convert.ToBase64String(byteArrayValue); } + if (htmlAttributes == null) + { + htmlAttributes = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + // In DefaultHtmlGenerator(), GenerateTextBox() calls GenerateInput() _almost_ identically to how // GenerateHidden() does and the main switch inside GenerateInput() handles InputType.Text and // InputType.Hidden identically. No behavior differences at all when a type HTML attribute already exists. - var htmlAttributes = new Dictionary - { - { "type", "hidden" } - }; + htmlAttributes["type"] = "hidden"; - return Generator.GenerateTextBox( - ViewContext, - modelExplorer, - For.Name, - value: value, - format: Format, - htmlAttributes: htmlAttributes); + return Generator.GenerateTextBox(ViewContext, modelExplorer, For.Name, value, Format, htmlAttributes); } // Get a fall-back format based on the metadata. @@ -462,4 +505,4 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/SelectTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/SelectTagHelper.cs index c7657f17f1..f0e5dd2d06 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/SelectTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/SelectTagHelper.cs @@ -57,6 +57,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlAttributeName(ItemsAttributeName)] public IEnumerable Items { get; set; } + /// + /// The name of the <input> element. + /// + /// + /// Passed through to the generated HTML in all cases. Also used to determine whether is + /// valid with an empty . + /// + public string Name { get; set; } + /// public override void Init(TagHelperContext context) { @@ -89,11 +98,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var realModelType = For.ModelExplorer.ModelType; _allowMultiple = typeof(string) != realModelType && typeof(IEnumerable).IsAssignableFrom(realModelType); - _currentValues = Generator.GetCurrentValues( - ViewContext, - For.ModelExplorer, - expression: For.Name, - allowMultiple: _allowMultiple); + _currentValues = Generator.GetCurrentValues(ViewContext, For.ModelExplorer, For.Name, _allowMultiple); // Whether or not (not being highly unlikely) we generate anything, could update contained