stokarenko/dynamic-fields-for

View on GitHub
README.md

Summary

Maintainability
Test Coverage
DynamicFieldsFor
================
[![Version](https://badge.fury.io/rb/dynamic-fields-for.svg)](http://badge.fury.io/rb/dynamic-fields-for)
[![Build](https://travis-ci.org/stokarenko/dynamic-fields-for.svg?branch=master)](https://travis-ci.org/stokarenko/dynamic-fields-for)
[![Climate](https://codeclimate.com/github/stokarenko/dynamic-fields-for/badges/gpa.svg)](https://codeclimate.com/github/stokarenko/dynamic-fields-for)
[![Coverage](https://codeclimate.com/github/stokarenko/dynamic-fields-for/badges/coverage.svg)](https://codeclimate.com/github/stokarenko/dynamic-fields-for/coverage)

DynamicFieldsFor is a Rails plugin which provides the dynamic association fieldsets to your forms without pain. And it does nothing else.

The main features are:
* Doesn't break the HTML layout - no wrappers, additional divs etc;
* Works with fields block, i.e. doesn't require the separated partial for them;
* Doesn't provide new form helpers, but extends the existing one;
* Simple and predictable interface and behavior;
* Doesn't require any special HTML entities inside templates;
* Supports [Simple Form](https://github.com/plataformatec/simple_form).
* Supports not ActiveRecord models
* Supports nested dynamic fields

## Alternatives
* [cocoon](https://github.com/nathanvda/cocoon)
* [Nested Form](https://github.com/ryanb/nested_form)

## Dependencies
* Ruby >= 2.2.2
* [rails](https://github.com/rails/rails) >= 5.0.0
* [jquery-rails](https://github.com/rails/jquery-rails)
* [activerecord-devkit](https://github.com/stokarenko/activerecord-devkit), `association_soft_build` feature

For older versions of ruby and rails - please use gem version `1.1.0`.

## Getting started

Add to your Gemfile:

```ruby
  gem 'dynamic-fields-for'
```

Run the bundle command to install it.

Add to `app/assets/javascripts/application.js`:
```js
//= require dynamic-fields-for
```

## Usage
Lets say that we have the models:

```ruby
class User < ActiveRecord::Base
  has_many :roles
end

class Role < ActiveRecod::Base
  belongs_to :user

  validates :user, presence: true
end
```

First, apply `inverse_of` to User's `:roles` associations, otherwise there is no chance to pass
the validation of Role's user presence on user creation:
```ruby
class User < ActiveRecord::Base
  has_many :roles, inverse_of: :user
end
```

Add to User model:
```ruby
accepts_nested_attributes_for :roles, allow_destroy: true
```

Skip `allow_destroy` definition if you don't need to use `remove_fields_link` helper).

Take care about strong parameters in controller like this:
```ruby
params.require(:user).permit(roles_attributes: [:id, :_destroy])
```

It is important to permit `id` role's parameter, don't miss it. As for `_destroy`,
skip it if you don't need to use `remove_fields_link` helper.

Then, in view:
```haml
= form_for resource do |f|
  = f.text_field :user_name

  = f.fields_for :roles, dynamic: true do |rf|
    = rf.text_field :role_name
    = rf.remove_fields_link 'Remove role'

  = f.add_fields_link :roles, 'Add role'

  = f.submit
```

DynamicFieldsFor supports SimpleForm:
```haml
= simple_form_for resource do |f|
  = f.input :user_name

  = f.simple_fields_for :roles, dynamic: true do |rf|
    = rf.input :role_name
    = rf.remove_fields_link 'Remove role'

  = f.add_fields_link :roles, 'Add role'

  = f.submit
```

## Not ActiveRecord models
To use DynamicFieldsFor with not ActiveRecord, it's necessary to define two methods in your model, `{association}_soft_build` and `{association}_attributes=`:

```ruby
class EmailForm
  include ActiveAttr::Model

  attribute :recipients, type: Object, default: []

  def recipients_attributes=(attributes)
    self.recipients = attributes.values.map{ |attrs| recipients_soft_build(attrs) }
  end

  def recipients_soft_build(attrs = {})
    Recipient.new(attrs)
  end

end

class Recipient
  include ActiveAttr::Model

  attribute :email

  validates :email,  presence: true
end
```

Template will stay to be as usual:
```haml
= form_for resource do |f|
  = f.fields_for :recipients, dynamic: true do |rf|
    = rf.text_field :email
    = rf.remove_fields_link 'Remove recipient'

  = f.add_fields_link :recipients, 'Add recipient'

  = f.submit
```

## JavaScript events
There are the events which will be triggered on `add_fields_link` click, in actual order:
* `dynamic-fields:before-add-into` touched to dynamic fields parent node;
* `dynamic-fields:after-add` touched to each first-level elements which were inserted;
* `dynamic-fields:after-add-into` touched to dynamic fields parent node;

Like that, these events will be triggered on `add_fields_link` click, in actual order:
* `dynamic-fields:before-remove-from` touched to dynamic fields parent node;
* `dynamic-fields:before-remove` touched to each first-level elements which are going to be removed;
* `dynamic-fields:after-remove-from` touched to dynamic fields parent node;

Typical callback for dynamic fields parent node looks like:
```js
$(document).on('dynamic-fields:after-add-into', function(event){
  $(event.target).find('li').order();
})
```

As for first-level elements, compatible callbacks
will be triggered to each of them. To deal with this,
use `$.find2` javascript helper, which provided by DynamicFieldsFor:
```js
$('#some_id').find2('.some_class');
// doing the same as...
$('#some_id').find('.some_class').add($('#some_id').filter('.some_class'));
```

Typical event callback first-level elements should look like:
```js
$(document).on('dynamic-fields:after-add', function(event){
  $(event.target).find2('.datepicker').datetimepicker();
})
```

## License
MIT License. Copyright (c) 2015 Sergey Tokarenko