Search code examples
ruby-on-railsrubyformspartials

How to DRY a Rails 6 partial form


This works but I want to DRY up my form code.

/app/views/jobs/edit.html.erb:

<h1>Edit Job</h1>
<%= form_with model: @job, url: user_job_path(current_user.id), method: "patch", local: true do |form| %>
    <!-- contents of form -->
<% end %>

/app/views/jobs/new.html.erb:

<h1>Add Job</h1>
<%= form_with scope: :job, url: user_jobs_path(current_user.id), local: true do |form| %>
  <!-- contents of form -->
<% end %>

/app/controllers/jobs_controller.rb:

class JobsController < ApplicationController
  def new
    @job = Job.new
  end

  def edit
    @job = Job.find(params[:id])
  end

  def create
    @job = Job.new(job_params)
    @job.user_id = current_user.id

    if @job.save
      redirect_to user_jobs_path(current_user.id)
    else
      render 'new'
    end
  end

  def update
    @job = Job.find(params[:id])
    @job.user_id = current_user.id

    if @job.update(job_params)
      redirect_to user_jobs_path(current_user.id)
    else
      render 'edit'
    end
  end
  ...
end

/config/routes.rb:

Rails.application.routes.draw do
  resources :users do
    resources :jobs
  end
  ...
end

I tried changing the edit and new views to pass the URL.

new.html.erb:

<%= render partial: "form", locals: {job: @job, url: user_jobs_path(current_user.id)} %>

edit.html.erb:

<%= render partial: "form", locals: {job: @job, url: user_jobs_path(current_user.id, job.id)} %>

_form.html.erb:

<%= form_for(job) url: url do |form| %>
  <!-- contents of form -->
<% end %>

But it causes a syntax error:

Encountered a syntax error while rendering template: check <%= form_for(job) url: url do |form| %>  

In _form.html.erb, if I simply include the form using <%= render 'form' %> with:

<%= form_with scope: :job, url: user_job_path(current_user.id), method: "patch", local: true do |form| %>
...

editing works but new results in this error:

No route matches {:action=>"show", :controller=>"jobs", :user_id=>1}, missing required keys: [:id]

When I try <%= form_for(@user_job) do |form| %> I get:

"First argument in form cannot contain nil or be empty" 

for new.

When I try <%= form_with model: @user_job, local: true do |form| %> I get:

No route matches [POST] "/users/1/jobs/new"

which seems wrong because rails routes shows:

new_user_job GET    /users/:user_id/jobs/new(.:format)                                                       jobs#new

etc. I searched and most of the advice from several years ago is to pass a URL. I read the Rails documentation for forms, partials, and the blog tutorial. I suspect I'm making a really simple mistake.

<%= form_with scope: :job, url: user_job_path(current_user.id), method: "patch", local: true do |form| %> 

Solution

  • To create a form that routes to a nested route you pass an array:

    # app/views/jobs/_form.html.erb
    <%= form_with(model: [user, job]) %>
      # ...
    <% end %>
    
    # app/views/jobs/new.html.erb
    <%= render 'form' user: current_user, job: @job %>
    
    # app/views/jobs/edit.html.erb
    <%= render 'form' user: current_user, job: @job %>
    

    But its kind of questionable why you nested the route in the first place as you're not using it as a nested resource at all. GET /users/:user_id/jobs should really show only the jobs belonging to that user and POST /users/:user_id/jobs should create a job belonging to that user and not the current user.