hotwire_nested_form 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -1
- data/README.md +40 -0
- data/lib/hotwire_nested_form/form_builder_detector.rb +18 -0
- data/lib/hotwire_nested_form/helpers/add_association.rb +3 -1
- data/lib/hotwire_nested_form/version.rb +1 -1
- data/lib/hotwire_nested_form.rb +1 -0
- data/npm/.npmignore +6 -0
- data/npm/README.md +79 -0
- data/npm/package.json +32 -0
- data/npm/src/index.js +2 -0
- data/npm/src/nested_form_controller.js +80 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 614ce7e4bb9ee960608d18431bd94319feac263ce6a83f79bc36525f5c2d5061
|
|
4
|
+
data.tar.gz: 3c8c196d819cf4954fbedf3444070a95244636427bb73147d3edc71465d5afb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ec8765adbc4c27dbd3297f6497971f29a22872381c114c12ce03c7ee8649e0e47e6c500be06c7f1ee71a3512f7897f9a94bc96f95474e730874828188784579
|
|
7
|
+
data.tar.gz: d90976447adf75d242bdb1f200af575e3724036b7441876e48134a28f4abd7c603cc62cb9c997679a8234b19b3a6a3465e77e51fb944a144b25b0f4c4e1dc7ad
|
data/.rubocop.yml
CHANGED
|
@@ -107,6 +107,15 @@ RSpec/DescribeClass:
|
|
|
107
107
|
RSpec/SpecFilePathFormat:
|
|
108
108
|
Exclude:
|
|
109
109
|
- 'spec/helpers/**/*'
|
|
110
|
+
- 'spec/lib/**/*'
|
|
111
|
+
- 'spec/integration/**/*'
|
|
112
|
+
|
|
113
|
+
# Allow regular doubles when mocking classes that may not exist (e.g., SimpleForm)
|
|
114
|
+
RSpec/VerifiedDoubles:
|
|
115
|
+
Exclude:
|
|
116
|
+
- 'spec/lib/**/*'
|
|
117
|
+
- 'spec/helpers/**/*'
|
|
118
|
+
- 'spec/integration/**/*'
|
|
110
119
|
|
|
111
120
|
RSpec/LetSetup:
|
|
112
121
|
Exclude:
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-02-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- SimpleForm auto-detection and compatibility
|
|
14
|
+
- NPM package `@hotwire-nested-form/stimulus` for JavaScript-only users
|
|
15
|
+
- `FormBuilderDetector` module for form builder type detection
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Improved documentation for SimpleForm usage
|
|
19
|
+
|
|
10
20
|
## [1.0.0] - 2026-02-05
|
|
11
21
|
|
|
12
22
|
### Added
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
hotwire_nested_form (1.
|
|
4
|
+
hotwire_nested_form (1.1.0)
|
|
5
5
|
rails (>= 7.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -270,6 +270,9 @@ GEM
|
|
|
270
270
|
rexml (~> 3.2, >= 3.2.5)
|
|
271
271
|
rubyzip (>= 1.2.2, < 4.0)
|
|
272
272
|
websocket (~> 1.0)
|
|
273
|
+
simple_form (5.4.1)
|
|
274
|
+
actionpack (>= 7.0)
|
|
275
|
+
activemodel (>= 7.0)
|
|
273
276
|
sqlite3 (2.9.0-aarch64-linux-gnu)
|
|
274
277
|
sqlite3 (2.9.0-aarch64-linux-musl)
|
|
275
278
|
sqlite3 (2.9.0-arm-linux-gnu)
|
|
@@ -319,6 +322,7 @@ DEPENDENCIES
|
|
|
319
322
|
rubocop-rails
|
|
320
323
|
rubocop-rspec
|
|
321
324
|
selenium-webdriver (~> 4.10)
|
|
325
|
+
simple_form (~> 5.3)
|
|
322
326
|
sqlite3 (>= 1.6)
|
|
323
327
|
|
|
324
328
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -84,6 +84,46 @@ end
|
|
|
84
84
|
|
|
85
85
|
That's it! Click "Add Task" to add fields, "Remove" to remove them.
|
|
86
86
|
|
|
87
|
+
## SimpleForm Support
|
|
88
|
+
|
|
89
|
+
Works automatically with SimpleForm! No configuration needed.
|
|
90
|
+
|
|
91
|
+
```erb
|
|
92
|
+
<%= simple_form_for @project do |f| %>
|
|
93
|
+
<%= f.input :name %>
|
|
94
|
+
|
|
95
|
+
<div data-controller="nested-form">
|
|
96
|
+
<%= f.simple_fields_for :tasks do |task_form| %>
|
|
97
|
+
<%= render "task_fields", f: task_form %>
|
|
98
|
+
<% end %>
|
|
99
|
+
|
|
100
|
+
<%= link_to_add_association "Add Task", f, :tasks %>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<%= f.button :submit %>
|
|
104
|
+
<% end %>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## NPM Package (JavaScript-only)
|
|
108
|
+
|
|
109
|
+
For non-Rails projects using Stimulus, install via npm:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm install @hotwire-nested-form/stimulus
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Register the controller:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
import { Application } from "@hotwired/stimulus"
|
|
119
|
+
import NestedFormController from "@hotwire-nested-form/stimulus"
|
|
120
|
+
|
|
121
|
+
const application = Application.start()
|
|
122
|
+
application.register("nested-form", NestedFormController)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
See [NPM package documentation](npm/README.md) for full details.
|
|
126
|
+
|
|
87
127
|
## API Reference
|
|
88
128
|
|
|
89
129
|
### link_to_add_association
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HotwireNestedForm
|
|
4
|
+
module FormBuilderDetector
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def simple_form?(form_builder)
|
|
8
|
+
return false unless form_builder
|
|
9
|
+
|
|
10
|
+
builder_class = form_builder.class.name.to_s
|
|
11
|
+
builder_class.include?('SimpleForm')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def simple_form_available?
|
|
15
|
+
defined?(::SimpleForm) ? true : false
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -78,7 +78,9 @@ module HotwireNestedForm
|
|
|
78
78
|
# Determine partial name
|
|
79
79
|
partial_name = partial || "#{association.to_s.singularize}_fields"
|
|
80
80
|
|
|
81
|
-
# Render the fields
|
|
81
|
+
# Render the fields using fields_for
|
|
82
|
+
# This works with both standard Rails FormBuilder and SimpleForm::FormBuilder
|
|
83
|
+
# SimpleForm overrides fields_for to use simple_fields_for internally
|
|
82
84
|
form.fields_for(association, new_object, child_index: 'NEW_RECORD') do |builder|
|
|
83
85
|
locals = (render_options[:locals] || {}).merge(f: builder)
|
|
84
86
|
render(partial: partial_name, locals: locals)
|
data/lib/hotwire_nested_form.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'hotwire_nested_form/version'
|
|
4
4
|
require_relative 'hotwire_nested_form/engine'
|
|
5
5
|
require_relative 'hotwire_nested_form/helpers'
|
|
6
|
+
require_relative 'hotwire_nested_form/form_builder_detector'
|
|
6
7
|
|
|
7
8
|
module HotwireNestedForm
|
|
8
9
|
class Error < StandardError; end
|
data/npm/.npmignore
ADDED
data/npm/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @hotwire-nested-form/stimulus
|
|
2
|
+
|
|
3
|
+
A Stimulus controller for dynamic nested forms. Add and remove nested form fields with ease.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hotwire-nested-form/stimulus
|
|
9
|
+
# or
|
|
10
|
+
yarn add @hotwire-nested-form/stimulus
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Register the Controller
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { Application } from "@hotwired/stimulus"
|
|
19
|
+
import NestedFormController from "@hotwire-nested-form/stimulus"
|
|
20
|
+
|
|
21
|
+
const application = Application.start()
|
|
22
|
+
application.register("nested-form", NestedFormController)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### HTML Structure
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<div data-controller="nested-form">
|
|
29
|
+
<div id="items">
|
|
30
|
+
<!-- Existing nested fields go here -->
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<a href="#"
|
|
34
|
+
data-action="nested-form#add"
|
|
35
|
+
data-template="<div class='nested-fields'><input name='items[][name]'><a href='#' data-action='nested-form#remove'>Remove</a></div>">
|
|
36
|
+
Add Item
|
|
37
|
+
</a>
|
|
38
|
+
</div>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Data Attributes
|
|
42
|
+
|
|
43
|
+
| Attribute | Description | Default |
|
|
44
|
+
|-----------|-------------|---------|
|
|
45
|
+
| `data-template` | HTML template for new fields (use `NEW_RECORD` as placeholder) | Required |
|
|
46
|
+
| `data-insertion` | Where to insert: `before`, `after`, `append`, `prepend` | `before` |
|
|
47
|
+
| `data-count` | Number of fields to add per click | `1` |
|
|
48
|
+
| `data-target` | CSS selector for insertion container | Parent element |
|
|
49
|
+
|
|
50
|
+
### Events
|
|
51
|
+
|
|
52
|
+
| Event | Cancelable | Detail |
|
|
53
|
+
|-------|------------|--------|
|
|
54
|
+
| `nested-form:before-add` | Yes | `{ wrapper }` |
|
|
55
|
+
| `nested-form:after-add` | No | `{ wrapper }` |
|
|
56
|
+
| `nested-form:before-remove` | Yes | `{ wrapper }` |
|
|
57
|
+
| `nested-form:after-remove` | No | `{ wrapper }` |
|
|
58
|
+
|
|
59
|
+
### Example: Listen for Events
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
document.addEventListener("nested-form:after-add", (event) => {
|
|
63
|
+
console.log("Added:", event.detail.wrapper)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
document.addEventListener("nested-form:before-remove", (event) => {
|
|
67
|
+
if (!confirm("Are you sure?")) {
|
|
68
|
+
event.preventDefault()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### With Rails
|
|
74
|
+
|
|
75
|
+
For Rails users, we recommend using the [hotwire_nested_form](https://rubygems.org/gems/hotwire_nested_form) gem which provides view helpers.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
data/npm/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hotwire-nested-form/stimulus",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Stimulus controller for dynamic nested forms - works with Rails, React, Vue, or any Stimulus app",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"module": "src/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"stimulus",
|
|
13
|
+
"hotwire",
|
|
14
|
+
"nested-forms",
|
|
15
|
+
"rails",
|
|
16
|
+
"dynamic-forms",
|
|
17
|
+
"cocoon-alternative"
|
|
18
|
+
],
|
|
19
|
+
"author": "BhumitBhadani <bhumit2520@gmail.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/bhumit4220/hotwire_nested_form.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/bhumit4220/hotwire_nested_form/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/bhumit4220/hotwire_nested_form#readme",
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@hotwired/stimulus": "^3.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
data/npm/src/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
wrapperClass: { type: String, default: "nested-fields" }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
add(event) {
|
|
9
|
+
event.preventDefault()
|
|
10
|
+
|
|
11
|
+
const template = event.currentTarget.dataset.template
|
|
12
|
+
const insertion = event.currentTarget.dataset.insertion || "before"
|
|
13
|
+
const targetSelector = event.currentTarget.dataset.target
|
|
14
|
+
const count = parseInt(event.currentTarget.dataset.count) || 1
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < count; i++) {
|
|
17
|
+
this.insertFields(template, insertion, targetSelector, event.currentTarget)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
remove(event) {
|
|
22
|
+
event.preventDefault()
|
|
23
|
+
|
|
24
|
+
const wrapper = event.currentTarget.closest(`.${this.wrapperClassValue}`)
|
|
25
|
+
if (!wrapper) return
|
|
26
|
+
|
|
27
|
+
const beforeEvent = this.dispatch("before-remove", {
|
|
28
|
+
cancelable: true,
|
|
29
|
+
detail: { wrapper }
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if (beforeEvent.defaultPrevented) return
|
|
33
|
+
|
|
34
|
+
const destroyInput = wrapper.querySelector("input[name*='_destroy']")
|
|
35
|
+
|
|
36
|
+
if (destroyInput) {
|
|
37
|
+
destroyInput.value = "true"
|
|
38
|
+
wrapper.style.display = "none"
|
|
39
|
+
} else {
|
|
40
|
+
wrapper.remove()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.dispatch("after-remove", { detail: { wrapper } })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
insertFields(template, insertion, targetSelector, trigger) {
|
|
47
|
+
const newId = new Date().getTime()
|
|
48
|
+
const content = template.replace(/NEW_RECORD/g, newId)
|
|
49
|
+
|
|
50
|
+
const fragment = document.createRange().createContextualFragment(content)
|
|
51
|
+
const wrapper = fragment.firstElementChild
|
|
52
|
+
|
|
53
|
+
const beforeEvent = this.dispatch("before-add", {
|
|
54
|
+
cancelable: true,
|
|
55
|
+
detail: { wrapper }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (beforeEvent.defaultPrevented) return
|
|
59
|
+
|
|
60
|
+
const container = targetSelector
|
|
61
|
+
? document.querySelector(targetSelector)
|
|
62
|
+
: trigger.parentElement
|
|
63
|
+
|
|
64
|
+
switch (insertion) {
|
|
65
|
+
case "after":
|
|
66
|
+
trigger.after(fragment)
|
|
67
|
+
break
|
|
68
|
+
case "append":
|
|
69
|
+
container.append(fragment)
|
|
70
|
+
break
|
|
71
|
+
case "prepend":
|
|
72
|
+
container.prepend(fragment)
|
|
73
|
+
break
|
|
74
|
+
default:
|
|
75
|
+
trigger.before(fragment)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.dispatch("after-add", { detail: { wrapper } })
|
|
79
|
+
}
|
|
80
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hotwire_nested_form
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BhumitBhadani
|
|
@@ -47,10 +47,16 @@ files:
|
|
|
47
47
|
- lib/generators/hotwire_nested_form/templates/nested_form_controller.js
|
|
48
48
|
- lib/hotwire_nested_form.rb
|
|
49
49
|
- lib/hotwire_nested_form/engine.rb
|
|
50
|
+
- lib/hotwire_nested_form/form_builder_detector.rb
|
|
50
51
|
- lib/hotwire_nested_form/helpers.rb
|
|
51
52
|
- lib/hotwire_nested_form/helpers/add_association.rb
|
|
52
53
|
- lib/hotwire_nested_form/helpers/remove_association.rb
|
|
53
54
|
- lib/hotwire_nested_form/version.rb
|
|
55
|
+
- npm/.npmignore
|
|
56
|
+
- npm/README.md
|
|
57
|
+
- npm/package.json
|
|
58
|
+
- npm/src/index.js
|
|
59
|
+
- npm/src/nested_form_controller.js
|
|
54
60
|
homepage: https://github.com/bhumit4220/hotwire_nested_form
|
|
55
61
|
licenses:
|
|
56
62
|
- MIT
|