parent
30f25fec99
commit
2d32420f01
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.Razor.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITagHelper"/> implementation targeting <option> elements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This <see cref="ITagHelper"/> works in conjunction with <see cref="SelectTagHelper"/>. It has
|
||||
/// <see cref="ContentBehavior.Modify"/> in order to read element's content but does not modify that content. The
|
||||
/// only modification it makes is to add a <c>selected</c> attribute in some cases.
|
||||
/// </remarks>
|
||||
[ContentBehavior(ContentBehavior.Modify)]
|
||||
public class OptionTagHelper : TagHelper
|
||||
{
|
||||
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
|
||||
[Activate]
|
||||
protected internal IHtmlGenerator Generator { get; set; }
|
||||
|
||||
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
|
||||
[Activate]
|
||||
protected internal ViewContext ViewContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that this <option> is pre-selected.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Passed through to the generated HTML in all cases.
|
||||
/// </remarks>
|
||||
public string Selected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a value for the <option> element.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Passed through to the generated HTML in all cases.
|
||||
/// </remarks>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Does nothing unless <see cref="FormContext.FormData"/> contains a
|
||||
/// <see cref="SelectTagHelper.SelectedValuesFormDataKey"/> entry and that entry is a non-empty
|
||||
/// <see cref="ICollection{string}"/> instance. Also does nothing if the associated <option> is already
|
||||
/// selected.
|
||||
/// </remarks>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
// Pass through attributes that are also well-known HTML attributes.
|
||||
if (Value != null)
|
||||
{
|
||||
output.CopyHtmlAttribute(nameof(Value), context);
|
||||
}
|
||||
|
||||
if (Selected != null)
|
||||
{
|
||||
// This <option/> will always be selected.
|
||||
output.CopyHtmlAttribute(nameof(Selected), context);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Is this <option/> element a child of a <select/> element the SelectTagHelper targeted?
|
||||
object formDataEntry;
|
||||
ViewContext.FormContext.FormData.TryGetValue(
|
||||
SelectTagHelper.SelectedValuesFormDataKey,
|
||||
out formDataEntry);
|
||||
|
||||
// ... And did the SelectTagHelper determine any selected values?
|
||||
var selectedValues = formDataEntry as ICollection<string>;
|
||||
if (selectedValues != null && selectedValues.Count != 0)
|
||||
{
|
||||
// Encode all selected values for comparison with element content.
|
||||
var encodedValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var selectedValue in selectedValues)
|
||||
{
|
||||
encodedValues.Add(Generator.Encode(selectedValue));
|
||||
}
|
||||
|
||||
// Select this <option/> element if value attribute or content matches a selected value. Callers
|
||||
// encode values as-needed before setting TagHelperOutput.Content. But TagHelperOutput itself
|
||||
// encodes attribute values later, when GenerateStartTag() is called.
|
||||
var text = output.Content;
|
||||
var selected = (Value != null) ? selectedValues.Contains(Value) : encodedValues.Contains(text);
|
||||
if (selected)
|
||||
{
|
||||
output.Attributes.Add("selected", "selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
public class OptionTagHelperTest
|
||||
{
|
||||
// Original content, selected attribute, value attribute, selected values (to place in FormContext.FormData)
|
||||
// and expected output (concatenation of TagHelperOutput generations).
|
||||
public static TheoryData<string, string, string, ICollection<string>, string> GeneratesExpectedDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string, string, string, ICollection<string>, string>
|
||||
{
|
||||
{ null, null, null, null,
|
||||
"<not-option label=\"my-label\"></not-option>" },
|
||||
{ null, string.Empty, "value", null,
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"\"></not-option>" },
|
||||
{ null, "selected", "value", null,
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"selected\"></not-option>" },
|
||||
{ null, null, "value", new string[0],
|
||||
"<not-option label=\"my-label\" value=\"value\"></not-option>" },
|
||||
{ null, null, "value", new [] { string.Empty, },
|
||||
"<not-option label=\"my-label\" value=\"value\"></not-option>" },
|
||||
{ null, string.Empty, "value", new [] { string.Empty, },
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"\"></not-option>" },
|
||||
{ null, null, "value", new [] { "value", },
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"selected\"></not-option>" },
|
||||
{ null, null, "value", new [] { string.Empty, "value", },
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"selected\"></not-option>" },
|
||||
|
||||
{ string.Empty, null, null, null,
|
||||
"<not-option label=\"my-label\"></not-option>" },
|
||||
{ string.Empty, string.Empty, null, null,
|
||||
"<not-option label=\"my-label\" selected=\"\"></not-option>" },
|
||||
{ string.Empty, "selected", null, null,
|
||||
"<not-option label=\"my-label\" selected=\"selected\"></not-option>" },
|
||||
{ string.Empty, null, null, new string[0],
|
||||
"<not-option label=\"my-label\"></not-option>" },
|
||||
{ string.Empty, null, null, new [] { string.Empty, },
|
||||
"<not-option label=\"my-label\" selected=\"selected\"></not-option>" },
|
||||
{ string.Empty, string.Empty, null, new [] { string.Empty, },
|
||||
"<not-option label=\"my-label\" selected=\"\"></not-option>" },
|
||||
{ string.Empty, null, null, new [] { "text", },
|
||||
"<not-option label=\"my-label\"></not-option>" },
|
||||
{ string.Empty, null, null, new [] { string.Empty, "text", },
|
||||
"<not-option label=\"my-label\" selected=\"selected\"></not-option>" },
|
||||
|
||||
{ "text", null, null, null,
|
||||
"<not-option label=\"my-label\">text</not-option>" },
|
||||
{ "text", string.Empty, null, null,
|
||||
"<not-option label=\"my-label\" selected=\"\">text</not-option>" },
|
||||
{ "text", "selected", null, null,
|
||||
"<not-option label=\"my-label\" selected=\"selected\">text</not-option>" },
|
||||
{ "text", null, null, new string[0],
|
||||
"<not-option label=\"my-label\">text</not-option>" },
|
||||
{ "text", null, null, new [] { string.Empty, },
|
||||
"<not-option label=\"my-label\">text</not-option>" },
|
||||
{ "text", null, null, new [] { "text", },
|
||||
"<not-option label=\"my-label\" selected=\"selected\">text</not-option>" },
|
||||
{ "text", string.Empty, null, new [] { "text", },
|
||||
"<not-option label=\"my-label\" selected=\"\">text</not-option>" },
|
||||
{ "text", null, null, new [] { string.Empty, "text", },
|
||||
"<not-option label=\"my-label\" selected=\"selected\">text</not-option>" },
|
||||
|
||||
{ "text", string.Empty, "value", null,
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"\">text</not-option>" },
|
||||
{ "text", "selected", "value", null,
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"selected\">text</not-option>" },
|
||||
{ "text", null, "value", new string[0],
|
||||
"<not-option label=\"my-label\" value=\"value\">text</not-option>" },
|
||||
{ "text", null, "value", new [] { string.Empty, },
|
||||
"<not-option label=\"my-label\" value=\"value\">text</not-option>" },
|
||||
{ "text", string.Empty, "value", new [] { string.Empty, },
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"\">text</not-option>" },
|
||||
{ "text", null, "value", new [] { "text", },
|
||||
"<not-option label=\"my-label\" value=\"value\">text</not-option>" },
|
||||
{ "text", null, "value", new [] { "value", },
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"selected\">text</not-option>" },
|
||||
{ "text", null, "value", new [] { string.Empty, "value", },
|
||||
"<not-option label=\"my-label\" value=\"value\" selected=\"selected\">text</not-option>" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Original content, selected attribute, value attribute, selected values (to place in FormContext.FormData)
|
||||
// and expected output (concatenation of TagHelperOutput generations). Excludes non-null selected attribute,
|
||||
// null selected values, and empty selected values cases.
|
||||
public static IEnumerable<object []> DoesNotUseGeneratorDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return GeneratesExpectedDataSet.Where(
|
||||
entry => (entry[1] != null || entry[3] == null || (entry[3] as ICollection<string>).Count == 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Original content, selected attribute, value attribute, selected values (to place in FormContext.FormData)
|
||||
// and expected output (concatenation of TagHelperOutput generations). Excludes non-null selected attribute
|
||||
// cases.
|
||||
public static IEnumerable<object[]> DoesNotUseViewContextDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return GeneratesExpectedDataSet.Where(entry => entry[1] != null);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GeneratesExpectedDataSet))]
|
||||
public async Task ProcessAsync_GeneratesExpectedOutput(
|
||||
string originalContent,
|
||||
string selected,
|
||||
string value,
|
||||
ICollection<string> selectedValues,
|
||||
string expectedOutput)
|
||||
{
|
||||
// Arrange
|
||||
var originalAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "label", "my-label" },
|
||||
};
|
||||
var originalTagName = "not-option";
|
||||
|
||||
var contextAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "label", "my-label" },
|
||||
{ "selected", selected },
|
||||
{ "value", value },
|
||||
};
|
||||
var tagHelperContext = new TagHelperContext(contextAttributes);
|
||||
var output = new TagHelperOutput(originalTagName, originalAttributes, originalContent)
|
||||
{
|
||||
SelfClosing = false,
|
||||
};
|
||||
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(
|
||||
model: null,
|
||||
htmlGenerator: htmlGenerator,
|
||||
metadataProvider: metadataProvider);
|
||||
viewContext.FormContext.FormData[SelectTagHelper.SelectedValuesFormDataKey] = selectedValues;
|
||||
var tagHelper = new OptionTagHelper
|
||||
{
|
||||
Generator = htmlGenerator,
|
||||
Selected = selected,
|
||||
Value = value,
|
||||
ViewContext = viewContext,
|
||||
};
|
||||
|
||||
// Act
|
||||
await tagHelper.ProcessAsync(tagHelperContext, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
expectedOutput,
|
||||
output.GenerateStartTag() + output.GenerateContent() + output.GenerateEndTag());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DoesNotUseGeneratorDataSet))]
|
||||
public async Task ProcessAsync_DoesNotUseGenerator_IfSelectedNullOrNoSelectedValues(
|
||||
string originalContent,
|
||||
string selected,
|
||||
string value,
|
||||
ICollection<string> selectedValues,
|
||||
string ignored)
|
||||
{
|
||||
// Arrange
|
||||
var originalAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "label", "my-label" },
|
||||
};
|
||||
var originalTagName = "not-option";
|
||||
|
||||
var contextAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "label", "my-label" },
|
||||
{ "selected", selected },
|
||||
{ "value", value },
|
||||
};
|
||||
var tagHelperContext = new TagHelperContext(contextAttributes);
|
||||
var output = new TagHelperOutput(originalTagName, originalAttributes, originalContent)
|
||||
{
|
||||
SelfClosing = false,
|
||||
};
|
||||
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(
|
||||
model: null,
|
||||
htmlGenerator: htmlGenerator,
|
||||
metadataProvider: metadataProvider);
|
||||
viewContext.FormContext.FormData[SelectTagHelper.SelectedValuesFormDataKey] = selectedValues;
|
||||
var tagHelper = new OptionTagHelper
|
||||
{
|
||||
Selected = selected,
|
||||
Value = value,
|
||||
ViewContext = viewContext,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
// Tag helper would throw an NRE if it used Generator value.
|
||||
await Assert.DoesNotThrowAsync(() => tagHelper.ProcessAsync(tagHelperContext, output));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DoesNotUseViewContextDataSet))]
|
||||
public async Task ProcessAsync_DoesNotUseViewContext_IfSelectedNotNull(
|
||||
string originalContent,
|
||||
string selected,
|
||||
string value,
|
||||
ICollection<string> ignoredValues,
|
||||
string ignoredOutput)
|
||||
{
|
||||
// Arrange
|
||||
var originalAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "label", "my-label" },
|
||||
};
|
||||
var originalTagName = "not-option";
|
||||
|
||||
var contextAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "label", "my-label" },
|
||||
{ "selected", selected },
|
||||
{ "value", value },
|
||||
};
|
||||
var tagHelperContext = new TagHelperContext(contextAttributes);
|
||||
var output = new TagHelperOutput(originalTagName, originalAttributes, originalContent)
|
||||
{
|
||||
SelfClosing = false,
|
||||
};
|
||||
|
||||
var tagHelper = new OptionTagHelper
|
||||
{
|
||||
Selected = selected,
|
||||
Value = value,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
// Tag helper would throw an NRE if it used ViewContext or Generator values.
|
||||
await Assert.DoesNotThrowAsync(() => tagHelper.ProcessAsync(tagHelperContext, output));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue