Nested Forms With Turbo (without dependencies)

railsdesigner

Rails Designer

Posted on August 15, 2024

Nested Forms With Turbo (without dependencies)

This article was originally published on Rails Designer


Nested forms is a common concept in most (Rails) SaaS apps. Examples include a survey that accepts multiple questions, recipe with many ingredients or an (eCommerce) product with many variants.

Out-of-the-box Rails has all the elements to support this kind of business logic through the accepts_nested_attributes_for β€œmacro” and the fields_for helper. But on the front-end you had to previously use third-party dependencies. But with Turbo, specifically with the introduction of supporting get requests, that's all you need today.

The end result will look something like this:

Image description
It looks crap, but it works! πŸ˜„

Let's go over the basic steps needed to create a Survey with multiple Questions. For this I assume you have a modern Rails app at hand.

Getting the Basics Done

Let's generate the two models and make sure all the logic for nested forms is in place.

  • Survey; rails generate model Survey name
  • Question; rails generate model Question survey:belongs_to content:text

Then tweak the Survey model:

class Survey < ApplicationRecord
  has_many :questions
  accepts_nested_attributes_for :questions
end
Enter fullscreen mode Exit fullscreen mode

Next, a simple controller: rails generate controller Surveys show new. Let's extend this created controller like so:

class SurveysController < ApplicationController
  def show
    @survey = Survey.find(params[:id])
  end

  def new
    @survey = Survey.new
  end

  def create
    @survey = Survey.new(survey_params)

    if @survey.save
      redirect_to @survey
    else
      render :new
    end
  end

  private

  def survey_params
    params.require(:survey).permit(:name, questions_attributes: [:content])
  end
end
Enter fullscreen mode Exit fullscreen mode

πŸ§‘β€πŸŽ¨πŸŽ¨ Need a way to up your Rails app's UI? Check out Rails Designer

The above nested survey_params is often the tricky bit to get right. Getting the passed attributes from the params hash is often an exercise in patience (just copy above and you're fine). I've personally wasted hours over the last 10 years on this. πŸ˜…

Let's update the app/views/surveys/new.html.erb view.

<h1>New Survey</h1>

<%= form_with model: @survey do |form| %>
  <div>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div id="questions"></div>

  <div>
    <%= button_tag "Add Question" %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>
Enter fullscreen mode Exit fullscreen mode

And something simple for the show-action (app/views/surveys/show.html.erb) so we can see our glorious work.

<h1><%= @survey.name %></h1>

<% @survey.questions.each do |question| %>
  <p>
    <%= question.content %>
  </p>
<% end %>
Enter fullscreen mode Exit fullscreen mode

And with adding resources :surveys, only: %w[show new create] to config/routes.rb, and removing the unneeded generated routes, all the basics are in place and you can navigate to localhost:3000/surveys/new and create your first Survey. But no nested question's yet…

Using Turbo Streams to Create Nested Forms

First the partial that holds the fields for the Question object (app/views/surveys/_question_fields.html.erb):

<div>
  <%= form.label :content, "Question" %>
  <%= form.text_area :content %>
</div>
Enter fullscreen mode Exit fullscreen mode

Simple enough.

Let's wire up the button with the Add Question value. Replace it like so:

<%= button_tag "Add Question", formaction: new_question_path, formmethod: :get, data: {turbo_stream: true} %>
Enter fullscreen mode Exit fullscreen mode

This uses two attributes that might be new to you: formaction and formmethod. Check out this article about forms inside forms for all the details. Next to that is data: {turbo_stream: true}. This will request a turbo_stream response that will be created in a minute.

The above new_question_path is not created. A simple controller and route will do:

  • rails generate controller Questions new;
  • add resources :questions, only: %w[new] to config/routes.rb.

The last thing needed is a turbo_stream response at app/views/questions/new.turbo_stream.erb.

<%= turbo_stream.append "questions" do %>
  <%= fields_for 'survey[questions_attributes][]', Question.new, index: Time.current.to_i do |form| %>
    <%= render "surveys/question_fields", form: form %>
  <% end %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

The only exciting part here is index: Time.current.to_i. This generates a unique identifier for each new set of nested form fields to prevent conflicts when adding multiple questions (otherwise it would only create one Question).

Now when you navigate to the surveys/new screen, you should be able to click Add Question and a new instance of surveys/question_fields.html.erb should appear! 🀯

And what's more mind-blowing. You can create as many questions as you want, and then click Create Survey and your Survey and all your questions are saved to the database. 🀯🀯

If you have ever used any third-party plugin (like Cocoon), you know how amazing it is to do this without any gems! ☺️

Of course this is missing a fair bit of functionality, like removing a question. But with the same technique using Questions#destroy you should be able to pull that off yourself fairly easily. ✌️


No time do it yourself? Rails Designer comes with a pre-built solution for nested forms with Turbo.

πŸ’– πŸ’ͺ πŸ™… 🚩
railsdesigner
Rails Designer

Posted on August 15, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related