Rendering Forms with Rails 6 (RED)

After testing my controllers on my previous post, I encountered I wanted to add an edit button to my views and allow the chance to edit a previous entry in case there was an error. So I began finding more and more questions and how can this be implemented. First of all, I began asking my self. Do I really know the difference between Update and Edit in my controller? Answer: No.

I wrote this test which passed as green:

describe '#edit' do
    context 'authenticated' do
      let(:user) { FactoryBot.create(:user, :admin) }
      let(:item) { FactoryBot.create(:meetings_item) }

      it 'edits an item' do
        sign_in(user)
        item_params = FactoryBot.attributes_for(:meetings_item, reason: 'test reason')
        patch(:update, params: { meetings_list_id: item.meetings_list.id,
                                 id: item.id,
                                 meetings_item: item_params })
        expect(item.reload.reason).to eq('test reason')
        expect(flash[:notice]).to match(/Item erfolgreich aktualisiert/)
      end
    end
  end

which passed with Green. And this line was the one who began making some noise: patch(:update, params: { ... I began my test with the word Edit, but this is Updating it. Which lead me to the question. Isn't this not the same? In this context I want to edit an item, inside of a list. Whenever a person creates an item this data can be wrong, for example, the person enters "22.00 €" but after proofing this was actually "22.50 €". Before I even wrote this test, the only way to do this was Deleting the item, and create a New one with the right amount. So this was not useful at all. The question here is, by doing this "change" am I editing the existing item? or am updating it? (or both?!).

I got confused and I said, well, why not, go for it and make both. So this is my controller:


class MeetingsItemsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_meeting_list
  before_action :set_meetings_item, except: %i[create]

  def create
    @meetings_item = @meetings_list.meetings_items.create(meeting_item_params)
    if @meetings_item.valid?
      @meetings_item.save
      redirect_to @meetings_list, notice: 'Neuer Eintrag hinzugefügt'
    else
      render :new
    end
  end

  def new
    @meetings_item = MeetingsItem.new
  end

  def destroy
    # puts @meetings_item
    authorize @meetings_item
    # flash[:notice] = ('Item erfolgreich vernichtet.' if @meetings_item.destroy)
    # redirect_to @meetings_list
    @meetings_item.destroy
    respond_to do |format|
      format.html do
        redirect_to @meetings_list, notice: 'Item erfolgreich vernichtet.'
      end
      format.json { head :no_content }
    end
  end

  def update
    respond_to do |format|
      if @meetings_item.update(meeting_item_params)
        format.html do
          redirect_to @meetings_list,
                      notice: 'Item erfolgreich aktualisiert'
        end
        format.json { render :show, status: :ok, location: @meetings_list }
      else
        format.html { render :edit }
        format.json { render json: @meetings_list.errors, status: :unprocessable_entity }
      end
    end
  end

  def edit; end

  def complete
    @meetings_item.update_attribute(:completed_at, Time.now)
    redirect_to @meetings_list, notice: 'Aufgabe erledigt'
  end

  private

  def set_meeting_list
    # puts params[:meetings_list_id]
    @meetings_list = MeetingsList.find(params[:meetings_list_id])
  end

  def set_meetings_item
    # puts params[:id]
    @meetings_item = @meetings_list.meetings_items.find_by(id: params[:id])
  end

  def meeting_item_params
    params[:meetings_item].permit(:date, :reason, :amount)
  end
end

so I created 2 new methods, the #edit, which only consist in one line and the #update which has more "juice" in it. Then I created an edit view page, in which I am expecting to appear the selected meeting item with the previews entered data with a submit button which leads me to update the item. Reads easy right? NOPE. Hold your horses right here, more questions ahead.

Inside of my meetings_items folder inside of views I created the edit.html.slim file.

h1.meetings_list_title
  | Neuer Eintrag
#meetings_items_wrapper
  #form
    = render 'form'
  .links
    = link_to 'Cancel', @meetings_list

the form:

- provide(:title, "#{@meetings_list.title}")
= form_for [@meetings_list, @meetings_list.meetings_items.build] do |f|
  = render 'eintrag_errors'
  .date_wrapper
    = f.label :date, "Datum:"
    = f.text_field :date, data: {controller: "flatpickr", 
                                 flatpickr_alt_format: t("date.formats.long"),
                                 flatpickr_alt_input: true,
                                 },
                                 placeholder: "Wähle das Datum aus"
  .currency-input
    = f.label :amount, "Betrag:"
    = f.text_field :amount, class: "currency-input-mask"
  .reason-input
    = f.label :reason, "Grund:"
    = f.text_field :reason, placeholder: "Gebe bitte den Verwendungszweck an"
  .actions
    = f.submit value: "Neuer Eintrag"

And this was added on the view from my item, the icon and the path:

.edit
      = link_to edit_meetings_list_meetings_item_path(@meetings_list, meetings_item.id)
        i.fa.fa-edit

which I thought will lead me to edit the selected item from the current list, but it lead me to the form to create a new item. The form was not filled with the item and just rendered a "new item" entry form. Which this is not what I'm looking for. So I began having questions one by one and was not able to go today further.

  1. In the edit view from the item, in this line, is it necessary to specify the parameters of the list and the item in order to be rendered? = render 'form' (select the list, then the item.id in order to retrieve the data)

  2. While researching about forms I found a lot of articles regarding to form_with and form_for, and many are pointing that as a best practice is today better to use form_with to create a form, but I have no clue why? and if this is useful here. For learning practices I would like to implement it but I would also like to know which benefits does it bring?

= form_for [@meetings_list, @meetings_list.meetings_items.build] do |f|
# with form with:
= form_with(model: @meetings_item, local: true) do |f|

# this last one even throws me an error: 
#undefined method `meetings_item_path' for #<#<Class:0x00007f5f14084010>:0x00007f5eec4d9cc8>
#Did you mean?  meetings_list_path
#                           meetings_lists_path
  1. Last, I took a look at my routes and was wondering if the member block has something to do with this method not working (edit, update) as desired.
Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
  devise_scope :user do
    get '/users/sign_out' => 'devise/sessions#destroy'
    get '/users/edit' => 'devise/sessions#edit'
  end
  get 'static_pages/help'
  get 'static_pages/contact'
  resources :users
  resources :meetings_lists do
    resources :meetings_items do
      member do
        patch :complete
      end
    end
  end
  root 'static_pages#home'
end

No Comments Yet