[Fixes #4047] SelectTagHelpers: asp-items shouldn't require/depend on asp-for.

This commit is contained in:
Kiran Challa 2016-02-05 15:11:51 -08:00
parent fd3ee49987
commit 4123d83d26
4 changed files with 106 additions and 5 deletions

View File

@ -15,9 +15,11 @@ using Microsoft.AspNetCore.Razor.TagHelpers;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;select&gt; elements with an <c>asp-for</c> attribute.
/// <see cref="ITagHelper"/> implementation targeting &lt;select&gt; elements with <c>asp-for</c> and/or
/// <c>asp-items</c> attribute(s).
/// </summary>
[HtmlTargetElement("select", Attributes = ForAttributeName)]
[HtmlTargetElement("select", Attributes = ItemsAttributeName)]
public class SelectTagHelper : TagHelper
{
private const string ForAttributeName = "asp-for";
@ -70,6 +72,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(context));
}
if (For == null)
{
// Informs contained elements that they're running within a targeted <select/> element.
context.Items[typeof(SelectTagHelper)] = null;
return;
}
// Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient.
// IHtmlGenerator will enforce name requirements.
if (For.Metadata == null)
@ -101,9 +110,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <see cref="Items"/> is non-<c>null</c> but <see cref="For"/> is <c>null</c>.
/// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
@ -119,6 +125,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
// Ensure GenerateSelect() _never_ looks anything up in ViewData.
var items = Items ?? Enumerable.Empty<SelectListItem>();
if (For == null)
{
var options = Generator.GenerateGroupsAndOptions(optionLabel: null, selectList: items);
output.PostContent.AppendHtml(options);
return;
}
var tagBuilder = Generator.GenerateSelect(
ViewContext,
For.ModelExplorer,

View File

@ -1457,7 +1457,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return newSelectList;
}
private IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList)
/// <inheritdoc />
public IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList)
{
var listItemBuilder = new HtmlContentBuilder();

View File

@ -307,6 +307,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
bool allowMultiple,
object htmlAttributes);
/// <summary>
/// Generates &lt;optgroup&gt; and &lt;option&gt; elements.
/// </summary>
/// <param name="optionLabel">Optional text for a default empty &lt;option&gt; element.</param>
/// <param name="selectList">
/// A collection of <see cref="SelectListItem"/> objects used to generate &lt;optgroup&gt; and &lt;option&gt;
/// elements.
/// </param>
/// <returns>
/// An <see cref="IHtmlContent"/> instance for &lt;optgroup&gt; and &lt;option&gt; elements.
/// </returns>
IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList);
TagBuilder GenerateTextArea(
ViewContext viewContext,
ModelExplorer modelExplorer,

View File

@ -356,6 +356,80 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal(savedValue, items.Select(item => item.Value));
}
[Fact]
public async Task ProcessAsync_WithItems_AndNoModelExpression_GeneratesExpectedOutput()
{
// Arrange
var originalAttributes = new TagHelperAttributeList
{
{ "class", "form-control" },
};
var originalPostContent = "original content";
var expectedAttributes = new TagHelperAttributeList(originalAttributes);
var selectItems = new SelectList(Enumerable.Range(0, 5));
var expectedOptions = "<option>HtmlEncode[[0]]</option>" + Environment.NewLine
+ "<option>HtmlEncode[[1]]</option>" + Environment.NewLine
+ "<option>HtmlEncode[[2]]</option>" + Environment.NewLine
+ "<option>HtmlEncode[[3]]</option>" + Environment.NewLine
+ "<option>HtmlEncode[[4]]</option>" + Environment.NewLine;
var expectedPreContent = "original pre-content";
var expectedContent = "original content";
var expectedPostContent = originalPostContent + expectedOptions;
var expectedTagName = "select";
var tagHelperContext = new TagHelperContext(
allAttributes: new TagHelperAttributeList(
Enumerable.Empty<TagHelperAttribute>()),
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
expectedTagName,
originalAttributes,
getChildContentAsync: (useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.AppendHtml("Something");
return Task.FromResult<TagHelperContent>(tagHelperContent);
})
{
TagMode = TagMode.SelfClosing,
};
output.PreContent.AppendHtml(expectedPreContent);
output.Content.AppendHtml(expectedContent);
output.PostContent.AppendHtml(originalPostContent);
var metadataProvider = new TestModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var viewContext = TestableHtmlGenerator.GetViewContext(
model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var tagHelper = new SelectTagHelper(htmlGenerator)
{
Items = selectItems,
ViewContext = viewContext,
};
// Act
tagHelper.Init(tagHelperContext);
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.Equal(TagMode.SelfClosing, output.TagMode);
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, HtmlContentUtilities.HtmlContentToString(output.PostContent));
Assert.Equal(expectedTagName, output.TagName);
var kvp = Assert.Single(tagHelperContext.Items);
Assert.Equal(typeof(SelectTagHelper), kvp.Key);
Assert.Null(kvp.Value);
}
[Theory]
[MemberData(nameof(GeneratesExpectedDataSet))]
public async Task ProcessAsyncInTemplate_WithItems_GeneratesExpectedOutput_DoesNotChangeSelectList(