diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs
index 7214156022..923bcd158c 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs
@@ -258,6 +258,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// The order value of the current metadata.
public abstract int Order { get; }
+ ///
+ /// Gets the text to display as a placeholder value for an editor.
+ ///
+ public abstract string Placeholder { get; }
+
///
/// Gets the text to display when the model is null.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs
index 5e16176c41..8d22c36ef8 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs
@@ -432,6 +432,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
}
}
+ ///
+ public override string Placeholder
+ {
+ get
+ {
+ if (DisplayMetadata.Placeholder == null)
+ {
+ return null;
+ }
+
+ return DisplayMetadata.Placeholder();
+ }
+ }
+
///
public override ModelPropertyCollection Properties
{
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DisplayMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DisplayMetadata.cs
index 503ace4b5d..4d02a152e8 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DisplayMetadata.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DisplayMetadata.cs
@@ -115,6 +115,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
///
public int Order { get; set; } = 10000;
+ ///
+ /// Gets or sets a delegate which is used to get a value for the
+ /// model's placeholder text. See .
+ ///
+ public Func Placeholder { get; set; }
+
///
/// Gets or sets a value indicating whether or not to include in the model value in display.
/// See
diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs
index 20258331bb..65ad0f789e 100644
--- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs
@@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
if (displayAttribute != null)
{
displayMetadata.Description = () => displayAttribute.GetDescription();
+ displayMetadata.Placeholder = () => displayAttribute.GetPrompt();
}
// DisplayFormatString
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs
index 305bfc801a..134d956f2a 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs
@@ -26,6 +26,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private static readonly MethodInfo ConvertEnumFromStringMethod =
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 IClientModelValidatorProvider _clientModelValidatorProvider;
private readonly IModelMetadataProvider _metadataProvider;
@@ -1134,12 +1138,24 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
nameof(expression));
}
+ var inputTypeString = GetInputTypeString(inputType);
var tagBuilder = new TagBuilder("input");
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
tagBuilder.MergeAttributes(htmlAttributes);
- tagBuilder.MergeAttribute("type", GetInputTypeString(inputType));
+ tagBuilder.MergeAttribute("type", inputTypeString);
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 usedModelState = false;
switch (inputType)
diff --git a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs
index 3ee5b90906..1abe824129 100644
--- a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs
@@ -520,6 +520,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
}
+ public override string Placeholder
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
public override ModelPropertyCollection Properties
{
get
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataTest.cs
index 69ae9a55e0..806117d35a 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataTest.cs
@@ -62,6 +62,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
Assert.Null(metadata.NullDisplayText);
Assert.Null(metadata.TemplateHint);
Assert.Null(metadata.SimpleDisplayProperty);
+ Assert.Null(metadata.Placeholder);
Assert.Equal(10000, ModelMetadata.DefaultOrder);
Assert.Equal(ModelMetadata.DefaultOrder, metadata.Order);
diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs
index aee4901206..44e5ddc1bd 100644
--- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs
@@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ new DisplayAttribute() { Description = "d" }, d => d.Description(), "d" },
{ new DisplayAttribute() { Name = "DN" }, d => d.DisplayName(), "DN" },
{ 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" },
diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs
index a15db6556a..7ce894697a 100644
--- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs
@@ -194,6 +194,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{
new DisplayAttribute { Name = "value" }, metadata => metadata.DisplayName
},
+ {
+ new DisplayAttribute { Prompt = "value" }, metadata => metadata.Placeholder
+ },
{
new DisplayFormatAttribute { DataFormatString = "value" },
metadata => metadata.DisplayFormatString
@@ -422,6 +425,22 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
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]
public void DisplayName_FromResources_GetsRecomputed()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs
index 1bb0db369a..bcbeadd133 100644
--- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs
@@ -399,6 +399,21 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
HtmlContentUtilities.HtmlContentToString(passwordResult));
}
+ [Fact]
+ public void PasswordFor_GeneratesPlaceholderAttribute_WhenDisplayAttributePromptIsSet()
+ {
+ // Arrange
+ var expected = @"";
+ 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 GetViewDataWithNullModelAndNonEmptyViewData()
{
return new ViewDataDictionary(new EmptyModelMetadataProvider())
@@ -443,6 +458,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
public Dictionary Property3 { get; } = new Dictionary();
public NestedClass Property4 { get; } = new NestedClass();
+
+ [Display(Prompt = "placeholder")]
+ public string Property7 { get; set; }
}
public class NestedClass
diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextBoxTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextBoxTest.cs
new file mode 100644
index 0000000000..d18b6e5367
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextBoxTest.cs
@@ -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; }
+ }
+ }
+}
\ No newline at end of file