From 17208ca6738b37f0f5f7599f086bccea72b5a6c4 Mon Sep 17 00:00:00 2001 From: Ricky Tobing Date: Tue, 17 Oct 2023 13:08:58 -0400 Subject: [PATCH 1/4] ClassIfTagHelper initial check-in --- src/TagHelperPack/ClassIfTagHelper.cs | 76 +++++++++++++++ tests/UnitTests/ClassIfTagHelperTests.cs | 118 +++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/TagHelperPack/ClassIfTagHelper.cs create mode 100644 tests/UnitTests/ClassIfTagHelperTests.cs diff --git a/src/TagHelperPack/ClassIfTagHelper.cs b/src/TagHelperPack/ClassIfTagHelper.cs new file mode 100644 index 0000000..03ecbf3 --- /dev/null +++ b/src/TagHelperPack/ClassIfTagHelper.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TagHelperPack; + +/// +/// Add/Remove class name based on a condition. +/// Supports different variations: +/// asp-class-if-my-class, +/// asp-class-if-class, +/// asp-class-if-my_class, +/// asp-class-if-MyClass, +/// asp-class-if-myClass, +/// asp-class-if-_my-class, +/// asp-class-if-__myclass +/// +[HtmlTargetElement("*", Attributes = "asp-class-if*")] +public class ClassIfTagHelper : TagHelper +{ + private const string Space = " "; + + /// + /// Add/Remove class name based on a condition + /// + [HtmlAttributeName(DictionaryAttributePrefix = "asp-class-if-")] + public IDictionary ClassIfAttributes { get; set; } = new Dictionary(); + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + var existingClassNames = GetExistingClassNames(context); + + var addClassNames = ClassIfAttributes + .Where(c => c.Value) + .Select(c => GenerateClassName(c.Key)) + .ToList(); + + var removeClassNames = ClassIfAttributes + .Where(c => !c.Value) + .Select(c => GenerateClassName(c.Key)) + .ToList(); + + // remove where false + var endResultClassNames = existingClassNames + .Union(addClassNames) + .Where(c => !removeClassNames.Contains(c)); + + var endResultClassNameString = string.Join(Space, endResultClassNames); + + output.Attributes.SetAttribute("class", endResultClassNameString); + } + + private static HashSet GetExistingClassNames(TagHelperContext context) + { + var existingClassAttribute = context.AllAttributes["class"]; + if (existingClassAttribute?.Value != null) + { + var existingClasses = existingClassAttribute.Value.ToString()! + .Split(Space.ToCharArray(), StringSplitOptions.RemoveEmptyEntries) + .ToList(); + + return new HashSet(existingClasses); + } + + return new HashSet(); + } + + private static string GenerateClassName(string className) + { + // Handle className transformation here + // for something that we hadn't consider + return className; + } +} diff --git a/tests/UnitTests/ClassIfTagHelperTests.cs b/tests/UnitTests/ClassIfTagHelperTests.cs new file mode 100644 index 0000000..d6ea49b --- /dev/null +++ b/tests/UnitTests/ClassIfTagHelperTests.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; +using System.Threading.Tasks; +using TagHelperPack; +using Xunit; + +namespace UnitTests; + +public class ClassIfTagHelperTests +{ + private readonly TagHelperAttributeList _attributeList; + private readonly TagHelperContext _context; + private readonly TagHelperOutput _output; + private readonly ClassIfTagHelper _tagHelper; + + public ClassIfTagHelperTests() + { + _attributeList = new TagHelperAttributeList(); + + _context = new TagHelperContext("div", _attributeList, new Dictionary(), "unqiueId"); + + _output = new TagHelperOutput("div", _attributeList, (_, _) => Task.FromResult(new DefaultTagHelperContent())); + _output.TagName = "div"; + _output.Content.SetHtmlContent("hello"); + + _tagHelper = new ClassIfTagHelper(); + } + [Fact] + public void IsIntanceOf_TagHelper() + { + Assert.IsAssignableFrom(_tagHelper); + } + + [Theory] + // ADD 'text-muted' to existing [class] 'm-0 p-0' + [InlineData("m-0 p-0", "text-muted", true, "m-0 p-0 text-muted")] + // ADD 'text-muted' to null [class] + [InlineData(null, "text-muted", true, "text-muted")] + // ADD 'text-muted' to empty [class] + [InlineData("", "text-muted", true, "text-muted")] + // ADD Duplicate 'text-muted' to existing [class]='m-0 p-0 text-muted' (should not duplicate) + [InlineData("m-0 p-0 text-muted", "text-muted", true, "m-0 p-0 text-muted")] + // REMOVE 'text-muted' to existing [class]='m-0 p-0 text-muted' (removed because condition is false) + [InlineData("m-0 p-0 text-muted", "text-muted", false, "m-0 p-0")] + // REMOVE 'text-muted' to exising [class]='m-0 p-0 text-muted' (will not do anything, text-muted does not exist in the existing class) + [InlineData("m-0 p-0", "text-muted", false, "m-0 p-0")] + public void Process_ExistingClassNames(string existingClassNames, string classIfAttribute, bool classIfCondition, string expected) + { + _attributeList.Add("class", existingClassNames); + + _tagHelper.ClassIfAttributes.Add(classIfAttribute, classIfCondition); + + _tagHelper.Process(_context, _output); + + var actual = (string)_output.Attributes["class"].Value; + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("m-0 p-0", "col-2", true, "m-0 p-0 col-2")] // with number + [InlineData("m-0 p-0", "kebab-case", true, "m-0 p-0 kebab-case")] // keba-case + [InlineData("m-0 p-0", "under_score", true, "m-0 p-0 under_score")] // underscore + [InlineData("m-0 p-0", "_under_score", true, "m-0 p-0 _under_score")] // underscore first + [InlineData("m-0 p-0", "__under_score", true, "m-0 p-0 __under_score")] // double underscore first + [InlineData("m-0 p-0", "camelCase", true, "m-0 p-0 camelCase")] // camelCase + [InlineData("m-0 p-0", "PascalCase", true, "m-0 p-0 PascalCase")] // PascalCase + [InlineData("m-0 p-0", "kebab-case-under_score", true, "m-0 p-0 kebab-case-under_score")] // kebab-case + underscore + public void Process_VariousClassNames(string existingClassNames, string classIfAttribute, bool classIfCondition, string expected) + { + _attributeList.Add("class", existingClassNames); + + _tagHelper.ClassIfAttributes.Add(classIfAttribute, classIfCondition); + + _tagHelper.Process(_context, _output); + + var actual = (string)_output.Attributes["class"].Value; + + Assert.Equal(expected, actual); + } + + [Fact] + public void Process_Multiple_Entries() + { + _attributeList.Add("class", "m-0 p-0"); + + _tagHelper.ClassIfAttributes.Add("one", true); + _tagHelper.ClassIfAttributes.Add("one-false", false); + _tagHelper.ClassIfAttributes.Add("two", true); + _tagHelper.ClassIfAttributes.Add("two-false", false); + _tagHelper.ClassIfAttributes.Add("three", true); + _tagHelper.ClassIfAttributes.Add("three-false", false); + + _tagHelper.Process(_context, _output); + + var actual = (string)_output.Attributes["class"].Value; + + Assert.Equal("m-0 p-0 one two three", actual); + } + + [Theory] + [InlineData("m-0 p-0", "m-0 p-0 one ONe OnE")] + [InlineData("m-0 p-0 one two", "m-0 p-0 one two ONe OnE")] + public void Process_Multiple_Entries_CaseSensitive(string existing, string expected) + { + _attributeList.Add("class", existing); + + _tagHelper.ClassIfAttributes.Add("one", true); + _tagHelper.ClassIfAttributes.Add("ONe", true); + _tagHelper.ClassIfAttributes.Add("OnE", true); + + _tagHelper.Process(_context, _output); + + var actual = (string)_output.Attributes["class"].Value; + + Assert.Equal(expected, actual); + } +} From 75fe8e3f1fe3412f7244994914fa180be135bd2a Mon Sep 17 00:00:00 2001 From: Ricky Tobing Date: Tue, 17 Oct 2023 13:37:34 -0400 Subject: [PATCH 2/4] Added [asp-class-if-*] Sample --- samples/TagHelperPack.Sample/Pages/Index.cshtml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/samples/TagHelperPack.Sample/Pages/Index.cshtml b/samples/TagHelperPack.Sample/Pages/Index.cshtml index fee5589..8d6f182 100644 --- a/samples/TagHelperPack.Sample/Pages/Index.cshtml +++ b/samples/TagHelperPack.Sample/Pages/Index.cshtml @@ -354,6 +354,23 @@ public Customer Customer { get; set; } <div asp-if="(DateTime.UtcNow.Second % 2) == 1">This paragraph will only render during <strong>odd</strong> seconds.</div> +

Class-If Tag Helper

+

+ Use <any-element asp-class-if-{className}="..."> to condtionally add class name when the provided expression is true. +

+

Example

+
+
+
This paragraph will have bg-primary class during even seconds.
+
This paragraph will have bg-primary class during odd seconds.
+
+
+

Source

+
+
<div asp-class-if-bg-primary="(DateTime.UtcNow.Second % 2) == 0">This paragraph will have <code>bg-primary</code> class during <strong>even</strong> seconds.</div>
+<div asp-class-if-bg-primary="(DateTime.UtcNow.Second % 2) == 1">This paragraph will have <code>bg-primary</code> class during <strong>even</strong> seconds.</div>
+
+

If combined with RenderPartial

Remember, you can compose certain Tag Helpers together on the same element, assuming they all apply to the specific element name. From 41e7bf255349645700c0d94bd0998ef14231fc6c Mon Sep 17 00:00:00 2001 From: Ricky Tobing Date: Tue, 17 Oct 2023 13:46:39 -0400 Subject: [PATCH 3/4] Fixed Typo --- src/TagHelperPack/ClassIfTagHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TagHelperPack/ClassIfTagHelper.cs b/src/TagHelperPack/ClassIfTagHelper.cs index 03ecbf3..de3c911 100644 --- a/src/TagHelperPack/ClassIfTagHelper.cs +++ b/src/TagHelperPack/ClassIfTagHelper.cs @@ -6,7 +6,7 @@ namespace TagHelperPack; ///

-/// Add/Remove class name based on a condition. +/// Add/Remove a class name based on a condition. /// Supports different variations: /// asp-class-if-my-class, /// asp-class-if-class, @@ -22,7 +22,7 @@ public class ClassIfTagHelper : TagHelper private const string Space = " "; /// - /// Add/Remove class name based on a condition + /// Add a class name based on a condition /// [HtmlAttributeName(DictionaryAttributePrefix = "asp-class-if-")] public IDictionary ClassIfAttributes { get; set; } = new Dictionary(); @@ -70,7 +70,7 @@ private static HashSet GetExistingClassNames(TagHelperContext context) private static string GenerateClassName(string className) { // Handle className transformation here - // for something that we hadn't consider + // for something that we hadn't considered return className; } } From 5294b332743deafce49dfea7455912ee901fe0d7 Mon Sep 17 00:00:00 2001 From: Ricky Tobing Date: Tue, 24 Oct 2023 13:37:25 -0400 Subject: [PATCH 4/4] Fixed typo --- samples/TagHelperPack.Sample/Pages/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/TagHelperPack.Sample/Pages/Index.cshtml b/samples/TagHelperPack.Sample/Pages/Index.cshtml index 8d6f182..d1d7841 100644 --- a/samples/TagHelperPack.Sample/Pages/Index.cshtml +++ b/samples/TagHelperPack.Sample/Pages/Index.cshtml @@ -356,7 +356,7 @@ public Customer Customer { get; set; }

Class-If Tag Helper

- Use <any-element asp-class-if-{className}="..."> to condtionally add class name when the provided expression is true. + Use <any-element asp-class-if-{className}="..."> to conditionally add class name when the provided expression is true.

Example