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_radiowhich 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
nilbe correctly translated as “Unset” instead of “False”.
Usage
For a Boolean column with 3 possible states:
f.input :am_i_awake, as: :tristate_radioYou 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 youselfYou 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
endwhich 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" #=> trueWeb 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
# etcSo 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
endAnd 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) #=> nilWarning: 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_radiofor Boolean columns withnull - Decouple
ActiveModel::Type::Booleanthing 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.