TagHelpers attribute targeting - part 1

This commit is contained in:
Ajay Bhargav Baaskaran 2015-03-19 12:42:21 -07:00
parent 2b246e7acc
commit bfdeda797d
10 changed files with 91 additions and 265 deletions

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;input&gt; elements with an <c>asp-for</c> attribute. /// <see cref="ITagHelper"/> implementation targeting &lt;input&gt; elements with an <c>asp-for</c> attribute.
/// </summary> /// </summary>
[TargetElement("input", Attributes = ForAttributeName)]
public class InputTagHelper : TagHelper public class InputTagHelper : TagHelper
{ {
private const string ForAttributeName = "asp-for"; private const string ForAttributeName = "asp-for";
@ -121,93 +122,79 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
output.CopyHtmlAttribute(nameof(Value), context); output.CopyHtmlAttribute(nameof(Value), context);
} }
if (For == null) // Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient.
// IHtmlGenerator will enforce name requirements.
var metadata = For.Metadata;
var modelExplorer = For.ModelExplorer;
if (metadata == null)
{ {
// Regular HTML <input/> element. Just make sure Format wasn't specified. throw new InvalidOperationException(Resources.FormatTagHelpers_NoProvidedMetadata(
if (Format != null) "<input>",
{ ForAttributeName,
throw new InvalidOperationException(Resources.FormatInputTagHelper_UnableToFormat( nameof(IModelMetadataProvider),
"<input>", For.Name));
ForAttributeName, }
FormatAttributeName));
} string inputType;
string inputTypeHint;
if (string.IsNullOrEmpty(InputTypeName))
{
// Note GetInputType never returns null.
inputType = GetInputType(modelExplorer, out inputTypeHint);
} }
else else
{ {
// Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient. inputType = InputTypeName.ToLowerInvariant();
// IHtmlGenerator will enforce name requirements. inputTypeHint = null;
var metadata = For.Metadata; }
var modelExplorer = For.ModelExplorer;
if (metadata == null)
{
throw new InvalidOperationException(Resources.FormatTagHelpers_NoProvidedMetadata(
"<input>",
ForAttributeName,
nameof(IModelMetadataProvider),
For.Name));
}
string inputType; // inputType may be more specific than default the generator chooses below.
string inputTypeHint; if (!output.Attributes.ContainsKey("type"))
if (string.IsNullOrEmpty(InputTypeName)) {
{ output.Attributes["type"] = inputType;
// Note GetInputType never returns null. }
inputType = GetInputType(modelExplorer, out inputTypeHint);
}
else
{
inputType = InputTypeName.ToLowerInvariant();
inputTypeHint = null;
}
// inputType may be more specific than default the generator chooses below. TagBuilder tagBuilder;
if (!output.Attributes.ContainsKey("type")) switch (inputType)
{ {
output.Attributes["type"] = inputType; case "checkbox":
} GenerateCheckBox(modelExplorer, output);
return;
TagBuilder tagBuilder; case "hidden":
switch (inputType) tagBuilder = Generator.GenerateHidden(
{ ViewContext,
case "checkbox": modelExplorer,
GenerateCheckBox(modelExplorer, output); For.Name,
return; value: For.Model,
useViewData: false,
htmlAttributes: null);
break;
case "hidden": case "password":
tagBuilder = Generator.GenerateHidden( tagBuilder = Generator.GeneratePassword(
ViewContext, ViewContext,
modelExplorer, modelExplorer,
For.Name, For.Name,
value: For.Model, value: null,
useViewData: false, htmlAttributes: null);
htmlAttributes: null); break;
break;
case "password": case "radio":
tagBuilder = Generator.GeneratePassword( tagBuilder = GenerateRadio(modelExplorer);
ViewContext, break;
modelExplorer,
For.Name,
value: null,
htmlAttributes: null);
break;
case "radio": default:
tagBuilder = GenerateRadio(modelExplorer); tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType);
break; break;
}
default: if (tagBuilder != null)
tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType); {
break; // This TagBuilder contains the one <input/> element of interest. Since this is not the "checkbox"
} // special-case, output is a self-closing element no longer guarunteed.
output.MergeAttributes(tagBuilder);
if (tagBuilder != null) output.Content.Append(tagBuilder.InnerHtml);
{
// This TagBuilder contains the one <input/> element of interest. Since this is not the "checkbox"
// special-case, output is a self-closing element no longer guarunteed.
output.MergeAttributes(tagBuilder);
output.Content.Append(tagBuilder.InnerHtml);
}
} }
} }

View File

@ -10,6 +10,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;label&gt; elements with an <c>asp-for</c> attribute. /// <see cref="ITagHelper"/> implementation targeting &lt;label&gt; elements with an <c>asp-for</c> attribute.
/// </summary> /// </summary>
[TargetElement("label", Attributes = ForAttributeName)]
public class LabelTagHelper : TagHelper public class LabelTagHelper : TagHelper
{ {
private const string ForAttributeName = "asp-for"; private const string ForAttributeName = "asp-for";
@ -32,34 +33,32 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks> /// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
if (For != null) var tagBuilder = Generator.GenerateLabel(
ViewContext,
For.ModelExplorer,
For.Name,
labelText: null,
htmlAttributes: null);
if (tagBuilder != null)
{ {
var tagBuilder = Generator.GenerateLabel(ViewContext, output.MergeAttributes(tagBuilder);
For.ModelExplorer,
For.Name,
labelText: null,
htmlAttributes: null);
if (tagBuilder != null) // We check for whitespace to detect scenarios such as:
// <label for="Name">
// </label>
if (!output.IsContentModified)
{ {
output.MergeAttributes(tagBuilder); var childContent = await context.GetChildContentAsync();
// We check for whitespace to detect scenarios such as: if (childContent.IsWhiteSpace)
// <label for="Name">
// </label>
if (!output.IsContentModified)
{ {
var childContent = await context.GetChildContentAsync(); // Provide default label text since there was nothing useful in the Razor source.
output.Content.SetContent(tagBuilder.InnerHtml);
if (childContent.IsWhiteSpace) }
{ else
// Provide default label text since there was nothing useful in the Razor source. {
output.Content.SetContent(tagBuilder.InnerHtml); output.Content.SetContent(childContent);
}
else
{
output.Content.SetContent(childContent);
}
} }
} }
} }

View File

@ -74,22 +74,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_InvalidExpressionResult"), p0, p1, p2, p3, p4, p5); return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_InvalidExpressionResult"), p0, p1, p2, p3, p4, p5);
} }
/// <summary>
/// Unable to format without a '{1}' expression for {0}. '{2}' must be null if '{1}' is null.
/// </summary>
internal static string InputTagHelper_UnableToFormat
{
get { return GetString("InputTagHelper_UnableToFormat"); }
}
/// <summary>
/// Unable to format without a '{1}' expression for {0}. '{2}' must be null if '{1}' is null.
/// </summary>
internal static string FormatInputTagHelper_UnableToFormat(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_UnableToFormat"), p0, p1, p2);
}
/// <summary> /// <summary>
/// '{1}' must not be null for {0} if '{2}' is '{3}'. /// '{1}' must not be null for {0} if '{2}' is '{3}'.
/// </summary> /// </summary>

View File

@ -129,9 +129,6 @@
<data name="InputTagHelper_InvalidExpressionResult" xml:space="preserve"> <data name="InputTagHelper_InvalidExpressionResult" xml:space="preserve">
<value>Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'.</value> <value>Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'.</value>
</data> </data>
<data name="InputTagHelper_UnableToFormat" xml:space="preserve">
<value>Unable to format without a '{1}' expression for {0}. '{2}' must be null if '{1}' is null.</value>
</data>
<data name="InputTagHelper_ValueRequired" xml:space="preserve"> <data name="InputTagHelper_ValueRequired" xml:space="preserve">
<value>'{1}' must not be null for {0} if '{2}' is '{3}'.</value> <value>'{1}' must not be null for {0} if '{2}' is '{3}'.</value>
</data> </data>

View File

@ -8,10 +8,10 @@ using Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;span&gt; elements with an <c>asp-validation-for</c> /// <see cref="ITagHelper"/> implementation targeting any HTML element with an <c>asp-validation-for</c>
/// attribute. /// attribute.
/// </summary> /// </summary>
[TargetElement("span")] [TargetElement("span", Attributes = ValidationForAttributeName)]
public class ValidationMessageTagHelper : TagHelper public class ValidationMessageTagHelper : TagHelper
{ {
private const string ValidationForAttributeName = "asp-validation-for"; private const string ValidationForAttributeName = "asp-validation-for";

View File

@ -8,10 +8,10 @@ using Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;div&gt; elements with an <c>asp-validation-summary</c> /// <see cref="ITagHelper"/> implementation targeting any HTML element with an <c>asp-validation-summary</c>
/// attribute. /// attribute.
/// </summary> /// </summary>
[TargetElement("div")] [TargetElement("div", Attributes = ValidationSummaryAttributeName)]
public class ValidationSummaryTagHelper : TagHelper public class ValidationSummaryTagHelper : TagHelper
{ {
private const string ValidationSummaryAttributeName = "asp-validation-summary"; private const string ValidationSummaryAttributeName = "asp-validation-summary";

View File

@ -69,7 +69,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-offset-2 col-md-10"> <div class="col-md-offset-2 col-md-10">
<input class="btn btn-default" type="submit" value="Create" /> <input type="submit" value="Create" class="btn btn-default" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -67,7 +67,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-offset-2 col-md-10"> <div class="col-md-offset-2 col-md-10">
<input class="btn btn-default" type="submit" value="Create" /> <input type="submit" value="Create" class="btn btn-default" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -617,62 +617,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName); Assert.Equal(expectedTagName, output.TagName);
} }
[Fact]
public async Task TagHelper_RestoresTypeAndValue_IfForNotBound()
{
// Arrange
var expectedAttributes = new Dictionary<string, string>
{
{ "class", "form-control" },
{ "type", "datetime" },
{ "value", "2014-10-15T23:24:19.000-7:00" },
};
var expectedPreContent = "original pre-content";
var expectedContent = "original content";
var expectedPostContent = "original post-content";
var expectedTagName = "input";
var tagHelperContext = new TagHelperContext(
allAttributes: new Dictionary<string, object>(),
items: new Dictionary<object, object>(),
uniqueId: "test",
getChildContentAsync: () =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var output = new TagHelperOutput(expectedTagName, expectedAttributes)
{
SelfClosing = false,
};
output.PreContent.SetContent(expectedPreContent);
output.Content.SetContent(expectedContent);
output.PostContent.SetContent(expectedPostContent);
var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider())
{
ValidationAttributes =
{
{ "valid", "from validation attributes" },
}
};
var tagHelper = GetTagHelper(htmlGenerator, model: null, propertyName: nameof(Model.Text));
tagHelper.For = null;
// Act
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Assert.False(output.SelfClosing);
Assert.Equal(expectedTagName, output.TagName);
}
private static InputTagHelper GetTagHelper( private static InputTagHelper GetTagHelper(
IHtmlGenerator htmlGenerator, IHtmlGenerator htmlGenerator,
object model, object model,
@ -689,38 +633,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
metadataProvider: metadataProvider); metadataProvider: metadataProvider);
} }
[Fact]
public async Task ProcessAsync_Throws_IfForNotBoundButFormatIs()
{
// Arrange
var contextAttributes = new Dictionary<string, object>();
var originalAttributes = new Dictionary<string, string>();
var expectedTagName = "select";
var expectedMessage = "Unable to format without a 'asp-for' expression for <input>. 'asp-format' must " +
"be null if 'asp-for' is null.";
var tagHelperContext = new TagHelperContext(
allAttributes: new Dictionary<string, object>(),
items: new Dictionary<object, object>(),
uniqueId: "test",
getChildContentAsync: () =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var output = new TagHelperOutput(expectedTagName, originalAttributes);
var tagHelper = new InputTagHelper
{
Format = "{0}",
};
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => tagHelper.ProcessAsync(tagHelperContext, output));
Assert.Equal(expectedMessage, exception.Message);
}
private static InputTagHelper GetTagHelper( private static InputTagHelper GetTagHelper(
IHtmlGenerator htmlGenerator, IHtmlGenerator htmlGenerator,
object container, object container,

View File

@ -228,59 +228,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName); Assert.Equal(expectedTagName, output.TagName);
} }
[Fact]
public async Task TagHelper_LeavesOutputUnchanged_IfForNotBound2()
{
// Arrange
var expectedAttributes = new Dictionary<string, string>
{
{ "class", "form-control" },
};
var expectedPreContent = "original pre-content";
var expectedContent = "original content";
var expectedPostContent = "original post-content";
var expectedTagName = "label";
var metadataProvider = new TestModelMetadataProvider();
var modelExplorer = metadataProvider
.GetModelExplorerForType(typeof(Model), model: null)
.GetExplorerForProperty(nameof(Model.Text));
var modelExpression = new ModelExpression(nameof(Model.Text), modelExplorer);
var tagHelper = new LabelTagHelper();
var tagHelperContext = new TagHelperContext(
allAttributes: new Dictionary<string, object>(),
items: new Dictionary<object, object>(),
uniqueId: "test",
getChildContentAsync: () =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});
var output = new TagHelperOutput(expectedTagName, expectedAttributes);
output.PreContent.SetContent(expectedPreContent);
output.Content.SetContent(expectedContent);
output.PostContent.SetContent(expectedPostContent);
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
Model model = null;
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
tagHelper.ViewContext = viewContext;
tagHelper.Generator = htmlGenerator;
// Act
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Assert.Equal(expectedTagName, output.TagName);
}
public class TagHelperOutputContent public class TagHelperOutputContent
{ {
public TagHelperOutputContent(string originalChildContent, public TagHelperOutputContent(string originalChildContent,