Nested Forms from Scratch with StimulusJS

Episode #186 by Teacher's Avatar David Kimura

Summary

Using StimulusJS controllers, adding nested forms to a Rails application is easy and unobtrusive. In this episode, we look at an alternative way of creating nested forms without the Cocoon gem.
rails form javascript stimulusjs 16:54

Resources

Summary

# Terminal
gem install rails --pre
rails g scaffold todo_list name
rails g model task todo_list:belongs_to description
yarn add stimulus

# models/todo_list.rb
class TodoList < ApplicationRecord
  has_many :tasks, dependent: :destroy
  accepts_nested_attributes_for :tasks, allow_destroy: true, reject_if: proc { |attr| attr['description'].blank? }
end

# models/task.rb
class Task < ApplicationRecord
  belongs_to :todo_list
end

# todo_lists_controller.rb
def todo_list_params
  params.require(:todo_list).permit(:name, tasks_attributes: [:_destroy, :id, :description])
end

# app/javascript/packs/application.js
import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("../controllers", true, /\.js$/)
application.load(definitionsFromContext(context))

# app/javascript/controllers/nested_form_controller.js
import { Controller } from "stimulus"

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

  add_association(event) {
    event.preventDefault()  
    var content = this.templateTarget.innerHTML.replace(/TEMPLATE_RECORD/g, new Date().valueOf())
    this.add_itemTarget.insertAdjacentHTML('beforebegin', content)
  }

  remove_association(event) {
    event.preventDefault()
    let item = event.target.closest(".nested-fields")
    item.querySelector("input[name*='_destroy']").value = 1
    item.style.display = 'none'
  }
}

# todo_lists/_form.html.erb
  <h1>Tasks</h1>
  <div data-controller="nested-form">
    <template data-target='nested-form.template'>
      <%= form.fields_for :tasks, Task.new, child_index: 'TEMPLATE_RECORD' do |task| %>
        <%= render 'task_fields', form: task %>
      <% end %>
    </template>

    <%= form.fields_for :tasks do |task| %>
      <%= render 'task_fields', form: task %>
    <% end %>

    <div data-target="nested-form.add_item">
      <%= link_to "Add Task", "#", data: { action: "nested-form#add_association" } %>
    </div>
  </div>

# _task_fields.html.erb
<div class='nested-fields'>
  <div class='form-group'>
    <%= form.hidden_field :_destroy %>
    <%= form.text_field :description, placeholder: 'Description', class: 'form-control' %>
    <small>
      <%= link_to "Remove", "#", data: { action: "click->nested-form#remove_association" } %>
    </small>
  </div>
</div>