@trx = Trx.new # new every time, so nothing changes.
When the form is submitted you get params
from it. You have to use these params to recreate the object that the form builder accepts when it renders the form again.
def new
# NOTE: Initialize new `Trx` with submitted `line_items` params
# you can use `accepts_nested_attributes_for` for nested
# line items and `fields_for` in the form.
@trx = Trx.new(trx_params)
@line_item = @trx.line_items.build
# You can keep using line_count, but it is not needed
# @line_count = params.dig(:add_lines, :line_count) || 1
# NOTE: @trx has `line_items` initialized and we've just added
# a new item. At least one `line_item` is always here.
@line_count = @trx.line_items.size
end
private
def trx_params
# NOTE: Use fetch and fallback to "{}" if :trx param is missing
# when `new` is rendered for the first time.
# Adjust `permit` to fit your params.
params.fetch(:trx, {}).permit!
end
Update
You can replace params.require(:trx)
with params.fetch(:trx, {})
. require
will raise an error if :trx
is missing. fetch
will fallback to {}
, which is what we need. Also, fetch
is the default rails scaffold and if something isn't working, you can see parameters in the log.
I'm not sure which part got you confused, because you explained exactly how the request happens.
Input name
and value
is how parameters are handled in the form.
<input name="trx[line_items_attributes][0][id]" value="">
When the form is submitted, name
and value
get converted using x-www-form-urlencoded
encoding.
trx%5Bline_items_attributes%5D%5B0%5D%5Bid%5D=
If form method is POST, data is sent in the request body.
POST /trxes/new
body: trx%5Bline_items_attributes%5D%5B0%5D%5Bid%5D=
# to see post body in rails controller `request.raw_post`
If form method is GET, data is sent as a url query.
GET /trxes/new?trx%5Bline_items_attributes%5D%5B0%5D%5Bid%5D=
# GET requests can have body as well, but it doesn't apply here, and it's rarely used.
ActionDispatch will process incoming request, parsing GET and POST data into parameters. Router will then match url path (/trxes/new, /trxes/1) and request method (GET/POST) with one of your routes. Which will break it down into :action
, :controller
, and parameters defined in the route pattern (:id). Then controller action is processed and along the line TrxesController#:action is called for which you're responsible. Parameters gathered along the way are available in params
.
trx_params
returns a hash kind of an object which is ActionController::Parameters that has permitted parameters only.
trx_params # => {"line_items_attributes"=>{"0"=>{"id"=>""}}}
When you pass it to Trx.new
, this hash is broken down and is passed to individual setter methods:
Trx.new({"line_items_attributes"=>{"0"=>{"id"=>""}}}) # this is the same..
trx = Trx.new
trx.line_items_attributes = {"0"=>{"id"=>""}} # ..as this
trx.line_items_attributes = [{"id"=>""}] # or this
trx.line_items << [LineItem.new(id: "")] # or this
trx
Now you have a Trx object with one line item. When you call line_items.build
, another LineItem is added. In the form, these 2 line items are broken down into inputs. If you click add item again, then 2 line items are submitted as parameters, Trx is initialized again with 2 line items, calling build
again adds another line item on top. Repeat forever, until url limit is reached, which is not that big.
Here is what I've come up with:
post "/trxes(/:id)/build/:association", to: "trxes#build", as: :build_trx
resources :trxes do
post :new, on: :new # add POST /trxes/new
end
<%= form_with(model: @trx, class: "contents") do |f| %>
<%= f.fields_for :line_items do |ff| %>
<%= ff.text_field :id, placeholder: "ID" %>
<% end %>
<!-- doesn't work for edit -->
<%= f.submit "Get new item", formmethod: :get, formaction: new_trx_path(id: f.object) %>
<!-- because `:id` is dropped and replaced by form params -->
<!-- a hidden input would be a solution to send :id from edit page -->
<!-- works for new/edit -->
<%= f.submit "Post new item", formmethod: :post, formaction: new_trx_path(id: f.object) %>
<!-- works for new/edit and any other association -->
<%= f.submit "Build item", formmethod: :post, formaction: build_trx_path(f.object, :line_items) %>
<%= f.submit %>
<% end %>
class TrxesController < ApplicationController
# GET/POST /trxes/new
def new
@trx = Trx.new(trx_params) # or trx(trx_params) to make edit work.
@trx.line_items.build
render :new, status: :unprocessable_entity if request.post?
# NOTE: you don't need `:unprocessable_entity` if you're not
# using `Turbo`, which makes above `render` call also
# unnecessary.
end
# POST /trxes(/:id)/build/:association
def build
trx(trx_params)
build_permitted_association params[:association]
action = trx.persisted? ? :edit : :new
render action, status: :unprocessable_entity
end
###
def index; @trxes = Trx.all end
def show; trx end
def edit; trx end
# POST /trxes
def create
if trx(trx_params).save
redirect_to trx_url(trx), notice: "Saved."
else
render :new, status: :unprocessable_entity
end
end
# PATCH/PUT /trxes/:id
# `update` became literally the same as `create` here.
alias_method :update, :create
# DELETE /trxes/:id
def destroy
trx.destroy
redirect_to trxes_url, notice: "Deleted."
end
private
def build_permitted_association association
# NOTE: Specify associations that can be built.
permitted_associations = %w[line_items]
# NOTE: Alternatively, use reflections to look up existing associations.
#
# permitted_associations = Trx.reflect_on_all_associations(:has_many).map(&:name)
# # => ["line_items"]
# NOTE: Guard against sending something else, like, "destroy".
unless permitted_associations.include?(association)
logger.debug { "\e[31mSkipping unpermitted association:\e[0m @trx.#{association} in #{self.class}##{action_name}. Permitted: #{permitted_associations}" }
return
end
# NOTE: Now if you add another :has_many association to `Trx`, it can
# be built by updating `permitted_associations` or without touching
# this method at all when using reflections.
trx.public_send(association).build
end
# NOTE: This is just a personal preference. Instead of having
# `before_action`, `set_trx`, and using `@trx`, just call `trx`
# in any action.
#
# trx
# trx(trx_params)
#
def trx attributes = {}
@trx ||= begin
model = params[:id] ? Trx.find(params[:id]) : Trx.new
model.attributes = attributes
model
end
end
def trx_params
params.fetch(:trx, {}).permit!
end
end
class Trx < ApplicationRecord
has_many :line_items, dependent: :destroy
accepts_nested_attributes_for :line_items
end
class LineItem < ApplicationRecord
belongs_to :trx
end
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for
https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for