TagHelperOutput extension methods for class value manipulation (#6565)
* Added TagHelperOutput extension methods for class value manipulation
This commit is contained in:
parent
4f18d99d02
commit
db397d812b
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -150,4 +150,7 @@
|
|||
<data name="FormActionTagHelper_CannotOverrideFormAction" xml:space="preserve">
|
||||
<value>Cannot override the '{0}' attribute for <{1}>. <{1}> 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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue