Allow the use of the Prompt property of DisplayAttribute for the placeholder attribute of input fields (addresses #3723)
This commit is contained in:
parent
78e7179221
commit
8b726ddc0c
|
|
@ -258,6 +258,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
/// <value>The order value of the current metadata.</value>
|
/// <value>The order value of the current metadata.</value>
|
||||||
public abstract int Order { get; }
|
public abstract int Order { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the text to display as a placeholder value for an editor.
|
||||||
|
/// </summary>
|
||||||
|
public abstract string Placeholder { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the text to display when the model is <c>null</c>.
|
/// Gets the text to display when the model is <c>null</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Placeholder
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (DisplayMetadata.Placeholder == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DisplayMetadata.Placeholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override ModelPropertyCollection Properties
|
public override ModelPropertyCollection Properties
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Order { get; set; } = 10000;
|
public int Order { get; set; } = 10000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a delegate which is used to get a value for the
|
||||||
|
/// model's placeholder text. See <see cref="ModelMetadata.Placeholder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Func<string> Placeholder { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether or not to include in the model value in display.
|
/// Gets or sets a value indicating whether or not to include in the model value in display.
|
||||||
/// See <see cref="ModelMetadata.ShowForDisplay"/>
|
/// See <see cref="ModelMetadata.ShowForDisplay"/>
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
if (displayAttribute != null)
|
if (displayAttribute != null)
|
||||||
{
|
{
|
||||||
displayMetadata.Description = () => displayAttribute.GetDescription();
|
displayMetadata.Description = () => displayAttribute.GetDescription();
|
||||||
|
displayMetadata.Placeholder = () => displayAttribute.GetPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayFormatString
|
// DisplayFormatString
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
private static readonly MethodInfo ConvertEnumFromStringMethod =
|
private static readonly MethodInfo ConvertEnumFromStringMethod =
|
||||||
typeof(DefaultHtmlGenerator).GetTypeInfo().GetDeclaredMethod(nameof(ConvertEnumFromString));
|
typeof(DefaultHtmlGenerator).GetTypeInfo().GetDeclaredMethod(nameof(ConvertEnumFromString));
|
||||||
|
|
||||||
|
// See: (http://www.w3.org/TR/html5/forms.html#the-input-element)
|
||||||
|
private static readonly string[] _placeholderInputTypes =
|
||||||
|
new[] { "text", "search", "url", "tel", "email", "password", "number" };
|
||||||
|
|
||||||
private readonly IAntiforgery _antiforgery;
|
private readonly IAntiforgery _antiforgery;
|
||||||
private readonly IClientModelValidatorProvider _clientModelValidatorProvider;
|
private readonly IClientModelValidatorProvider _clientModelValidatorProvider;
|
||||||
private readonly IModelMetadataProvider _metadataProvider;
|
private readonly IModelMetadataProvider _metadataProvider;
|
||||||
|
|
@ -1134,12 +1138,24 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
nameof(expression));
|
nameof(expression));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inputTypeString = GetInputTypeString(inputType);
|
||||||
var tagBuilder = new TagBuilder("input");
|
var tagBuilder = new TagBuilder("input");
|
||||||
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
|
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
|
||||||
tagBuilder.MergeAttributes(htmlAttributes);
|
tagBuilder.MergeAttributes(htmlAttributes);
|
||||||
tagBuilder.MergeAttribute("type", GetInputTypeString(inputType));
|
tagBuilder.MergeAttribute("type", inputTypeString);
|
||||||
tagBuilder.MergeAttribute("name", fullName, replaceExisting: true);
|
tagBuilder.MergeAttribute("name", fullName, replaceExisting: true);
|
||||||
|
|
||||||
|
var suppliedTypeString = tagBuilder.Attributes["type"];
|
||||||
|
if (_placeholderInputTypes.Contains(suppliedTypeString))
|
||||||
|
{
|
||||||
|
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider);
|
||||||
|
var placeholder = modelExplorer.Metadata.Placeholder;
|
||||||
|
if (!string.IsNullOrEmpty(placeholder))
|
||||||
|
{
|
||||||
|
tagBuilder.MergeAttribute("placeholder", placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var valueParameter = FormatValue(value, format);
|
var valueParameter = FormatValue(value, format);
|
||||||
var usedModelState = false;
|
var usedModelState = false;
|
||||||
switch (inputType)
|
switch (inputType)
|
||||||
|
|
|
||||||
|
|
@ -520,6 +520,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string Placeholder
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override ModelPropertyCollection Properties
|
public override ModelPropertyCollection Properties
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
||||||
Assert.Null(metadata.NullDisplayText);
|
Assert.Null(metadata.NullDisplayText);
|
||||||
Assert.Null(metadata.TemplateHint);
|
Assert.Null(metadata.TemplateHint);
|
||||||
Assert.Null(metadata.SimpleDisplayProperty);
|
Assert.Null(metadata.SimpleDisplayProperty);
|
||||||
|
Assert.Null(metadata.Placeholder);
|
||||||
|
|
||||||
Assert.Equal(10000, ModelMetadata.DefaultOrder);
|
Assert.Equal(10000, ModelMetadata.DefaultOrder);
|
||||||
Assert.Equal(ModelMetadata.DefaultOrder, metadata.Order);
|
Assert.Equal(ModelMetadata.DefaultOrder, metadata.Order);
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
{ new DisplayAttribute() { Description = "d" }, d => d.Description(), "d" },
|
{ new DisplayAttribute() { Description = "d" }, d => d.Description(), "d" },
|
||||||
{ new DisplayAttribute() { Name = "DN" }, d => d.DisplayName(), "DN" },
|
{ new DisplayAttribute() { Name = "DN" }, d => d.DisplayName(), "DN" },
|
||||||
{ new DisplayAttribute() { Order = 3 }, d => d.Order, 3 },
|
{ new DisplayAttribute() { Order = 3 }, d => d.Order, 3 },
|
||||||
|
{ new DisplayAttribute() { Prompt = "Enter Value" }, d => d.Placeholder(), "Enter Value" },
|
||||||
|
|
||||||
{ new DisplayColumnAttribute("Property"), d => d.SimpleDisplayProperty, "Property" },
|
{ new DisplayColumnAttribute("Property"), d => d.SimpleDisplayProperty, "Property" },
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
{
|
{
|
||||||
new DisplayAttribute { Name = "value" }, metadata => metadata.DisplayName
|
new DisplayAttribute { Name = "value" }, metadata => metadata.DisplayName
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
new DisplayAttribute { Prompt = "value" }, metadata => metadata.Placeholder
|
||||||
|
},
|
||||||
{
|
{
|
||||||
new DisplayFormatAttribute { DataFormatString = "value" },
|
new DisplayFormatAttribute { DataFormatString = "value" },
|
||||||
metadata => metadata.DisplayFormatString
|
metadata => metadata.DisplayFormatString
|
||||||
|
|
@ -422,6 +425,22 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
Assert.Equal("description", result);
|
Assert.Equal("description", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisplayAttribute_PromptAsPlaceholder()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var display = new DisplayAttribute() { Prompt = "prompt" };
|
||||||
|
var provider = CreateProvider(new[] { display });
|
||||||
|
|
||||||
|
var metadata = provider.GetMetadataForType(typeof(string));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = metadata.Placeholder;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("prompt", result);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DisplayName_FromResources_GetsRecomputed()
|
public void DisplayName_FromResources_GetsRecomputed()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -399,6 +399,21 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
HtmlContentUtilities.HtmlContentToString(passwordResult));
|
HtmlContentUtilities.HtmlContentToString(passwordResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PasswordFor_GeneratesPlaceholderAttribute_WhenDisplayAttributePromptIsSet()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = @"<input id=""HtmlEncode[[Property7]]"" name=""HtmlEncode[[Property7]]"" placeholder=""HtmlEncode[[placeholder]]"" type=""HtmlEncode[[password]]"" />";
|
||||||
|
var model = new PasswordModel();
|
||||||
|
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = helper.PasswordFor(m => m.Property7, htmlAttributes: null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
|
||||||
|
}
|
||||||
|
|
||||||
private static ViewDataDictionary<PasswordModel> GetViewDataWithNullModelAndNonEmptyViewData()
|
private static ViewDataDictionary<PasswordModel> GetViewDataWithNullModelAndNonEmptyViewData()
|
||||||
{
|
{
|
||||||
return new ViewDataDictionary<PasswordModel>(new EmptyModelMetadataProvider())
|
return new ViewDataDictionary<PasswordModel>(new EmptyModelMetadataProvider())
|
||||||
|
|
@ -443,6 +458,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
public Dictionary<string, string> Property3 { get; } = new Dictionary<string, string>();
|
public Dictionary<string, string> Property3 { get; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
public NestedClass Property4 { get; } = new NestedClass();
|
public NestedClass Property4 { get; } = new NestedClass();
|
||||||
|
|
||||||
|
[Display(Prompt = "placeholder")]
|
||||||
|
public string Property7 { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NestedClass
|
public class NestedClass
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc.TestCommon;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
|
{
|
||||||
|
public class HtmlHelperTextBoxTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("text")]
|
||||||
|
[InlineData("search")]
|
||||||
|
[InlineData("url")]
|
||||||
|
[InlineData("tel")]
|
||||||
|
[InlineData("email")]
|
||||||
|
[InlineData("number")]
|
||||||
|
public void TextBoxFor_GeneratesPlaceholderAttribute_WhenDisplayAttributePromptIsSetAndTypeIsValid(string type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new TextBoxModel();
|
||||||
|
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = HtmlContentUtilities.HtmlContentToString(helper.TextBoxFor(m => m.Property1, new { type }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.Contains(@"placeholder=""HtmlEncode[[placeholder]]"""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("hidden")]
|
||||||
|
[InlineData("date")]
|
||||||
|
[InlineData("time")]
|
||||||
|
[InlineData("range")]
|
||||||
|
[InlineData("color")]
|
||||||
|
[InlineData("checkbox")]
|
||||||
|
[InlineData("radio")]
|
||||||
|
[InlineData("submit")]
|
||||||
|
[InlineData("reset")]
|
||||||
|
[InlineData("button")]
|
||||||
|
[InlineData("image")]
|
||||||
|
[InlineData("file")]
|
||||||
|
public void TextBoxFor_DoesNotGeneratePlaceholderAttribute_WhenDisplayAttributePromptIsSetAndTypeIsInvalid(string type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new TextBoxModel();
|
||||||
|
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = HtmlContentUtilities.HtmlContentToString(helper.TextBoxFor(m => m.Property1, new { type }));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.Contains(@"placeholder=""HtmlEncode[[placeholder]]"""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TextBoxModel
|
||||||
|
{
|
||||||
|
[Display(Prompt = "placeholder")]
|
||||||
|
public string Property1 { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue