Giter Club home page Giter Club logo

formeze's Introduction

formeze

Gem Version Test Status

Ruby gem for parsing and validating form data.

Motivation

Most web applications built for end users will need to process form data. Registration forms, profile forms, checkout forms, contact forms, and forms for adding/editing application specific data.

With formeze you can define form objects that explicitly define what your application expects as input. This is more secure, and leads to much better separation of responsibilities, and also allows for implementing different validation rules in different contexts.

Install

Using Bundler:

$ bundle add formeze

Using RubyGems:

$ gem install formeze

Usage

Here is a minimal example, which defines a form with a single field:

require 'formeze'

class ExampleForm < Formeze::Form
  field :title
end

You can then parse and validate form data in Rails or Sinatra like this:

form = ExampleForm.new.parse(request)

if form.valid?
  # do something with form data
else
  # display form.errors to user
end

Formeze will automatically ignore the Rails "authenticity_token", "commit", and "utf8" parameters.

If you prefer not to inherit from the Formeze::Form class then you can instead call the Formeze.setup method on your classes like this:

class ExampleForm
  Formeze.setup(self)

  field :title
end

Both styles of setup will include the formeze class methods and instance methods but will otherwise leave the object untouched (i.e. you can define your own initialization logic).

Validation errors

Formeze distinguishes between validation errors (which are expected in the normal running of your application), and key/value errors (which most likely indicate either developer error, or form tampering). For the latter case, the parse method that formeze provides will raise a Formeze::KeyError or a Formeze::ValueError exception if the structure of the form data does not match the field definitions.

After calling parse you can check that the form is valid by calling the valid? method. If it isn't you can call the errors method which will return an array of error messages to display to the end user. You can also use the errors_on? and errors_on methods to check for and select error messages specific to a single field.

Field options

By default fields are required (i.e. they cannot be blank), they are limited to 64 characters, and they cannot contain newlines. These restrictions can be overridden by setting various field options.

Defining a field without any options works well for a simple text input. If the default length limit is too big or too small you can override it by setting the maxlength option. For example:

field :title, maxlength: 200

Similarly there is a minlength option for defining a minimum length:

field :password, minlength: 8

Fields are required by default. Specify the required option if the field is optional. For example:

field :title, required: false

You might want to return a different value for blank fields, such as nil, zero, or a "null" object. Use the blank option to specify this behaviour. For example:

field :title, required: false, blank: nil

If you are dealing with textareas (i.e. multiple lines of text) then you can set the multiline option to allow newlines. For example:

field :description, maxlength: 500, multiline: true

Error messages will include the field label, which by default is set to the field name, capitalized, and with underscores replace by spaces. If you want to override this, set the label option. For example:

field :twitter, label: 'Twitter Username'

If you want to validate that the field value matches a specific pattern you can specify the pattern option. This is useful for validating things with well defined formats, like numbers. For example:

field :number, pattern: /\A[1-9]\d*\z/

field :card_security_code, maxlength: 5, pattern: /\A\d+\z/

If you want to validate that the field value belongs to a set of predefined values then you can specify the values option. This is useful for dealing with input from select boxes, where the values are known upfront. For example:

field :card_expiry_month, values: (1..12).map(&:to_s)

The values option is also useful for checkboxes. Specify the key_required option to handle the case where the checkbox is unchecked. For example:

field :accept_terms, values: %w(true), key_required: false

Sometimes you'll have a field with multiple values, such as a multiple select input, or a set of checkboxes. For this case you can specify the multiple option, for example:

field :colour, multiple: true, values: Colour.keys

Sometimes you'll only want the field to be defined if some condition is true. The condition may depend on the state of other form fields, or some external state accessible from the form object. You can do this by specifying either the defined_if or defined_unless options with a proc. Here's an example of using the defined_if option:

field :business_name, defined_if: ->{ @account.business? }

In this example the business_name field will only be defined and validated for business accounts. The proc is evaluated in the context of the form object, so has full access to instance variables and methods defined on the object. Here's an example of using the defined_unless option:

field :same_address, values: %w(true), key_required: false

field :billing_address_line_one, defined_unless: ->{ same_address? }

def same_address?
  same_address == 'true'
end

In this example, the billing_address_line_one field will only be defined and validated if the same_address checkbox is checked.

Validation errors can be a frustrating experience for end users, so ideally we want to be liberal in what we accept, but at the same time ensuring that data is consistently formatted to make it easy for us to process. The scrub option can be used to specify methods for "cleaning" input data before validation. For example:

field :postcode, scrub: [:strip, :squeeze, :upcase]

The input for this field will have leading/trailing whitespace stripped, double (or more) spaces squeezed, and the result upcased automatically. Custom scrub methods can be defined by adding a symbol/proc entry to the Formeze.scrub_methods hash.

Multipart form data

For file fields you can specify the accept and maxsize options, for example:

class ExampleForm < Formeze::Form
  field :image, accept: 'image/jpg,image/png', maxsize: 1000
end

For this to work you need to make sure your application includes the mime-types gem, and that the form is submitted with the multipart/form-data mime type.

Custom validation

You may need additional validation logic beyond what the field options described above provide, such as validating the format of a field without using a regular expression, validating that two fields are equal etc. This can be accomplished using the validates class method. Pass the name of the field to be validated, and a block/proc that encapsulates the validation logic. For example:

class ExampleForm < Formeze::Form
  field :email

  validates :email, &EmailAddress.method(:valid?)
end

If the block/proc takes no arguments then it will be evaluated in the scope of the form instance, which gives you access to the values of other fields (and methods defined on the form). For example:

class ExampleForm < Formeze::Form
  field :password
  field :password_confirmation

  validates :password_confirmation do
    password_confirmation == password
  end
end

Specify the if option with a proc to peform the validation conditionally. Similar to the defined_if and defined_unless field options, the proc is evaluated in the scope of the form instance. For example:

class ExampleForm < Formeze::Form
  field :business_name, defined_if: :business_account?
  field :vat_number, defined_if: :business_account?

  validates :vat_number, if: :business_account? do
    # ...
  end

  def initialize(account)
    @account = account
  end

  def business_account?
    @account.business?
  end
end

Specify the error option with a symbol to control which error the validation generates. The I18n integration described below can be used to specify the error message used, both for errors that are explicitly specified using this option, and the default "invalid" error. For example:

class ExampleForm < Formeze::Form
  field :email
  field :password
  field :password_confirmation

  validates :email, &EmailAddress.method(:valid?)

  validates :password_confirmation, error: :does_not_match do
    password_confirmation == password
  end
end

The error for the email field validation would include the value of the formeze.errors.invalid I18n key, defaulting to "is invalid" if the I18n key does not exist. The error for the password_confirmation field validation would include the value of the formeze.errors.does_not_match I18n key.

I18n integration

Formeze integrates with the i18n gem so that you can define custom error messages and field labels within your locales (useful both for localization, and when working with designers).

Here is an example of how you would change the "required" error message:

# config/locales/en.yml
en:
  formeze:
    errors:
      required: "cannot be blank"

Error messages defined in this way apply globally to all Formeze forms.

You can also change error messages on a per field basis, for example:

# config/locales/en.yml
en:
  ExampleForm:
    errors:
      comments:
        required: 'are required'

Here is an example of how to define a custom label for "first_name" fields:

# config/locales/en.yml
en:
  formeze:
    labels:
      first_name: "First Name"

Labels defined in this way apply globally to all Formeze forms, but can be overridden using the label field option which will take precedence.

formeze's People

Contributors

antillas21 avatar timcraft avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

formeze's Issues

Formeze in the context of nested object attributes

Hi @timcraft ,

I know that the purpose of formeze is to be self-contained and independent of using rails, sinatra, or other Ruby-based project, and maybe this is an overly dumb-question, but how can this gem be effectively used in the context of nested object attributes?

Rails 4 relies on strong params to use/ignore passed in params and this is somewhat useful when dealing with a form having a complex object layout.

Take the following example:

# app/forms/client_form.rb
require 'formeze'
class ClientForm < Formeze::Form
  field :name, scrubs: [:strip]
  field :email, pattern: /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
end

# app/controllers/clients_controller.rb
class ClientsController < ApplicationController
  # ... other methods
  def create
    @form = ClientForm.new.parse(request)

    respond_to do |format|
      if @form.valid?
        client = Client.create(@form.to_h)
        format.html { redirect_to client, notice: 'Client was successfully created.' }
      else
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end

  private
  # added here as an example on how to do this via strong params
  def client_params
    params.require(:client).permit(:name, :email)
  end
end
<!-- app/views/clients/_form.html.erb -->
<%= form_tag(action: :create) do %>
  <% if @form.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@form.errors.size, "error") %> prohibited this client from being saved:</h2>

      <ul>
        <% @contact_form.errors.each do |error| %>
          <li><%= error %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= label_tag 'Name' %><br>
    <%= text_field_tag 'name', @form.name %>
  </div>
  <div class="field">
    <%= label_tag 'Email' %><br>
    <%= text_field_tag 'email', @form.email %>
  </div>
  <div class="actions">
    <%= submit_tag 'Create Client' %>
  </div>
<% end %>
# logs/environment.log
# Started POST "/clients" for 127.0.0.1 at 2015-06-26 11:37:50 -0700
# Processing by ClientsController#create as HTML
#  Parameters: {
#   "utf8"=>"✓", "authenticity_token"=>"7dbWLcho5BjMR/yWExpLlB9qjbsAIqWR2Gf5q7J+j5s=", 
#   "name"=>"Antonio", "email"=>"[email protected]", "commit"=>"Create Client"
# }

# using pry-rails to inspect the action
pry(<ClientsController>)> Formeze::RequestCGI.new(request: request).params
=> {"utf8"=>["✓"],
 "authenticity_token"=>["7dbWLcho5BjMR/yWExpLlB9qjbsAIqWR2Gf5q7J+j5s="],
 "name"=>["Antonio"],
 "email"=>["[email protected]"],
 "commit"=>["Create Client"]}

pry(<ClientsController>)> @form.to_h
=> {:name=>"Antonio", :email=>"[email protected]"}

This works well with a simple case/object like the one above.

But when I have a slightly more complicated object, and need to "nest" properties, How can I achieve this using formeze?. Building on the previous example:

# app/controllers/clients_controller.rb
class ClientsController < ApplicationController
  # ... other methods
  def create
    @form = ClientForm.new.parse(request)

    respond_to do |format|
      if @form.valid?
        client = Client.create(@form.to_h)
        format.html { redirect_to client, notice: 'Client was successfully created.' }
      else
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end

  private
  # added here as an example on how to do this via strong params
  def client_params
    params.require(:client).permit(
      :name, :email,
      phone_number_attributes: [:type, :number, :extension]
    )
  end
end
<!-- app/views/clients/_form.html.erb -->
<%= form_tag(action: :create) do %>
  <% if @form.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@form.errors.size, "error") %> prohibited this client from being saved:</h2>

      <ul>
        <% @form.errors.each do |error| %>
          <li><%= error %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= label_tag 'Name' %><br>
    <%= text_field_tag 'name', @form.name %>
  </div>
  <div class="field">
    <%= label_tag 'Email' %><br>
    <%= text_field_tag 'email', @form.email %>
  </div>
  <legend>Phone Number</legend>
  <div class="field">
    <%= label_tag 'Mobile Phone' %><br>
    <%= hidden_field_tag 'phone_numbers_attributes[0][type]', 'mobile' %>
    <%= text_field_tag 'phone_numbers_attributes[0][number]', nil %>
    <%= text_field_tag 'phone_numbers_attributes[0][extension]', nil %>
  </div>
  <div class="field">
    <%= label_tag 'Work Phone' %><br>
    <%= hidden_field_tag 'phone_numbers_attributes[1][type]', 'work' %>
    <%= text_field_tag 'phone_numbers_attributes[1][number]', nil %>
    <%= text_field_tag 'phone_numbers_attributes[1][extension]', nil %>
  </div>

<!-- Suppose user can click an "Add another Phone Number" button and insert another
fieldset to the Phone Number section -->

  <div class="actions">
    <%= submit_tag 'Create Client' %>
  </div>
<% end %>
# logs/environment.log
# Started POST "/clients" for 127.0.0.1 at 2015-06-26 12:11:47 -0700
# Processing by ClientsController#create as HTML
# Parameters: {
#    "utf8"=>"✓", "authenticity_token"=>"7dbWLcho5BjMR/yWExpLlB9qjbsAIqWR2Gf5q7J+j5s=",
#    "name"=>"Antonio", "email"=>"[email protected]", "phone_numbers_attributes"=>{
#      "0"=>{"type"=>"mobile", "number"=>"555-555-7777", "extension"=>""},
#      "1"=>{"type"=>"work", "number"=>"555-123-4567", "extension"=>"123"}
#    }, "commit"=>"Create Client"
# }

pry(<ClientsController>)> Formeze::RequestCGI.new(request: request).params
=> {"utf8"=>["✓"],
 "authenticity_token"=>["7dbWLcho5BjMR/yWExpLlB9qjbsAIqWR2Gf5q7J+j5s="],
 "name"=>["Antonio"],
 "email"=>["[email protected]"],
 "phone_numbers_attributes[0][type]"=>["mobile"],
 "phone_numbers_attributes[0][number]"=>["555-555-7777"],
 "phone_numbers_attributes[0][extension]"=>[""],
 "phone_numbers_attributes[1][type]"=>["work"],
 "phone_numbers_attributes[1][number]"=>["555-123-4567"],
 "phone_numbers_attributes[1][extension]"=>["123"],
 "commit"=>["Create Client"]}
# app/forms/client_form.rb
require 'formeze'
class ClientForm < Formeze::Form
  # how should I describe this class fields?
end

And so, my question is, how should I describe a Formeze based object to handle a case like this?, where I don't know how many nested params I can have (1 to N phone numbers, each with a set of attributes). Where the decision to store them is either create related PhoneNumber records or store them in a JSON type field in the same Client record.

License missing from gemspec

RubyGems.org doesn't report a license for your gem. This is because it is not specified in the gemspec of your last release.

via e.g.

spec.license = 'MIT'
# or
spec.licenses = ['MIT', 'GPL-2']

Including a license in your gemspec is an easy way for rubygems.org and other tools to check how your gem is licensed. As you can imagine, scanning your repository for a LICENSE file or parsing the README, and then attempting to identify the license or licenses is much more difficult and more error prone. So, even for projects that already specify a license, including a license in your gemspec is a good practice. See, for example, how rubygems.org uses the gemspec to display the rails gem license.

There is even a License Finder gem to help companies/individuals ensure all gems they use meet their licensing needs. This tool depends on license information being available in the gemspec. This is an important enough issue that even Bundler now generates gems with a default 'MIT' license.

I hope you'll consider specifying a license in your gemspec. If not, please just close the issue with a nice message. In either case, I'll follow up. Thanks for your time!

Appendix:

If you need help choosing a license (sorry, I haven't checked your readme or looked for a license file), GitHub has created a license picker tool. Code without a license specified defaults to 'All rights reserved'-- denying others all rights to use of the code.
Here's a list of the license names I've found and their frequencies

p.s. In case you're wondering how I found you and why I made this issue, it's because I'm collecting stats on gems (I was originally looking for download data) and decided to collect license metadata,too, and make issues for gemspecs not specifying a license as a public service :). See the previous link or my blog post about this project for more information.

Consider using active_attr

Have you considered delegating the attribute declaration and ActiveMode interface aspect to active_attr, and focusing on building a thinner layer of form-specific support on top?

That's what I frequently do in an ad-hoc manner, it'd be nice to see a gem do it even better ;)

Doesn't appear to deal with nested attributes

I'm trying out Formeze right now, and I've already thought of lots of applications for it. Rails really misses out on first-class forms, and this is absolutely the closest thing I've come across that does the job.

I'm running into an issue (not sure if it's me or not): When an attribute name is something like user[email_address], I can't seem to find a way to define this correctly in a Formeze::Form interface. Am I missing something, or is there no support for attributed named in this way at this time? Please let me know.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.