Skip to content

stokarenko/dynamic-fields-for

Repository files navigation

DynamicFieldsFor

Version Build Climate 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.
  • Supports not ActiveRecord models
  • Supports nested dynamic fields

Alternatives

Dependencies

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

Getting started

Add to your Gemfile:

  gem 'dynamic-fields-for'

Run the bundle command to install it.

Add to app/assets/javascripts/application.js:

//= require dynamic-fields-for

Usage

Lets say that we have the models:

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:

class User < ActiveRecord::Base
  has_many :roles, inverse_of: :user
end

Add to User model:

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:

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:

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

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

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:

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

$(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:

$('#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:

$(document).on('dynamic-fields:after-add', function(event){
  $(event.target).find2('.datepicker').datetimepicker();
})

License

MIT License. Copyright (c) 2015 Sergey Tokarenko