Formtastic tri-state radio
What is “tri-state”?
— that which has 3 states.
By defenition Boolean values have 2 states: True & False.
However, if you store a Boolean value in a database column with no NOT NULL
restriction, it aquires a 3d possible state: null
.
Some may consider this practice questionable — I don’t think so. In real life you always have a case when the answer to your question may be only “yes” or “no”, but you don’t know the answer yet. Using a string type column, storing there "yes"
, "no"
and "unset"
+ using a state machine + validations — feels overkill to me.
What the gem does
- Provides a custom Formtastic input type
:tristate_radio
which renders 3 radios (“Yes”, “No”, “Unset”) instead of a checkbox (only where you put it). - Teaches Rails recognize
"null"
and"nil"
param values asnil
. See “How it works” ☟ section for technical details on this. - Encourages you to add translations for ActiveAdmin “status tag” so that
nil
be correctly translated as “Unset” instead of “False”.
Usage
For a Boolean column with 3 possible states:
f.input :am_i_awake, as: :tristate_radio
You get (HTML is simplified, actually there are more classes etc.):
<fieldset>
<legend>Am i awake?</legend>
<input name="am_i_awake" type="radio" value="true"> <label>Yes</label>
<input name="am_i_awake" type="radio" value="false"> <label>No</label>
<input name="am_i_awake" type="radio" value="null"> <label>Unset</label>
</fieldset>
Installation
Gem
gem "formtastic_tristate_radio"
Translations
Add translation for the new “unset” option:
ru:
formtastic:
# :yes: Да # <- these two fall back to translations
# :no: Нет # in Formtastic gem but have only English
null: Неизвестно # <- this you must provide youself
You can override individual translations like so:
f.input :attribute, as: :tristate_radio, null: "Your text"
ActiveAdmin translations
ActiveAdmin will automatically translate nil
as “No”, so if you use ActiveAdmin, add translation like so:
ru:
active_admin:
status_tag:
:yes: Да
:no: Нет
unset: Неизвестно
Notice that the key ActiveAdmin uses is “unset”, not “null”.
Configuration
It’s difficult to come up with a reasonable use case for that, but you can configure what will be used as inputs value:
# config/initializers/formtastic.rb
FormtasticTristateRadio.configure do |config|
config.unset_key = "__unset" # default is :null
end
which will result in:
<input type="radio" name="am_i_awake" value="true">
<input type="radio" name="am_i_awake" value="false">
<input type="radio" name="am_i_awake" value="__unset">
Mind that for your custom value to work, you also need to configure ActiveModel
to recognize that value as nil
. Currently that is done like so.
Documentation
Low-level methods are properly documented in RubyDoc here.
Dependencies
Now the gem depends on Formtastic (naturally) and Rails. Frankly I am not sure whether I will have time to make it work with other frameworks.
How it works
In Ruby any String is cast to true
:
!!"" #=> true
!!"false" #=> true
!!"nil" #=> true
!!"no" #=> true
!!"null" #=> true
Web form params are passed as plain text and are interpreted as String by Rack.
So how are Boolean values transfered as strings if a "no"
or "0"
and even ""
is truthy in Ruby?
Frameworks just have a list of string values to be recognized and mapped to Boolean values:
ActiveModel::Type::Boolean::FALSE_VALUES
#=> [
0, "0", :"0",
"f", :f, "F", :F,
false, "false", :false, "FALSE", :FALSE,
"off", :off, "OFF", :OFF,
]
so that
ActiveModel::Type::Boolean.new.cast("0") #=> false
ActiveModel::Type::Boolean.new.cast("f") #=> false
ActiveModel::Type::Boolean.new.cast(:FALSE) #=> false
ActiveModel::Type::Boolean.new.cast("off") #=> false
# etc
So what I do in this gem is extend ActiveModel::Type::Boolean
in a consistent way to teach it recognize null-ish values as nil
:
module ActiveModel
module Type
class Boolean < Value
NULL_VALUES = [nil, "", "null", :null, "nil", :nil].to_set.freeze
private def cast_value(value)
NULL_VALUES.include?(value) ? nil : !FALSE_VALUES.include?(value)
end
end
end
end
And voila!
ActiveModel::Type::Boolean.new.cast("") #=> nil
ActiveModel::Type::Boolean.new.cast("null") #=> nil
ActiveModel::Type::Boolean.new.cast(:null) #=> nil
ActiveModel::Type::Boolean.new.cast("nil") #=> nil
ActiveModel::Type::Boolean.new.cast(:nil) #=> nil
Warning: as you might have noticed, default Rails behavior is changed. If you rely on Rails’ automatic conversion of strings with value "null"
into true
, this gem might not be for you (and you are definitely doing something weird).
Roadmap
- Remove
require_relative "../app/models/active_record/base"
from main file - Make the gem configurable
- Pull the key used for “unset” choice value into configuration
- Add translations into most popular languages
- Load translations from gem
- Rgister
:tristate_radio
for Boolean columns withnull
- Decouple
ActiveModel::Type::Boolean
thing from Formtastic things, maybe into a separate gem - Decouple from Rails
License
The gem is available as open source under the terms of the MIT License.