Ruby on Rails options and configurations
Ruby on Rails Development Bootstrap
November 1, 2022 - Robert C.

Options and Configurations in Ruby on Rails

So, the bootstrap helpers gem is starting to come together and be useful in projects. But not every project is alike. To make matters more interesting, not even uses inside the same project need the same options.

A very common example would be icon sizes. Sometimes you need a big icon, some times you need a small icon. Even on the same page, you may need the same icons in different sizes.

Another contrived example may be progress bars. You may decide you want animated progress bars everywhere in one project, but in another project, you may want fewer animated progress bars. In both projects, we may want to override the default.

In a previous post, I also spoke about having custom breakpoints for the grid. I have also wrote about cleaning things up a bit. This post is going to tackle all of that. It seems like a bit step, but it’s really not.

History Lesson

int main( int argc, char *argv[] )  {
}

If you have ever written a C program, you have certainly seen this before. argc is the number of arguments and argv is a pointer to an array that contains the values of the arguments. Let’s not dig too deep into that as these posts are about Ruby and not C. The important thing to take away is that you can call main with just about any number of arguments. Then you can parse them however you want.

What Does Ruby Do That’s Interesting?

def example(*args){
}

That looks pretty similar, but it’s not. The * is called the splat operator, and what is interesting about it is that it turns all arguments into an array that can be parsed however you want, but only with arguments that are not defined.

def example_two(first, second, *rest)
  puts "The first is: #{first}"
  puts "The second is: #{second}"
  puts "The rest is: #{rest.join(' ')}"
end

example_two('1st', '2nd', '3rd', '4th', '5th')

# The first is: 1st
# The second is: 2nd
# The rest is: 3rd 4th 5th

Ruby also has another trick. ** The double splat. This does the same as the splat operator only with “named” arguments.

def example_three(first, second, *rest, **keyword_arguments)
  puts "The first is: #{first}"
  puts "The second is: #{second}"
  puts "The rest is: #{rest.join(' ')}"
  puts "The keyword is: #{keyword_arguments[:key]}"
end

example_three('1st', '2nd', '3rd', '4th', '5th', key: 'wow')
# The first is: 1st
# The second is: 2nd
# The rest is: 3rd 4th 5th
# The keyword is: wow

Finally, there is one more “trick” that we can use. That is the options hash. I tend to use this one a lot, as do many others. It’s the old-school way. It works well, but it has a lot of problems. Nothing that can’t be worked around though.

def example_four(first, second, options = {})
  puts "The first is: #{first}"
  puts "The second is: #{second}"
  puts "The keyword is: #{options[:key]}" if options[:key]
end

example_four('1st', '2nd', key: 'wow')
# The first is: 1st
# The second is: 2nd
# The keyword is: wow

How to Make Use of Ruby’s Tricks

So we have a lot of methods for bringing in arguments, but one the most important things we can do for a gem like bootstrap helpers is to unify the arguments and how we handle them. There may always be exceptions, but let’s look at arguments that we always want to handle even if we don’t always want them to be required.

For example, we know we always want to handle the class argument. Also, the style argument seems like we should always handle it. Also, we want to do something with breakpoints. And the display utility seems very important. I also want to set up a print argument. But we don’t really need to parse for all the utility classes (like text-dark) for example, because we can just pass those in as a css class.

  def link_helper(title, url, **options)
  end

  # we can call it like this

  link_helper('Test', '/', class: 'btn btn-success', style: 'height; 90px;', display: { sm: true, large: false }, print: false)

  # or maybe like this

  link_helper('Test', '/', display: false, print: true)

  # or maybe

  link_helper('Test', '/', sm: { display: true, size: 2, offset: 6 }, md: { display: true, size: 4, offset: 5 }, lg: { display: false })

All those examples have meaning to us as humans, but to the computer, they all need to be addressed. Thankfully, we have a way to do that. We kind of have a start for it in the grid helper; we just need to improve it a bit and set up some simple rules.

One of the projects I worked on in the past was an AIML engine. I won’t go into too much detail, but the general idea was reductionism.

A Quick Primer on Reductionism

the practice of analyzing and describing a complex phenomenon in terms of phenomena that are held to represent a simpler or more fundamental level, especially when this is said to provide a sufficient explanation.

In other words, every task, no matter how complex, can be broken down into small non-complex parts. For example, a house is complex, but the board and nail is not.

What Can We Reduce From Our Desired Argument Examples

In all of our examples, we have some straight-up arguments. Test and / are just normal arguments, and it makes sense that link_helper will need at least a title and a place to link to.

We can take a look at classand style and tell that they are pretty straight forward named arguments. We’re not going to have different class and style arguments passed in based on the breakpoints because our side of the code has no idea what breakpoints may be activated.

In the first and third examples, we have a bit of a problem. In one case we start a chain starting with the breakpoint. In another case, we start from the utility (in this case display). We want both to be valid. Both make sense.

In the examples we can also see that not all break points are used. We are actually trying to take advantage of bootstraps way of using breakpoints. Meaning that if we set something up on sm it stays active until overridden, in this case with md or lg.

Building the Class

We need to go a bit slower here, as the goal of this class is to be used to clean up the other helpers. So it won’t do us any good to say, “We will come back to it later.” This is the coming back; this is the later.

So, let’s start with something simple:

# lib/bootstrap_helpers/options.rb

module BootstrapHelpers
  class Options
    def initialize(*args, **named_args)
      @unnamed_args = args
      @named_args = named_args
    end

    def id
      return @named_args[:id] if @named_args[:id]

      @named_args[:id] = SecureRandom.uuid
    end
  end
end

Here, we have started a simple pattern. Options class requires nothing and will still work. However, we will look out for unnamed and named arguments. I’m not really sure what the unnamed arguments will do for us, but it could be useful later on. More importantly, for now, it lets our higher-level classes through just about anything at this class and gets back some options.

We also started our first override and default. id. It is something that is always nice to have, but oftentimes we want it to be specific so, here we can call it, and if we supplied an id then we use it, otherwise, we use something random. But, we also need it to return the same id every time.

def classes
  to_return = []
  %i[class classes css].each do |argument|
    value = @named_args[argument]
    next if value.blank?

    to_return << value.split(' ') if value.is_a? String
    to_return << value unless value.is_a? String
  end
  to_return.flatten
end

With this chunk of code, we start another pattern. We have a classes method (that we will need to extend later), but it covers many arguments. class is the one that most people would use, but sometimes it can cause a problem because it’s a protected keyword. classes makes sense, as does css so we will parse them all.

We also started a pattern of returning an array of strings and not just one big long string. That should make it easier to process the classes if we need to.

This does not, however, process the classes that need to be added for the utility classes. We will come back around to that in a moment. For now, let’s extend the Options class to handle styles.

def styles
  to_return = []
  %i[style styles].each do |argument|
    value = @named_args[argument]
    next if value.blank?

    to_return << value.split(';') if value.is_a? String
    to_return << value unless value.is_a? String
  end
  to_return.flatten
end

Again, we return an array this time of styles. And we assume that they are valid. We don’t check; for now, we just split on ; and go from there. However, styles and classes are a lot alike so let’s DRY a bit.

def classes
  attribute_parse(%i[class classes css], ' ')
end

def styles
  attribute_parse(%i[style styles], ';')
end

private

def attribute_parse(arguments, split)
  return [] if arguments.blank?

  to_return = []
  arguments.each do |argument|
    value = @named_args[argument]
    next if value.blank?

    to_return << value.split(split) if value.is_a? String
    to_return << value unless value.is_a? String
  end
  to_return.flatten
end

Now, onto parsing out display values in both directions.

def parse_display(values)
  return unless values.key? :display

  if values[:display].is_a? Hash
    parse_size_options(values[:display], 'd')
  else
    values[:display] = 'none' if values[:display] == false
    values[:display] = 'block' if values[:display] == true
    @classes << "d-#{values[:display]}"
  end
end

private

def parse_size_options(options, prepend = '')
  return unless options.is_a? Hash

  options.each do |size, value|
    if prepend == 'd'
      value = 'none' if value == false
      value = 'block' if value == true
    end
    @classes << "#{prepend}-#{size}-#{value}"
  end
end

Well, it’s ugly, but it works for one part. Passing in things like display: { sm: false, md: 'inline', lg: 'block' } will work fine, but there’s a lot of repeating and it doesn’t handle {sm: { display: 'block'}, lg: { display: 'none' } } at all. But we’re getting somewhere.

You will also notice that I have to convert false and true to default display properties. This is starting to get complicated. There’s a lot to tackle here but let’s stick with parsing arguments for right now.

def parse_out_things(values, size: nil, attrib: nil)
  sizes = %i[xs sm md lg xl xxl]
  if values.is_a? Hash
    values.each do |key, value|
      if sizes.include? key.to_sym
        parse_out_things(value, size: key, attrib: attrib)
      elsif value.is_a? Hash
        parse_out_things(value, attrib: key, size: size)
      else
        parse_out_things(value, size: size, attrib: key)
      end
    end
  else
    attrib = 'd' if attrib&.to_sym == :display
    @classes << "#{attrib}-#{size}-#{values}" if size
    @classes << "#{attrib}-#{values}" unless size
  end
end

This is getting better. There’s still a lot of clean up to be done, but we are getting closer.

Recursive Methods in Ruby on Rails

I am a bit surprised by how infrequently these are used these days. It’s like devs forgot about them. What’s more likely is that someone said “they’re bad” and so nowadays a lot of people avoid them. The truth is they can be tricky, but powerful. It’s also true that you don’t need to use a bucket crane to put a nail in a wall. But sometimes they are just the best answer.

Back to parsing options

We will walk through this method a bit when we’re done with it. But we have two problems. First, we are not converting attrib very nicely. Second, the actual value also needs converting. For example, display: false to d-none.

def parse_out_classes(values, size: nil, attrib: nil)
  if values.is_a? Hash
    values.each do |key, value|
      if @sizes.include? key.to_sym
        parse_out_classes(value, size: key, attrib: attrib)
      elsif value.is_a? Hash
        parse_out_classes(value, attrib: key, size: size)
      else
        parse_out_classes(value, size: size, attrib: key)
      end
    end
  else
    build_class(attrib: attrib, value: values, size: size)
  end
end

def build_class(attrib:, value:, size:)
  prefix, value = sanitize_values(attrib, value)
  @classes << "#{prefix}-#{size}-#{value}" if size
  @classes << "#{prifix}-#{value}" unless size
end

private

def sanitize_values(attribute, value)
  @equivalents.each do |equal|
    next unless equal[:attributes].include? attribute.to_s

    attribute = equal[:prefix]
    return attribute, value unless equal[:equals].is_a? Array

    equal[:equals].each do |eq_value|
      value = eq_value[value] if eq_value.key? value
    end
  end

  return attribute, value
end

Let’s walk through this a bit. parse_out_classes is a recursive method that calls itself to help map @sizes, and to help map plain attributes. If there is a hash for an attribute value, it calls itself again. It is a recursive method, but it’s limited in that it will only go down the stack. So it’s safe. build_class just holds the string concatenations to make things cleaner, and gives us a place to hook into should be need to later on for some reason. Finally, sanitize_values looks at a YAML file to find things to swap out, it just makes things easier. You can take a look at the YAML file in the repo.

Adding to the Output

We have one more thing we need to do. That is to have the output be useful. Let’s override to_s for that purpose.

  def to_s(include_classes: [])
    include_classes = include_classes.join(' ') if include_classes.is_a? Array
    "style=\"#{styles.join(';')}\" class=\"#{include_classes} #{classes.join(' ')}\" id=\"#{id}\"".html_safe
  end

This allows us to use to_s as we normally would, but if we need to, we can add some classes in. A great example of this is a call to the row helper.

<%= row class: 'text-white bg-dark', display: {sm: 'block', md: false} do %>
  Your screen is too small to see this content
<% end %>
<%= row class: 'text-white bg-dark', display: {sm: false, md: true} do %>
  Your screen is just right to see this content
<% end %>

Which uses this view (in the helper)

<div <%= options.to_s(include_classes: ['row']) %>>
  <%= content %>
</div>

And generates this markup

<div style="" class="row d-sm-none d-md-block text-white bg-dark" id="0dd8a3d7-3426-4aab-a61d-f0c69a1d72dd">
  Your screen is just right to see this content
</div>

Adding on Some Configuration

As I stated before, part of the things I wanted to add was the ability for custom breakpoints. In building this options parser, I also wanted to add in some options for a few other things like equivalents. Most people will never need to use these options, but for the few times you do, this is a great option.

# lib/bootstrap_helpers/configuration.rb

module BootstrapHelpers
  class << self
    def configure
      yield configuration
    end

    def configuration
      @configuration ||= Configuration.new
    end

    alias_method :config, :configuration
  end

  class Configuration
    attr_accessor :equivalents
    attr_accessor :sizes
    attr_accessor :excluded_attributes

    def initialize
      @equivalents = YAML.safe_load(File.read(File.expand_path('../../config/equivalents.yaml', __dir__))).deep_symbolize_keys[:equivalents]
      @sizes = %i[xs sm md lg xl xxl].freeze
      @excluded_attributes = %i[styles classes css style class id].freeze
    end
  end
end

This defines what we want to configure and some defaults. You can configure these in the normal way. And because I don’t suspect anyone to really access them, I am not going to go into examples and how to use and set them. I will say that in your application you can configure these in an initializer. Keep in mind that like all initializers these are not affected by auto-reloading.

We should add some notes to the README though.

Configuration

You can configure some settings in the usual way

BootstrapHelper.configure do |config|
  config.equivalents # see gem for example of structure
  config.sizes # array of bootstrap breakpoints to process
  config.excluded_attributes # attributes to exclude from option processing. Take great care
end

YOU MAY ALSO LIKE

Adding helpers
August 12, 2022 - Robert C.

Adding the First Helpers in Ruby on Rails

Software developer writing code
September 14, 2022 - Robert C.

Bootstrap: Accessibility and the Grid