BLOG
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 class
and 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