1

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!

3
  • 1
    How do you know what record to update when there is no id? Do you use the ids of the underlying provider and service records? Why do not use an aggregate of those ids as an id for the transit provider? Commented Jul 3, 2022 at 13:10
  • 1
    may be adding a method option (post or put) to the form_with can solve problem. if you are rendering this form from new and edit html.erb files then u can pass the method parameter when rendering the form. Commented Jul 3, 2022 at 17:51
  • Turns out Alex had it right in the answer. I just needed to define persisted? and I was good to go. I defined that as: def persisted?; id.present? end Commented Jul 7, 2022 at 1:20

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

3 Comments

Alex, thank you! This is exactly what I was looking for. I was able to get this working by defining persisted? and then getting the create and update methods working separately. It's still odd because I have an object that presents an id but the id is really the id of one of the constituent objects. However, it does work and gets the job done. Thank you!
@DanSharp see the update. hope it helps.
Thanks Alex! Yeah, the ID is a delegated ID. Basically if the ActiveModel is populated via a persisted Provider and Service, it should act as if it's a persisted record. So checking to see if the delegated Provider is persisted or not and having that be the answer to whether the TransitProvider is persisted seems to work just fine.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.