Pre-order

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.