A new package is born: Umamimolecule.ClassNames

I’d been hearing a fair bit about WebAssembly over the last several months, and finally started to look a bit further into it. One thing that caught my attention was Blazor, which uses WebAssembly to allow you to write your web app in C# and have it execute in the browser.

So after playing around with Blazor a bit, one thing I wanted was a clean way to dynamically use class names for elements. In particular, as certain parts of the component state change I wanted to change the CSS classes used by some elements.

So I made a thing: Umamimolecule.ClassNames, a NuGet package to fill this need.

If you’re from a JavaScript web app background you’ll probably be familiar with the classnames NPM package, and this is basically achieving the same end result.

Table of contents

The params keyword

I wanted a flexible way of defining an arbitrary amount of conditions without requiring a lot of ceremony.

This is where the params keyword in C# comes in. When added to an array parameter on a method, it allows the method to be called with individual elements instead of having to construct and pass in an array:

public void MyMethod(params object[] values)
{
  // Within this method, the values parameter
  // behaves like a regular ordinary array
}

We can call this method like this:

MyMethod("a", 1, true, 0.123)

And the method will see the following values in the values parameter:

public void MyMethod(params object[] values)
{
  // values[0] = "a"
  // values[1] = 1
  // values[2] = true
  // values[3] = 0.123
}

Defining conditions

At a minimum, the following needs to be supported:

  • A string literal containing the classname, these will always be added
  • A string/boolean pair to conditionally add the classname

The classnames NPM package supports using objects for the conditions, where the keys are the classnames and are included in the result if the corresponding value evaluates as truthy. However creating objects in C# is a bit more verbose, so I wanted to focus on other ways of achieving this.

Tuple

Tuples are a neat way of bundling data together without having to create a class or struct.

We can bundle a string and boolean value together using the tuple definition:

(string, boolean) myTuple = ("my-class-name", true);

So now I’m thinking we can have something like this for the classname component, where we have the following method signature:

public static class CN
{
  public static string Create(params object[] values)
  {
    // TODO
  }
}

And we can call it like this:

// Returns "btn btn-primary"
CN.Create("btn", ("btn-primary", true), ("dark", false));

That looks pretty simple and minimal! No dealing with newing up objects or arrays. Of course, you probably wouldn’t use the boolean literals in your code, but rather some condition that returns a boolean value.

Other condition types

Rather than force users to have to use tuples, I decided to add support for other types:

  • KeyValuePair<string, bool>
  • Dictionary<string, bool>
  • Func<string>

Implementation

I used pattern matching to determine how to evaluate the various conditions:

  • If tuple, then return the string part if the bool part is true
  • If KeyValuePair, then return the key if the value is true
  • Fallback to converting the value to a string

Which looks something like this:

return value switch
{
    ValueTuple<string, bool> tuple => tuple.Item2 ? tuple.Item1 : null,
    KeyValuePair<string, bool> kvp => kvp.Value ? kvp.Key : null,
    Func<string> f => f(),
    _ => value.ToString(),
};

Also any collections passed in are flattened. This means if you pass in a Dictionary it will get converted to a collection of KeyValuePair elements.

var flat = values.SelectMany<object, object>(x =>
{
    return x switch
    {
        string s => new object[] { s },
        IEnumerable e => e.Cast<object>(),
        _ => new object[] { x },
    };
});

Note that string is handled first - this is because the string type implements IEnumerable and if it weren’t there would convert the string into its individual characters.

Summary

Head over to my Github project to have a look and see some more examples of conditions, and if you have any ideas or enhancement requests, feel free to log it as an issue.