Home > Mobile >  Using form_with with an ActiveModel object?
Using form_with with an ActiveModel object?

Time:07-07

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

  • Related