## A function for generating HTML attribute values

When using the ActionView content_tag helper, you can pass either an array or a scalar value as the value of an HTML attribute. For example, these two method calls produce the exact same output:

 1 2 3 content_tag(:div, 'Hello world!', class: ['strong', 'highlight']) content_tag(:div, 'Hello world!', class: 'strong highlight') # =>
Hello world!


This is a nifty and helpful small feature. However, it has a few limitations.

First, it leaves a trailing space in sitations like this

 1 2 3 content_tag(:div, 'Hello world!', class: ['strong', ('active' if i_am_an_active_item?)]) # =>
Hello world!
# =>
Hello world!


Second, it can only work with one-dimensional arrays and scalar values. Now, it makes sense for this function to have a restricted type signature, but when working with this helper in other contexts, this limitation can be irksome.

I recently found myself in such a context and was thereby irked. This irk got me to thinking: How might I write a function that was as flexible as possible in its type signature, and yet still predictable and sane in its output of HTML attribute values?

I began by writing some expectations:

 1 2 3 4 5 6 7 8 9 10 expect(my_method('a')).to eq 'a' expect(my_method('a', 'b')).to eq 'a b' expect(my_method('a', nil)).to eq 'a' expect(my_method(['a'])).to eq 'a' expect(my_method(['a', 'b'])).to eq 'a b' expect(my_method(['a', nil])).to eq 'a' expect(my_method('a', ['b'])).to eq 'a b' expect(my_method(['a'], nil)).to eq 'a' 

I want my method to handle n number of params and to handle arrays; I also want it to handle nils intelligently.

In order to get these expectations passing, I wrote a method that looked like this:

 1 2 3 def my_method(*args) args.flatten.compact.join(' ') end 

With those expectations met, I began considering other edge cases I wanted to cover. First, I don’t want duplicate values:

 1 2 3 4 5 expect(my_method('a', 'a')).to eq 'a' expect(my_method('a', ['a'])).to eq 'a' expect(my_method('a', 'b', 'a')).to eq 'a b' expect(my_method('a', ['b', 'a'])).to eq 'a b' expect(my_method('a', [nil, 'a'])).to eq 'a' 

This required a minor update:

 1 2 3 def my_method(*args) args.flatten.compact.uniq.join(' ') end 

Next, I wanted to handle extraneous whitespace:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 expect(my_method(' a ')).to eq 'a' expect(my_method('a ')).to eq 'a' expect(my_method(' a')).to eq 'a' expect(my_method(' a ', 'b')).to eq 'a b' expect(my_method('a ', ['b'])).to eq 'a b' expect(my_method([' a'], 'b')).to eq 'a b' expect(my_method(' a ', nil)).to eq 'a' expect(my_method('a ', [nil])).to eq 'a' expect(my_method(' a', 'a')).to eq 'a' expect(my_method(' a ', ['a'])).to eq 'a' expect(my_method(['a '], 'a')).to eq 'a' 

Another minor update to get these specs passing:

 1 2 3 4 5 def my_method(*args) # NOTE: strip must come before uniq # or else duplicates will sneak in args.flatten.compact.map(&:strip).uniq.join(' ') end 

Finally, I wanted to handle non-string scalar values:

 1 2 3 4 5 6 7 8 9 10 expect(my_method(2**64)).to eq '18446744073709551616' expect(my_method(true)).to eq 'true' expect(my_method(false)).to eq 'false' expect(my_method(1.day.from_now.to_date)).to eq '2017-11-16' expect(my_method(1.day.from_now.to_datetime)).to eq '2017-11-16T16:38:32-05:00' expect(my_method(1.day.from_now.to_time)).to eq '2017-11-16 16:38:45 -0500' expect(my_method(1.11)).to eq '1.11' expect(my_method(1)).to eq '1' expect(my_method(nil)).to eq '' expect(my_method(:s)).to eq 's' 

Once again, this was a very minor update:

 1 2 3 def my_method(*args) args.flatten.compact.map(&:to_s).map(&:strip).uniq.join(' ') end 

I tend to prefer pipelines of Enumerable methods like this to be formatted with each “pipe” on a separate line. I also wanted to give it a more meaningful name. Since the key (and final) action is join, I wanted a name that communicated this essence in addition to the data-munging that goes on. After some consideration, I went with:

 1 2 3 4 5 6 7 8 def meld(*args) args.flatten .compact .map(&:to_s) .map(&:strip) .uniq .join(' ') end