diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOutputExtensions.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOutputExtensions.cs new file mode 100644 index 0000000000..9b6ef20c2d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOutputExtensions.cs @@ -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 +{ + /// + /// Utility related extensions for . + /// + public static class TagHelperOutputExtensions + { + /// + /// Copies a user-provided attribute from 's + /// to 's + /// . + /// + /// The this method extends. + /// The name of the bound attribute. + /// The . + /// Only copies the attribute if 's + /// does not contain an attribute with the given + /// + 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()); + } + } + + /// + /// Returns all attributes from 's + /// that have the given . + /// + /// The this method extends. + /// A prefix to look for. + /// s with + /// starting with the given . + public static IEnumerable> 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; + } + + /// + /// Merges the given into the . + /// + /// The this method extends. + /// The to merge. + /// 's has the given + /// s appended to it. This is to ensure + /// multiple s running on the same HTML tag don't overwrite each other; therefore, + /// this method may not be appropriate for all scenarios. + public static void Merge(this TagHelperOutput tagHelperOutput, TagBuilder tagBuilder) + { + tagHelperOutput.TagName = tagBuilder.TagName; + tagHelperOutput.Content += tagBuilder.InnerHtml; + + MergeAttributes(tagHelperOutput, tagBuilder); + } + + /// + /// Merges the given 's into the + /// . + /// + /// The this method extends. + /// The to merge attributes from. + /// Existing on the given + /// are not overriden; "class" attributes are merged with spaces. + 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; + } + } + } + + /// + /// Removes the given from 's + /// . + /// + /// The this method extends. + /// Attributes to remove. + public static void RemoveRange( + this TagHelperOutput tagHelperOutput, IEnumerable> attributes) + { + foreach (var attribute in attributes) + { + tagHelperOutput.Attributes.Remove(attribute); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs new file mode 100644 index 0000000000..19378c4585 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs @@ -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(), + content: string.Empty); + var tagHelperContext = new TagHelperContext( + new Dictionary(StringComparer.Ordinal) + { + { attributeName, attributeValue } + }); + var expectedAttribute = new KeyValuePair(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() + { + { attributeName, "world2" } + }, + content: string.Empty); + var expectedAttribute = new KeyValuePair(attributeName, "world2"); + var tagHelperContext = new TagHelperContext( + new Dictionary(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() + { + { "route-Hello", "World" }, + { "Route-I", "Am" } + }, + content: string.Empty); + var expectedAttribute = new KeyValuePair("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() + { + { "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(), + content: string.Empty); + var expectedAttribute = new KeyValuePair("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(), + content: string.Empty); + tagHelperOutput.Attributes.Add("class", "Hello"); + + var tagBuilder = new TagBuilder("p"); + tagBuilder.Attributes.Add("class", "btn"); + + var expectedAttribute = new KeyValuePair("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(), + content: string.Empty); + + var tagBuilder = new TagBuilder("p"); + var expectedAttribute = new KeyValuePair("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(), + content: string.Empty); + + var tagBuilder = new TagBuilder("p"); + var expectedAttribute1 = new KeyValuePair("class", "btn"); + var expectedAttribute2 = new KeyValuePair("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(), + content: string.Empty); + var expectedAttribute = new KeyValuePair("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(), + content: string.Empty); + var expectedOutputAttribute = new KeyValuePair("class", "btn"); + tagHelperOutput.Attributes.Add(expectedOutputAttribute); + + var tagBuilder = new TagBuilder("p"); + var expectedBuilderAttribute = new KeyValuePair("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(), + content: "Hello from tagHelperOutput"); + var expectedOutputAttribute = new KeyValuePair("class", "btn"); + tagHelperOutput.Attributes.Add(expectedOutputAttribute); + + var tagBuilder = new TagBuilder("div"); + var expectedBuilderAttribute = new KeyValuePair("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); + } + } +}