Skip to content

Repository files navigation

Action Form

DISCLAIMER: This project is an experiment and should not be used in applications.

Build Status

Set your models free from the accepts_nested_attributes_for helper. Action Form provides an object-oriented approach to represent your forms by building a form object, rather than relying on Active Record internals for doing this. Form objects provide an API to describe the models involved in the form, their attributes and validations. A form object deals with create/update actions of nested objects in a more seamless way.

Installation

Add this line to your Gemfile:

gem'actionform'

Defining Forms

Consider an example where you want to create/update a conference that can have many speakers which can present a single presentation with one form submission. You start by defining a form to represent the root model, Conference:

classConferenceForm < ActionForm::Baseself.main_model=:conferenceattributes:name,:cityvalidates:name,:city,presence: trueend

Your form object has to subclass ActionForm::Base in order to gain the necessary API. When defining the form, you have to specify the main_model the form represents with the following line:

self.main_model=:conference

To add fields to the form, use the attributes or attribute class method. The form can also define validation rules for the model it represents. For the presence validation rule there is a short inline syntax:

classConferenceForm < ActionForm::Baseattributes:name,:city,required: trueend

The API

The ActionForm::Base class provides a simple API with only a few instance/class methods. Below are listed the instance methods:

  1. initialize(model) accepts an instance of the model that the form represents.
  2. submit(params) updates the main form's model and nested models with the posted parameters. The models are not saved/updated until you call save.
  3. errors returns validation messages in a classy Active Model style.
  4. save will call save on the model and nested models. This method will validate the model and nested models and if no error arises then it will save them and return true.

The following are the class methods:

  1. attributes accepts the names of attributes to define on the form. If you want to declare a presence validation rule for the given attributes, you can pass in the required: true option as showcased above. The attribute method is aliased to the attributes method.
  2. association(name, options={}, &block) defines a nested form for the name model. If the model is a has_many association you can pass in the records: x option and fields to create x objects will be rendered. If you pass a block, you can define another nested form the same way.

In addition to the main API, forms expose accessors to the defined attributes. This is used for rendering or manual operations.

Setup

In your controller you create a form instance and pass in the model you want to work on.

classConferencesControllerdefnewconference=Conference.new@conference_form=ConferenceForm.new(conference)endend

You can also setup the form for editing existing items.

classConferencesControllerdefeditconference=Conference.find(params[:id])@conference_form=ConferenceForm.new(conference)endend

Action Form will read property values from the model in setup. Given the following form class.

classConferenceForm < ActionForm::Baseattribute:nameend

Internally, this form will call conference.name to populate the name field.

Rendering Forms

Your @conference_form is now ready to be rendered, either do it yourself or use something like Rails' form_for, simple_form or formtastic.

<%= form_for @conference_form do |f| %><%= f.text_field :name %><%= f.text_field :city %><% end %>

Nested forms and collections can be easily rendered with fields_for, etc. Just use Action Form as if it would be an Active Model instance in the view layer.

Syncing Back

After setting up your form object, you can populate the models with the submitted parameters.

classConferencesControllerdefcreateconference=Conference.new@conference_form=ConferenceForm.new(conference)@conference_form.submit(conference_params)endend

This will write all the properties back to the model. In a nested form, this works recursively, of course.

Saving Forms

After the form is populated with the posted data, you can save the model by calling save.

classConferencesControllerdefcreateconference=Conference.new@conference_form=ConferenceForm.new(conference)@conference_form.submit(conference_params)if@conference_form.saveredirect_to@conference_form,notice: "Conference: #{@conference_form.name} was successfully created."}elserender:newendendend

If the save method returns false due to validation errors defined on the form, you can render it again with the data that has been submitted and the errors found.

Nesting Forms: 1-n Relations

Action Form also gives you nested collections.

Let's define the has_many :speakers collection association on the Conference model.

classConference < ActiveRecord::Basehas_many:speakersvalidates:name,uniqueness: trueend

The form should look like this.

classConferenceForm < ActionForm::Baseattributes:name,:city,required: trueassociation:speakersdoattributes:name,:occupation,required: trueendend

By default, the association :speakers declaration will create a single Speaker object. You can specify how many objects you want in your form to be rendered with the new action as follows: association: speakers, records: 2. This will create 2 new Speaker objects, and of course fields to create 2 Speaker objects. There are also some link helpers to dynamically add/remove objects from collection associations. Read below.

This basically works like a nested property that iterates over a collection of speakers.

has_many: Rendering

Action Form will expose the collection using the speakers method.

<%= form_for @conference_form |f| %><%= f.text_field :name %><%= f.text_field :city %><%= f.fields_for :speakers do |s| %><%= s.text_field :name %><%= s.text_field :occupation %><% end %><% end %>

Nesting Forms: 1-1 Relations

Speakers are allowed to have 1 Presentation.

classSpeaker < ActiveRecord::Basehas_one:presentationbelongs_to:conferencevalidates:name,uniqueness: trueend

The full form should look like this:

classConferenceForm < ActionForm::Baseattributes:name,:city,required: trueassociation:speakersdoattribute:name,:occupation,required: trueassociation:presentationdoattribute:topic,:duration,required: trueendendend

has_one: Rendering

Use fields_for in a Rails environment to correctly setup the structure of params.

<%= form_for @conference_form |f| %><%= f.text_field :name %><%= f.text_field :city %><%= f.fields_for :speakers do |s| %><%= s.text_field :name %><%= s.text_field :occupation %><%= s.fields_for :presentation do |p| %><%= p.text_field :topic %><%= p.text_field :duration %><% end %><% end %><% end %>

Dynamically Adding/Removing Nested Objects

Action Form comes with two helpers to deal with this functionality:

  1. link_to_add_association will display a link that renders fields to create a new object.
  2. link_to_remove_association will display a link to remove a existing/dynamic object.

In order to use it you have to insert this line: //= require action_form to your app/assets/javascript/application.js file.

In our ConferenceForm we can dynamically create/remove Speaker objects. To do that we would write in the app/views/conferences/_form.html.erb partial:

<%= form_for @conference_form do |f| %><% if @conference_form.errors.any? %><divid="error_explanation"><h2><%=pluralize(@conference_form.errors.count,"error")%> prohibited this conference from being saved:</h2><ul><%@conference_form.errors.full_messages.eachdo |message| %><li><%=message%></li><%end%></ul></div><%end%><h2>Conference Details</h2><divclass="field"><%=f.label:name,"Conference Name"%><br><%=f.text_field:name%></div><divclass="field"><%=f.label:city%><br><%=f.text_field:city%></div><h2>Speaker Details</h2><%=f.fields_for:speakersdo |speaker_fields| %><%=render"speaker_fields",:f=>speaker_fields%><%end%><divclass="links"><%=link_to_add_association"Add a Speaker",f,:speakers%></div><divclass="actions"><%=f.submit%></div><%end%>

Our app/views/conferences/_speaker_fields.html.erb would be:

<divclass="nested-fields"><divclass="field"><%=f.label:name,"Speaker Name"%><br><%=f.text_field:name%></div><divclass="field"><%=f.label:occupation%><br><%=f.text_field:occupation%></div><h2>Presentantions</h2><%=f.fields_for:presentationdo |presentations_fields| %><%=render"presentation_fields",:f=>presentations_fields%><%end%><%=link_to_remove_association"Delete",f%></div>

And app/views/conferences/_presentation_fields.html.erb would be:

<divclass="field"><%=f.label:topic%><br><%=f.text_field:topic%></div><divclass="field"><%=f.label:duration%><br><%=f.text_field:duration%></div>

Plain Old Ruby Object Forms

ActionForm also can accept ActiveModel::Model instances as a model.

classFeedbackincludeActiveModel::Modelattr_accessor:name,:body,:emaildefsaveFeedbackMailer.send_email(email,name,body)endend

The form should look like this.

classFeedbackForm < ActionForm::Baseattributes:name,:body,:email,required: trueend

And then in controller:

classFeedbacksControllerdefcreatefeedback=Feedback.new@feedback_form=FeedbackForm.new(feedback)@feedback_form.submit(feedback_params)if@feedback_form.savehead:okelserenderjson: @feedback_form.errorsendend

Demos

You can find a list of applications using this gem in this repository: https://github.com/m-Peter/nested-form-examples . All the examples are implemented in before/after pairs. The before is using the accepts_nested_attributes_for, while the after uses this gem to achieve the same functionality.

Credits

Special thanks to the owners of the great gems that inspired this work:

About

Create nested forms with ease.

Resources

Stars

292 stars

Watchers

24 watching

Forks

Releases

Packages

Used by

Contributors

Languages