Nested records when creating
A lot of you asked for the ability to create nested has_many
records on the New
view. Although it's fairly "easy" to implement using accepts_nested_attributes_for
for simple cases, it's a different story to extract it, make it available, and cover most edge cases for everyone. That's why Avo and no other similar gems dont't offer this feature as a first-party feature. But, that doesn't mean that it's impossible to implement it yourself. It's actually similar to how you'd implement it for your own app.
We prepared this scenario where a Fish
model has_many
Review
s. I know, it's not the Slider
has_many
Item
s example, but you'll get the point.
Full set of changes
The full code is available in Avo's dummy app and the changes in this PR.
Guide to add it to your app
You can add this functionality using these steps.
1. Add accepts_nested_attributes_for
on your parent model
class Fish < ApplicationRecord
has_many :reviews, as: :reviewable
accepts_nested_attributes_for :reviews
end
WARNING
Ensure you have the has_many
association on the parent model.
2. Add a JS helper package that dynamically adds more review forms
yarn add stimulus-rails-nested-form
In your JS file register the controller.
// Probably app/javascript/avo.custom.js
import { Application } from '@hotwired/stimulus'
import NestedForm from 'stimulus-rails-nested-form'
const application = Application.start()
application.register('nested-form', NestedForm)
INFO
Use this guide to add custom JavaScript to your Avo app.
3. Generate a new resource tool
bin/rails generate avo:resource_tool nested_fish_reviews
This will generate two files. The NestedFishReviews
ruby file you'll register on the Avo::Resources::Fish
file and we'll edit the template to contain our fields.
4. Register the tool on the resource
We'll display it only on the New
view.
class Avo::Resources::Fish < Avo::BaseResource
# other fields actions, filters and more
def fields
field :reviews, as: :has_many
tool Avo::ResourceTools::NestedFishReviews, only_on: :new
end
end
5. Create a partial for one new review
This partial will have the fields for one new review which we'll add more on the page.
<!-- app/views/avo/partials/_fish_review.html.erb -->
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_body do %>
<div class="nested-form-wrapper divide-y" data-new-record="<%= f.object.new_record? %>">
<%= avo_edit_field :body, as: :trix, form: f, help: "What should the review say", required: true %>
<%= avo_edit_field :user, as: :belongs_to, form: f, help: "Who created the review", required: true %>
</div>
<% end %>
<% end %>
6. Update the resource tool partial
It's time to put it all together. In the resource tool partial we're wrapping the whole thing with the nested-form
controller div, creating a new form
helper to reference the nested fields with form.fields_for
and wrapping the "new" template so we can use replicate it using the nested-form
package. In the footer we'll also add the button that will add new reviews on the page.
<!-- app/views/avo/resource_tools/_nested_fish_reviews.html.erb -->
<div class="flex flex-col">
<%= content_tag :div,data: { controller: 'nested-form', nested_form_wrapper_selector_value: '.nested-form-wrapper' } do %>
<%= render Avo::PanelComponent.new(name: "Reviews", description: "Create some reviews for this fish") do |c| %>
<% c.with_bare_content do %>
<% if form.present? %>
<template data-nested-form-target="template">
<%= form.fields_for :reviews, Review.new, multiple: true, child_index: 'NEW_RECORD' do |todo_fields| %>
<%= render "avo/partials/fish_review", f: todo_fields %>
<% end %>
</template>
<div class="space-y-4">
<%= form.fields_for :reviews, Review.new, multiple: true do |todo_fields| %>
<%= render "avo/partials/fish_review", f: todo_fields %>
<% end %>
<div data-nested-form-target="target"></div>
</div>
<% end %>
<% end %>
<% c.with_footer_tools do %>
<div class="mt-4">
<%= a_link 'javascript:void(0);', icon: 'plus', color: :primary, style: :outline, data: {action: "click->nested-form#add"} do %>
Add another review
<% end %>
</div>
<% end %>
<% end %>
<% end %>
</div>
7. Permit the new nested params
There's one more step we need to do and that's to whitelist the new reviews_attributes
params to be passed to the model.
class Avo::Resources::Fish < Avo::BaseResource
self.extra_params = [reviews_attributes: [:body, :user_id]]
# other fields actions, filters and more
def fields
field :reviews, as: :has_many
tool Avo::ResourceTools::NestedFishReviews, only_on: :new
end
end
Conclusion
There you have it!
Apart from the resource tool and the extra_params
attribute, we wrote regular Rails code that we would have to write to get this functionality in our app.