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