Building a Dynamic Form System

Posted on Aug 02, 2015 in Code

Carrot is Moby's nascent platform for running custom contests, promotions, and sweepstakes. In Carrot, program owners design custom entry forms, which participants fill out to submit their entries. Since Carrot aims to support a diverse array of contest types and is meant to be used by customers with widely different requirements, we needed a way to give owners a great deal of flexibility in their form designs.

Let’s assume the following requirements for the system:

  1. Forms and their submissions must be attachable to any record (e.g., programs and rounds)
  2. Owners can compose their forms from a variety of field types (defined by the application)
  3. Owners can add input validation rules
  4. Submitters can fill out the forms and see errors when their input is invalid
  5. Owners can view submissions of their forms

With our requirements in mind, we can work toward a solution. Requirement 1 tells us immediately that we’ll need two separate concepts to represent a single dynamic form system: the form itself (the fields that make it up), and a submission of the form (a group of filled-in fields). In our domain, forms are attached to Programs and Rounds, while their submissions are attached to ProgramRegistrations and Entrys. These two components, which I’ll call Form and Submission, will be present for each form system.

Requirement 2 suggests we’ll need the concept of a FormField, and each FormField will need a type. Furthermore, each FormField will need to accommodate validation logic (requirement 3), but each field will not have the same types of validations. For example, an integer field might naturally have a max and a min option that owners can use to control the range of accepted inputs; but max and min would not make sense in the context of a checkbox field. So it's clear that we’ll need to define the "meta-validation" of each field (essentially, the validation menu from which the creator can choose) separately.

Note: We'll be implementing this solution in Ruby on Rails, but you could easily adapt this approach to other platforms. In order to understand the solution we implement, you should be comfortable with Rails development—we won’' be showing how to create database tables or wire views into controllers. You'll also need to be familiar with polymorphic associations and single table inheritance.

A note about single-table inheritance: One of the biggest lessons we learned while implementing custom forms is that single-table inheritance is about trade-offs. While the subclassing provides an easy way to override behavior and validations, the cost comes when trying to change a field’s type. When this occurs, Rails attempts to apply the validation logic from the original model class, instead of the new one. To get around this limitation, you can first update the field’s type column, reload the field, and then process the validation logic.

The models are the first and most important step in this process. Remember that we want our forms and their submissions to be attachable to any pair of models, so we have to design our form models to be easily re-usable; that means outfitting them with polymorphic associations. Furthermore, we’ll extract the form and submission logic each into their own concern, so we can just include them in any pair of models.

Example of a simple form created with this system

Our behaviors:

Formable – the ability to have a dynamic form attached to a record

# app/models/concerns/submittable.rb
module Formable
  extend ActiveSupport::Concern

  included do
    has_many :form_fields, as: :formable
    accepts_nested_attributes_for :form_fields, allow_destroy: true
  end
end

Submittable – the ability to have dynamic form submissions attached to a record

#ruby
# app/models/concerns/submittable.rb
module Submittable
  extend ActiveSupport::Concern

  included do
    has_many :form_values, as: :submittable
    accepts_nested_attributes_for :form_values, allow_destroy: true
  end
end

This is quite simple, but we’ll soon see how easy it will make it for us to include the form behavior in any pair of models.

Next, we need a model that defines the data in form fields themselves. Note the distinction between a form *field*, which the form creator defines, and a form *value*, which is what a user enters into the form. We’re going to use single-table inheritance (STI) to achieve our goal of multiple types of form fields; that means setting up the database table with a type column. Setting things up this way makes it easy to add new field types. Our FormField model will be the base, and all field types will inherit from it. It is responsible for deciding what can go into the form field itself as well as its attributes and validations.

The attributes and behavior of your form fields is of course up to you, as differing requirements call for different solutions. Here we’ll see how to wire up just five attributes: label, placeholder, min, max, and required. label and placeholder match exactly to their HTML input attributes. min and max represents the minimum and maximum values a form field accepts; we'll see how to use this to specify a range of accepted values for an integer field and a range of accepted dates for a date field. required, of course, marks the field as mandatory. Users will see a validation error when ignoring a field marked required.

# database columns: id, min, max, required, label, type
class FormField < ActiveRecord::Base
  belongs_to :formable, polymorphic: true

  validates :type, presence: true

  def validate_value(form_value)
    form_value.errors.add :value, "can't be blank" if required? && form_value.value.blank?

    add_validation_errors(form_value)
    form_value
  end

  def add_validation_errors(form_value)
    fail NotImplementedError
  end
end

You'll notice we’ve included a method, add_validation_errors, that seems to only complain that it’s not implemented. But remember, this is the functionality *common to all field types*. We’ll then need a model for each specific field type where this behavior will be specified to suit each type’s own needs. In those child models, add_validation_errors must be overridden to specify the validation logic. In FormField this method is just a reminder, so that any sub-classes get a failure when they forget to implement that method. This allows us to call valididate_value, whose responsibility is always to make sure a value is present if the field is required and then hand the value off to the field’s add_validation_errors method.

Now for some actual field types. We’ll create a text field, an integer field, and a date field. We’ll see how to leverage the min and max fields to implement different behavior on the integer and date fields.

class TextField < FormField
  # min and max don’t make sense on a text field, so disallow them
  validates :max,
            :min,
            absence: { message: "can't be specified on text field" }

  # no additional validations required for text field
  def add_validation_errors(value); end
end

class IntegerField < FormField
  def add_validation_errors(value)
    unless value.blank? || value.match(/\A[+-]?\d+\Z/)
      form_value.errors.add :value, 'must be an integer'
    end

    int_value = value.to_i
    if max.present? && int_value > max.to_i
      form_value.errors.add :value, "can't be greater than #{max}"
    end

    return unless min.present? && int_value < min.to_i

    form_value.errors.add :value, "can't be less than #{min}"
  end
end

class DateField < FormField
  def add_validation_errors(form_value)
    return if form_value.blank?

    unless valid_date?(value)
      form_value.errors.add :value, 'must be a valid date'
      return
    end

    date = Date.parse(value)

    if min.present?
      min_date = Date.parse(min)
      form_value.errors.add :value, "can't be before #{min_date}" if date < min_date
    end
    if max.present?
      max_date = Date.parse(max)
      form_value.errors.add :value, "can't be after #{max_date}" if date > max_date
    end
  end

  private

  def valid_date?(date_string)
    date_components = date_string.split('-').first(3).map(&:to_i)
    return false unless date_components.size == 3 && date_components.all?

    Date.valid_date?(*date_components)
  end
end

Now that we have our form field types pinned down, let’s make sure we have somewhere to record submissions to those fields.

class FormValue < ActiveRecord::Base
  belongs_to :form_field
  belongs_to :submittable, polymorphic: true

  validate :value_is_valid

  private

  def value_is_valid
    form_field.validate_value(self)
  end
end

The next step is to include this functionality in as many model pairs as we want. For Carrot, this means adding the Formable functionality to Program and Round, and the Submittable functionality to ProgramRegistration and Entry:

class Program < ActiveRecord::Base
  include Formable
end

class ProgramRegistration < ActiveRecord::Base
  include Submittable
end

class Round < ActiveRecord::Base
  include Formable
end

class Entry < ActiveRecord::base
  include Submittable
end

That’s it for the models! The most important part has been completed and we now have the bare minimum you’ll need to incorporate dynamic forms. Before we build a UI, we can create form fields programatically and validate values against them:

birth_date = DateField.new(label: 'Birth Date', min: '1910-01-01', max: '1996-01-01')

value1 = FormValue.new(form_field: birth_date, value: 'string')
value1.valid? #=> false
value1.errors.full_messages #=> ["Value must be a valid date"]

value2 = FormValue.new(form_field: birth_date, value: '2000-05-10')
value2.valid? #=> false
value2.errors.full_messages #=> ["Value can't be after 1996-01-01"]

value3 = FormValue.new(form_field: birth_date, value: '1990-05-10')
value3.valid? #=> true

All that’s left now is to integrate these models with the rest of your app. Here is where your custom app and requirements may begin to diverge significantly from ours, so feel free to stop here—the models we’ve devised are the most important part. Otherwise, read on to see an example of how this system might be added to the rest of an application. We’ll use traditional Rails ERB templates and partials.

App Integration

First, let’s get our controllers ready to process forms and submissions. Just like we did with our models, we’ll extract these two behaviors into concerns, which we can then include in any controller:

# app/controllers/concerns/form_behaviors.rb
module FormBehaviors
  extend ActiveSupport::Concern

  def form_fields_attributes
    [form_fields_attributes: [:id, :label, :max, :min, :required, :type]]
  end
end

# app/controllers/concerns/submission_behaviors.rb
module SubmissionBehaviors
  extend ActiveSupport::Concern

  def form_values_attributes
    [form_values_attributes: [:form_field_id, :value]]
  end
end

And now we can incorporate those into our controllers:

class ProgramsController
  include FormBehaviors

  def program_params
    program_attributes = [:id, :name]
    params.require(:program).permit(program_attributes + form_fields_attributes)
  end
end

class ProgramRegistrationsController
  include SubmissionBehaviors

  def program_registration_params
    program_registration_attributes = [:id]
    params.require(:program_registration).permit(program_registration_attributes + form_values_attributes)
  end
end

Since we've incorporated the form and submission fields into the controllers' strong params, we can now use those params in our create and update methods just like we would any other. Because we set up our Submittable concern to validate values and accept nested attributes, the form values will be both validated and saved automatically when the parent model is updated. Any validation errors in the form values will be added as expected and accessible from the parent record’s errors hash.

Note: These examples use SimpleForm and cocoon for easy association management, but they should be intelligible even if you’re unfamiliar with those tools.

User Interface

We want users to be able to create forms, so let’s give them a form for that:

<%# app/views/application/_new_form.html.erb %>
<%= f.simple_fields_for :form_fields do |form_field| %>
  <%= render 'form_field_fields', f: form_field %>
<% end %>

<div id="links">
  <p><strong><%= link_to_add_association 'Add Field', f, :form_fields %></strong></p>
</div>

And we can then incorporate the _new_form partial into any other forms we want to add this functionality to:

<%# app/views/programs/new.html.erb %>
<%= simple_form_for @program do |f| %>
  <%= render 'new_form' %>
<% end %>

<%# app/views/rounds/new.html.erb %>
<%= simple_form_for @program do |f| %>
  <%= render 'new_form' %>
<% end %>
Example of a simple form creation interface.

Submit Form

That’s it for the form creation side of things. Now we need to add the ability for users to also submit these forms.

<%# app/views/application/_new_form_submission.html.erb %>
<% if resource.errors.any? %>
  There were some problems with your submission:
  <ul>
    <% resource.form_value_errors.each do |error| %>
      <li><%= error %></li>
    <% end %>
  </ul>
<% end %>

<%= simple_fields_for user_program do |u| %>
  <%= render 'form_values', f: u %>
<% end %>

<%# app/views/application/_form_values.html.erb %>
<%= f.simple_fields_for :form_values, f.object.built_values do |v| %>
  <% field = v.object.form_field %>
    <%= v.input :form_field_id, as: :hidden, value: field.id %>
    <%= v.input :value, {
      label: field.display_label,
      input_html: field.input_html,
      required: field.required_html(v.object.file_cache.present?),
      wrapper: field.wrapper,
      wrapper_html: field.wrapper_html
    }, &field.input_block(v) %>
  <% end %>
<% end %>

<%# app/views/program_registrations/new.html.erb %>
<%= simple_form_for @program_registration do |f| %>
  <%= render 'new_form_submission', resource: @program_registration %>
<% end %>

<%# app/views/entries/new.html.erb %>
<%= simple_form_for @entry do |f| %>
  <%= render 'new_form_submission', resource: @entry %>
<% end %>

Finally, all of this is no good if form creators can’t see the submissions to their forms–so let’s add some views for that, too.

<%# app/views/application/_form_submission.html.erb %>
<% resource.form_values.each do |form_value| %>
  <p><%= form_value.form_field_label %>: <%= form_value.value %></p>
<% end %>

<%# app/views/program_registrations/show.html.erb %>
<%= render 'form_submission', resource: @program_registration %>

<%# app/views/entries/show.html.erb %>
<%= render 'form_submission', resource: @entry %>

And we’re done!

Users can now create custom forms based on the field types and attributes we’ve made available to them; users can submit those forms and see validation errors based on the validations defined by the form creator; and form creators can see the submissions to their forms.

Integrate Craft Forms with OnePageCRM Signal Processing with Moving Averages