Why Superform?
Forms are a critical part of web applications and traditionally have been difficult to customize in Rails, especially with Phlex. Superform is a powerful form builder library built entirely with Phlex that makes it possible to build different types of forms in your apps and use them with ease.
Rails form helpers are difficult to extend
Rails form helpers can take you really far in building web applications, but extending them can get cumbersome for large or complex applications. There’s a FormBuilder
library built into Rails—at the core of Rails form builders—that’s a worse implementation of Phlex.
If you wanted to wrap all of your form fields in a <div>
with a class of form-field
, you could create a custom form builder like this:
# app/helpers/wrapped_form_builder.rb
class WrappedFormBuilder < ActionView::Helpers::FormBuilder
FIELD_HELPERS = %i[
text_field password_field email_field telephone_field
number_field search_field url_field text_area select
date_select datetime_select time_select
]
FIELD_HELPERS.each do |method_name|
define_method(method_name) do |field, *args, **options|
@template.content_tag(:div, class: "form-field") do
super(field, *args, **options)
end
end
end
end
Each time you want to render this form, you have to pass in WrappedFormBuilder
.
<%= form_with model: @user, builder: WrappedFormBuilder do |form| %>
<%= form.text_field :name %>
<%= form.email_field :email %>
<%= form.password_field :password %>
<%= form.submit "Sign Up" %>
<% end %>
Form libraries like Simple Form and Formtastic have tried to improve the situation, but they’re fundamentally limited by FormBuilder
and partials.
What, then, is the best way to compose forms in Phlex?
Superform
Fortunately, the author of this course struggled with all Rails form builders in large projects and concluded that the only way out was to build a form helper from scratch, called Superform.
First, you create your form in a class. You can put it in ./app/views/user/form.rb
.
class UserForm < ApplicationForm
def view_template
row field(:name).input(type: :text)
row field(:email).input(type: :email)
row field(:password).input(type: :password)
submit("Sign Up")
end
end
Here’s what it looks like in ERB.
<%= render UserForm.new @user %>
For resources, you might render this form in the new.html.erb
and edit.html.erb
files.
It also looks great when rendered from other Phlex templates:
class Views::Users::New < Views::Base
def initialize(user)
@user = user
end
def view_template
h1 { "Sign up" }
render UserForm.new @user
end
end
A great way to build forms in ERB
Superform is built from the ground up using Phlex components, making it very easy to extend. It also happens to work great in ERB. Here’s what a form looks like.
<!-- app/views/posts/new.html.erb -->
<h1>New Post</h1>
<%= render Components::Form.new @post do
it.Field(:title).text
it.Field(:body).textarea
it.Field(:publish_at).date
it.Field(:featured).checkbox
it.submit "Create Post"
end %>
Extract forms into Superform classes
Want to use the form in multiple places, like the edit
and show
views in your controller? Extract the Superform into a class:
# app/views/posts/form.rb
class Views::Posts::Form < Components::Form
def view_template
Field(:title).text
Field(:body).textarea(rows: 10)
Field(:publish_at).date
Field(:featured).checkbox
submit
end
end
Then render it through your application in ERB.
<!-- app/views/posts/new.html.erb -->
<h1>New Post</h1>
<%= render Views::Posts::Form.new @post %>
Or from other Phlex views.
class Views::Posts::New < Views::Base
def initialize(post)
@post = post
end
def view_template
h1 { "Create a new post" }
render Views::Posts::Form.new @post
end
end
Why does a Superform have to be extracted into a class? So that you can use the form to replace strong parameters.
Automatic strong parameters
Here’s what a controller that replaces Rails strong parameters with a Superform looks like.
class PostsController < ApplicationController
include Superform::Rails::StrongParameters
def create
@post = Post.new
if save Views::Posts::Form.new(@post)
redirect_to @post, notice: 'Post created!'
else
render :new, status: :unprocessable_entity
end
end
# ... other actions ...
end
Since a Superform understands the structure of a form, the form itself can permit only the parameters that are in the form. That means you don’t have to maintain a separate list of permitted attributes for the form, independent from the form defined in the Rails view. Just build the form once and use it to allow only the attributes that are in the form.
A concise way to build forms
Superform ships with HTML5 form helpers and “Field Kits”, making it possible to create concise and understandable forms like this:
class UserForm < Components::Form
def view_template
Field(:email).email # type="email"
Field(:password).password # type="password"
Field(:website).url # type="url"
Field(:phone).tel # type="tel"
Field(:age).number(min: 18) # type="number"
Field(:birthday).date # type="date"
Field(:appointment).datetime # type="datetime-local"
Field(:favorite_color).color # type="color"
Field(:bio).textarea(rows: 5)
Field(:terms).checkbox
submit
end
end
Highly customizable
Superform is built from the ground up with Phlex components, making it more straightforward to customize than Rails form helpers. Here’s how you’d extend the base Superform in a Rails application.
# ./app/components/form.rb
class Components::Form < Superform::Rails::Form
class MyInput < Superform::Rails::Components::Input
def view_template(&)
div class: "form-field" do
input(**attributes)
end
end
end
# Redefining the base Field class lets us override every field component.
class Field < Superform::Rails::Form::Field
def input(**attributes)
MyInput.new(field, attributes:)
end
end
end
Work more directly with HTML and data
Rails form helpers require extra steps to get your application data working with form helpers. For example, a select tag in Rails commonly looks like this:
<%= form_for @post do |f| %>
<%= f.select :blog, @post.blogs.map { |b| [ b.id, b.name ] }%>
<% end %>
In Superform, the same select tag looks like this:
<%= render Components::Base.new(@post) do
it.Field(:blog).select @post.blogs.select(:id, :name)
end %>
#select
is an ActiveRecord method that queries only the id and name columns for blogs and skips the step of mapping data in your database into an array.
Full control over HTML
Superform starts by being concise, but it gets out of your way when you need to sweat the details over the HTML your form emits. Here’s an example of a form that does it all.
# Everything below is intentionally verbose!
class SignupForm < Components::Form
def view_template
# The most basic type of input, which will be autofocused.
Field(:name).input.focus
# Input field with a lot more options on it.
Field(:email).input(type: :email, placeholder: "We will sell this to third parties", required: true)
# You can put fields in a block if that's your thing.
field(:reason) do |f|
div do
render f.label { "Why should we care about you?" }
render f.textarea(rows: 3, cols: 80)
end
end
# Let's get crazy with selects. They can accept values as simple as two-element arrays.
div do
Field(:contact).label { "Would you like us to spam you to death?" }
Field(:contact).select(
[true, "Yes"], # <option value="true">Yes</option>
[false, "No"], # <option value="false">No</option>
"Hell no", # <option value="Hell no">Hell no</option>
nil # <option></option>
)
end
div do
Field(:source).label { "How did you hear about us?" }
Field(:source).select do |s|
# Renders a blank option.
s.blank_option
# Pretend WebSources is an ActiveRecord scope with a "Social" category that has "Facebook, X, etc."
# and a "Search" category with "AltaVista, Yahoo, etc."
WebSources.select(:id, :name).group_by(:category) do |category, sources|
s.optgroup(label: category) do
s.options(sources)
end
end
end
end
div do
Field(:agreement).label { "Check this box if you agree to give us your firstborn child" }
Field(:agreement).checkbox(checked: true)
end
render button { "Submit" }
end
end
Forms for all the different parts of your app
Most web applications have several different types of forms depending on what part of the app they’re being used in. Here’s what it looks like extending the base form into another set of forms suitable for use in a customer service admin panel.
class AdminForm < Components::Form
class AdminInput < Components::Base
def view_template(&)
input(**attributes)
small { admin_tool_tip_for field.key }
end
end
class Field < Field
def tooltip_input(**attributes)
AdminInput.new(self, attributes: attributes)
end
end
end
Then, from your app, you’d render the admin form.
class Admin::Users::Form < AdminForm
def view_template(&)
labeled field(:name).tooltip_input
labeled field(:email).tooltip_input(type: :email)
submit "Save"
end
end
Forms could be created for all the different parts of your website, including the admin panel, application, marketing website, and more.