You're viewing Avo 3 docs — go to Avo 4

Skip to content

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 Reviews. I know, it's not the Slider has_many Items 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 ​

ruby
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.

js
// 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.

ruby
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.

erb
<!-- 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.

erb
<!-- 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.

ruby
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.