Create a form object
built from ActiveModel::Attributes
.
This is similar to Virtus and its successors, but specfically built for Rails.
The form can be initialized directly using the user submitted params
from a controller. Only the user data matching defined attributes will be set and the submitted data will be automatically type cast.
This moves the responsibility for setting up your StrongParameters
filters from the controller to the form object.
Nested models, arrays, and proc/method defaults are supported.
# Define a form
class Form
include StrongAttributes
attribute :my_string, :string, default: "new default"
attribute :my_number, :integer, default: :some_method
end
# Define a controller
class MyController < ApplicationController
def create
@form = Form.new(params.require(:name).permit!)
end
end
# POST /my/new
# name[:my_string] = "foo"
# name[:my_number] = "1"
# name[:not_allowed] = "danger"
@form # => <Form my_string="foo" my_number=1>
Attributes are set using the Rails attributes API.
StrongAttributes
includes ActiveModel validations and AdviceModel dirty.
There are a number of built-in types: string
, float
, decimal
, integer
, time
, boolean
, binary
, date
, datetime
.
You can also create your own.
class Form
include StrongAttributes
attribute :my_string, :string
attribute :my_number, :integer
end
The form is intended to be initialized with user submitted data. Only defined attributes will be set.
class Form
include StrongAttributes
attribute :string, :string, default: "new default"
attribute :number, :integer, default: :some_method
end
# Unknown attributes are ignored
form = Form.new(string: "foo", number: "1", unknown: "bar")
form # => <Form string="foo" number=1>
You can also initialize with ActionController::Parameters
and these will automatically be marked as permitted.
# { "bar" => { "string" => "foo", "number" => 1 } }
form = Form.new(params.require(:foo))
# equivalent of `Form.new(params.require(:foo).permit!.to_hash)`
form # => <Form string="foo" number=1>
It is useful to initialize a form with non-user input. For example you might want to pass in model loaded by the controller, but not allow potentially unsafe user input to set this value.
If the object is initialized with a hash, and keyword arguments, any keyword arguments passed to the constructor will be passed directly to setters on the model.
class Form
include StrongAttributes
attr_accessor :not_set_by_user
attr_accessor :safe_from_user
attribute :my_string, :string
attribute :my_number, :integer
end
form = Form.new(
{ my_string: "foo", safe_from_user: "bad_value" },
not_set_by_user: "something"
)
form.not_set_by_user # => "something"
form.safe_from_user # => nil
# If you want to set non-user values you must supply a hash as the first value
Form.new(safe_from_user: "foo").safe_from_user # => nil
Form.new({}, safe_from_user: "foo").safe_from_user # => "foo"
It is possible to mark a setter as "safe". It will then by initialized with user provided values.
class Form
include StrongAttributes
attr_accessor :safe_from_user_setter
attr_accessor :custom_setter
safe_setter :custom_setter
end
Form.new({ safe_from_user_setter: "foo" }).safe_from_user_setter # => nil
Form.new({ custom_setter: "foo" }).custom_setter # => "foo"
StrongAttributes
includes ActiveModel validations.
class Form
include StrongAttributes
attribute :name, :string
validates :name, presence: true
end
form = Form.new
form.valid? # => false
form.errors.full_messages # => ["Form Name can't be blank"]
Attributes can have defaults set as either as a constant, proc or method.
# As a constant
class Form
include StrongAttributes
attribute :my_string, :string, default: "new default"
end
Form.new.my_string # => "new default"
# As a proc
class Form
include StrongAttributes
attribute :my_string, :string, default: ->(name) { "new default" }
end
Form.new.my_string # => "new default"
# As a method
class Form
include StrongAttributes
attribute :message, :string, default: :default_message
private
def default_message
"Hello world!"
end
end
Form.new.message # => "Hello World!"
You can override attriute setters if the setter needs more complex logic.
Note the setter is called before the value is cast to the attribute type.
class Form
include StrongAttributes
attribute :postcode, :string
def postcode=(value)
super value&.upcase
end
end
You can define normalization functions like in Rails
class Form
include StrongAttributes
normalizes :postcode, with: ->(value) { FormatPostcode.format(value) }
attribute :postcode, :string
end
You can create your own custom attributes using ActiveModel attributes.
class StrippedString < ActiveModel::Type::String
def case(value)
super(value)&.strip
end
end
class Form
include StrongAttributes
attribute :postcode, StrippedString.new
end
This library extends the attribute
syntax to allow the definition of array attributes.
class Form
include StrongAttributes
# This is a shortcut for
# attribute :numbers, StrongAttributes::Type::Array.new(type: :string)
attribute :numbers, :array, :string
# You can also use a positional argument to set the array type
# attribute :numbers, :array, type: :string
end
Form.new({ numbers: %w[one two]).numbers # => ["one", "two"]
attributes_from_user
allows you to just get the attributes provided by the user, and defaults, and not any unset attributes.
class Form
include StrongAttributes
attribute :foo, :string
attribute :fizz, :string
end
# "fizz" attribute is not included as it wasn't set
Form.new({ foo: "bar" }).attributes_from_user # => { "foo" => "bar" }
You can include nested models in the form using nested_attributes
.
These are intended to work like ActiveRecord nested attributes and support many of the same options.
They can be defined using both in an inline definition, and using concrete classes.
# Using an inline defintion
class Form
include StrongAttributes
nested_attributes :person do
attribute :name, :string, default: :default_name
attribute :date_of_birth, :date
# This is passed to class_eval, you can add methods and validations here
validates :name, presence: true
def default_name
"Frank"
end
end
end
Form.new({ person: { name: "Bob" } }).person # => <Person name="Bob" age=nil>
# Using a class
class Person
include StrongAttributes
attribute :name, :string
attribute :date_of_birth, :date
end
class Form
include StrongAttributes
# Can be a String, or the class
nested_attributes :person, "Person"
end
# Defining the super class for an inline definition
class Form
include StrongAttributes
nested_attributes :enhanced_person, Person do
attribute :superpower, :string
end
end
A setter is created for the nested attributes. Updates to the will merge in the new values, unless the replace
option is set to true
class Form
include StrongAttributes
nested_attributes :person do
attribute :name, :string
attribute :date_of_birth, :date
end
end
form = Form.new({ person: { name: "Bob" } })
form.person = { date_of_birth: "1980-01-01" }
form.person # => <Person date_of_birth=1980-01-01 name="Bob">
A name_attributes=
setter is also created. This allows better compatibility with the Rails helper fields_for
, which is hard coded to look for the a name_attributes=
setter.
form = Form.new({ person: { name: "Bob" } })
form.person_attributes = { date_of_birth: "1980-01-01" }
form.person # => <Person date_of_birth=1980-01-01 name="Bob">
Setting to nil will remove the object.
form = Form.new({ person: { name: "Bob" } })
form.person = nil
form.person # => nil
If allow_destroy
is set to true
passing the _destroy
key will remove the object.
class Form
include StrongAttributes
nested_attributes :person, allow_destroy: true do
attribute :name, :string
end
end
form = Form.new({ person: { name: "Bob" } })
form.person = { _destroy: true }
form.person # => nil
Initial values can be set using a proc, or symbol referring to a method.
This is called before the value is set by the user.
class Form
include StrongAttributes
nested_attributes :person, initial_value: -> { { name: "Bob" } } do
attribute :name, :string
attribute :date_of_birth, :date
end
end
form = Form.new(person: { date_of_birth: "1980-01-01" } })
form.person # => <Person date_of_birth=1980-01-01 name="Bob">
The default value can be set using a proc, or symbol referring to a method.
This is only called if no value is set by the user.
class Form
include StrongAttributes
nested_attributes :person, default: -> { { name: "Bob" } } do
attribute :name, :string
attribute :date_of_birth, :date
end
end
form = Form.new
form.person # => <Person date_of_birth=nil name="Bob">
initial_value
: The initial value, can be a value, proc or a symboldefault
: The default value if no value is set, can be a value, proc or a symbolallow_destroy
(boolean
): iftrue
if a_destroy: true
key is passed then an existing record will be set tonil
, ormark_for_destruction
, will be called if the object responds to that method.reject_if
(proc
): if the proc returns true the update will be rejected/replace
(boolean
): iftrue
an update will replace the existing record rather than merging values in.copy_errors
(boolean
|object
): settings for copying errors. Defaults toallow_blank: true
. SeeCopyErrorsValidator
attributes_setter
(boolean
): iffalse
do not create aname_attributes=
setter.
Nested attributes also support arrays. These are designed to work like ActiveRecord nested attributes for has_many
records.
class Form
include StrongAttributes
nested_array_attributes :people do
attribute :name, :string
attribute :date_of_birth, :date
end
end
form = Form.new({ people: [{ name: "Bob" }, { name: "Harry" }] })
form.people # => [<Person name="Bob">, <Person name="Harry">]
Updates to nested array attributes work like ActiveRecord nested attributes.
The updates can either be an array or hash. If it is a hash only the values are used.
class Form
include StrongAttributes
nested_array_attributes :people do
attribute :name, :string
end
end
# You can update using an array
form = Form.new
form.people = [{ name: "Bob" }, { name: "Harry" }]
form.people # => [<Person name="Bob">, <Person name="Harry">]
# Or using a hash
form = Form.new
form.people = { "1" => { name: "Bob" }, "2" => { name: "Harry" } }
form.people # => [<Person name="Bob">, <Person name="Harry">]
# New records are appended, unless the `replace` option is set to true
form = Form.new(people: [name: "Bob"])
form.people = [name: "Harry"]
form.people # => [<Person name="Bob">, <Person name="Harry">]
If an id
attribute is present it will update the record with the same id. It supports using primary_key
to customise this id.
class Form
include StrongAttributes
nested_array_attributes :people do
attribute :id, :integer
attribute :name, :string
end
end
form = Form.new(people: [id: 1, name: "Bob"])
form.people = [id: 1, name: "Harry"]
form.people # => [<Person id=1 name="Harry">]
An name_attributes=
setter is also created. This allows better compatibility with the Rails helper fields_for
, which is hard coded to look for the a name_attributes=
setter.
form = Form.new({ people: [name: "Bob"] })
form.people_attributes = [name: "Harry"]
form.people # => [<Person id=1 name="Harry">]
If allow_destroy
is set to true the _destroy
key can be used to remove a record.
class Form
include StrongAttributes
nested_array_attributes :people, allow_destroy: true do
attribute :id, :integer
attribute :name, :string
end
end
form = Form.new(people: [{ id: 1, name: "Bob" }, { id: 2, name: "Harry }])
form.people = [id: "1", _destroy: true]
form.people # => [<Person id=2 name="Harry">]
Initial values can be set using a proc, or symbol referring to a method.
This is called before the value is set by the user.
class Form
include StrongAttributes
nested_array_attributes :people, initial_value: -> { [name: "Frank"] } do
attribute :name, :string
attribute :date_of_birth, :date
end
end
form = Form.new({ people: [name: "Bob"] })
form.people # => [<Person name="Bob">, <Person name="Frank">]
The default value can be set using a proc, or symbol referring to a method.
This is only called if no value is set by the user.
class Form
include StrongAttributes
nested_array_attributes :people, default: -> { [name: "Frank"] } do
attribute :name, :string
attribute :date_of_birth, :date
end
end
form = Form.new()
form.people # => [<Person name="Frank">]
initial_value
: The initial value, can be a value, proc or a symboldefault
: The default value if not value is set, can be a value, proc or a symbolallow_destroy
(boolean
): iftrue
if a_destroy: true
key is passed then an existing record will either be removed, ormark_for_destruction
, will be called if the object responds to that method.reject_if
(proc
): If the proc returns true the update will be rejectedlimit
(Integer
): If provided, raise aTooManyRecords
error if the limit is exceededreplace
(boolean
): iftrue
an update will replace the existing record rather than merging values in.copy_errors
(boolean
|object
): settings for copying errors. Defaults toallow_blank: true
. SeeCopyErrorsValidator
attributes_setter
(boolean
): iffalse
do not create aname_attributes=
setter.
This validator will copy errors from a nested model.
By default, it is automatically set when using nested_array_attributes
or nested_attributes
with the option allow_blank: true
class Form
include StrongAttributes
nested_attributes :people, default: {} do # the same as setting `copy_errors: { allow_blank: true }
attribute :name, :string
end
end
form = Form.new()
form.valid? # => false
form.errors.full_messages # => ["People name can't be blank"]
It can be customised by passing options as you would with a Rails EachValidator
.
Setting copy_errors: false
will not set the validator.
You can also use it on other attributes:
class Form
include StrongAttributes
attr_accessor :model
validates :model, copy_errors: true
end
form = Form.new()
form.valid? # => false
form.errors.full_messages # => ["Model can't be blank"]
If the prefix option is set to false, then nested attribute names will not include the model name.
class Form
include StrongAttributes
nested_attributes :people, default: {}, copy_errors: { prefix: false } do
attribute :name, :string
end
end
form = Form.new()
form.valid? # => false
form.errors.full_messages # => ["Name can't be blank"]