[Fixes #4047] SelectTagHelpers: asp-items shouldn't require/depend on asp-for.
This commit is contained in:
parent
fd3ee49987
commit
4123d83d26
|
|
@ -15,9 +15,11 @@ using Microsoft.AspNetCore.Razor.TagHelpers;
|
|||
namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITagHelper"/> implementation targeting <select> elements with an <c>asp-for</c> attribute.
|
||||
/// <see cref="ITagHelper"/> implementation targeting <select> 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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -307,6 +307,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
bool allowMultiple,
|
||||
object htmlAttributes);
|
||||
|
||||
/// <summary>
|
||||
/// Generates <optgroup> and <option> elements.
|
||||
/// </summary>
|
||||
/// <param name="optionLabel">Optional text for a default empty <option> element.</param>
|
||||
/// <param name="selectList">
|
||||
/// A collection of <see cref="SelectListItem"/> objects used to generate <optgroup> and <option>
|
||||
/// elements.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// An <see cref="IHtmlContent"/> instance for <optgroup> and <option> elements.
|
||||
/// </returns>
|
||||
IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList);
|
||||
|
||||
TagBuilder GenerateTextArea(
|
||||
ViewContext viewContext,
|
||||
ModelExplorer modelExplorer,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue