December 28, 2019

Start using Rails' 'class_names' helper today

Rails will soon support conditional classes on its tag helpers, but why wait?

It's commonplace to have conditional based CSS classes on HTML tags for a lot of reasons. Mainly, you could use the active link of a navigation list as a good example. One of the recent changes merged in to Rails is that of the 'class_names' helper to support conditional classes.

While the pull request supports this functionality on 'content_tag' and 'tag' helpers natively, what we're going to be building today won't go quite as far as that. That's simply because allowing arrays as a class list within the tag attributes requires further work than building the simple helper. There is nothing stopping us calling our helper when we set that attribute, but that will mean it won't be compatible with the Rails' way moving forward, so I wouldn't recommend that.

Getting started

If you've not already got an application to test this with, or would like to follow along, create your application as follows:

rails new too_eager_to_wait

Create a simple page

We're going to output on a simple page, so let's do that:

rails g controller Homes show

Then open app/views/homes/show.html.erb and start your server:

bundle exec rails server

Head over to http://localhost:3000/homes/show and prepare to build!

Building the helper

Breaking down the pull request, we can see that the method accepts a hash or an array, so we're going to do the same.

Looking at the documentation within the file, it's possible to have the following scenarios:

# ==== Examples
#   class_names("foo", "bar")
#    # => "foo bar"
#   class_names({ foo: true, bar: false })
#    # => "foo"
#   class_names(nil, false, 123, "", "foo", { bar: true })
#    # => "foo bar"

As a result, we will allow multiple arguments using the asteriks splat operator; this denotes that the method accepts a variable amount of arguments.

To get started, run:

rails g helper ClassName

Then, create the class name methods with the asteriks operator within app/helpers/class_name_helper.rb:

def class_names(*args)
end

This is the start of our method.

You will note that in the pull request, there is another method referenced within the 'class_names' method; it's named 'build_tag_values'. We will need to do something similar to determine which classes need to be output and also use the 'safe_join' method to ensure that our output is HTML safe.

For our testing, let's copy the example from the documentation to our view, with an additional false value within the hash and an array for testing. Head over to app/views/homes/show.html.erb and update your preview to match:

<div class="<%= class_names(nil, false, 123, "", "foo", { bar: true, foobar: false }, ['begin-array', '', nil, 'end-array']) %>">
  Hello world
</div>

Then, in your web browser, refresh your page and inspect the DIV. You should see an empty class attribute, which is correct so far.

We're then going to create a private method within our helper for processing the conditionals.

Let's update our 'class_names' helper to reflect this:

#
# Build a list of conditional class names
#
# @param [mixed] *args
#
# @return [string] class names based on the conditions
#
def class_names(*args)
  safe_join(conditional_class_names(*args), ' ')
end

Underneath our method, create a new private method as follows:

private

#
# Conditional class names
#
# @param [mixed] *args 
#
# @return [string] 
#
def conditional_class_names(*args)
end

All we now need to do is ensure that only the class names are outputted, so no empty values and no class names within the hash that have false values.

Thinking about how the method accepts any lots of data-types, we must consider:

  • if the argument is a string and it has a value, it should be allowed
  • if the argument is an array, include only those with an array value (i.e. don't include blank or empty array values)
  • if it contains a hash, only those with true values should be allowed

To do this, we could copy the 'build_tag_values' method from the pull requests, but we're not really learning anything by doing that.

Build up the 'conditional_class_name' method as follows, complete with comments:

#
# Conditional class names
#
# @param [mixed] *args
#
# @return [string]
#
def conditional_class_names(*args)
  # Define an array to store the output to
  class_names = []

  # Loop through each argument
  args.each do |value|
    # Move on unless the value is present
    next unless value.present?

    # Case statement to determine the value type
    case value
    # If the value is a hash, loop through the key and value to ensure it is not empty, false or invalid
    when Hash
      # Remove those with empty values and use the keys for class names
      class_names << value.delete_if { |_key, val| !val }.keys
    # If the value is an array, remove the empty elements
    when Array
      # Call itself to process the presence of array values
      class_names << conditional_class_names(*value).presence
    # Otherwise, convert to a string if the value is present
    else
      # Convert to string unless it's numeric, class names must start with letters
      class_names << value.to_s unless value.is_a?(Numeric)
    end
  end

  class_names.compact.flatten
end

As you can see, this is like the 'build_tag_values' method, except for the fact that the hash is conversion is done on one line only and the case statement is skipped if the value is blank.

Refreshing your web browser should allow you to inspect the code and see the output:

<div class="foo bar begin-array end-array">
  Hello world
</div>

Our code is working correctly!

For completeness, here is our app/helpers/class_name_helper.rb file:

# frozen_string_literal: true

#
# Class name helper
#
module ClassNameHelper
  #
  # Build a list of conditional class names
  #
  # @param [mixed] *args
  #
  # @return [string] class names based on the conditions
  #
  def class_names(*args)
    safe_join(conditional_class_names(*args), ' ')
  end

  private

  #
  # Conditional class names
  #
  # @param [mixed] *args
  #
  # @return [string]
  #
  def conditional_class_names(*args)
    # Define an array to store the output to
    class_names = []

    # Loop through each argument
    args.each do |value|
      # Move on unless the value is present
      next unless value.present?

      # Case statement to determine the value type
      case value
      # If the value is a hash, loop through the key and value to ensure it is not empty, false or invalid
      when Hash
        # Remove those with empty values and use the keys for class names
        class_names << value.delete_if { |_key, val| !val }.keys
      # If the value is an array, remove the empty elements
      when Array
        # Call itself to process the presence of array values
        class_names << conditional_class_names(*value).presence
      # Otherwise, convert to a string if the value is present
      else
        # Convert to string unless it's numeric, class names must start with letters
        class_names << value.to_s unless value.is_a?(Numeric)
      end
    end

    class_names.compact.flatten
  end
end

As this is running in a dedicated helper, it can simply be removed when our Rails application gets upgraded in the future. This means that our functionality will match that of a new version of Rails, before it's released, and save us having ternary operators within our class attributes.

Feedback and comments

Has this revolutionised the way you will assign CSS classes in the future? Hop over to our contact page and let us know!