Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions samples/TagHelperPack.Sample/Pages/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,23 @@ public Customer Customer { get; set; }
&lt;div <strong>asp-if="(DateTime.UtcNow.Second % 2) == 1"</strong>&gt;This paragraph will only render during &lt;strong&gt;odd&lt;/strong&gt; seconds.&lt;/div&gt;</pre>
</figure>

<h3>Class-If Tag Helper</h3>
<p>
Use <code>&lt;<em>any-element</em> asp-class-if-<i>{className}</i>="..."&gt;</code> to conditionally add class name when the provided expression is <code>true</code>.
</p>
<h4>Example</h4>
<div class="panel panel-default">
<div class="panel-body">
<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>odd</strong> seconds.</div>
</div>
</div>
<h4>Source</h4>
<figure>
<pre>&lt;div <strong>asp-class-if-bg-primary="(DateTime.UtcNow.Second % 2) == 0"&gt;</strong>This paragraph will have &lt;code&gt;bg-primary&lt;/code&gt; class during &lt;strong&gt;even&lt;/strong&gt; seconds.&lt;/div&gt;
&lt;div <strong>asp-class-if-bg-primary="(DateTime.UtcNow.Second % 2) == 1"&gt;</strong>This paragraph will have &lt;code&gt;bg-primary&lt;/code&gt; class during &lt;strong&gt;even&lt;/strong&gt; seconds.&lt;/div&gt;</pre>
</figure>

<h3>If combined with RenderPartial</h3>
<p>
Remember, you can compose certain Tag Helpers together on the same element, assuming they all apply to the specific element name.
Expand Down
76 changes: 76 additions & 0 deletions src/TagHelperPack/ClassIfTagHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Linq;

namespace TagHelperPack;

/// <summary>
/// Add/Remove a 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
/// </summary>
[HtmlTargetElement("*", Attributes = "asp-class-if*")]
public class ClassIfTagHelper : TagHelper
{
private const string Space = " ";

/// <summary>
/// Add a class name based on a condition
/// </summary>
[HtmlAttributeName(DictionaryAttributePrefix = "asp-class-if-")]
public IDictionary<string, bool> ClassIfAttributes { get; set; } = new Dictionary<string, bool>();

/// <inheritdoc />
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<string> 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<string>(existingClasses);
}

return new HashSet<string>();
}

private static string GenerateClassName(string className)
{
// Handle className transformation here
// for something that we hadn't considered
return className;
}
}
118 changes: 118 additions & 0 deletions tests/UnitTests/ClassIfTagHelperTests.cs
Original file line number Diff line number Diff line change
@@ -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<object, object>(), "unqiueId");

_output = new TagHelperOutput("div", _attributeList, (_, _) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
_output.TagName = "div";
_output.Content.SetHtmlContent("hello");

_tagHelper = new ClassIfTagHelper();
}
[Fact]
public void IsIntanceOf_TagHelper()
{
Assert.IsAssignableFrom<TagHelper>(_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);
}
}