Search code examples
ruby-on-railsroutessimple-form

Use the same form on the same page for new and edit actions (simple_form & Rails)


I'm learning Rails and would like to be able to use the same form on the same page to add an element (here a "dose") or edit an existing one by prefilling the form (after a page refresh, I don't need to do it dynamically).

Here is a screenshot of the page (views/cocktails/show.html.erb)

Creating a new "dose" works fine, but when trying to edit Rails raises the "DosesController#edit is missing a template for this request format and variant" error.

As I'm trying to really understand the routes logic, I'm doing this just for the sake of learning, even if this is probably a bad practice, and I'd like to achieve this with Rails only.

views/doses/_form.html.erb:

I'm putting the logic into the form partial to decide whether to create or update the @dose:

<% if @dose.new_record? %>
  <%= simple_form_for [@cocktail, @dose] do |f| %>
    <%= f.input :description %>
    <%= f.association :ingredient %>
    <%= f.button :submit %>
  <% end %>
<% else %>
  <%= simple_form_for @dose, url: url_for("doses#update") do |f| %>
    <%= f.input :description %>
    <%= f.association :ingredient %>
    <%= f.button :submit %>
  <% end %>
<% end %>

views/cocktails/show.html.erb:

There I'm calling a "new or edit" path helper, passing it the cocktail id and dose id, as dose belongs to one cocktail.

  <% @cocktail.doses.each do |dose| %>
    <li>
    <h4><%= dose.description %> - <%= dose.ingredient.name %></h4>
    <%= link_to "<i class=\"fas fa-pencil-alt\"></i>".html_safe, new_or_edit_dose_path(@cocktail.id, dose.id) %>
    <%= link_to "<i class=\"far fa-trash-alt\"></i>".html_safe, dose_path(dose.id), method: :delete, data: {confirm: "Are you sure?"} %>
    </li>
  <% end %>
  </ul>
  <%= render "doses/form" %>
  <%= link_to "Back", cocktails_path %>
</div>

routes.rb

This path helper is bound to a GET route, associated with the edit action of the doses controller. I would therefore not be using the doses#new route action at all.

  root to: "cocktails#index"
  resources :cocktails, only: [:index, :show, :new, :create] do
    resources :doses, only: [ :create]
  end
  resources :doses, only: [ :update, :destroy]
  get "cocktails/:id/doses/:dose_id", to: "doses#edit", as: :new_or_edit_dose

doses_controller.rb

  def edit
    @cocktail = Cocktail.find(params[:id])
    @dose = Dose.find(params[:dose_id])
  end

  def update
    if @dose.update(set_params)
      redirect_to cocktail_path(@dose.cocktail)
    else
      render(:edit)
    end
  end

  def set_params
    params.require(:dose).permit(:description, :ingredient_id)
  end

My comprehension of the problem

I guess the problem is that I'm using a cocktails/:id/doses/:id route, but displaying the form on a cocktails/:id/show view. I've tried to use a redirect_to at the end of doses#edit and passing it the params, but it didn't worked.

Could someone tell me if it's feasible, and give me a hint on what I should be looking for?


Solution

  • Solution:

    Regarding

    ... but when trying to edit Rails raises the DosesController#edit is missing a template for this request format and variant

    ... means that you are missing the file app/views/doses/edit.html.erb. So just create that file, or... because you said...

    I guess the problem is that I'm using a cocktails/:id/doses/:id route, but displaying the form on a cocktails/:id/show view

    ... which if understood it correctly, you want this url cocktails/:id/doses/:id to show a page exactly as your cocktails/:id/ (which presumably mapped to your cocktails#show page, then you can just do the following:

    app/controllers/doses_controller.rb:

    # ...
    def edit
      @cocktail = Cocktail.find(params[:id])
      @dose = Dose.find(params[:dose_id])
    
      render 'cocktails/show'
    
      # rails by default has an invisible line at the end of the actions, unless `render` is called manually like line above. But removing the line above, you'll get something like below
      # render 'CONTROLLER_NAME/ACTION_NAME.FORMAT.TEMPLATE_ENGINE'
      # so in this specific case:
      #   CONTROLLER_NAME == doses
      #   ACTION_NAME == edit
      #   FORMAT == html
      #   TEMPLATE_ENGINE == erb
      # which translates to:
      #   render 'doses/edit.html.erb'
      # which you can also do like the following
      #   render 'edit.html.erb' # because same controller name as the template being called
      # which you can also do like the following
      #   render 'edit' # because it automatically identifies what format using the value of `request.format`, and what default template engine your Rails app is configured to use
      # ... and this is the reason why you got that error "DosesController#edit is missing a template...", because app/views/doses/edit.html.erb does not exist, which by default is being rendered
    

    Recommendation:

    • Please clarify first why you want the following route, and I'll see if I have a better solution, in my opinion:

      get "cocktails/:id/doses/:dose_id", to: "doses#edit", as: :new_or_edit_dose
      

    ... I asked this because normally a "create or update" / "new or edit" request is routed against some unique attribute identifier. For example, in your case, the unique attribute identifier seems to be :dose_id, because you "create" a new Dose record if the :dose_id does not yet exist, or you "update" the Dose record if it already exists which has the id value equal to :dose_id. Now, this is my opinion but I do not think it is a good idea to create a Dose record from a manually-set "id", because you normally let the database auto-assign the primary key id values.

    ...To explain my point, for example, to simulate the said route above, say I open this URL in the browser: "/cocktails/1/doses/1", so far so good assuming that Dose(id: 1) already exists, then I will just see on the page a form to UPDATE the Dose. However, let's say I open "/cocktails/1/doses/9999999999", and let's say that Dose(id: 9999999999) does not exist yet, then I will see a form to CREATE the Dose... and this is still valid at this point. The problem occurs when you already deleted the Dose record. So let's say, Dose(id: 1) has already been deleted, and then I tried opening "/cocktails/1/doses/1" again, because it's already been deleted, then I will see a form to CREATE the Dose (which is still ok), but becomes problematic because you are reusing the id values which should be a "primary key" and should uniquely identify a resource, because your associations / models that depend on this unique id value would potentially be messed up: like for example I have a Car record of attributes {name: 'Porsche', category_id: 23} that belongs_to a Category(23) record that has an attribute {name: 'vehicle'}, and then somehow out of the blue Category(23) has been destroyed, and then recreated with the same ID! but this time the attributes created let's say has been become {name: 'fruit'}. Now, my Porsche Car becomes a Fruit!!

    However, I can think of some reasons why you'd intentionally do this (i.e. if you want to treat the id value as uuid which can potentially never be reused programatically), but my thoughts above just in case you are not aware of yet with these possible consequences.