Dynamic nested form for Ruby On Rails
npm install vanilla-nestedRails dynamic nested forms using vanilla JS
Similar to cocoon, but with no jquery dependency!
Example:
Just add it to your gemfile
``ruby
gem 'vanilla_nested'
Using Sprockets
Require the js:
`
//= require vanilla_nested
`Using Webpacker
Add the package too (gem is required for the helper methods) using:
`sh
yarn add vanilla-nestedor, to use latest code from git:
yarn add arielj/vanilla-nested
`And then use it in your application.js as:
`js
import "vanilla-nested";
`Using Importmaps in Rails 7
Add the importmap config:
`rb
pin "vanilla-nested", to: "vanilla_nested.js", preload: true
`And then use it in your application.js as:
`js
import "vanilla-nested";
`Updating
To update the gem use either:
`
gem update vanilla_nested # if using the gem from RubyGemsor
gem update --source vanilla_nested # if using the gem from github
`If using webpacker, you need to update the node package too with:
`
yarn upgrade vanilla-nested
`> You can clear the webpacker cache just in case if changes are not reflecting with
rails webpacker:clobberUsage
Backend prerequisites
`ruby
models/order.rb
class Order < ApplicationRecord
has_many :order_items
accepts_nested_attributes_for :order_items, reject_if: :all_blank, allow_destroy: true
``ruby
models/order_item.rb
class OrderItem < ApplicationRecord
belongs_to :order
end
``ruby
class OrdersController < ApplicationController
...
def order_params
params.require(:order).permit(:attr, order_items_attributes: [:id, :attr1, :attr2, :_destroy])
end
end
`$3
`HTML+ERB
orders/_order_item_fields.html.erb
<%= form.text_field :attr1 %>
<%= form.select :attr2 ..... %>
<%= link_to_remove_nested(form) %>
``HTML+ERB
order/form.html.erb
<%= form_for product do |form| %>
<%= form.text_field :attr %> <%= link_to_add_nested(form, :order_items, '#order-items') %>
<%= form.fields_for :order_items do |order_item_f| %>
<%= render 'order_item_fields', form: order_item_f %>
<% end %>
<% end %>
`Note that:
-
link_to_remove_nested receives the nested form as a parameter, it adds a hidden [_destroy] field
- link_to_add_nested expects the form builder, the name of the association and the selector of the container where the gem will insert the new fieldsCustomizing link_to_add_nested
#### Link text
The default value is
Add , but it can be changed using the parameter link_text:`Ruby
link_to_add_nested(form, :order_items, '#order-items', link_text: I18n.t(:some_key))
`This way you can, for example, internationalize the text.
#### Link classes
By default, the link to add new fields has the class
vanilla-nested-add which is required, but you can add more classes passing a string:`Ruby
link_to_add_nested(form, :order_items, '#order-items', link_classes: 'btn btn-primary')
`This way you can style the link with no need of targeting the specific vanilla nested class.
#### Insert position
By default, new fields are appended to the container, you can change that with the
insert_method option. For now, only :append and :prepend are supported:`Ruby
link_to_add_nested(form, :order_items, '#order-items', insert_method: :prepend)
`#### Partial name
The default partial's name is inferred using the association's class name. If your Order has_many :order_items and the class of those items is OrderItem, then the inferred name is
order_item_fields. You can use any partial name you like, though:`Ruby
link_to_add_nested(form, :order_items, '#order-items', partial: 'my_partial')
`#### Form variable used in the partial
link_to_add_nested needs to render an empty template in order to later append/prepend it. To do that, it passes the form as a local variable. If your partial uses form, you don't have to do anything, but if you using another variable name, just customize it here.`HTML+ERB
orders/_order_item_fields.html.erb
<%= ff.text_field :attr1 %>
<%= ff.select :attr2 ..... %>
<%= link_to_remove_nested(ff) # adds a link to remove the element %>
``Ruby
link_to_add_nested(form, :order_items, '#order-items', partial_form_variable: :ff)
`You can also pass more local variables to the partial by setting the partial_locals value.
`Ruby
link_to_add_nested(form, :order_items, '#order-items', partial_locals: { key: 'value' })
`#### Tag
The HTML tag that will be generated. An
tag by default.`Ruby
link_to_add_nested(form, :order_items, '#order-items', tag: 'span')
`#### Tag Attributes
HTML attributes to set for the generated HTML tag. It's a has with key value pairs user for the
content_tag call, so it support any attribute/value pair supported by content_tag`Ruby`
link_to_add_nested(form, :order_items, '#order-items', link_text: '+', tag_attributes: {title: "Add Order"})+
class attributes are appended to the vanilla-nested-add class and the custom classes specified with the link_classes argument.
`Ruby`
link_to_add_nested(form, :order_items, '#order-items', link_text: '+', tag_attributes: {class: "some-class"}, link_classes: 'another-class')+
#### Link content
If you need html content, you can use a block:
`erb`
<%= link_to_add_nested(form, :order_items, '#order-items') do %>
<% end %>
#### Link text
The default value is "X", but it can be changed using the parameter link_text:
`Ruby`
link_to_remove_nested(form, link_text: "remove")
#### Link content
If you need html content, you can use a block:
`erb`
<%= link_to_remove_nested(form) do %>
<% end %>
#### Link classes
By default, the link to remove fields has the class vanilla-nested-remove which is required, but you can add more classes passing a space separated string:
`Ruby`
link_to_remove_nested(form, link_classes: 'btn btn-primary')
This way you can style the link with no need of targeting the specific vanilla nested class.
#### Fields wrapper
By default, the link to remove the fields assumes it's a direct child of the wrapper of the fields. You can customize this if you can't make it a direct child.
`HTML+ERB`orders/_order_item_fields.html.erb
<%= link_to_remove_nested(ff, fields_wrapper_selector: '.wrapper-div') # if we don't set this, it will only hide the span %>
Note that:
- The link MUST be a descendant of the fields wrapper, it may not be a direct child, but the look up of the wrapper uses JavaScript's closest() method, so it looks on the ancestors.closest()
- Since this uses JavaScript's , there is no IE supported (https://caniuse.com/#search=closest). You may want to add a polyfill or define the method manually if you need to support it.
#### Tag
The HTML tag that will be generated. An tag by default.
`Ruby`
link_to_remove_nested(ff, tag: 'p')
#### Tag Attributes
HTML attributes to set for the generated HTML tag. It's a has with key value pairs user for the content_tag call, so it support any attribute/value pair supported by content_tag
`Ruby`
link_to_remove_nested(ff, link_text: 'X', tag_attributes: {title: "Delete!"})X
class attributes are appended to the vanilla-nested-remove class and the custom classes specified with the link_classes argument, just like the link_to_add_nested helper.
#### Undoing
You can tell the plugin to add an "undo" link right after removing the fields (as a direct child of the fields wrapper! this is not customizable!).
`Ruby`
link_to_remove_nested(ff, undo_link_timeout: 2000, undo_link_text: I18n.t('undo_remove_fields'), undo_link_classes: 'btn btn-secondary')
Options are:
- undo_link_timeout: milliseconds, greater than 0 to turn the feature on, default: nilundo_link_text
- : string with the text of the link, great for internationalization, default: 'Undo'undo_link_classes
- : space separated string, default: ''
There are some events that you can listen to add custom callbacks on different moments. All events bubbles up the dom, so you can listen for them on any ancestor.
#### 'vanilla-nested:fields-added'
Triggered right after the fields wrapper was inserted on the container.
`Javascript`
document.addEventListener('vanilla-nested:fields-added', function(e){
// e.type == 'vanilla-nested:fields-added'
// e.target == container div of the fields
// e.detail.triggeredBy == the "add" link
// e.detail.added == the fields wrapper just inserted
})
#### 'vanilla-nested:fields-limit-reached'
Triggered right after the fields wrapper was inserted on the container if the current count is >= limit, where limit is the value configured on the model: accepts_nested_attributes_for :assoc, limit: 5. You can listen to this event to disable the "add" link for example, or to show a warning.
`Javascript`
document.addEventListener('vanilla-nested:fields-limit-reached', function(e){
// e.type == 'vanilla-nested:fields-added'
// e.target == container div of the fields
// e.detail.triggeredBy == the "add" link
})
#### 'vanilla-nested:fields-removed'
Triggered when the fields wrapper if fully hidden (aka ""removed""), that is: after clicking the "remove" link with no timeout OR after the timeout finished.
`Javascript`
document.addEventListener('vanilla-nested:fields-removed', function(e){
// e.type == 'vanilla-nested:fields-removed'
// e.target == fields wrapper ""removed""
// e.detail.triggeredBy == the "remove" link if no undo action, the 'undo' link if it was triggered by the timeout })
#### 'vanilla-nested:fields-hidden'
Triggered when the fields wrapper if hidden with an undo option.
`Javascript`
document.addEventListener('vanilla-nested:fields-hidden', function(e){
// e.type == 'vanilla-nested:fields-hidden'
// e.target == fields wrapper hidden
// e.detail.triggeredBy == the "remove" link
})
> Remove vs Hidden
>
> Behind the scene, the wrapper is never actually removed, because we need to send the [_destroy] parameter. But there are 2 different stages when removing it.display: none
>
> - If there's no "undo" action configured, the wrapped is set to and considered "removed".hidden
> - If you use the "undo" feature, first the children of the wrapper are hidden (triggering the event) and then, after the timeout passes, the wrapper is set to display: none (triggering the removed event).
#### 'vanilla-nested:fields-hidden-undo'
Triggered when the user undo the removal using the "undo" link.
`Javascript`
document.addEventListener('vanilla-nested:fields-hidden-undo', function(e){
// e.type == 'vanilla-nested:fields-hidden-undo'
// e.target == fields wrapper unhidden
// e.detail.triggeredBy == the "undo" link
})
You can run the tests following these commands:
- cd test/VanillaNestedTests # move to the rails app dir
- bin/setup # install bundler, gems and yarn packages
- rails test # unit tests
- rails test:system # system tests
> If you make changes in the JS files, you have to tell yarn to refresh the code inside the node_modules folder running ./bin/update-gem (or yarn upgrade vanilla-nested and rails webpacker:clobber), and then restart the rails server or re-run the tests.
#### Change the method to infere the name of the partial
Before, it used SomeClass.name.downcase, this created a problem for classes with more than one word:
- User => 'user_fields'
- SomeClass => 'someclass_fields'
Now it uses SomeClass.name.underscore:
- User => 'user_fields'
- SomeClass => 'some_class_fields'
If you used the old version, you'll need to change the partial name or provide the old name as the partial: argument.
#### Fix some RuboCop style suggestions
Mostly single/double quotes, spacing, etc.
#### Added some Solagraph related doc for the view helpers
Just so Solargraph plugins on editors like VS-Code can give you some documentation.
#### Added some documentation on the code
Mostly on the javascript code
#### Added node module config
So it can be used as a node module using yarn to integrate it using webpacker.
#### New event for the "limit" option of accepts_nested_attributes_for
You can listen to the vanilla-nested:fields-limit-reached event that will fire when container has more or equals the amount of children than the limit option set on the accepts_nested_attributes_for configuration.
#### Removed "onclick" attribute for helpers and add event listeners within js
If you were using webpacker, remember to replace the vanilla_nested.js file in your app/javascript folder
#### Added "link_classes" option to "link_to_remove_nested"
You can set multiple classes for the "X" link
#### Added a "link_content" block parameter for both link helpers
You can pass a block to use as the content for the add and remove links
#### Fix using nested html elements as the content for buttons
There was an error when using the helpers with things like:
`erb`
<%= link_to_add_nested(form, :pets, '#pets') do %>
Add Pet
<% end %>
It would detect the wrong element for the click event, making the JS fail.
Play nicely with Turbolinks' turbolinks:load event.
License change from GPL to MIT
#### Custom generated HTML element tag
The default HTML tag generated by link_to_add/remove_nested is an A tag, but it can now be customized with the tag: keyword argument:
`erb`
<%= link_to_add_nested(form, :pets, '#pets', tag: 'span') %>
#### Extra class added to dynamically added fields
Elements added dynamically now have an extra added-by-vanilla-nested class, used internally and helpful for styling.
#### Removed elements are actually removed from the document
Before, the elements were just hidden using display: none on the wrapper. That lead to issues when the input elements had validations that were triggered by the browser but the elements where not accessible anymore. With this new version, removing a nested set of fields removes all the content.
#### Correct calculation for the limit-reached event
If the accepts_nested_attributes_for configuration has a limit, this gem was counting the number of children wrong (it was counting removed elements and extra children of the wrapper). This fixes that by only counting the [_destroy] hidden fields with value 0.
#### Custom HTML attributes for the generated HTML element tag
Both link_to_add_nested and link_to_remove_nested now support a tag_attributes keyword argument with key value pairs representing attributes and values for the generated HTML tag.
You can use this to customize the tag as you need:
- you can add a title for your a tag to make them more accessible:
`Ruby`
link_to_remove_nested(ff, link_text: 'X', tag_attributes: {title: "Delete!"})X
- you can set a type, name and value for a button tag if you want to have a non-javascript submit action to fallback in case the user has Javascript disabled:
`Rubycommit
link_to_add_nested(form, :order_items, '#order-items', tag: 'button', tag_attributes: {type: 'submit', name: 'commit', value: 'add-nested' })now you have a button tag that will submit your form with a
param with add-nested as the value to handle a non-javascript fallback to add nested fields an re-render the form!`
- you can set any valid html attribute accepted by content_tag
#### Added Integration with the turbo Gem
The JavaScript part of the gem now plays nicely with the turbo gem by initializing the needed events when the turbo:load event is fired.
#### Yarn/NPM Packages
Node package can be installed using npm or yarn without using the GitHub repo. This improves the size of the bundle and allows version flags.
#### Fix undeclared variables
https://github.com/arielj/vanilla-nested/pull/45 thanks @gmeir.
#### Added engine config to support importmaps
You can pin the vanilla-nested module. A Rails 7 sample app is added to the test directory.
#### Fix elements' style after undo
When undoing a removal, the gem was setting display: initial to all the elements. Now it sets the display value it had before hidding the element.
> Remember to update both gem and package https://github.com/arielj/vanilla-nested#update
#### Event listeners are now added once to the document
Attach the vanilla-nested event listeners to the document object. This fixes issues with turbo/hotwire where the listener was not being attached to the new elements added to the DOM. Thanks to @lenilsonjr for testing these changes!
#### Fix initialization of Vanilla Nested when using importmaps and Safari
The shim to support importmaps in Safari was not firing the load events as the code was expecting. This patches that by considering if the DOM is already ready when the code loads.
#### New classes added
When removing a nested element, a hidden-by-vanilla-nested class is added if there's a timeout. The class is removed if the action is undone.
When removing a nested element, a removed-by-vanilla-nested class is added if there's no timeout or after the timeout expires.
Thanks to @kikyous for the contribution!
#### Tests in multiple Ruby versions
Automated tests now run in multiple Ruby versions in CI.
This change has no impact in the use of the gem, but I wanted to thank @petergoldstein for their contribution!
#### New partial_locals option for link_to_add_nested
Now you can pass a new option with locals` for the fields wrapper partial. Check https://github.com/arielj/vanilla-nested/pull/64 for an example. Thanks to @gregogalante for the contribution!