Skip to content

Instantly share code, notes, and snippets.

@john-hamnavoe
Created December 14, 2022 16:27
Show Gist options
  • Save john-hamnavoe/3259ac37904e4395a7903180393d23b4 to your computer and use it in GitHub Desktop.
Save john-hamnavoe/3259ac37904e4395a7903180393d23b4 to your computer and use it in GitHub Desktop.
Dymanic nested forms rails 7

Introduction

Model with accepted nested attributes stimulus controller to allow adding/deleting of rows to the nested input. e.g. TimeLog might have many TimeLogEntries

Model Changes

Change to accepts nested attributes

  has_many :time_log_entries
  accepts_nested_attributes_for :time_log_entries, allow_destroy: true

Controller changes

Updated the permitted params on the controller

  # Only allow a list of trusted parameters through.
  def time_log_params
    params.require(:time_log).permit(:date, :notes,
                                      time_log_entries_attributes: [:id, :schedule_id, :employee_id, :end_time, :employee_role_id, :duty_type_id, :hours_worked, :_destroy])
  end

Stimulus Controller

Using example by @excid3

import { Controller } from "@hotwired/stimulus"

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

  connect() {
    this.wrapperClass = this.data.get("wrapperClass") || "nested-fields"
    console.log(this.wrapperClass)
  }

  add_association(event) {
    console.log("add association")
    event.preventDefault()

    var content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
    this.linksTarget.insertAdjacentHTML('beforebegin', content)
  }

  remove_association(event) {
    event.preventDefault()

    let wrapper = event.target.closest("." + this.wrapperClass)

    // New records are simply removed from the page
    if (wrapper.dataset.newRecord == "true") {
      wrapper.remove()

    // Existing records are hidden and flagged for deletion
    } else {
      wrapper.querySelector("input[name*='_destroy']").value = 1
      wrapper.style.display = 'none'
    }
  }
}

Add to app/javascript/controllers/index.js

import NestedFormController from "./nested_form_controller"
application.register("nested-form", NestedFormController)

View Changes

Nested Attributes Partial

Need a partial for the nested fields

<%= content_tag :div, class: "nested-fields", data: { new_record: form.object.new_record? } do %>
  <div class="grid grid-cols-11">
    <div class="col-span-2 border-b border-r border-dashed px-1 text-sm text-gray-700 font-semibold">
        SCHEDULE
    </div>
    <div class="col-span-2 border-b border-r border-dashed px-1">
      <%= render Forms::FieldComponent.new(form, :employee_id, label: "Employee", label_class: "sr-only") do |field| %>
        <% field.with_input_select(options: employees_for_select(form.object.employee_id), include_blank: true) %>
      <% end %>
    </div>            
    <div class="col-span-2 border-b border-r border-dashed px-1">
      <%= render Forms::FieldComponent.new(form, :employee_role_id, label: "Role", label_class: "sr-only") do |field| %>
        <% field.with_input_select(options: employee_roles_for_select(form.object.employee_role_id), include_blank: true) %>
      <% end %>
    </div>    
    <div class="col-span-2 border-b border-r border-dashed px-1">
      <%= render Forms::FieldComponent.new(form, :duty_type_id, label: "Duty", label_class: "sr-only") do |field| %>
        <% field.with_input_select(options: duty_types_for_select(form.object.duty_type_id), include_blank: true) %>
      <% end %>
    </div>  
    <div class="border-b border-r border-dashed px-1">
      <%= render Forms::FieldComponent.new(form, :start_time, label: "Start", label_class: "sr-only") do |field| %>
        <% field.with_input_time %>
      <% end %>
    </div>  
    <div class="border-b border-r border-dashed px-1">
      <%= render Forms::FieldComponent.new(form, :end_time, label: "End", label_class: "sr-only") do |field| %>
        <% field.with_input_time %>
      <% end %>
    </div> 
    <div class="border-b border-r border-dashed px-1">
      <%= render Forms::FieldComponent.new(form, :hours_worked, label: "Hours", label_class: "sr-only") do |field| %>
        <% field.with_input_text read_only: true %>
      <% end %>
    </div>                                                  
  </div>
<% end %>

Then in main form call this as template for new time entries, and for showing all the times.

<div data-controller="nested-form">
  <template data-target="nested-form.template">
    <%= form.fields_for :time_log_entries, TimeLogEntry.new, child_index: 'NEW_RECORD' do |time_log_entry| %>
      <%= render "time_log_entry_fields", form: time_log_entry %>
    <% end %>
  </template>

  <%= render "time_log_entry_fields_header" %>
  <%= form.fields_for :time_log_entries do |time_log_entry| %>
    <%= render "time_log_entry_fields", form: time_log_entry %>
  <% end %>

  <div class="m-2" data-target="nested-form.links">
    <%= link_to "Add Time Entry", "#", class: "inline-flex items-center rounded-full border border-transparent bg-indigo-100 px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2", data: { action: "click->nested-form#add_association" } %>
  </div>
</div>   
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment