Companion package for the cocooned Ruby gem.
npm install @notus.sh/cocoonedThis is a companion package for the cocooned Ruby gem.
Cocooned makes it easier to handle nested forms in Rails.
Cocooned is form builder-agnostic: it works with standard Rails (>= 7.0, < 8.2) form helpers, Formtastic or SimpleForm.
1. Installation
2. Import, default, custom
3. Stimulus integration
4. jQuery integration
5. Usage
6. Options
7. Events
8. Migration from a previous version or from Cocoon
Install it with your favorite package manager:
``shell`bun
$ bun install @notus.sh/cocoonedimportmap (Rails ~7.0 default)
$ bin/importmap pin @notus.sh/cocoonednpm
$ npm install @notus.sh/cocoonedpnpm
$ pnpm add @notus.sh/cocoonedyarn
$ yarn add @notus.sh/cocooned
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.
Once installed, you have different ways to import Cocooned, depending on what you need.
To get the default build of Cocooned with all built-in plugins available, load package main file:
`javascript`
import Cocooned from '@notus.sh/cocooned'
Cocooned.start()
If you know you'll not use the plugin, you can load the core version:
`javascript`
import Cocooned from '@notus.sh/cocooned/src/cocooned/cocooned.js'
Cocooned.start()
If you want only one of the plugins, you can load the core Cocooned and extend it with the plugin:
`javascript
import { Cocooned as Base } from '@notus.sh/cocooned/src/cocooned/cocooned.js'
import { limitMixin } from '@notus.sh/cocooned/src/cocooned/plugins/limit.js'
class Cocooned extends limitMixin(Base) {
static create (container, options) {
if ('cocoonedUuid' in container.dataset) {
return Cocooned.getInstance(container.dataset.cocoonedUuid)
}
const cocooned = new this.constructor(container, options)
cocooned.start()
return cocooned
}
static start () {
document.querySelectorAll('[data-cocooned-container]')
.forEach(element => this.constructor.create(element))
}
}
Cocooned.start()
`
Note: Classes, methods and options other than those documented here can not be considered as a public API and are subject to changes between versions. See (and feel free to collaborate to) this issue for a future stable public API.
`javascript
import Cocooned from '@notus.sh/cocooned'
// Detect, create and setup all your Cocooned container at once
Cocooned.start()
// Or initialize them individually
// Without options
const cocooned = Cocooned.create(document.querySelector('a selector to match container'))
// With options
const cocooned = Cocooned.create(document.querySelector('a selector to match container'), { limit: 12 })
`
Options can also be provided as a JSON string in a data-cocooned-options on your container HTML tag. This is the recommended way to pass options from Ruby-land (and is bundled in the coconned_container helper provided by the cocooned Ruby gem).
Although they come with their own challenges, as building a comprehensive interface for your users or dealing with validations on multiple levels, in some complex use cases you have no choices but to build forms with multiple levels of sub-components you may want to manage with Cocooned.
As Cocooned is commonly initialized on page load with Cocooned.start(), action triggers in dynamically added child items (to add grand-child items) or to manipulate grand-child items (to delete them or to move them up or down if you use the reorderable plugin) will not be handled at first.
To handle them, you need to tell Cocooned to initialize their event handlers when a new item is added with:
`javascript`
document.addEventListener('cocooned:after-insert', e => Cocooned.create(e.detail.node))
Cocooned comes with an optional Stimulus integration.
#### Register the Cocooned Stimulus Controller
If your application use Stimulus, instead of calling Cocooned.start(), you can register the Cocooned Stimulus Controller with:
`javascript
import { Application } from "@hotwired/stimulus"
import { registerCocoonedController } from '@notus.sh/cocooned/src/integrations/stimulus.js'
const Stimulus = Application.start()
registerCocoonedController(Stimulus)
`
Stimulus will connect an instance of the Cocooned controller to every cocooned_container in the DOM. That controller automatically create a Cocooned instance with its element as container and will properly dispose of it when disconnected.
#### The useCocooned mixin
For more complex scenarios, you can use the useCocooned mixin to integrate Cocooned functionalities to your own controllers.
Some examples:
* Create a Cocooned instance on a controller target instead of its main element
* Use a custom Cooconed implementation with less plugins (or with your owns!)
* Pass custom options to the created Cocooned instance
* Add listener on Cocooned events
Inspired by stimulus-use, useCocooned should be called from the connect method of a Stimulus controller. It creates a Cocooned instance that will be disposed when the controller is disconnected and bind Cocooned events to controller methods.
`javascript
import { Controller } from '@hotwired/stimulus'
import { useCocooned } from '@notus.sh/cocooned/src/integrations/stimulus.js'
export default class extends Controller {
connect() {
useCocooned(this, options)
}
}
`
Following options are supported:
* element: HTMLElement to use as container. Use controller's element by default.cocooned
* : Cocooned implementation to use to create instance. See Customizationoptions
* : Options to be passed to the created instance. See Optionsevents
* : Array of event names that should be bound to controller methods. E.g.: ['after-insert', 'before-insert']
By default, all events emitted by Cocooned are supported and bound to the controller method that match with their camelized name:
| Event name | Controller method |
|-----------------|-------------------|
| after-insert | afterInsert |after-move
| | afterMove |after-remove
| | afterRemove |before-insert
| | beforeInsert |before-move
| | beforeMove |before-remove
| | beforeRemove |limit-reached
| | limitReached |
You can also access the Cocooned instance through a cocooned method.
Cocooned does not require jQuery (anymore) but comes with a jQuery integration (inherited from previous versions). If you use jQuery, you may want to use Cocooned with jQuery:
`javascript`
import Cocooned from '@notus.sh/cocooned/jquery.js'
Note: You don't need to call Cocooned.start() here as the jQuery integration automatically bind it to $.ready.
You can use Cocooned as a jQuery plugin:
`javascript
// Without options
$('a selector to match container').cocooned()
// With options
$('a selector to match container').cocooned(options)
`
Cocooned instances implements JavaScript explicit resource management and expose a dipose method to unset what their start method setups.
To correctly destroy a Cocooned instance and prevent memory leaks, please use it.
`javascript
const cocooned = Cocooned.create(document.querySelector('a selector to match container'))
// When you're done with this instance, dispose of it with:
cocooned.dispose()
`
If you want your browser to automatically dispose of your Cocooned instance, you can create it with a using declaration and it will be properly disposed when it goes out of scope.
By the time of writing this (early 2026), not all major browsers support explicit resource management (mainly, Safari is late). When support is missing in a browser, Cocooned will emit a console warning to warn you.
If your application needs to support such browsers, you'll need to import a polyfill of your choice. For example:
1. core-js
2. disposable-stack-polyfill
3. disposablestack
Any Cocooned instance supports following options:
#### animate: [Boolean]
Toggle animations when moving or removing an item from the form. Default value depend on detected support of the Web Animation API and browser's accessibility settings about reduced motion.
#### duration: [Integer]
Duration of animations, in milliseconds. Defaults to 450.
#### animator: [Function]
A function returning animation keyframes, either an array of keyframe objects or a keyframe object whose properties are arrays of values to iterate over. See Keyframe Formats documentation for more details.
For an example of the expected function behavior, look at the default animator
#### limit: {Integer}
Available with the limit plugin. Set maximum number of items in your container to the specified limit.
#### reorderable: [Boolean|Object]
Available with the reorderable plugin. Allows items in your container to be reordered (with their respective position field updated).
Can be specified as a boolean (reorderable: true) or with a startAt value updated positions will be counted from (reorderable: { startAt: 0 }, defaults to 1)
When your collection is modified, the following events can be triggered:
* cocooned:before-insert: before inserting a new item, can be canceledcocooned:after-insert
* : after inserting a new itemcocooned:before-remove
* : before removing an item, can be canceledcocooned:after-remove
* : after removing an item
The limit plugin can trigger its own event:
* cocooned:limit-reached: when the limit is reached (when a new item should be inserted but won't)
Note: Listeners on this event receive the event object that originally triggered the refused insertion.
And so does the reorderable plugin:
* cocooned:before-move: before moving an item, can be canceledcocooned:after-move
* : after moving an itemcocooned:before-reindex
* : before updating the position fields of items, can be canceled (even if I honestly don't know why you would)cocooned:after-reindex
* : after updating the position fields of items
To listen to the events in your JavaScript:
`javascript`
container.addEventListener('cocooned:before-insert', event => {
/ Do something /
});
Event handlers receive a CustomEvent with following detail:
* event.detail.link, the clicked link or buttonevent.detail.node
* , the item that will be added, removed or moved. cocooned:limit-reached
Unavailable on , cocooned:before-reindex and cocooned:after-reindexevent.nodes
* , the nested items that will be or just have been reindexed. cocooned:before-reindex
Available on and cocooned:after-reindexevent.detail.cocooned
* , the Cocooned instance.event.detail.originalEvent
* , the original (browser) event.
You can cancel an action within the cocooned:before- callback using event.preventDefault().
New items need to have a bunch of attributes updated before insert to make their name, id and for attributes consistent and unique. If you need additional substitutions to be made, you can configure them with:
`javascript
import Cocooned from '@notus.sh/cocooned'
/**
* These 4 replacements are already set by default.
*/
Cocooned.registerReplacement({ attribute: 'name', delimiters: ['[', ']'] })
// Same start and end? Don't repeat yourself.
Cocooned.registerReplacement({ attribute: 'id', delimiters: ['_'] })
// You can target specific tags (else '*' is implied).
Cocooned.registerReplacement({ tag: 'label', attribute: 'for', delimiters: ['_'] })
Cocooned.registerReplacement({ tag: 'trix-editor', attribute: 'input', delimiters: ['_'] })
`
These migrations steps only highlight major changes. When upgrading from a previous version, always refer to the CHANGELOG for new features and breaking changes.
Cocooned events have been rewritten around CustomEvents and standard addEventListener / dispatchEvent in version 2.0. This does not allow more than a single event parameter in listeners, in opposition to what was in use with jQuery listeners.
It is up to you to still bind your listeners with jQuery but you need to rewrite them as follow:
`diff`
- $(element).on('cocooned:
- { originalEvent } = e.detail
+ $(element).on('cocooned:
+ { node, cocooned, originalEvent } = e.detail
})
Cocooned is a rewrite of Cocoon by Nathan Van der Auwera and is still almost 100% compatible with it.
Cocoon 1.2.13 introduced the original browser event as a third parameter to all event handlers when Cocooned had already started to use this to pass the Cocooned object instance (since 1.3.0).
If you are updating from Cocoon ~1.2.13, you need to rewrite your event listeners as follow:
`diff`
- $(element).on('cocoon:
+ $(element).on('cocooned:
+ { node, originalEvent } = e.detail
})
#### Events
Cocooned uses its own namespace for event names. Cocooned 2.0 still trigger events in the original cocoon namespace but this will be dropped in the next major release.
You need to rename events when binding your listeners:
* cocoon:before-insert is renamed to cocooned:before-insertcocoon:after-insert
* is renamed to cocooned:after-insertcocoon:before-remove
* is renamed to cocooned:before-removecocoon:after-remove
* is renamed to cocooned:after-remove
The original Cocoon does not support any of the other events.
#### Auto-start
Cocoon auto-start was based on a detection of the links to add item (identified by their .add_fields class) to a (non-identified) container. Cocooned reverses this logic to identify containers first and look for add triggers (links or buttons) with an insertion point inside of them.
This means you need to have your container clearly identified in your HTML. The easiest way to do so is to use the cocooned_container and cocooned_item` helpers provided by the Ruby gem.