Add extensions for TagHelperOutput to copy and merge from TagBuilder.

- Added tests to validate functionality of all TagHelperOutputExtension methods.

#1319
This commit is contained in:
N. Taylor Mullen 2014-10-13 13:50:42 -07:00 committed by Doug Bunting
parent 20cc294c70
commit 63f8f7de6d
2 changed files with 401 additions and 0 deletions

View File

@ -0,0 +1,118 @@
// 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 System.Linq;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// Utility related extensions for <see cref="TagHelperOutput"/>.
/// </summary>
public static class TagHelperOutputExtensions
{
/// <summary>
/// Copies a user-provided attribute from <paramref name="context"/>'s
/// <see cref="TagHelperContext.AllAttributes"/> to <paramref name="tagHelperOutput"/>'s
/// <see cref="TagHelperOutput.Attributes"/>.
/// </summary>
/// <param name="tagHelperOutput">The <see cref="TagHelperOutput"/> this method extends.</param>
/// <param name="attributeName">The name of the bound attribute.</param>
/// <param name="context">The <see cref="TagHelperContext"/>.</param>
/// <remarks>Only copies the attribute if <paramref name="tagHelperOutput"/>'s
/// <see cref="TagHelperOutput.Attributes"/> does not contain an attribute with the given
/// <paramref name="attributeName"/></remarks>
public static void CopyHtmlAttribute(this TagHelperOutput tagHelperOutput,
string attributeName,
TagHelperContext context)
{
// We look for the original attribute so we can restore the exact attribute name the user typed.
var entry = context.AllAttributes.First(attribute =>
attribute.Key.Equals(attributeName, StringComparison.OrdinalIgnoreCase));
if (!tagHelperOutput.Attributes.ContainsKey(entry.Key))
{
tagHelperOutput.Attributes.Add(entry.Key, entry.Value.ToString());
}
}
/// <summary>
/// Returns all attributes from <paramref name="tagHelperOutput"/>'s
/// <see cref="TagHelperOutput.Attributes"/> that have the given <paramref name="prefix"/>.
/// </summary>
/// <param name="tagHelperOutput">The <see cref="TagHelperOutput"/> this method extends.</param>
/// <param name="prefix">A prefix to look for.</param>
/// <returns><see cref="KeyValuePair{string, string}"/>s with <see cref="KeyValuePair{string, string}.Key"/>
/// starting with the given <paramref name="prefix"/>.</returns>
public static IEnumerable<KeyValuePair<string, string>> FindPrefixedAttributes(
this TagHelperOutput tagHelperOutput, string prefix)
{
// TODO: We will not need this method once https://github.com/aspnet/Razor/issues/89 is completed.
// We're only interested in HTML attributes that have the desired prefix.
var prefixedAttributes = tagHelperOutput.Attributes
.Where(attribute => attribute.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.ToArray();
return prefixedAttributes;
}
/// <summary>
/// Merges the given <paramref name="tagBuilder"/> into the <paramref name="tagHelperOutput"/>.
/// </summary>
/// <param name="tagHelperOutput">The <see cref="TagHelperOutput"/> this method extends.</param>
/// <param name="tagBuilder">The <see cref="TagBuilder"/> to merge.</param>
/// <remarks><paramref name="tagHelperOutput"/>'s <see cref="TagHelperOutput.Content"/> has the given
/// <paramref name="tagBuilder"/>s <see cref="TagBuilder.InnerHtml"/> appended to it. This is to ensure
/// multiple <see cref="ITagHelper"/>s running on the same HTML tag don't overwrite each other; therefore,
/// this method may not be appropriate for all <see cref="ITagHelper"/> scenarios.</remarks>
public static void Merge(this TagHelperOutput tagHelperOutput, TagBuilder tagBuilder)
{
tagHelperOutput.TagName = tagBuilder.TagName;
tagHelperOutput.Content += tagBuilder.InnerHtml;
MergeAttributes(tagHelperOutput, tagBuilder);
}
/// <summary>
/// Merges the given <see cref="tagBuilder"/>'s <see cref="TagBuilder.Attributes"/> into the
/// <paramref name="tagHelperOutput"/>.
/// </summary>
/// <param name="tagHelperOutput">The <see cref="TagHelperOutput"/> this method extends.</param>
/// <param name="tagBuilder">The <see cref="TagBuilder"/> to merge attributes from.</param>
/// <remarks>Existing <see cref="TagHelperOutput.Attributes"/> on the given <paramref name="tagHelperOutput"/>
/// are not overriden; "class" attributes are merged with spaces.</remarks>
public static void MergeAttributes(this TagHelperOutput tagHelperOutput, TagBuilder tagBuilder)
{
foreach (var attribute in tagBuilder.Attributes)
{
if (!tagHelperOutput.Attributes.ContainsKey(attribute.Key))
{
tagHelperOutput.Attributes.Add(attribute.Key, attribute.Value);
}
else if (attribute.Key.Equals("class", StringComparison.Ordinal))
{
tagHelperOutput.Attributes["class"] += " " + attribute.Value;
}
}
}
/// <summary>
/// Removes the given <paramref name="attributes"/> from <paramref name="tagHelperOutput"/>'s
/// <see cref="TagHelperOutput.Attributes"/>.
/// </summary>
/// <param name="tagHelperOutput">The <see cref="TagHelperOutput"/> this method extends.</param>
/// <param name="attributes">Attributes to remove.</param>
public static void RemoveRange(
this TagHelperOutput tagHelperOutput, IEnumerable<KeyValuePair<string, string>> attributes)
{
foreach (var attribute in attributes)
{
tagHelperOutput.Attributes.Remove(attribute);
}
}
}
}

View File

@ -0,0 +1,283 @@
// 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 System.Linq;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Xunit;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
public class TagHelperOutputExtensionsTest
{
[Theory]
[InlineData("hello", "world")]
[InlineData("HeLlO", "wOrLd")]
public void CopyHtmlAttribute_CopiesOriginalAttributes(string attributeName, string attributeValue)
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>(),
content: string.Empty);
var tagHelperContext = new TagHelperContext(
new Dictionary<string, object>(StringComparer.Ordinal)
{
{ attributeName, attributeValue }
});
var expectedAttribute = new KeyValuePair<string, string>(attributeName, attributeValue);
// Act
tagHelperOutput.CopyHtmlAttribute("hello", tagHelperContext);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal(expectedAttribute, attribute);
}
[Fact]
public void CopyHtmlAttribute_DoesNotOverrideAttributes()
{
// Arrange
var attributeName = "hello";
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>()
{
{ attributeName, "world2" }
},
content: string.Empty);
var expectedAttribute = new KeyValuePair<string, string>(attributeName, "world2");
var tagHelperContext = new TagHelperContext(
new Dictionary<string, object>(StringComparer.Ordinal)
{
{ attributeName, "world" }
});
// Act
tagHelperOutput.CopyHtmlAttribute(attributeName, tagHelperContext);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal(expectedAttribute, attribute);
}
[Fact]
public void RemoveRange_RemovesProvidedAttributes()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>()
{
{ "route-Hello", "World" },
{ "Route-I", "Am" }
},
content: string.Empty);
var expectedAttribute = new KeyValuePair<string, string>("type", "btn");
tagHelperOutput.Attributes.Add(expectedAttribute);
var attributes = tagHelperOutput.FindPrefixedAttributes("route-");
// Act
tagHelperOutput.RemoveRange(attributes);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal(expectedAttribute, attribute);
}
[Fact]
public void FindPrefixedAttributes_ReturnsEmpty_AttributeListIfNoAttributesPrefixed()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>()
{
{ "routeHello", "World" },
{ "Routee-I", "Am" }
},
content: string.Empty);
// Act
var attributes = tagHelperOutput.FindPrefixedAttributes("route-");
// Assert
Assert.Empty(attributes);
var attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("routeHello"));
Assert.Equal(attribute.Value, "World");
attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("Routee-I"));
Assert.Equal(attribute.Value, "Am");
}
[Fact]
public void MergeAttributes_DoesNotReplace_TagHelperOutputAttributeValues()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>(),
content: string.Empty);
var expectedAttribute = new KeyValuePair<string, string>("type", "btn");
tagHelperOutput.Attributes.Add(expectedAttribute);
var tagBuilder = new TagBuilder("p");
tagBuilder.Attributes.Add("type", "hello");
// Act
tagHelperOutput.MergeAttributes(tagBuilder);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal(expectedAttribute, attribute);
}
[Fact]
public void MergeAttributes_AppendsClass_TagHelperOutputAttributeValues()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>(),
content: string.Empty);
tagHelperOutput.Attributes.Add("class", "Hello");
var tagBuilder = new TagBuilder("p");
tagBuilder.Attributes.Add("class", "btn");
var expectedAttribute = new KeyValuePair<string, string>("class", "Hello btn");
// Act
tagHelperOutput.MergeAttributes(tagBuilder);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal(expectedAttribute, attribute);
}
[Fact]
public void MergeAttributes_DoesNotEncode_TagHelperOutputAttributeValues()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>(),
content: string.Empty);
var tagBuilder = new TagBuilder("p");
var expectedAttribute = new KeyValuePair<string, string>("visible", "val < 3");
tagBuilder.Attributes.Add(expectedAttribute);
// Act
tagHelperOutput.MergeAttributes(tagBuilder);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal(expectedAttribute, attribute);
}
[Fact]
public void MergeAttributes_CopiesMultiple_TagHelperOutputAttributeValues()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>(),
content: string.Empty);
var tagBuilder = new TagBuilder("p");
var expectedAttribute1 = new KeyValuePair<string, string>("class", "btn");
var expectedAttribute2 = new KeyValuePair<string, string>("class2", "btn");
tagBuilder.Attributes.Add(expectedAttribute1);
tagBuilder.Attributes.Add(expectedAttribute2);
// Act
tagHelperOutput.MergeAttributes(tagBuilder);
// Assert
Assert.Equal(2, tagHelperOutput.Attributes.Count);
var attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("class"));
Assert.Equal(expectedAttribute1.Value, attribute.Value);
attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("class2"));
Assert.Equal(expectedAttribute2.Value, attribute.Value);
}
[Fact]
public void MergeAttributes_Maintains_TagHelperOutputAttributeValues()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>(),
content: string.Empty);
var expectedAttribute = new KeyValuePair<string, string>("class", "btn");
tagHelperOutput.Attributes.Add(expectedAttribute);
var tagBuilder = new TagBuilder("p");
// Act
tagHelperOutput.MergeAttributes(tagBuilder);
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal(expectedAttribute, attribute);
}
[Fact]
public void MergeAttributes_Combines_TagHelperOutputAttributeValues()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>(),
content: string.Empty);
var expectedOutputAttribute = new KeyValuePair<string, string>("class", "btn");
tagHelperOutput.Attributes.Add(expectedOutputAttribute);
var tagBuilder = new TagBuilder("p");
var expectedBuilderAttribute = new KeyValuePair<string, string>("for", "hello");
tagBuilder.Attributes.Add(expectedBuilderAttribute);
// Act
tagHelperOutput.MergeAttributes(tagBuilder);
// Assert
Assert.Equal(tagHelperOutput.Attributes.Count, 2);
var attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("class"));
Assert.Equal(expectedOutputAttribute.Value, attribute.Value);
attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("for"));
Assert.Equal(expectedBuilderAttribute.Value, attribute.Value);
}
[Fact]
public void Merge_CombinesAllTagHelperOutputAndTagBuilderProperties()
{
// Arrange
var tagHelperOutput = new TagHelperOutput(
"p",
attributes: new Dictionary<string, string>(),
content: "Hello from tagHelperOutput");
var expectedOutputAttribute = new KeyValuePair<string, string>("class", "btn");
tagHelperOutput.Attributes.Add(expectedOutputAttribute);
var tagBuilder = new TagBuilder("div");
var expectedBuilderAttribute = new KeyValuePair<string, string>("for", "hello");
tagBuilder.Attributes.Add(expectedBuilderAttribute);
tagBuilder.InnerHtml = "Hello from tagBuilder.";
// Act
tagHelperOutput.Merge(tagBuilder);
// Assert
Assert.Equal("div", tagHelperOutput.TagName);
Assert.Equal("Hello from tagHelperOutputHello from tagBuilder.", tagHelperOutput.Content);
Assert.Equal(tagHelperOutput.Attributes.Count, 2);
var attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("class"));
Assert.Equal(expectedOutputAttribute.Value, attribute.Value);
attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("for"));
Assert.Equal(expectedBuilderAttribute.Value, attribute.Value);
}
}
}