Skip to content

Commit b419bcf

Browse files
committed
vanilla nestedform.js
1 parent b74d093 commit b419bcf

9 files changed

Lines changed: 219 additions & 2734 deletions

File tree

docs/assets/css/nested-form-demo.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
text-shadow: none;
1818
font-family: var(--font-family-sans);
1919
}
20-
.nested-form-wrapper {
20+
.nestedform {
2121
margin-top: 1rem;
2222
}
2323
label {
@@ -45,7 +45,7 @@
4545
color: inherit;
4646
background: var(--color-white);
4747
}
48-
[data-action="nested-form#remove"] {
48+
[data-nestedform-remove] {
4949
display: inline-flex;
5050
align-items: center;
5151
justify-content: center;
@@ -60,7 +60,7 @@
6060
border-top-right-radius: .375rem;
6161
cursor: pointer;
6262
}
63-
[data-action="nested-form#remove"]:hover {
63+
[data-nestedform-remove]:hover {
6464
color: #374151;
6565
}
6666
.actions {

docs/assets/js/nested-form-controller.js

Lines changed: 0 additions & 59 deletions
This file was deleted.

docs/assets/js/nestedform.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Add/Remove nested form rows dynamically.
3+
Copyright © 2026 JPScaletti, MIT License
4+
5+
Usage:
6+
7+
```js
8+
<div data-nestedform>
9+
10+
<!-- existing rows -->
11+
<div data-nestedform-target>
12+
<fieldset class="nestedform">
13+
...
14+
<button data-nestedform-remove>Remove</button>
15+
</fieldset>
16+
...
17+
</div>
18+
19+
<!-- template of a new row -->
20+
<template data-nestedform-template>
21+
<fieldset class="nestedform">
22+
...
23+
<button data-nestedform-remove>Remove</button>
24+
</fieldset>
25+
</template>
26+
27+
<button data-nestedform-add>Add</button>
28+
</div>
29+
```
30+
*/
31+
32+
const parser = new DOMParser();
33+
const initialized = new WeakSet();
34+
35+
function initNestedForm(root) {
36+
if (initialized.has(root)) return;
37+
38+
const target = root.querySelector('[data-nestedform-target]');
39+
const template = root.querySelector('[data-nestedform-template]');
40+
const wrapperSelector = root.dataset.wrapperSelector || ".nestedform";
41+
42+
root.addEventListener("click", (e) => {
43+
const addBtn = e.target.closest("[data-nestedform-add]");
44+
if (addBtn) {
45+
e.preventDefault();
46+
47+
const i = Date.now().toString().slice(4);
48+
const content = template.innerHTML.replace(/NEW_RECORD/g, i);
49+
50+
const doc = parser.parseFromString(content, "text/html");
51+
const wrapper = doc.body.firstElementChild;
52+
wrapper.setAttribute("data-new", "true");
53+
54+
wrapper.querySelectorAll("[id]").forEach((el) => {
55+
el.setAttribute("id", `${el.getAttribute("id")}_${i}`);
56+
});
57+
wrapper.querySelectorAll("[for]").forEach((el) => {
58+
el.setAttribute("for", `${el.getAttribute("for")}_${i}`);
59+
});
60+
61+
target.appendChild(wrapper);
62+
root.dispatchEvent(new CustomEvent("nestedform:add", { bubbles: true }));
63+
return;
64+
}
65+
66+
const removeBtn = e.target.closest("[data-nestedform-remove]");
67+
if (removeBtn) {
68+
e.preventDefault();
69+
70+
const wrapper = removeBtn.closest(wrapperSelector);
71+
if (wrapper.dataset.newRecord) {
72+
wrapper.remove();
73+
} else {
74+
wrapper.style.display = "none";
75+
const input = wrapper.querySelector('input[name*="_destroy"]');
76+
if (input) input.value = "1";
77+
}
78+
79+
root.dispatchEvent(new CustomEvent("nestedform:remove", { bubbles: true }));
80+
}
81+
});
82+
initialized.add(root);
83+
}
84+
85+
document.addEventListener('DOMContentLoaded', () => {
86+
document.querySelectorAll("[data-nestedform]").forEach(initNestedForm);
87+
});

0 commit comments

Comments
 (0)