This 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 (>= 6.0, < 7.2) form helpers, Formtastic or SimpleForm.
- Installation
- Import, default, custom or with jQuery integration
- Usage
- Options
- Events
- Migration from a previous version or from Cocoon
If you use import maps (Rails 7.0+ default), add it with:
$ bin/importmap pin @notus.sh/cocooned
If you use Yarn and Webpack (Rails 5.1+ default), add it with:
$ 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:
import Cocooned from '@notus.sh/cocooned'
Cocooned.start()
If you know you'll not use the plugin, you can load the core version:
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:
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.
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:
document.addEventListener('cocooned:after-insert', e => Cocooned.create(e.detail.node))
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:
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:
// Without options
$('a selector to match container').cocooned()
// With options
$('a selector to match container').cocooned(options)
Any Cocooned instance supports following options:
Toggle animations when moving or removing an item from the form. Default value depend on detected support of the Web Animation API.
Duration of animations, in milliseconds. Defaults to 450.
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
Available with the limit plugin. Set maximum number of items in your container to the specified limit.
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 theposition
fields of items, can be canceled (even if I honestly don't know why you would)cocooned:after-reindex
: after updating theposition
fields of items
To listen to the events in your 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.
Unavailable oncocooned:limit-reached
,cocooned:before-reindex
andcocooned:after-reindex
event.nodes
, the nested items that will be or just have been reindexed.
Available oncocooned:before-reindex
andcocooned:after-reindex
event.detail.cocooned
, the Cocooned instance.event.detail.originalEvent
, the original (browser) event.
You can cancel an action within the cocooned:before-<action>
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:
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 CustomEvent
s 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:
- $(element).on('cocooned:<event-name>', (e, node, cocooned) => {
- { originalEvent } = e.detail
+ $(element).on('cocooned:<event-name>', e => {
+ { 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:
- $(element).on('cocoon:<event-name>', (e, node, originalEvent) => {
+ $(element).on('cocooned:<event-name>', e => {
+ { node, originalEvent } = e.detail
})
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 tococooned:before-insert
cocoon:after-insert
is renamed tococooned:after-insert
cocoon:before-remove
is renamed tococooned:before-remove
cocoon:after-remove
is renamed tococooned:after-remove
The original Cocoon does not support any of the other events.
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.