Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
[Fixes #4047] SelectTagHelpers: asp-items shouldn't require/depend on…
Browse files Browse the repository at this point in the history
… asp-for.
  • Loading branch information
kichalla committed Feb 11, 2016
1 parent a493a97 commit 5cb0979
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 57 deletions.
14 changes: 14 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/SelectTagHelper.cs
Expand Up @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// <see cref="ITagHelper"/> implementation targeting &lt;select&gt; elements with an <c>asp-for</c> attribute.
/// </summary>
[HtmlTargetElement("select", Attributes = ForAttributeName)]
[HtmlTargetElement("select", Attributes = ItemsAttributeName)]
public class SelectTagHelper : TagHelper
{
private const string ForAttributeName = "asp-for";
Expand Down Expand Up @@ -70,6 +71,12 @@ public override void Init(TagHelperContext context)
throw new ArgumentNullException(nameof(context));
}

if (Items != null && For == null)
{
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)
Expand Down Expand Up @@ -119,6 +126,13 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
// Ensure GenerateSelect() _never_ looks anything up in ViewData.
var items = Items ?? Enumerable.Empty<SelectListItem>();

if (For == null)
{
var options = Generator.GenerateSelectGroupsAndOptions(optionLabel: null, selectList: items);
output.PostContent.AppendHtml(options);
return;
}

var tagBuilder = Generator.GenerateSelect(
ViewContext,
For.ModelExplorer,
Expand Down
Expand Up @@ -560,7 +560,7 @@ public virtual IHtmlContent GenerateAntiforgery(ViewContext viewContext)
}

// Convert each ListItem to an <option> tag and wrap them with <optgroup> if requested.
var listItemBuilder = GenerateGroupsAndOptions(optionLabel, selectList);
var listItemBuilder = GenerateSelectGroupsAndOptions(optionLabel, selectList);

var tagBuilder = new TagBuilder("select");
tagBuilder.InnerHtml.SetContent(listItemBuilder);
Expand All @@ -587,6 +587,63 @@ public virtual IHtmlContent GenerateAntiforgery(ViewContext viewContext)
return tagBuilder;
}

/// <inheritdoc />
public IHtmlContent GenerateSelectGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList)
{
var listItemBuilder = new HtmlContentBuilder();

// Make optionLabel the first item that gets rendered.
if (optionLabel != null)
{
listItemBuilder.AppendLine(GenerateOption(new SelectListItem()
{
Text = optionLabel,
Value = string.Empty,
Selected = false,
}));
}

// Group items in the SelectList if requested.
// Treat each item with Group == null as a member of a unique group
// so they are added according to the original order.
var groupedSelectList = selectList.GroupBy<SelectListItem, int>(
item => (item.Group == null) ? item.GetHashCode() : item.Group.GetHashCode());
foreach (var group in groupedSelectList)
{
var optGroup = group.First().Group;
if (optGroup != null)
{
var groupBuilder = new TagBuilder("optgroup");
if (optGroup.Name != null)
{
groupBuilder.MergeAttribute("label", optGroup.Name);
}

if (optGroup.Disabled)
{
groupBuilder.MergeAttribute("disabled", "disabled");
}

groupBuilder.InnerHtml.AppendLine();
foreach (var item in group)
{
groupBuilder.InnerHtml.AppendLine(GenerateOption(item));
}

listItemBuilder.AppendLine(groupBuilder);
}
else
{
foreach (var item in group)
{
listItemBuilder.AppendLine(GenerateOption(item));
}
}
}

return listItemBuilder;
}

/// <inheritdoc />
public virtual TagBuilder GenerateTextArea(
ViewContext viewContext,
Expand Down Expand Up @@ -1457,62 +1514,6 @@ private static string GetInputTypeString(InputType inputType)
return newSelectList;
}

private IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList)
{
var listItemBuilder = new HtmlContentBuilder();

// Make optionLabel the first item that gets rendered.
if (optionLabel != null)
{
listItemBuilder.AppendLine(GenerateOption(new SelectListItem()
{
Text = optionLabel,
Value = string.Empty,
Selected = false,
}));
}

// Group items in the SelectList if requested.
// Treat each item with Group == null as a member of a unique group
// so they are added according to the original order.
var groupedSelectList = selectList.GroupBy<SelectListItem, int>(
item => (item.Group == null) ? item.GetHashCode() : item.Group.GetHashCode());
foreach (var group in groupedSelectList)
{
var optGroup = group.First().Group;
if (optGroup != null)
{
var groupBuilder = new TagBuilder("optgroup");
if (optGroup.Name != null)
{
groupBuilder.MergeAttribute("label", optGroup.Name);
}

if (optGroup.Disabled)
{
groupBuilder.MergeAttribute("disabled", "disabled");
}

groupBuilder.InnerHtml.AppendLine();
foreach (var item in group)
{
groupBuilder.InnerHtml.AppendLine(GenerateOption(item));
}

listItemBuilder.AppendLine(groupBuilder);
}
else
{
foreach (var item in group)
{
listItemBuilder.AppendLine(GenerateOption(item));
}
}
}

return listItemBuilder;
}

private IHtmlContent GenerateOption(SelectListItem item)
{
var tagBuilder = GenerateOption(item, item.Text);
Expand Down
Expand Up @@ -307,6 +307,8 @@ public interface IHtmlGenerator
bool allowMultiple,
object htmlAttributes);

IHtmlContent GenerateSelectGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList);

TagBuilder GenerateTextArea(
ViewContext viewContext,
ModelExplorer modelExplorer,
Expand Down
Expand Up @@ -356,6 +356,96 @@ public class SelectTagHelperTest
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 = string.Join(
Environment.NewLine,
selectItems.Select(i => $"<option>HtmlEncode[[{i.Text}]]</option>")) + Environment.NewLine;

var expectedPreContent = "original pre-content";
var expectedContent = "original content";
var expectedPostContent = originalPostContent + expectedOptions;
var expectedTagName = "select";

var metadataProvider = new TestModelMetadataProvider();

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 htmlGenerator = new TestableHtmlGenerator(metadataProvider)
{
ValidationAttributes =
{
{ "valid", "from validation attributes" },
}
};
var viewContext = TestableHtmlGenerator.GetViewContext(null, htmlGenerator, metadataProvider);

var items = new SelectList(new[] { "0", "1", "2", "3", "4"});
var savedDisabled = items.Select(item => item.Disabled).ToList();
var savedGroup = items.Select(item => item.Group).ToList();
var savedSelected = items.Select(item => item.Selected).ToList();
var savedText = items.Select(item => item.Text).ToList();
var savedValue = items.Select(item => item.Value).ToList();

var tagHelper = new SelectTagHelper(htmlGenerator)
{
For = null,
Items = items,
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);

Assert.Single(
tagHelperContext.Items,
entry => (Type)entry.Key == typeof(SelectTagHelper));

Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
Assert.Equal(savedGroup, items.Select(item => item.Group));
Assert.Equal(savedSelected, items.Select(item => item.Selected));
Assert.Equal(savedText, items.Select(item => item.Text));
Assert.Equal(savedValue, items.Select(item => item.Value));
}

[Theory]
[MemberData(nameof(GeneratesExpectedDataSet))]
public async Task ProcessAsyncInTemplate_WithItems_GeneratesExpectedOutput_DoesNotChangeSelectList(
Expand Down

0 comments on commit 5cb0979

Please sign in to comment.