I have an object that uses the ActiveModel::Model concepts:
class TransitProvider
include ActiveModel::Model
# ... more stuff
Underneath the covers, this object is an aggregate for a Provider
record and a Service
record.
Everything seems to be working very well but the form_with
helper doesn't recognize a TransitProvider
instance as persisted (because it doesn't have it's own ID) and thus the edit
action shows the form with data but submits it as a create
instead of an update
.
Is there a way to add to an ActiveModel class something so that form_with
will treat it as an existing instance instead of a new instance?
Do I need to define something around id
or persisted?
or something like that?
I can't seem to find anything specific to this use case.
Thanks!
CodePudding user response:
Override persisted?
method. It is defined in ActiveModel::API
:
def persisted?
false
end
This method is used by the form builder to decide if it needs to send a post or patch request.
# app/models/transit_provider.rb
class TransitProvider
include ActiveModel::Model
attr_accessor :provider, :service
# NOTE: This is set to `false` by default. See `ActiveModel::API`.
# TODO: Decide what it means for `TransitProvider`
# to be persisted. Could `provider` be persisted while
# `service` is not?
def persisted?
provider.persisted? && service.persisted?
end
# NOTE: `id` would be required for the update route
# for plural `resources`.
# Don't need it for a singular `resource`. See routes.rb.
# def id
# 1
# end
end
# config/routes.rb
Rails.application.routes.draw do
resource :transit_provider, only: [:create, :update]
# ^
# NOTE: Singular. Don't need `id` in routes, we're not asking
# for any data from this controller.
# NOTE: Make url mapping always resolve to singular route.
#
# `transit_provider_path`
#
# Otherwise, in the form url would resolve to undefined
# plural `transit_providers_path` for `create` action.
resolve("TransitProvider") { [:transit_provider] }
# NOTE: Change it if you need `id`. Also add `id` method to
# `TransitProvider`
# resources :transit_providers
end
# app/controllers/transit_providers_controller.rb
class TransitProvidersController < ApplicationController
def create
# TODO: create
end
def update
# TODO: update
end
end
# NOTE: persisted
<% model = TransitProvider.new(
provider: Provider.first,
service: Service.first)
%>
# NOTE: not persisted
# model = TransitProvider.new(provider: Provider.new, service: Service.new)
<%= form_with model: model do |f| %>
<%= f.fields_for :provider, model.provider do |ff| %>
<%= ff.text_field :id if ff.object.persisted? %>
<%= ff.text_field :name %>
<% end %>
<%= f.fields_for :service, model.service do |ff| %>
<%= ff.text_field :id if ff.object.persisted? %>
<%= ff.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>
For persisted TransitProvider form does a PATCH request to update.
Started PATCH "/transit_provider" for 127.0.0.1 at 2022-07-03 15:47:57 -0400
Processing by TransitProvidersController#update as TURBO_STREAM
Parameters: {"authenticity_token"=>"[FILTERED]", "transit_provider"=>{"provider"=>{"id"=>"1", "name"=>"provide"}, "service"=>{"id"=>"1", "name"=>"service"}}, "commit"=>"Update Transit provider"}
Otherwise it is a POST to create.
Started POST "/transit_provider" for 127.0.0.1 at 2022-07-03 16:13:43 -0400
Processing by TransitProvidersController#create as TURBO_STREAM
Parameters: {"authenticity_token"=>"[FILTERED]", "transit_provider"=>{"provider"=>{"name"=>""}, "service"=>{"name"=>""}}, "commit"=>"Create Transit provider"}
Update what is persisted?
ActiveModel gets its persisted? method from ActiveModel::API it is unrelated to ActiveRecord's persisted? method. Neither take id
attribute into account to decide if the record is persisted:
# ActiveModel's persisted? is just `false`
# ActiveRecord
Service.create(name: "one") # => #<Service: id: 1, name: "one">
Service.new.persisted? # => false
Service.first.persisted? # => true
Service.new(id: 1).persisted? # => false
Service.new(id: 1).reload.persisted? # => true
s = Service.select(:name).first
s.id # => nil
s.persisted? # => true
s = Service.first.destroy
s.id # => 1
s.persisted? # => false
This is important because form builder uses this method to choose between POST and PATCH method and url_for helper uses it to build polymorphic routes.
url_for(Service.first) # => "/services/1"
url_for(Service.new(id: 1)) # => "/services"
url_for(Service.new) # => "/services"
# NOTE: it is different from named route helpers,
# which will grab required `params` from anything
# argument, hash, model, or url params.
service_path(Service.first) # => "/services/1"
service_path(Service.new(id: 1)) # => "/services/1"
service_path({id: 1}) # => "/services/1"
service_path(1) # => "/services/1"
# and if `params` have id: 1 (as in show action)
service_path # => "/services/1"
Note that, by default, ActiveModel::API implements persisted? to return false, which is the most common case. You may want to override it in your class to simulate a different scenario.
https://api.rubyonrails.org/classes/ActiveModel/API.html#method-i-persisted-3F
https://api.rubyonrails.org/classes/ActiveModel/Model.html
https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Resources.html#method-i-resource
https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/CustomUrls.html#method-i-resolve