In [Chapter 9](/guide/second-resource-and-associations/) nested fields stayed
static: there was one ingredient and one step row on new and edit recipe form but there was no way to add more or remove existing nested fields. That was done intentionally to keep the chapter focused.

This chapter makes the nested fields inside the recipe form dynamic: Add ingredient, Add step, and Remove nested field rows by sprinkling some StimulusJS magic so the form feels like a real cookbook. The buttons are new UI you write by hand, so you use the same TDD habit as Chapter 9: scenario and test first (red), then the smallest form change with StimulusJS controller (green). You will also add an integration test for removing nested fields from a recipe but without using TDD because you already wired `_destroy` in the controller and `allow_destroy` in recipe model in [Chapter 9](/guide/second-resource-and-associations/#refactor-wire-nested-form-fields) to destroy nested fields from the recipe.

<%= render Guide::ChapterProgress.new(
  feature: "Add and remove ingredient and step rows with Stimulus (TDD via a system test), then an integration test that removes a saved ingredient and step on edit.",
  why: "Chapter 9 kept the form simple: static nested fields without a way to add more or remove existing ingredients and steps. Now you make it feel real: add and remove nested records, then prove it works the way a user uses it by adding system and integration tests.",
  test: "System: grow `updates a recipe` for Add and Remove nested rows on edit, then submit.
  Integration: remove existing ingredients and steps when updating a recipe using `_destroy` attribute.",
  later: "[Chapter 11](/guide/testing-turbo-frames-and-streams/) removes ingredients and steps from the show page with Turbo Streams. [Chapter 12](/guide/testing-authentication/) requires sign-in before you can create, update, or destroy a recipe."
) %>

## What you will do in this chapter

1. Extend the existing `updates a recipe` system test for Add on edit (red), wire partials and Stimulus for Add functionality (green), then surf Add in the browser.
2. Grow that same system smoke for Remove (red), wire Remove on the partials and Stimulus (green), surf Remove in the browser, then add an integration test for nested `_destroy` on edit (no TDD; server path already wired in Chapter 9).
3. Run the full suite and commit after a green run.

## Scenarios to automate

These are the outcomes you will prove by the end of the chapter. The browser smoke extends the existing edit test for the recipe; the integration test locks nested destroy over HTTP (already wired in Chapter 9).

| Scenario | Expected | Test Type |
| --- | --- | --- |
| Add ingredient on edit | On pancakes edit, click Add ingredient; a blank row appears; fill Onion and save | System |
| Add step on edit | On pancakes edit, click Add step; a blank row appears; fill an instruction and save | System |
| Remove a new row on edit | Add a blank row, then Remove it; it disappears from the form before save | System |
| Remove a saved row on edit | Remove Salt on pancakes edit, save; Salt gone from show; Flour remains | System |
| Remove saved ingredient and step over HTTP | Patch pancakes with Salt and the preheat step with the param `_destroy: "1"`; both counts drop by 1; show has no Salt and no preheat; Flour remains | Integration |

## Add nested fields when editing a recipe

You already added the ability to create, update, and destroy nested fields (ingredients and steps) in [Chapter 9](/guide/second-resource-and-associations/) by wiring `accepts_nested_attributes_for` in the recipe model and permitting nested attributes (ingredients_attributes and steps_attributes) in the recipe controller. But the form still shows one static row per association. Time to make the form dynamic: in this chapter, you add Add ingredient, Add step, and Remove buttons so a user can grow or trim the list of nested records on edit before updating the recipe.

Rails does not generate Add and Remove buttons for nested fields, you will need to write them by hand. To make the nested fields dynamic, you wire up a Stimulus controller to clone a [`<template>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template?utm_source=minitestrails.com) row when user clicks Add button and toggle `_destroy` attribute when user clicks Remove button. Don't worry if you don't understand `<template>` and Stimulus parts yet, all of these will come shortly in this chapter.

You could also build the same UX for dynamic nested fields with Turbo Streams. I have seen a lot of people try that first because it feels more "Rails-y". But for a blank nested row that doesn't rely on any data from the server, Turbo Streams are usually overkill. In practice you end up with:

- A round trip to the server on every Add (and often every Remove) even though nothing hits the database yet, only empty fields.
- Extra routes and controller actions just to render HTML markup for nested fields (for ingredients and again for steps).
- Turbo Stream templates (or `render turbo_stream:`) whose only job is to append or remove HTML markup.
- A different mental model than usual save all records at once approach

    You end up saving the recipe first, then adding or removing ingredients one request at a time. That works well on a show page, but it is a worse fit when the recipe and its rows must save together in one submit.

Overall it's not really worth it to add Turbo Streams for this functionality. For one form that submits everything together, Stimulus is the usual Rails 8 approach. You will learn about testing Turbo Streams/Frames and look at when to use StimulusJS vs Turbo features in [Chapter 11](/guide/testing-turbo-frames-and-streams/).

### Why extend the test for edit instead of create?

You already have a system test for `test "updates a recipe"` from [Chapter 7](/guide/testing-simple-crud-system-tests/#update), you will grow the same test instead of extending the test for create. You might be wondering "why extend edit and not create?"

Read on.

For system smoke tests, my usual approach of choosing where to put tests is to look for the happy path that gives the most coverage in one visit. Edit wins here: pancakes already has fixture ingredients and steps, so one flow can add a new nested row, remove a new unsaved row, remove a saved row, and submit. Create only starts with blank rows, so you never prove Remove on an existing child in that same session. That is why you extend the edit test instead of creating a new one for create.

### What counts as working?

| Scenario | You might say |
| --- | --- |
| Add ingredient on edit | On pancakes edit, click Add ingredient; fill Onion on the new row; save; Onion is displayed on show page |
| Add step on edit | On pancakes edit, click Add step; fill an instruction on the new row; save; that instruction is displayed on show page |

### Add scenarios to the test file

Update the scenario comment on the system test for `updates a recipe` (from [Chapter 7](/guide/testing-simple-crud-system-tests/)) at `test/system/recipes_test.rb` with the following:

```ruby
# test/system/recipes_test.rb
  # Actor: guest
  # Starting point: pancakes edit form in browser
  # Action: change title, add an ingredient, add a step, submit
  # Expected outcome: new title; Onion and the new step on show page; Salt and Flour still there
  # test "updates a recipe" do
  # end
```

### Red: extend the system test

Replace the body of `test "updates a recipe"` with the following:

```ruby
# test/system/recipes_test.rb
  test "updates a recipe" do
    recipe = recipes(:pancakes)
    visit edit_recipe_url(recipe)

    fill_in "Title", with: "Extra fluffy pancakes"

    click_on "Add ingredient"
    all("input[name*='[ingredients_attributes]'][name*='[name]']").last.set("Onion")

    click_on "Add step"
    all("textarea[name*='[steps_attributes]'][name*='[instruction]']").last.set("Flip when bubbles form")

    click_on "Update Recipe"

    assert_text "Extra fluffy pancakes"
    assert_text "Salt"
    assert_text "Flour"
    assert_text "Onion"
    assert_text "Flip when bubbles form"
  end
```

<%= render Shared::Tip.new(
  title: "New Capybara helper",
  markdown: <<~MD
    **`all("input[name*='[ingredients_attributes]'][name*='[name]']").last.set("Onion")`** asks "fill the newest matching field on the page?"
    Syntax: `all` plus a CSS query returns every match; `.last` picks the blank row Add just created; `.set("...")` types into that field. Use the same pattern on a `textarea` for the new step instruction. Prefer this over `fill_in` when several fields share similar labels or names after cloning.
  MD
) %>

This is what's happening in the code above:

- `click_on "Add ingredient"` and `click_on "Add step"` clicks on the buttons to add a new ingredient and step row.
- `.last.set(...)` fills the blank row each Add button just created.
- The assertions check the show page after Update Recipe. It ensures new ingredient and step are displayed, and existing ingredients and steps are still there.

Run the test:

```bash
bin/rails test test/system/recipes_test.rb
```

You should see an error:

```bash
E

Error:
RecipesTest#test_updates_a_recipe:
Capybara::ElementNotFound: Unable to find link or button "Add ingredient"
    test/system/recipes_test.rb:37:in 'block in <class:RecipesTest>'
```

Capybara is not able to find Add ingredient since you haven't wired up the buttons to add a new ingredient and step row in the recipe form yet. The error is expected. That is your red step.

### Green: partials and Stimulus

The test is throwing an error right now at "Add ingredient" but once you wire that up it will also throw an error at "Add step" and missing form fields for new rows of ingredients and steps. We won't TDD all of that here. Instead we will wire up the buttons and the form fields all at once in three small pieces:

1. Rails view partials that holds the fields for one ingredient row and one step row (name/quantity/unit, or position/instruction).
2. Stimulus controller that runs on Add clicks and clones a blank row into the form.
3. Form wiring that connects the Add buttons and a hidden `<template>` blank row on `_form.html.erb` to the Stimulus controller.

#### Partials for nested rows

Create `app/views/recipes/form/_ingredient_fields.html.erb` with the following. Putting the new file under `form/` makes it clear the partials belong to the recipe form, and not any other pages.

```erb
<%%# app/views/recipes/form/_ingredient_fields.html.erb %>
<div>
  <%%= form.hidden_field :id %>

  <div>
    <%%= form.label :name %>
    <%%= form.text_field :name %>
  </div>
  <div>
    <%%= form.label :quantity %>
    <%%= form.number_field :quantity, step: 0.01 %>
  </div>
  <div>
    <%%= form.label :unit %>
    <%%= form.text_field :unit %>
  </div>
</div>
```

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    **`step: 0.01`** on `number_field` is an HTML attribute. It sets how fine the quantity field can be. The browser default for a number input is whole numbers (`step` of 1). With that default, typing `0.5` or `1.25` often fails HTML5 validation (e.g. Chrome shows something like "Please enter a valid value"), and the up/down arrows jump by 1. `step: 0.01` allows hundredths, so cookbook amounts like `0.25` cup work.
  MD
) %>

Also create a new partial for steps at `app/views/recipes/form/_step_fields.html.erb` with the following:

```erb
<%%# app/views/recipes/form/_step_fields.html.erb %>
<div>
  <%%= form.hidden_field :id %>

  <div>
    <%%= form.label :position %>
    <%%= form.number_field :position %>
  </div>
  <div>
    <%%= form.label :instruction %>
    <%%= form.text_area :instruction %>
  </div>
</div>
```

This is what's happening in the code above:

- Each partial holds the fields for one nested row: ingredient fields (name, quantity, unit) or step fields (position, instruction).
- The hidden `id` is required to tell Rails which record to update when the form is submitted. This is the Rails way of updating existing records.

#### Stimulus: Add a row dynamically

Create a new Stimulus controller at `app/javascript/controllers/nested_form_controller.js` with the following:

```javascript
// app/javascript/controllers/nested_form_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["container", "template"]

  add(event) {
    event.preventDefault()
    const content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, Date.now())
    this.containerTarget.insertAdjacentHTML("beforeend", content)
  }
}
```

This is what's happening in the code above:

- `add` reads the HTML inside the `<template>` target, replaces every `NEW_RECORD` string with a unique number (`Date.now()`), then inserts that HTML into the page.
- `insertAdjacentHTML("beforeend", content)` is a [browser API](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML?utm_source=minitestrails.com). It pastes the `content` string as real DOM nodes just before the closing tag of the container (so after any rows already there). `"beforeend"` means "append inside this element," not "replace the whole list."
- You have not put `NEW_RECORD` in the view file yet. That placeholder shows up in the next step when you wire `_form.html.erb` with `child_index: "NEW_RECORD"` inside a `<template>`.

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    **Why `Date.now()`?**
    Nested params look like `recipe[ingredients_attributes][KEY][name]` when received by the recipes controller. Rails uses the `KEY` to uniquely identify each row. The `<template>` (in next section) ships with the placeholder `NEW_RECORD`. If every `Add` kept the same key, Rails would treat all new rows as updates to the first one and only keep the last. `Date.now()` swaps in a unique key each click (for example `1720000000123`) so Onion and a second new ingredient both survive the submit.
    
    Any unique value works here; we are using timestamps because they are simple and easy to use.
  MD
) %>

#### Connect the form to Stimulus

Open `app/views/recipes/_form.html.erb` and replace the content in the file with the following to include the code for dynamic nested fields:

```erb
<%%# app/views/recipes/_form.html.erb (nested sections) %>
<%%= form_with(model: recipe) do |form| %>
  <%% if recipe.errors.any? %>
    <div style="color: red">
      <h2><%%= pluralize(recipe.errors.count, "error") %> prohibited this recipe from being saved:</h2>

      <ul>
        <%% recipe.errors.each do |error| %>
          <li><%%= error.full_message %></li>
        <%% end %>
      </ul>
    </div>
  <%% end %>

  <div>
    <%%= form.label :title, style: "display: block" %>
    <%%= form.text_field :title %>
  </div>

  <div>
    <%%= form.label :description, style: "display: block" %>
    <%%= form.textarea :description %>
  </div>

  <div>
    <%%= form.label :prep_time, style: "display: block" %>
    <%%= form.number_field :prep_time %>
  </div>

  <div>
    <%%= form.label :servings, style: "display: block" %>
    <%%= form.number_field :servings %>
  </div>

  <div data-controller="nested-form">
    <h2>Ingredients</h2>
    <div data-nested-form-target="container">
      <%%= form.fields_for :ingredients do |ingredient_form| %>
        <%%= render "recipes/form/ingredient_fields", form: ingredient_form %>
      <%% end %>
    </div>

    <template data-nested-form-target="template">
      <%%= form.fields_for :ingredients, Ingredient.new, child_index: "NEW_RECORD" do |ingredient_form| %>
        <%%= render "recipes/form/ingredient_fields", form: ingredient_form %>
      <%% end %>
    </template>

    <button type="button" data-action="nested-form#add">Add ingredient</button>
  </div>

  <div data-controller="nested-form">
    <h2>Steps</h2>
    <div data-nested-form-target="container">
      <%%= form.fields_for :steps do |step_form| %>
        <%%= render "recipes/form/step_fields", form: step_form %>
      <%% end %>
    </div>

    <template data-nested-form-target="template">
      <%%= form.fields_for :steps, Step.new, child_index: "NEW_RECORD" do |step_form| %>
        <%%= render "recipes/form/step_fields", form: step_form %>
      <%% end %>
    </template>

    <button type="button" data-action="nested-form#add">Add step</button>
  </div>

  <div>
    <%%= form.submit %>
  </div>
<%% end %>
```

This is what's happening in the (new) code above:

- `data-controller="nested-form"` tells Stimulus to attach `nested_form_controller.js` to that `<div>` and everything inside it. The value `nested-form` maps to the file name: dashes become underscores, and Stimulus looks for `nested_form_controller.js`. Stimulus runs only inside the `<div>` container where the `data-controller="nested-form"` is declared.
- The `<div data-nested-form-target="container">` holds the rows the user 
can see (existing fixture rows on edit, plus anything Add clones in).
- `<template>` holds a blank row for cloning. Browsers treat `<template>` content as inert: it is not shown on the page and its fields are not submitted with the form until JavaScript copies that HTML into the live DOM. That is why the blank row stays off-screen until `add` clones it.
- `child_index: "NEW_RECORD"` prints the `NEW_RECORD` placeholder into nested field names (for example `recipe[ingredients_attributes][NEW_RECORD][name]`). Stimulus then swaps `NEW_RECORD` for `Date.now()` on each clone.
- `data-action="nested-form#add"` on the Add button means "on click, call the `add` method on the `nested-form` controller for this block."
- Existing rows (Salt, Flour, the preheat step) and new rows share the same partials under `app/views/recipes/form/` (`_ingredient_fields` and `_step_fields`).
- Ingredients and steps each get their own `data-controller="nested-form"` block on the form, but both use the same `nested_form_controller.js` file.

Run the Chapter 9 integration file to confirm nested create still passes after the refactor, you want 0 failures and 0 errors:

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

Then re-run the recipes system test:

```bash
bin/rails test test/system/recipes_test.rb
```

You want 0 failures and 0 errors. That is green for Add ingredients and steps.

### Try Add functionality in the browser

Before you add Remove functionality, click through the recipe form by hand so you see the Add functionality for nested fields working with Stimulus and form wiring away from the actual test suite.

<%= render Guide::LoadDevelopmentFixtures.new(
  after_load: <<~MD
    Load fixtures before you try Add ingredient and Add step on edit in the browser.
  MD
) %>

1. Start `bin/dev` if the server is not running.
2. Open the recipes list ([http://localhost:3000/recipes](http://localhost:3000/recipes)), click Fluffy pancakes, then Edit this recipe.
3. Click Add ingredient. A blank name / quantity / unit row should appear under Salt and Flour.
4. Fill that row with Onion (quantity and unit optional), then click Add step, fill an instruction "Flip when bubbles form", and click Update Recipe.
5. On the show page, confirm Onion and the new step appear with Salt, Flour, and Preheat the pan.

## Remove nested rows when editing a recipe

With that we have wired up the ability to add nested fields to the recipe form successfully. Next you will wire up the ability to remove nested fields.

For `remove`, you need to handle two different edge cases when clicking the Remove button:

1. A new row (unsaved record) is dropped from the DOM and never reaches the server.
2. A saved row (Salt on pancakes) stays in the DOM (but hidden), gets `_destroy` attribute toggled to `1`, and is deleted when you submit Update Recipe.

You already wired `allow_destroy: true` and permitted `_destroy` in [Chapter 9](/guide/second-resource-and-associations/) which means server side code is already in place, now you need to wire up the HTML side of the Remove functionality.

In this section, you will grow the same system smoke test for `updates a recipe` (red), wire Remove functionality in both the view and the Stimulus controller (green), then lock the saved-row path over HTTP with an integration test (no TDD for that HTTP slice).

### What counts as working?

| Scenario | You might say | Test type |
| --- | --- | --- |
| Remove a new row on edit | Add a blank ingredient row, click Remove button; the row is gone before save | System test |
| Remove a saved row on edit | Remove Salt, save; Salt gone from show; Flour and Onion remain | System test |
| Remove saved ingredient and step over HTTP | Patch pancakes by sending `_destroy` attribute for Salt (ingredient) and preheat (step); both gone from show page; Flour remains | Integration test |

### Red: extend the system test

Update the scenario comment with the following to include the new action of removing a new row and Salt before submit.

```ruby
# test/system/recipes_test.rb
  # Actor: guest
  # Starting point: pancakes edit form in browser
  # Action: change title, add ingredient and step, remove a new blank ingredient, remove Salt, submit
  # Expected outcome: new title; Onion and new step on show; Salt gone; Flour still there
  # test "updates a recipe" do
  # end
```

Then replace the body of `test "updates a recipe"` with the following:

```ruby
# test/system/recipes_test.rb
test "updates a recipe" do
  recipe = recipes(:pancakes)
  visit edit_recipe_url(recipe)

  fill_in "Title", with: "Extra fluffy pancakes"

  click_on "Add ingredient"
  all("input[name*='[ingredients_attributes]'][name*='[name]']").last.set("Onion")

  click_on "Add step"
  all("textarea[name*='[steps_attributes]'][name*='[instruction]']").last.set("Flip when bubbles form")

  click_on "Add ingredient"
  all("button", text: "Remove ingredient").last.click

  within find("input[name*='[ingredients_attributes]'][name*='[name]'][value='Salt']").ancestor("[data-nested-form-target='row']") do
    click_on "Remove ingredient"
  end

  click_on "Update Recipe"

  assert_text "Extra fluffy pancakes"
  assert_no_text "Salt"
  assert_text "Flour"
  assert_text "Onion"
  assert_text "Flip when bubbles form"
end
```

<%= render Shared::Tip.new(
  title: "New Capybara helpers",
  markdown: <<~MD
    1. **`all("button", text: "Remove ingredient").last.click`**
      Same idea as `all` + `.last` for Add functionality, but you click the newest Remove button (the blank row you just added) instead of typing into a field.

    2. **`within(...) { click_on "Remove ingredient" }`**
      Limits the click to a slice of the page. Everything inside the block only looks inside that element, so you do not hit Flour's Remove by accident instead of "Salt".

    3. **`find(...).ancestor("[data-nested-form-target='row']")`**
      `find` locates one element (here the Salt name input). `.ancestor(...)` traverses to the nested row wrapper that holds its Remove button.
  MD
) %>

This is what's happening in the code above:

- After Onion and the new step are filled, a second Add ingredient creates a blank new row.
- `all("button", text: "Remove ingredient").last.click` removes that unsaved row from the DOM (edge case: `Remove a new row on edit`).
- The "Remove ingredient" for Salt finds the name input whose value is Salt, traverses to that nested row with `ancestor`, then clicks Remove ingredient inside `within` (edge case: `Remove a saved row on edit`).
- Show assertions expect Salt gone, Flour and Onion still there.

Run:

```bash
bin/rails test test/system/recipes_test.rb
```

You should see an error when Capybara cannot find Remove ingredient. That is your red step for this section. Add works but Remove button is not on the form yet so it cannot be clicked and fails the test.

```bash
E

Error:
RecipesTest#test_updates_a_recipe:
NoMethodError: undefined method 'click' for nil
    test/system/recipes_test.rb:48:in 'block in <class:RecipesTest>'
```

### Green: partials update and Stimulus wiring

To fix failing tests, we will use the same approach as Add: update HTML to include Remove buttons first, then add the Stimulus action for actually removing the row from the form:

1. Rails view partials to include a Remove button, a `row` target, `data-new-record`, and a hidden `_destroy` field.
2. Stimulus controller to include a `remove` action that drops a new row or marks a saved row for destroy on the server.

#### Partials: Add Remove buttons to nested rows

Replace `app/views/recipes/form/_ingredient_fields.html.erb` with the following:

```erb
<%%# app/views/recipes/form/_ingredient_fields.html.erb %>
<div data-nested-form-target="row" data-new-record="<%%= form.object.new_record? %>">
  <%%= form.hidden_field :id %>
  <%%= form.hidden_field :_destroy, value: "0" %>

  <div>
    <%%= form.label :name %>
    <%%= form.text_field :name %>
  </div>
  <div>
    <%%= form.label :quantity %>
    <%%= form.number_field :quantity, step: 0.01 %>
  </div>
  <div>
    <%%= form.label :unit %>
    <%%= form.text_field :unit %>
  </div>

  <button type="button" data-action="nested-form#remove">Remove ingredient</button>
</div>
```

Replace `app/views/recipes/form/_step_fields.html.erb` with:

```erb
<%%# app/views/recipes/form/_step_fields.html.erb %>
<div data-nested-form-target="row" data-new-record="<%%= form.object.new_record? %>">
  <%%= form.hidden_field :id %>
  <%%= form.hidden_field :_destroy, value: "0" %>

  <div>
    <%%= form.label :position %>
    <%%= form.number_field :position %>
  </div>
  <div>
    <%%= form.label :instruction %>
    <%%= form.text_area :instruction %>
  </div>

  <button type="button" data-action="nested-form#remove">Remove step</button>
</div>
```

This is what's happening in the code above (description of new code only):

- `data-nested-form-target="row"` labels the wrapper div as a Stimulus `row` target. In the next step, we will add `remove` action to `nested_form_controller.js` that will call `.closest(...)` from the button up to this div so it knows which row to drop or hide. Without this attribute, Stimulus has no clear way to find the right ingredient or step to remove.
- `data-new-record="<%%= form.object.new_record? %>"` is a flag on the same div wrapper that tells Stimulus whether the row is new or already saved. Rails sets `new_record?` to `true` for a blank `Ingredient.new` / `Step.new` (the row Add just cloned) and `false` for Salt, Flour, or the preheat step already in the database. Stimulus will read this in the next step: new rows get deleted from the page only while saved rows get marked for destroy on submit.
- `form.hidden_field :_destroy, value: "0"` is used by nested attributes to mark a row for deletion; `0` means keep the row. When you Remove a saved row, Stimulus will flip this to `1` which tells the server to delete the row. Chapter 9 already permitted `_destroy` and set `allow_destroy: true` so the server knows what to do.
- The Remove button uses `type="button"` so a click does not submit the recipe form. `data-action="nested-form#remove"` means, call the `remove` method on the `nested-form` controller to actually remove the row from the form dynamically.

#### Stimulus: Remove a row

Replace `app/javascript/controllers/nested_form_controller.js` with the following so clicking on the Remove button will remove the row from the form:

```javascript
// app/javascript/controllers/nested_form_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["container", "template"]

  add(event) {
    event.preventDefault()
    const content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, Date.now())
    this.containerTarget.insertAdjacentHTML("beforeend", content)
  }

  remove(event) {
    event.preventDefault()
    const row = event.target.closest("[data-nested-form-target='row']")

    if (row.dataset.newRecord === "true") {
      row.remove()
    } else {
      row.querySelector("input[name*='[_destroy]']").value = "1"
      row.hidden = true
    }
  }
}
```

This is what's happening in the code above:

- `remove` action is responsible for removing the row from the form. It starts from the clicked button (`event.target`) and uses `.closest("[data-nested-form-target='row']")` to find that button's row wrapper.
- Do not reach for `this.rowTarget` here. Stimulus `this.rowTarget` is always the **first** `row` target inside the controller. With Salt, Flour, and a new Onion row, Remove on Flour would still grab Salt. `.closest` walks up from the click, so you get the row that owns that button. (`this.rowTargets` is every row; you would still need to pick one.)
- If that row was never saved (`data-new-record="true"`), it deletes the the node (`<div>` wrapper) from the DOM and nothing is sent to the server for that blank row.
- If the row was already in the database (Salt), it sets the hidden `_destroy` field to `1` and hides the row so the record is deleted from the database when submitting the form.

Re-run the system test, you want 0 failures and 0 errors:

```bash
bin/rails test test/system/recipes_test.rb
```

### Try Remove functionality in the browser

Before you add integration test for removing a saved row, click through Remove functionality in the browser by hand so you see both edge cases handled by Stimulus: drop a new row, and mark a saved row for destroy.

<%= render Guide::LoadDevelopmentFixtures.new(
  after_load: <<~MD
    Load fixtures before you try Remove on edit in the browser. If you already added Onion while surfing Add, start from a fresh pancakes edit so Salt and Flour are back.
  MD
) %>

1. Start `bin/dev` if the server is not already running.
2. Visit recipe list ([http://localhost:3000/recipes](http://localhost:3000/recipes)), click Fluffy pancakes, then Edit this recipe.
3. Click Add ingredient, then click Remove ingredient on that new blank row. The row should disappear from the form before you save.
4. Click Remove ingredient for the Salt row. The Salt row should hide (Stimulus sets `_destroy` and hides it).
5. Optionally click Remove step for the step Preheat the pan row.
6. Click Update Recipe. On the show page, confirm Salt (and the preheat step, if you removed it) are gone, and Flour remains.

<%= render Guide::SupportCta.new(
  variant: :mid_chapter,
  site_metadata: site.data.site_metadata,
  milestone_hook: "Dynamic nested Add and Remove functionality works in the browser now.",
  headline: "A form that grows and shrinks with tests behind it is a real milestone.",
  body_text: "You proved Stimulus Add/Remove with a system smoke without chasing Turbo markup. If that habit is sticking, fund the next chapter and help keep the guide free."
) %>

### Integration test: Remove a saved row over HTTP

The system smoke already proved Remove in the browser. This integration test skips Capybara and hits the same nested destroy path over HTTP: `patch` pancakes with `_destroy` for Salt and the preheat step, then check counts and show HTML. You will not use TDD for this slice since Chapter 9 already wired `allow_destroy: true` and permitted `_destroy` required for this test to pass.

#### Add scenarios to the test file

Add the following scenario comment at the end of `test/integration/recipes_integration_test.rb`:

```ruby
# test/integration/recipes_integration_test.rb
  # Actor: guest
  # Starting point: pancakes has salt, flour, and preheat in fixtures
  # Action: patch with salt's id and preheat's id, both with _destroy
  # Expected outcome: redirect to show; Salt and preheat gone; Flour still on page
  # test "removes existing ingredients and steps when updating a recipe" do
  # end
```

#### Add the integration test

Replace the body of the `test "removes existing ingredients and steps when updating a recipe"` with the following:

```ruby
# test/integration/recipes_integration_test.rb
test "removes existing ingredients and steps when updating a recipe" do
  recipe = recipes(:pancakes)
  salt = ingredients(:salt)
  preheat = steps(:preheat)

  assert_difference ["Ingredient.count", "Step.count"], -1 do
    patch recipe_url(recipe), params: {
      recipe: {
        title: recipe.title,
        ingredients_attributes: {
          "0" => { id: salt.id, _destroy: "1" }
        },
        steps_attributes: {
          "0" => { id: preheat.id, _destroy: "1" }
        }
      }
    }
  end

  assert_redirected_to recipe_url(recipe)
  follow_redirect!
  assert_response :success
  assert_no_match salt.name, response.body
  assert_no_match preheat.instruction, response.body
  assert_match "Flour (2 cups)", response.body
end
```

This is what's happening in the code above:

- `patch` sends `_destroy: "1"` for Salt and for the preheat step in the same update to mark them for deletion.
- `assert_difference ["Ingredient.count", "Step.count"], -1` expects one fewer ingredient and one fewer step when the update for the recipe succeeds.
- After redirect, Salt and the preheat instruction are absent from the show HTML and Flour (2 cups) remains.

Run:

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

You want 0 failures and 0 errors.

Rows you do not include in the patch are left alone. That is why Flour can stay when you only send Salt (and the preheat step) with `_destroy`.

## Commit your work

Run the full suite:

```bash
bin/rails test:all
```

You want 0 failures and 0 errors. Then:

```bash
git add .
git commit -m "Add dynamic nested fields with browser add/remove and nested destroy"
```

Small commits make it easier to roll back when Turbo or auth land in the next chapters.

## What is next

[Chapter 11](/guide/testing-turbo-frames-and-streams/) covers Turbo Streams and Frames: remove ingredients and steps from the recipe show page without a full reload, and use Turbo Frame for a quick filter in recipe index page.

Continue to [Testing Turbo Frames and Streams](/guide/testing-turbo-frames-and-streams/).
