TagHelperOutput extension methods for class value manipulation (#6565)

* Added TagHelperOutput extension methods for class value manipulation
This commit is contained in:
Nick Chapsas 2017-09-13 19:58:10 +01:00 committed by Doug Bunting
parent 4f18d99d02
commit db397d812b
4 changed files with 362 additions and 0 deletions

View File

@ -164,6 +164,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
internal static string FormatFormActionTagHelper_CannotOverrideFormAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9)
=> string.Format(CultureInfo.CurrentCulture, GetString("FormActionTagHelper_CannotOverrideFormAction"), p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
/// <summary>
/// Value cannot contain whitespace.
/// </summary>
internal static string ArgumentCannotContainHtmlSpace
{
get => GetString("ArgumentCannotContainHtmlSpace");
}
/// <summary>
/// Value cannot contain whitespace.
/// </summary>
internal static string FormatArgumentCannotContainHtmlSpace()
=> GetString("ArgumentCannotContainHtmlSpace");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -150,4 +150,7 @@
<data name="FormActionTagHelper_CannotOverrideFormAction" xml:space="preserve">
<value>Cannot override the '{0}' attribute for &lt;{1}&gt;. &lt;{1}&gt; elements with a specified '{0}' must not have attributes starting with '{2}' or an '{3}', '{4}', '{5}', '{6}', '{7}', '{8}' or '{9}' attribute.</value>
</data>
<data name="ArgumentCannotContainHtmlSpace" xml:space="preserve">
<value>Value cannot contain HTML space characters.</value>
</data>
</root>

View File

@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// </summary>
public static class TagHelperOutputExtensions
{
private static readonly char[] SpaceChars = { '\u0020', '\u0009', '\u000A', '\u000C', '\u000D' };
/// <summary>
/// Copies a user-provided attribute from <paramref name="context"/>'s
/// <see cref="TagHelperContext.AllAttributes"/> to <paramref name="tagHelperOutput"/>'s
@ -154,6 +156,167 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
}
/// <summary>
/// Adds the given <paramref name="classValue"/> to the <paramref name="tagHelperOutput"/>'s
/// <see cref="TagHelperOutput.Attributes"/>.
/// </summary>
/// <param name="tagHelperOutput">The <see cref="TagHelperOutput"/> this method extends.</param>
/// <param name="classValue">The class value to add.</param>
/// <param name="htmlEncoder">The current HTML encoder.</param>
public static void AddClass(
this TagHelperOutput tagHelperOutput,
string classValue,
HtmlEncoder htmlEncoder)
{
if (tagHelperOutput == null)
{
throw new ArgumentNullException(nameof(tagHelperOutput));
}
if (string.IsNullOrEmpty(classValue))
{
return;
}
var encodedSpaceChars = SpaceChars.Where(x => !x.Equals('\u0020')).Select(x => htmlEncoder.Encode(x.ToString())).ToArray();
if (SpaceChars.Any(classValue.Contains) || encodedSpaceChars.Any(value => classValue.IndexOf(value, StringComparison.Ordinal) >= 0))
{
throw new ArgumentException(Resources.ArgumentCannotContainHtmlSpace, nameof(classValue));
}
if (!tagHelperOutput.Attributes.TryGetAttribute("class", out TagHelperAttribute classAttribute))
{
tagHelperOutput.Attributes.Add("class", classValue);
}
else
{
var currentClassValue = ExtractClassValue(classAttribute, htmlEncoder);
var encodedClassValue = htmlEncoder.Encode(classValue);
if (string.Equals(currentClassValue, encodedClassValue, StringComparison.Ordinal))
{
return;
}
var arrayOfClasses = currentClassValue.Split(SpaceChars, StringSplitOptions.RemoveEmptyEntries)
.SelectMany(perhapsEncoded => perhapsEncoded.Split(encodedSpaceChars, StringSplitOptions.RemoveEmptyEntries))
.ToArray();
if (arrayOfClasses.Contains(encodedClassValue, StringComparer.Ordinal))
{
return;
}
var newClassAttribute = new TagHelperAttribute(
classAttribute.Name,
new HtmlString($"{currentClassValue} {encodedClassValue}"),
classAttribute.ValueStyle);
tagHelperOutput.Attributes.SetAttribute(newClassAttribute);
}
}
/// <summary>
/// Removes the given <paramref name="classValue"/> from the <paramref name="tagHelperOutput"/>'s
/// <see cref="TagHelperOutput.Attributes"/>.
/// </summary>
/// <param name="tagHelperOutput">The <see cref="TagHelperOutput"/> this method extends.</param>
/// <param name="classValue">The class value to remove.</param>
/// <param name="htmlEncoder">The current HTML encoder.</param>
public static void RemoveClass(
this TagHelperOutput tagHelperOutput,
string classValue,
HtmlEncoder htmlEncoder)
{
if (tagHelperOutput == null)
{
throw new ArgumentNullException(nameof(tagHelperOutput));
}
var encodedSpaceChars = SpaceChars.Where(x => !x.Equals('\u0020')).Select(x => htmlEncoder.Encode(x.ToString())).ToArray();
if (SpaceChars.Any(classValue.Contains) || encodedSpaceChars.Any(value => classValue.IndexOf(value, StringComparison.Ordinal) >= 0))
{
throw new ArgumentException(Resources.ArgumentCannotContainHtmlSpace, nameof(classValue));
}
if (!tagHelperOutput.Attributes.TryGetAttribute("class", out TagHelperAttribute classAttribute))
{
return;
}
var currentClassValue = ExtractClassValue(classAttribute, htmlEncoder);
if (string.IsNullOrEmpty(currentClassValue))
{
return;
}
var encodedClassValue = htmlEncoder.Encode(classValue);
if (string.Equals(currentClassValue, encodedClassValue, StringComparison.Ordinal))
{
tagHelperOutput.Attributes.Remove(tagHelperOutput.Attributes["class"]);
return;
}
if (!currentClassValue.Contains(encodedClassValue))
{
return;
}
var listOfClasses = currentClassValue.Split(SpaceChars, StringSplitOptions.RemoveEmptyEntries)
.SelectMany(perhapsEncoded => perhapsEncoded.Split(encodedSpaceChars, StringSplitOptions.RemoveEmptyEntries))
.ToList();
if (!listOfClasses.Contains(encodedClassValue))
{
return;
}
listOfClasses.RemoveAll(x => x.Equals(encodedClassValue));
if (listOfClasses.Any())
{
var joinedClasses = new HtmlString(string.Join(" ", listOfClasses));
tagHelperOutput.Attributes.SetAttribute(classAttribute.Name, joinedClasses);
}
else
{
tagHelperOutput.Attributes.Remove(tagHelperOutput.Attributes["class"]);
}
}
private static string ExtractClassValue(
TagHelperAttribute classAttribute,
HtmlEncoder htmlEncoder)
{
string extractedClassValue;
switch (classAttribute.Value)
{
case string valueAsString:
extractedClassValue = htmlEncoder.Encode(valueAsString);
break;
case HtmlString valueAsHtmlString:
extractedClassValue = valueAsHtmlString.Value;
break;
case IHtmlContent htmlContent:
using (var stringWriter = new StringWriter())
{
htmlContent.WriteTo(stringWriter, htmlEncoder);
extractedClassValue = stringWriter.ToString();
}
break;
default:
extractedClassValue = htmlEncoder.Encode(classAttribute.Value?.ToString());
break;
}
var currentClassValue = extractedClassValue ?? string.Empty;
return currentClassValue;
}
private static void CopyHtmlAttribute(
int allAttributeIndex,
TagHelperOutput tagHelperOutput,

View File

@ -6,9 +6,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers.Testing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.WebEncoders.Testing;
using Microsoft.AspNetCore.Mvc.TestCommon;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
@ -968,5 +971,184 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
attribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("for"));
Assert.Equal(expectedBuilderAttribute.Value, attribute.Value);
}
[Fact]
public void Single_AddClass()
{
// Arrange
var expectedValue = "class=\"HtmlEncode[[btn]]\"";
var htmlEncoder = new HtmlTestEncoder();
var tagHelperOutput = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
// Act
tagHelperOutput.AddClass("btn", htmlEncoder);
// Assert
var classAttribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("class"));
Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString(classAttribute));
}
[Fact]
public void Multiple_AddClass()
{
// Arrange
var expectedValue = "class=\"HtmlEncode[[btn]] HtmlEncode[[btn-primary]]\"";
var htmlEncoder = new HtmlTestEncoder();
var tagHelperOutput = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
// Act
tagHelperOutput.AddClass("btn", htmlEncoder);
tagHelperOutput.AddClass("btn-primary", htmlEncoder);
// Assert
var classAttribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("class"));
Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString(classAttribute));
}
[Fact]
public void Multiple_AddClass_RemoveClass_RemovesAllButOne()
{
// Arrange
var expectedValue = "class=\"HtmlEncode[[btn]]\"";
var htmlEncoder = new HtmlTestEncoder();
var tagHelperOutput = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
tagHelperOutput.AddClass("btn", htmlEncoder);
tagHelperOutput.AddClass("btn-success", htmlEncoder);
tagHelperOutput.AddClass("btn-primary", htmlEncoder);
// Act
tagHelperOutput.RemoveClass("btn-success", htmlEncoder);
tagHelperOutput.RemoveClass("btn-primary", htmlEncoder);
// Assert
var classAttribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("class"));
Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString(classAttribute));
}
[Fact]
public void AddClass_RemoveClass_ContainsSpace()
{
// Arrange
var classValue = "btn btn-success";
var expected = new ArgumentException(Resources.ArgumentCannotContainHtmlSpace, nameof(classValue)).Message;
var htmlEncoder = new HtmlTestEncoder();
var tagHelperOutput = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
// Act and Assert
var exceptionAdd = Assert.Throws<ArgumentException>(() => tagHelperOutput.AddClass(classValue, htmlEncoder));
var exceptionRemove = Assert.Throws<ArgumentException>(() => tagHelperOutput.RemoveClass(classValue, htmlEncoder));
Assert.Equal(expected, exceptionAdd.Message);
Assert.Equal(expected, exceptionRemove.Message);
}
[Fact]
public void Single_RemoveClass_RemovesDuplicates_RemovesEntirely()
{
// Arrange
var htmlEncoder = new HtmlTestEncoder();
var tagHelperOutput = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
tagHelperOutput.Attributes.SetAttribute("class", new HtmlString("HtmlEncode[[btn]] HtmlEncode[[btn]]"));
// Act
tagHelperOutput.RemoveClass("btn", htmlEncoder);
// Assert
var classAttribute = tagHelperOutput.Attributes["class"];
Assert.Null(classAttribute);
}
[Fact]
public void Single_RemoveClass_RemovesDuplicates()
{
// Arrange
var expectedValue = "class=\"HtmlEncode[[btn-primary]]\"";
var htmlEncoder = new HtmlTestEncoder();
var tagHelperOutput = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
tagHelperOutput.Attributes.SetAttribute("class", new HtmlString("HtmlEncode[[btn]] HtmlEncode[[btn-primary]] HtmlEncode[[btn]]"));
// Act
tagHelperOutput.RemoveClass("btn", htmlEncoder);
// Assert
var classAttribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("class"));
Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString(classAttribute));
}
[Fact]
public void Single_RemoveClass_RemovesEntirely()
{
var htmlEncoder = new HtmlTestEncoder();
var tagHelperOutput = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
tagHelperOutput.Attributes.SetAttribute("class", new HtmlString("HtmlEncode[[btn]]"));
// Act
tagHelperOutput.RemoveClass("btn", htmlEncoder);
// Assert
var classAttribute = tagHelperOutput.Attributes["class"];
Assert.Null(classAttribute);
}
[Fact]
public void Single_RemoveClass()
{
// Arrange
var expectedValue = "class=\"HtmlEncode[[btn]]\"";
var htmlEncoder = new HtmlTestEncoder();
var tagHelperOutput = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
tagHelperOutput.Attributes.SetAttribute("class", new HtmlString("HtmlEncode[[btn]] HtmlEncode[[btn-primary]]"));
// Act
tagHelperOutput.RemoveClass("btn-primary", htmlEncoder);
// Assert
var classAttribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("class"));
Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString(classAttribute));
}
}
}