Skip to content

Commit 90532c4

Browse files
Add support many nested forms
1 parent 292d240 commit 90532c4

File tree

3 files changed

+182
-3
lines changed

3 files changed

+182
-3
lines changed

index.html

+135
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,141 @@ <h1 class="text-4xl tracking-tight font-extrabold text-gray-900 sm:text-5xl md:t
202202
</section>
203203
</div>
204204

205+
<div class="relative h-full max-w-5xl mx-auto px-4">
206+
<section class="mt-16">
207+
<form data-controller="nested-form">
208+
<template data-nested-form-target="template" data-model-template="todo">
209+
<div class="mt-4 nested-form-wrapper" data-new-record="true">
210+
<label for="NEW_RECORD" class="block text-sm font-medium leading-5 text-gray-700">New todo</label>
211+
<div class="mt-1 flex relative rounded-md shadow-sm">
212+
<input
213+
id="NEW_RECORD"
214+
name="user[todos_attributes][NEW_RECORD][description]"
215+
class="appearance-none border w-full py-2 px-3 rounded-l-md text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
216+
placeholder="Your todo"
217+
/>
218+
219+
<button
220+
class="cursor-pointer inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-100 text-gray-500 sm:text-sm"
221+
type="button"
222+
data-action="nested-form#remove"
223+
title="Remove todo"
224+
>
225+
X
226+
</button>
227+
228+
<input type="hidden" name="user[todos_attributes][NEW_RECORD][_destroy]" />
229+
</div>
230+
</div>
231+
</template>
232+
233+
<template data-nested-form-target="template" data-model-template="note">
234+
<div class="mt-4 nested-form-wrapper" data-new-record="true">
235+
<label for="NEW_RECORD" class="block text-sm font-medium leading-5 text-gray-700">New note</label>
236+
<div class="mt-1 flex relative rounded-md shadow-sm">
237+
<input
238+
id="NEW_RECORD"
239+
type="text"
240+
name="user[note_attributes][NEW_RECORD][description]"
241+
class="appearance-none border w-full py-2 px-3 rounded-l-md text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
242+
placeholder="Your note"
243+
/>
244+
245+
<button
246+
class="cursor-pointer inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-100 text-gray-500 sm:text-sm"
247+
type="button"
248+
data-action="nested-form#remove"
249+
title="Remove note"
250+
>
251+
X
252+
</button>
253+
254+
<input type="hidden" name="user[note_attributes][NEW_RECORD][_destroy]" />
255+
</div>
256+
</div>
257+
</template>
258+
259+
<div class="mt-4 nested-form-wrapper" data-new-record="false">
260+
<label for="todo-1" class="block text-sm font-medium leading-5 text-gray-700">Your todo</label>
261+
<div class="mt-1 flex relative rounded-md shadow-sm">
262+
<input
263+
id="todo-1"
264+
name="user[todos_attributes][0][description]"
265+
class="appearance-none border w-full py-2 px-3 rounded-l-md text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
266+
value="Pet the cat"
267+
/>
268+
269+
<button
270+
class="cursor-pointer inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-100 text-gray-500 sm:text-sm"
271+
type="button"
272+
data-action="nested-form#remove"
273+
title="Remove todo"
274+
>
275+
X
276+
</button>
277+
278+
<input type="hidden" name="user[todos_attributes][0][_destroy]" />
279+
</div>
280+
</div>
281+
282+
<div id="nested-form-target-todo" data-nested-form-target="target" data-model-target="todo"></div>
283+
284+
<button
285+
id="nested-form-button-todo"
286+
type="button"
287+
data-action="nested-form#add"
288+
data-model="todo"
289+
class="mt-4 bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border rounded shadow nested-form-action"
290+
>
291+
Add todo
292+
</button>
293+
294+
<div class="mt-4 nested-form-wrapper" data-new-record="false">
295+
<label for="todo-1" class="block text-sm font-medium leading-5 text-gray-700">Your note</label>
296+
<div class="mt-1 flex relative rounded-md shadow-sm">
297+
<input
298+
id="note-1"
299+
type="text"
300+
name="user[notes_attributes][0][description]"
301+
class="appearance-none border w-full py-2 px-3 rounded-l-md text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
302+
value="Buy fancy food for cat"
303+
/>
304+
305+
<button
306+
class="cursor-pointer inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-100 text-gray-500 sm:text-sm"
307+
type="button"
308+
data-action="nested-form#remove"
309+
title="Remove note"
310+
>
311+
X
312+
</button>
313+
314+
<input type="hidden" name="user[notes_attributes][0][_destroy]" />
315+
</div>
316+
</div>
317+
318+
<div id="nested-form-target-note" data-nested-form-target="target" data-model-target="note"></div>
319+
320+
<button
321+
id="nested-form-button-note"
322+
type="button"
323+
data-action="nested-form#add"
324+
data-model="note"
325+
class="mt-4 bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border rounded shadow nested-form-action"
326+
>
327+
Add note
328+
</button>
329+
330+
<button
331+
type="submit"
332+
class="mt-4 bg-indigo-600 hover:bg-indigo-400 text-white font-semibold py-2 px-4 border rounded shadow"
333+
>
334+
Save
335+
</button>
336+
</form>
337+
</section>
338+
</div>
339+
205340
<!-- This example requires Tailwind CSS v2.0+ -->
206341
<footer class="bg-white">
207342
<div class="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8">

src/index.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,33 @@ import { Controller } from 'stimulus'
33
export default class extends Controller {
44
static targets = ['target', 'template']
55
static values = {
6-
wrapperSelector: String
6+
wrapperSelector: String,
7+
actionSelector: String
78
}
89

910
initialize () {
1011
this.wrapperSelector = this.wrapperSelectorValue || '.nested-form-wrapper'
12+
this.actionSelector = this.actionSelectorValue || '.nested-form-action'
1113
}
1214

1315
add (e) {
1416
e.preventDefault()
1517

16-
const content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
17-
this.targetTarget.insertAdjacentHTML('beforebegin', content)
18+
const actionSelector = e.target.closest(this.actionSelector)
19+
const insertHTML = (target, content) => {
20+
target.insertAdjacentHTML('beforebegin', content)
21+
}
22+
23+
if (actionSelector) {
24+
const model = actionSelector.dataset.model
25+
const templateTarget = this.templateTargets.find(el => el.dataset.modelTemplate === model)
26+
const target = this.targetTargets.find(el => el.dataset.modelTarget === model)
27+
const content = templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
28+
insertHTML(target, content)
29+
} else {
30+
const content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
31+
insertHTML(this.targetTarget, content)
32+
}
1833
}
1934

2035
remove (e) {

src/index.test.js

+29
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,33 @@ describe('#nestedForm', () => {
1818
content = await (await previousSibling.getProperty('innerHTML')).jsonValue()
1919
expect(content).toContain('New todo')
2020
})
21+
22+
it('should toggle with many nested forms', async () => {
23+
const todoButton = await page.$('#nested-form-button-todo')
24+
const todoTarget = await page.$('#nested-form-target-todo')
25+
const noteButton = await page.$('#nested-form-button-note')
26+
const noteTarget = await page.$('#nested-form-target-note')
27+
28+
let previousSiblingTodo = await todoTarget.evaluateHandle(element => element.previousElementSibling)
29+
let contentTodo = await (await previousSiblingTodo.getProperty('innerHTML')).jsonValue()
30+
31+
expect(contentTodo).toContain('Your todo')
32+
expect(contentTodo).not.toContain('Your note')
33+
await todoButton.click()
34+
35+
previousSiblingTodo = await todoTarget.evaluateHandle(element => element.previousElementSibling)
36+
contentTodo = await (await previousSiblingTodo.getProperty('innerHTML')).jsonValue()
37+
expect(contentTodo).toContain('New todo')
38+
39+
let previousSiblingNote = await noteTarget.evaluateHandle(element => element.previousElementSibling)
40+
let contentNote = await (await previousSiblingNote.getProperty('innerHTML')).jsonValue()
41+
42+
expect(contentNote).toContain('Your note')
43+
expect(contentNote).not.toContain('Your todo')
44+
await noteButton.click()
45+
46+
previousSiblingNote = await noteTarget.evaluateHandle(element => element.previousElementSibling)
47+
contentNote = await (await previousSiblingNote.getProperty('innerHTML')).jsonValue()
48+
expect(contentNote).toContain('New note')
49+
})
2150
})

0 commit comments

Comments
 (0)