Question

Rails: How does form_with know, which Controller-action to invoke?

Taken this form as an example:

<%= form_with model: @product do |f| %>
   <!-- ... --!>
<% end %>

The routes-file:

get "/products", to: "products#index"
get "/products/new", to: "products#new"
post "/products", to: "products#create"
get "/products/:id", to: "products#show", as: "product"

How does it know that it has to invoke it create-action of the products-controller?

What is the decision based upon?

 3  72  3
1 Jan 1970

Solution

 5

form_with guesses the route from the model. If the model is an instance of Product and it is a new_record? then is will POST /products. When it is a persisted? record, then it will PATCH /products/{:id} (see Form Helpers in the official Rails Guides).

This is in line with Ruby on Rails conventions and the default way for defining routes with rescources :products (see Routing). Which would generate the following routes:

GET       /products           => ProductsController#index 
GET       /products/new       => ProductsController#new 
POST      /products           => ProductsController#create 
GET       /products/:id       => ProductsController#show 
GET       /products/:id/edit  => ProductsController#edit 
PATCH/PUT /products/:id       => ProductsController#update 
DELETE    /products/:id       => ProductsController#destroy 

Note when you do not use the resources helper to generate routes, when from_with only works when you define the same routes that Rails would do. Otherwise, we will need to pass an additional url: to the form helper.

I suggest reading about FormHelper#form_with in the Rails' API docs and taking a look at this method's implementation.

2024-07-13
spickermann

Solution

 4

It doesn't.

form_with just uses the polymorphic routing helpers to find a routing helper method based on convention over configuration and calls it dynamically to get the path for the forms action attribute.

The routing helper methods are generated by calling the routing macros in your config/routes.rb file and available in the view and controllers. When you call resources :products to declare the conventional routes it will define the helper methods products_path and product_path (and many more).

This has nothing to do with the controller and you can actually create the form just fine with just a model and the expected helper methods.

The destination controller only actually matters after the user submits the form and the Rails router attempts to match the incoming HTTP request to a controller and action combo.

ActiveModel::Naming

The polymorphic routing helpers can guess what the helpers are named through the ActiveModel::Naming API. This just assumes that routes, tables etc can be derived from the name of the model class. This is a huge part of the "magic" in Rails which lets your models interact with the rest of the framework.

irb(main):006:0> class Product; include ActiveModel::Naming; end
=> Product
irb(main):007:0> Product.model_name
=>
#<ActiveModel::Name:0x00005570c2876c60
 @collection="products",
 @element="product",
 @human="Product",
 @i18n_key=:product,
 @klass=Product,
 @name="Product",
 @param_key="product",
 @plural="products",
 @route_key="products",
 @singular="product",
 @singular_route_key="product",
 @uncountable=false>     

New vs old records

polymorphic_url first figures out if its generating the collection path or member path by calling new_record? on the model.

If the record is a new record or the class itself it will call model_name.route_key and ends up calling the collection path helper products_path.

If the model is persisted it calls model_name.singular_route_key and passes model.to_param as the argument resulting in a call to the the member path helper product_path(product.to_param). to_param defaults to using product.id as the identifier for the record.

form_with also uses persisted? to determine if it should use the POST or PATCH HTTP method for creating or updating.

2024-07-14
max