Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/jbox-web/action_form

Create nested forms, easy ;)
https://github.com/jbox-web/action_form

form form-object form-validation rails ruby

Last synced: about 2 months ago
JSON representation

Create nested forms, easy ;)

Awesome Lists containing this project

README

        

# ActionForm

[![GitHub license](https://img.shields.io/github/license/jbox-web/action_form.svg)](https://github.com/jbox-web/action_form/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/jbox-web/action_form.svg)](https://github.com/jbox-web/action_form/releases/latest)
[![CI](https://github.com/jbox-web/action_form/workflows/CI/badge.svg)](https://github.com/jbox-web/action_form/actions)
[![Code Climate](https://codeclimate.com/github/jbox-web/action_form/badges/gpa.svg)](https://codeclimate.com/github/jbox-web/action_form)
[![Test Coverage](https://codeclimate.com/github/jbox-web/action_form/badges/coverage.svg)](https://codeclimate.com/github/jbox-web/action_form/coverage)

Set your models free from the `accepts_nested_attributes_for` helper. ActionForm 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

Put this in your `Gemfile` :

```ruby
git_source(:github){ |repo_name| "https://github.com/#{repo_name}.git" }

gem 'action_form', github: 'jbox-web/action_form', tag: '1.4.0'
```

then run `bundle install`.

## 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`:

```ruby
class ConferenceForm < ActionForm::Base
self.main_model = :conference

attributes :name, :city

validates :name, :city, presence: true
end
```

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:

```ruby
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:

```ruby
class ConferenceForm < ActionForm::Base
attributes :name, :city, required: true
end
```

## 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.

```ruby
class ConferencesController
def new
conference = Conference.new
@conference_form = ConferenceForm.new(conference)
end
end
```

You can also setup the form for editing existing items.

```ruby
class ConferencesController
def edit
conference = Conference.find(params[:id])
@conference_form = ConferenceForm.new(conference)
end
end
```

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

```ruby
class ConferenceForm < ActionForm::Base
attribute :name
end
```

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`.

```erb
<%= 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 ActionForm 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.

```ruby
class ConferencesController
def create
conference = Conference.new
@conference_form = ConferenceForm.new(conference)
@conference_form.submit(conference_params)
end
end
```

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`.

```ruby
class ConferencesController
def create
conference = Conference.new
@conference_form = ConferenceForm.new(conference)
@conference_form.submit(conference_params)

if @conference_form.save
redirect_to @conference_form, notice: "Conference: #{@conference_form.name} was successfully created." }
else
render :new
end
end
end
```

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

ActionForm also gives you nested collections.

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

```ruby
class Conference < ActiveRecord::Base
has_many :speakers
validates :name, uniqueness: true
end
```

The form should look like this.

```ruby
class ConferenceForm < ActionForm::Base
attributes :name, :city, required: true

association :speakers do
attributes :name, :occupation, required: true
end
end
```

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

ActionForm will expose the collection using the `speakers` method.

```erb
<%= 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.

```ruby
class Speaker < ActiveRecord::Base
has_one :presentation
belongs_to :conference
validates :name, uniqueness: true
end
```

The full form should look like this:

```ruby
class ConferenceForm < ActionForm::Base
attributes :name, :city, required: true

association :speakers do
attribute :name, :occupation, required: true

association :presentation do
attribute :topic, :duration, required: true
end
end
end
```

### has_one: Rendering

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

```erb
<%= 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

ActionForm 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:

```erb
<%= form_for @conference_form do |f| %>
<% if @conference_form.errors.any? %>


<%= pluralize(@conference_form.errors.count, "error") %> prohibited this conference from being saved:


    <% @conference_form.errors.full_messages.each do |message| %>
  • <%= message %>

  • <% end %>


<% end %>

Conference Details



<%= f.label :name, "Conference Name" %>

<%= f.text_field :name %>


<%= f.label :city %>

<%= f.text_field :city %>

Speaker Details


<%= f.fields_for :speakers do |speaker_fields| %>
<%= render "speaker_fields", :f => speaker_fields %>
<% end %>


<%= f.submit %>

<% end %>
```

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

```erb



<%= f.label :name, "Speaker Name" %>

<%= f.text_field :name %>


<%= f.label :occupation %>

<%= f.text_field :occupation %>

Presentantions


<%= f.fields_for :presentation do |presentations_fields| %>
<%= render "presentation_fields", :f => presentations_fields %>
<% end %>

<%= link_to_remove_association "Delete", f %>


```

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

```erb


<%= f.label :topic %>

<%= f.text_field :topic %>


<%= f.label :duration %>

<%= f.text_field :duration %>

```

## Plain Old Ruby Object Forms

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

```ruby
class Feedback
include ActiveModel::Model

attr_accessor :name, :body, :email

def save
FeedbackMailer.send_email(email, name, body)
end
end
```

The form should look like this.

```ruby
class FeedbackForm < ActionForm::Base
attributes :name, :body, :email, required: true
end
```

And then in controller:

```ruby
class FeedbacksController
def create
feedback = Feedback.new
@feedback_form = FeedbackForm.new(feedback)
@feedback_form.submit(feedback_params)

if @feedback_form.save
head :ok
else
render json: @feedback_form.errors
end
end
```

## 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:

* [Nick Sutterer](https://github.com/apotonick) - creator of [reform](https://github.com/apotonick/reform)
* [Nathan Van der Auwera](https://github.com/nathanvda) - creator of [cocoon](https://github.com/nathanvda/cocoon)