{"id":14955938,"url":"https://github.com/notus-sh/cocooned","last_synced_at":"2026-02-27T15:38:05.231Z","repository":{"id":54800179,"uuid":"144280944","full_name":"notus-sh/cocooned","owner":"notus-sh","description":"Dynamic nested forms in Rails made easy","archived":false,"fork":false,"pushed_at":"2025-01-06T22:33:43.000Z","size":1841,"stargazers_count":9,"open_issues_count":3,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-29T09:11:20.007Z","etag":null,"topics":["forms","nested","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"nathanvda/cocoon","license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/notus-sh.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"open_collective":"notus-sh"}},"created_at":"2018-08-10T12:02:21.000Z","updated_at":"2025-01-06T22:33:43.000Z","dependencies_parsed_at":"2024-01-15T22:14:19.723Z","dependency_job_id":"59df82b0-f4b8-4b69-9270-04c9da0325b6","html_url":"https://github.com/notus-sh/cocooned","commit_stats":{"total_commits":497,"total_committers":71,"mean_commits":7.0,"dds":0.6619718309859155,"last_synced_commit":"3df63562330b6c2adf07c53287a1cec4dbc8807f"},"previous_names":[],"tags_count":56,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notus-sh%2Fcocooned","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notus-sh%2Fcocooned/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notus-sh%2Fcocooned/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notus-sh%2Fcocooned/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/notus-sh","download_url":"https://codeload.github.com/notus-sh/cocooned/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":237944040,"owners_count":19391588,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["forms","nested","rails","ruby"],"created_at":"2024-09-24T13:12:02.995Z","updated_at":"2026-02-27T15:38:05.223Z","avatar_url":"https://github.com/notus-sh.png","language":"JavaScript","funding_links":["https://opencollective.com/notus-sh"],"categories":[],"sub_categories":[],"readme":"# Cocooned\n\n[![Unit tests](https://github.com/notus-sh/cocooned/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/notus-sh/cocooned/actions/workflows/unit-tests.yml)\n[![Gem Version](https://badge.fury.io/rb/composite_content.svg)](https://badge.fury.io/rb/cocooned)\n\nCocooned makes it easier to handle nested forms in Rails.\n\nCocooned is form builder-agnostic: it works with standard Rails (\u003e= 7.0, \u003c 8.2) form helpers, [Formtastic](https://github.com/justinfrench/formtastic) or [SimpleForm](https://github.com/plataformatec/simple_form).\n\n1. [Background](#some-background)\n2. [Installation](#installation)\n3. [Getting started](#getting-started)\n4. [Going further with plugins](#plugins)\n5. [Links or buttons ?](#links-or-buttons)\n5. [I18n integration](#internationalisation)\n6. [JavaScript](#javascript)\n7. [Styling](#styling-forms)\n8. [Migration from a previous version](#migration-from-a-previous-version) or from Cocoon\n\n## Some Background\n\nCocooned is a fork of [Cocoon](https://github.com/nathanvda/cocoon) by [Nathan Van der Auwera](https://github.com/nathanvda). He and all Cocoon contributors did a great job to maintain it for years. Many thanks to them!\n\nHowever, the project seems to have only received minimal fixes since 2018 and many pull requests, even simple ones, have been on hold for a long time. In 2019, as I needed more than what Cocoon provided at this time, I had the choice to either maintain an extension or to fork it and integrate everything that was waiting and more.\n\nOver the time, Cocooned turned into an almost complete rewrite of Cocoon with more functionnalities, a more fluent API (I hope) and integration with modern toolchains. Still, **Cocooned is compatible with Cocoon and can be used as a**(n almost) **drop-in replacement. This compatibility layer with the original Cocoon API will be dropped in the next major release.**\n\nOn the JavaScript side, Cocooned 2.0 removed the dependency to jQuery (Yeah! :tada:). See [JavaScript](#javascript) for details.\n\n## Installation\n\nAdd `cocooned` to your `Gemfile`:\n\n```ruby\ngem 'cocooned'\n```\n\n### Load Cocooned JavaScript\n\nCocooned comes with an NPM companion package: [`@notus.sh/cocooned`](https://www.npmjs.com/package/@notus.sh/cocooned).\nIt bundles JavaScript files to handles in-browser interactions with your nested forms.\n\nInstall it with your favorite package manager:\n\n```shell\n# bun\n$ bun install @notus.sh/cocooned\n# importmap (Rails ~7.0 default)\n$ bin/importmap pin @notus.sh/cocooned\n# npm\n$ npm install @notus.sh/cocooned\n# pnpm\n$ pnpm add @notus.sh/cocooned\n# yarn\n$ yarn add @notus.sh/cocooned\n```\n\n**Note:** To ensure you will always get the version of the companion package that match with the gem version, you should specify the same version constraint you used for the `cocooned` gem in your `Gemfile`.\n\nOnce installed, load it into your application with:\n\n```javascript\nimport Cocooned from '@notus.sh/cocooned'\nCocooned.start()\n```\n\nIf you still use Sprockets to bundle your javascripts (Rails 3.1+ default), you can either install the companion package from npmjs.org with the package manager of your choice, configure Sprockets to look for files in your application's `/node_modules` directory and load it as above (recommended) or require `cocooned` in your `application.js` with:\n\n```javascript\n//= require 'cocooned'\n```\n\n**This compatibility with aging Rails assets pipelines will be removed in the next major release.**\n\n## Getting started\n\nFor all the following examples, we will consider modelisation of an administrable list with items.\nHere are the two ActiveRecord models : `List` and `Item`:\n\n```ruby\n# == Schema Info\n#\n# Table name: lists\n#\n#  id           :integer(11)    not null, primary key\n#  name         :string\nclass List \u003c ApplicationRecord\n  has_many :items, inverse_of: :list\n  accepts_nested_attributes_for :items, reject_if: :all_blank, allow_destroy: true\nend\n\n# == Schema Info\n#\n# Table name: items\n#\n#  id           :integer(11)    not null, primary key\n#  list_id      :integer(11)    not null\n#  description  :text\n#  done         :bool           not null, default(false)\nclass Item \u003c ApplicationRecord\n  belongs_to :list\nend\n```\n\nWe will build a form where we can dynamically add items to a list, remove or reorder them.\n\n### Basic form\n\n[Rails natively supports nested forms](https://guides.rubyonrails.org/form_helpers.html#nested-forms) but does not support adding or removing nested items.\n\n```erb\n\u003c% # `app/views/lists/_form.html.erb` %\u003e\n\u003c%= form_for @list do |form| %\u003e\n  \u003c%= form.text_field :name %\u003e\n  \n  \u003ch3\u003eItems\u003c/h3\u003e\n  \u003c%= form.fields_for :items do |item_form| %\u003e\n    \u003c%= item_form.label :description %\u003e\n    \u003c%= item_form.text_field :description %\u003e\n    \u003c%= item_form.check_box :done %\u003e\n  \u003c% end %\u003e\n  \n  \u003c%= form.submit \"Save\" %\u003e\n\u003c% end %\u003e\n```\n\nTo enable Cocooned on this form, we need to:\n\n1. Move the nested form to a partial\n2. Signal to Cocooned it should handle your form\n3. Add a way to add a new item to the list\n4. Add a way to remove an item from the collection\n\nLet's do it.\n\n**Note:** In this example, we will use Cocooned helpers named with a `_link` suffix. If you want to use buttons in your forms instead, the same helpers exist with a `_button` suffix.\n\n### 1. Move the nested form to a partial\n\nChange your main form as follow:\n\n```diff\n\u003c% # `app/views/lists/_form.html.erb` %\u003e\n\u003c%= form_for @list do |form| %\u003e\n  \u003c%= form.text_field :name %\u003e\n  \n  \u003ch3\u003eItems\u003c/h3\u003e\n  \u003c%= form.fields_for :items do |item_form|\n-   \u003c%= item_form.label :description %\u003e\n-   \u003c%= item_form.text_field :description %\u003e\n-   \u003c%= item_form.check_box :done %\u003e\n+   \u003c%= render 'item_fields', f: item_form %\u003e\n  \u003c% end %\u003e\n    \n  \u003c%= form.submit \"Save\" %\u003e\n\u003c% end %\u003e\n```\n\nAnd create a new file where items fields are defined:\n\n```erb\n\u003c% # `app/views/lists/_item_fields.html.erb` %\u003e\n\u003c%= f.label :description %\u003e\n\u003c%= f.text_field :description %\u003e\n\u003c%= f.check_box :done %\u003e\n```\n\n### 2. Signal to Cocooned it should handle your form\n\nChange your main form as follow:\n\n```diff\n\u003c% # `app/views/lists/_form.html.erb` %\u003e\n\u003c%= form_for @list do |form| %\u003e\n  \u003c%= form.input :name %\u003e\n  \n  \u003ch3\u003eItems\u003c/h3\u003e\n+ \u003c%= cocooned_container do %\u003e\n    \u003c%= form.fields_for :items do |item_form| %\u003e\n      \u003c%= render 'item_fields', f: item_form %\u003e\n    \u003c% end %\u003e\n+ \u003c% end %\u003e\n  \n  \u003c%= form.submit \"Save\" %\u003e\n\u003c% end %\u003e\n```\n\nAnd your sub form as follow:\n\n```diff\n\u003c% # `app/views/lists/_item_fields.html.erb` %\u003e\n+ \u003c%= cocooned_item do %\u003e\n    \u003c%= f.label :description %\u003e\n    \u003c%= f.text_field :description %\u003e\n    \u003c%= f.check_box :done %\u003e\n+ \u003c% end %\u003e\n```\n\nThe `cocooned_container` and `cocooned_item` helpers will set for you the HTML attributes the JavaScript part of Cocooned expect to find to hook on. They will forward any option supported by ActionView's `content_tag` and accept a tag name as first argument if you don't want to use the default `\u003cdiv\u003e`.\n\n### 3. Add a way to add a new item to the list\n\nChange your main form as follow:\n\n```diff\n\u003c% # `app/views/lists/_form.html.erb` %\u003e\n\u003c%= form_for @list do |form| %\u003e\n  \u003c%= form.input :name %\u003e\n  \n  \u003ch3\u003eItems\u003c/h3\u003e\n  \u003c%= cocooned_container do %\u003e\n    \u003c%= form.fields_for :items do |item_form| %\u003e\n      \u003c%= render 'item_fields', f: item_form %\u003e\n    \u003c% end %\u003e\n+   \n+   \u003cp\u003e\u003c%= cocooned_add_item_link 'Add an item', form, :items %\u003e\u003c/p\u003e\n  \u003c% end %\u003e\n  \n  \u003c%= form.submit \"Save\" %\u003e\n\u003c% end %\u003e\n```\n\nBy default, new items will be inserted just before the immediate parent of the 'Add an item' link. You can have a look at the documentation of `cocooned_add_item_link` for more information about how to change that but we'll keep it simple for now.\n\n### 4. Add a way to remove an item from the collection\n\nChange your sub form as follow:\n\n```diff\n\u003c% # `app/views/lists/_item_fields.html.erb` %\u003e\n\u003c%= cocooned_item do %\u003e\n  \u003c%= f.label :description %\u003e\n  \u003c%= f.text_field :description %\u003e\n  \u003c%= f.check_box :done %\u003e\n+ \u003c%= cocooned_remove_item_link 'Remove', f %\u003e\n\u003c% end %\u003e\n```\n\nYou're done!\n\n### Gotchas\n\n#### Strong Parameters Gotcha\n\nTo destroy nested models, Rails uses a virtual attribute called `_destroy`. When `_destroy` is set, the nested model will be deleted. If a record has previously been persisted, Rails generates and uses an additional `id` field.\n\nWhen using Rails \u003e 4.0 (or strong parameters), you need to explicitly add both `:id` and `:_destroy` to the list of permitted parameters in your controller.\n\nIn our example:\n\n```ruby\n  def list_params\n    params.require(:list).permit(:name, tasks_attributes: [:id, :description, :done, :_destroy])\n  end\n```\n\n#### Has One Gotcha\n\nIf you have a `has_one` association, then you (probably) need to set `force_non_association_create: true` on `cocooned_add_item_link` or the associated object will be destroyed every time the edit form is rendered (which is probably not what you expect).\n\nSee the [original merge request](https://github.com/nathanvda/cocoon/pull/247) for more details.\n\n#### Complex nested forms\n\nIf you want to build complex forms with multiple levels of nesting, make sure you [initialize Cocooned event handlers correctly for dynamically added child items](https://github.com/notus-sh/cocooned/blob/main/npm/README.md#complex-nested-forms) or your form won't behave as you might expect.\n\n## Plugins\n\nCocooned comes with two built-in plugins:\n\n* **Limit**, to set a maximum limit of items the association can contain\n* **Reorderable**, that will automatically update a `position` field in each of your sub forms when you add, remove or move an item.\n\n### The limit plugin\n\nThe limit plugin requires you specify the maximum number of items allowed in the association. To do so, pass a `:limit` option to the `cocooned_container` helper:\n\n```erb\n\u003c%= cocooned_container limit: 12 do %\u003e\n  \u003c% # […] %\u003e\n\u003c% end %\u003e\n```\n\n### The reorderable plugin\n\n**Important:** To use the reorderable plugin, your model must have a `position` numeric attribute you use to order collections (as in [acts_as_list](https://rubygems.org/gems/acts_as_list)).\n\nThe reorderable plugin can be activated in two ways through the `cocooned_container` helper:\n\n- With a boolean: `cocooned_container reorderable: true`  \n  Will use plugin's defaults (and start counting positions at 1)\n- With a configuration hash: `cocooned_container reorderable: { startAt: 0 }`  \n  Will use given `:startAt` as base position\n\nTo be able to move items up and down in your form and for positions to be saved, you need to change your sub form as follow:\n\n```diff\n\u003c% # `app/views/lists/_item_fields.html.erb` %\u003e\n\u003c%= cocooned_item do %\u003e\n  \u003c%= f.label :description %\u003e\n  \u003c%= f.text_field :description %\u003e\n  \u003c%= f.check_box :done %\u003e\n+ \u003c%= f.hidden_field :position %\u003e\n+ \u003c%= cocooned_move_item_up_link 'Up', f %\u003e\n+ \u003c%= cocooned_move_item_down_link 'Down', f %\u003e\n  \u003c%= cocooned_remove_item_link 'Remove', f %\u003e\n\u003c% end %\u003e\n```\n\nRemember to add `:position` as a permitted parameter in your controller.\n\n## Links or buttons?\n\nEach helper provided by Cocooned with a name ending with `_link` has its `_button` equivalent, to generate a `\u003cbutton type=\"button\" /\u003e` instead of a `\u003ca href=\"#\" /\u003e`:\n\n- `cocooned_add_item_link` \u003c=\u003e `cocooned_add_item_button` ([Documentation](https://github.com/notus-sh/cocooned/blob/main/lib/cocooned/helpers/tags/add.rb))\n- `cocooned_remove_item_link` \u003c=\u003e `cocooned_remove_item_button` ([Documentation](https://github.com/notus-sh/cocooned/blob/main/lib/cocooned/helpers/tags/remove.rb))\n- `cocooned_move_item_up_link` \u003c=\u003e `cocooned_move_item_up_button` ([Documentation](https://github.com/notus-sh/cocooned/blob/main/lib/cocooned/helpers/tags/up.rb))\n- `cocooned_move_item_down_link` \u003c=\u003e `cocooned_move_item_down_button` ([Documentation](https://github.com/notus-sh/cocooned/blob/main/lib/cocooned/helpers/tags/down.rb))\n\nWhile all `_link` helpers accept and will politely forward any option supported by ActionView's `link_to`, `_button` helpers will do the same with options supported by ActionView's `button_tag`.\n\n## Internationalisation\n\nThe label of any action trigger can be given explicitly as helper's first argument or as a block, just as you can do with ActionView's `link_to` or `button_to`.\n\nAdditionally, Cocooned helpers will lookup I18n translations for a default label based on the action name (`add`, `remove`, `up`, `down`) and the association name. For `add` triggers, the association name used is the same as passed as argument. Other triggers extract the association name from form's `#object_name`.\n\nYou can declare default labels in your translation files with following keys:\n\n- `cocooned.{association}.{action}` (Ex: `cocooned.items.add`)\n- `cocooned.defaults.{action}`\n\nIf no translation is found, the default label will be the humanized action name.\n\n## Javascript\n\nFor more documentation about the JavaScript bundled in the companion package, please refer to [its own documentation](https://github.com/notus-sh/cocooned/blob/main/npm/README.md).\n\n## Styling forms\n\nCocooned now uses exclusively data-attribute to hook JavaScript methods on but usual classes are still here and will stay so you can style your forms:\n\n- `.cocooned-container` on a container\n- `.cocooned-item` on an item\n- `.cocooned-add` on an add trigger (link or button)\n- `.cocooned-remove` on a remove trigger (link or button)\n- `.cocooned-move-up` on a move up trigger (link or button)\n- `.cocooned-move-down` on a move down trigger (link or button)\n\n## Migration from a previous version\n\nThese migrations steps only highlight major changes. When upgrading from a previous version, always refer to [the CHANGELOG](https://github.com/notus-sh/cocooned/blob/main/CHANGELOG.md) for new features and breaking changes.\n\n### From Cocooned ~1.0\n\n#### Forms markup\n\nCocooned 2.0 introduced the `cocooned_container` and `cocooned_item` helpers to respectively wrap the container where items will be added and each of the items.\n\nIf you used Cocooned ~1.0, you should modify your main forms as follow:\n\n```diff\n- \u003cdiv data-cocooned-options=\"\u003c%= {}.to_json %\u003e\"\u003e\n+ \u003c%= cocooned_container do %\u003e\n    \u003c% # […] %\u003e\n+ \u003c% end %\u003e\n- \u003c/div\u003e\n```\n\nAnd your nested partials:\n\n```diff\n- \u003cdiv class=\"cocooned-item\"\u003e\n+ \u003c%= cocooned_item do %\u003e\n    \u003c% # […] %\u003e\n+ \u003c% end %\u003e\n- \u003c/div\u003e\n```\n\nSupport for the `data-cocooned-options` attribute to identify a container and the `.cocooned-item` class to identify an item is still here but it is not the recommended way to tag your containers and items anymore.\n\n**Compatibility with older markup will be dropped in the next major release.**\n\n#### Bundled styles\n\nCocooned ~2.0 does not provide any styles anymore. If you used to require (with Sprockets) or import the cocooned stylesheets into your application, you need to remove it.\n\n**Empty files are included to not break your assets pipeline but will be removed in the next major release.**\n\n### From Cocoon (any version)\n\nCocoon uses a `.nested_fields` class to identify items in a nested form and nothing to identify containers new items will be added to.\n\nIf you used Cocoon, you should:\n\n1. Modify your forms and sub forms to use the `cocooned_container` and `cocooned_item` helpers. (See above for examples)\n2. Replace calls to `link_to_add_association` by `cocooned_add_item_link`\n3. Replace calls to `link_to_remove_association` by `cocooned_remove_item_link`\n4. Rename your I18n keys to use the `cocooned` namespace instead of `cocoon`\n\n**Compatibility with the original Cocoon API will be dropped in the next major release.**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnotus-sh%2Fcocooned","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnotus-sh%2Fcocooned","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnotus-sh%2Fcocooned/lists"}