Skip to content

Instantly share code, notes, and snippets.

@digitalWestie
Last active January 19, 2024 13:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save digitalWestie/1d3bf145de906c0be4b1b027c55d62a1 to your computer and use it in GitHub Desktop.
Save digitalWestie/1d3bf145de906c0be4b1b027c55d62a1 to your computer and use it in GitHub Desktop.
Rails nested attributes view strategy

Putting related 0 to many resources within a form

For example, we have Paper and EvidenceItem models. We want to let the user add/remove/edit EvidenceItems while they are adding or editing Paper records.

The Paper model looks like this:

class Paper < ApplicationRecord

  has_many :evidence_items, dependent: :destroy
  accepts_nested_attributes_for :evidence_items, allow_destroy: true # add this to allow form nesting
  ...

The paper controller will need to permit the nested attributes:

def paper_params
  params.require(:paper).permit(
    :name, :description, :author,
    evidence_items_attributes: [ :id, :name, :summary, :link, :source_type, :_destroy]
  )
end

Then we use the above stimulus controller and form fields to manage the nested resource.

<div data-controller="nested-resource" class="mb-8">
<div data-nested-resource-target="container">
<!-- for existing resources -->
<%= form.fields_for :evidence_items do |evidence_item_fields| %>
<fieldset class="nested-resource-fields rounded relative mb-6 border px-6 pt-4 pb-2">
<button style="top: -0.74rem;" type="button" data-action="nested-resource#deleteResource" class="leading-none hover:text-gray-900 rounded-full absolute right-0 pt-1 pb-2 px-3 text-4xl text-gray-700 z-50 hover:bg-red-100 hover:text-red-800 "><span>×</span>
</button>
<legend class="font-bold">Evidence item <%= evidence_item_fields.index + 1 %></legend>
<%= evidence_item_fields.hidden_field :_destroy, class: "destroy-resource" %>
<%= render partial: 'evidence_items/fields', locals: { form: evidence_item_fields } %>
</fieldset>
<% end %>
</div>
<button class="px-2 py-1 rounded bg-emerald-100 text-emerald-800 text-lg cursor-pointer hover:bg-emerald-200 hover:text-emerald-900" type="button" data-action="nested-resource#newResource">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 relative inline-block" style="top: -1px">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13" />
</svg>
<span>Add evidence</span>
</button>
<template data-nested-resource-target="template">
<!-- template markup for new resource to be added -->
<fieldset class="nested-resource-fields rounded relative mb-6 border px-6 pt-4 pb-2">
<button style="top: -0.74rem;" type="button" data-action="nested-resource#removeResource" class="leading-none hover:text-gray-900 rounded-full absolute right-0 pt-1 pb-2 px-3 text-4xl text-gray-700 z-50 hover:bg-red-100 hover:text-red-800 "><span>×</span>
</button>
<legend class="font-bold">Evidence item __COUNTER__</legend>
<%= form.fields_for :evidence_items, form.object.evidence_items.build, child_index: "__INDEX__" do |evidence_item_fields| %>
<%= render partial: 'evidence_items/fields', locals: { form: evidence_item_fields } %>
<% end %>
</fieldset>
</template>
</div>
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["container", "template"];
newResource(e) {
const index = new Date().getTime();
let content = this.templateTarget.innerHTML.replace(/__INDEX__/g, index);
const resources = this.containerTarget.querySelectorAll(".nested-resource-fields");
console.log(this.containerTarget, resources);
content = content.replace(/__COUNTER__/g, resources.length + 1); // the counter is purely cosmetic
this.containerTarget.insertAdjacentHTML("beforeend", content);
}
removeResource(event) {
event.preventDefault();
const fieldset = event.target.closest(".nested-resource-fields");
fieldset.remove();
}
deleteResource(event) {
event.preventDefault();
const fieldset = event.target.closest(".nested-resource-fields");
fieldset.querySelector('input.destroy-resource').value = "1";
fieldset.classList.remove("nested-resource-fields");
fieldset.classList.add('hidden');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment