Ruby gem for forms with validations and nesting.
I need for service-like objects.
I've explored these projects:
But nothing of them supports all features I need for:
- nesting (into unlimited levels) of themselves;
- simple syntax;
- custom validations and coercions;
- unified output.
So, I've tried to combine these all into one library and got Formalism.
I've discovered that form object, only with validations, are useless without service objects. So, I've combined them: service objects include validations.
Because if we're combining them — it's more like forms with logic inside for me
than service objects built-in forms. Even in HTML we're writing <form>.
So, Formalism can accept all data from any-difficult <form> and process it,
also with nested forms (for example, if you have some request form
with contact data and want to pass contacts into something like user form).
You can use Formalism::Action, a parent of Formalism::Form.
Add this line to your application's Gemfile:
gem 'formalism'And then execute:
bundle installOr install it yourself as:
gem install formalismclass FindArtistForm < Formalism::Form
field :name
private
def validate
if name.to_s.empty?
errors.add 'Name is not provided'
end
end
def execute
Artist.first(fields_and_nested_forms)
end
end
class CreateAlbumForm < Formalism::Form
field :name, String
fiels :tags, Array, of: String
nested :artist, FindArtistForm
private
def validate
if name.to_s.empty?
errors.add 'Name is not provided'
end
end
def execute
Album.create(fields_and_nested_forms)
end
end
form = CreateAlbumForm.new(
name: 'Hits', tags: %w[Indie Rock Hits], artist: { name: 'Alex' }
)
form.runUsually you need to initialize a form and execute #run method.
Internally, it runs #valid? (public) and #execute (private) methods.
#valid? runs #validate (private) of a form itself and nested forms.
#run can be redefined for database transaction, for example.
Also you can call .run with arguments for #initialize,
it's the alias for #initialize + #run.
Any call of run returns Form::Outcome instance which has #success?,
#result and #errors methods. Result is a result of #execute method.
Be careful: calling #result for failed outcome will raise ValidationError.
Field receives type as the second argument. It's not required. It can be a constant, String or Symbol. If specified — there is a coercion to specified type, if not — data remains unchanged.
Nested forms — their class, as constant.
Type or :initialize block is required.
Formalism also supports Array type with the optional :of option
(type of elements).
Coercion will be applied to a data itself and to its elements.
There is built-in coercion into some types, if you try to coerce
to undefined type — you'll get Formalism::Form::NoCoercionError.
You can define a coercion to some type via definition of such class:
# frozen_string_literal: true
module Formalism
class Form < Action
class Coercion
## Class for coercion to String
class String < Base
private
def execute
@value&.to_s
end
end
end
end
endfield and nested accepts :default option.
It can be any value, if it's an instance of Proc — it'll be executed
in the form instance scope.
field supports :key option (Symbol) to receive data by a different key,
not as a field name.
By default, nested forms initialized with data by key as their name
in parent data. So, if a parent receive { foo: 1, bar: { baz: 2 } },
it's nested form :bar will receive { baz: 2 }.
If you want to prevent initialization at all, or pass custom arguments —
you should use :initialize option which accepts a proc
with a form class argument.
If you want to just refine incoming data (add or remove) — you should define
#params_for_nested_* private method, where * is a nested form name.
You can use super inside.
Fields and nested forms are filling in order of their definition.
But sometimes you want to change this order, for example,
if you have a nested forms in ancestors which depends on data in children forms.
For such cases you can use :depends_on option, which accepts fields
and nested forms names as Symbol or Array of symbols. They will be filled
(and initialized) before dependent.
There is Form#fields_and_nested_forms as final data
(after coercion, defaults, etc). But you may want to not include some fields
or nested forms into this data. You can do it via :merge option,
which can be true, false or Proc (executed in form's instance scope).
For example:
field :bar, merge: true
nested :only_valid, nested_form_class, merge: ->(form) { form.valid? }
# or `merge: lambda(&:valid?)`You can disable #valid? and #run of forms (including nested ones)
by setting form.runnable = false.
It can be helpful for some cases, for example, with policies (permissions):
def initialize_nested_form(name, options)
return unless (form = super)
form.runnable = allowed_to_change?(name)
form
endAny class ChildForm < ParentForm will have all fields and nested forms
from ParentForm.
But you're able to remove (usually inherited) fields by:
class ChildForm < ParentForm
remove_field :field_from_parent
endYou can define modules and use them later like this:
module CommonFields
include Formalism::Form::Fields
field :base_field
nested :base_nested
end
class SomeForm < Formalism::Form
include CommonFields
field :another_field
endYou can convert a Form back to (processed) params, for example, for view render:
form = CreateAlbumForm.new(
name: 'Hits', tags: %w[Indie Rock Hits], artist: { name: 'Alex' }
)
form.to_params
# {
# name: 'Hits',
# tags: %w[Indie Rock Hits],
# artist: { name: 'Alex' }
# }For actions without fields, nesting and validation you can use
Formalism::Action (the parent of Formalism::Form).
There is a few plugins which I personally need for:
-
formalism-model_formsDefault CRUD forms for Sequel DB models. Can be renamed! -
formalism-sequel_transactionsSequel transactions inside forms. -
formalism-r18n_errorsR18n errors inside forms, including validation helpers. Can be separated!
After checking out the repo, run bundle install to install dependencies.
Then, run toys rspec to run the tests.
To install this gem onto your local machine, run toys gem install.
To release a new version, run toys gem release %version%.
See how it works here.
Bug reports and pull requests are welcome on GitHub.
The gem is available as open source under the terms of the MIT License.