Edit/Update Forms Updating Model Objects with Forms
In this demonstration, I will show how to make an “edit/update” form for editing model objects and saving the changes to the database. The “edit” part refers to a edit-resource page that displays a form for editing existing model objects that users can fill out and submit. When a user submits form data, the browser sends the data in a PATCH request to the web server. The “update” part refers to an update-resource action that processes those PATCH requests.
General Steps
In general, the steps for creating an edit/update form for an existing model class are as follows. The first several steps are concerned with creating the edit-resource page to display the form.
-
Step 1 Add an Edit-Resource Route. This step specifies the URL resource-paths for the edit-resource page and tells the web server which controller action to call to process requests for the edit-resource page.
-
Step 2 Add a Model Controller (if Needed). If a controller class for the model objects to be created does not already exist, this step creates one.
-
Step 3 Add an Edit-Resource Controller Action. This step adds an action to the controller that renders the HTML.ERB view template for the edit-resource page.
-
Step 4 Add an Edit-Resource View Template. This step adds an edit-resource view template that displays the form for editing model objects to the view.
-
Step 5 Add Link(s) to the Edit-Resource Page (if Needed). This step generates links that enable users to access the edit-resource page.
The remaining steps are concerned with making the update-resource action for processing form submissions.
-
Step 6 Add an Update-Resource Route. This step specifies that requests resulting from submissions of the edit-resource form will be PATCH requests, specifies the URL resource-paths for such requests, and tells the web server which controller action to call to process such requests.
-
Step 7 Add an Update-Resource Controller Action. This step adds an action to the controller that retrieves the form data sent in the request. The action uses those data to update an existing model object and saves that object to the database. If the update is successful, the action sends an HTTP redirect response to a specified page. If the update fails, the action re-renders the edit-resource page, so the user can correct the error.
Making an Edit/Update Form for Todo Model Objects
To demonstrate the steps for creating an edit/update form, we will be making an edit/update form for a model class representing items in a to-do list. Figure 1 illustrates how the edit-resource page will look.
For this demo, we will build upon a base app (base-app-todo-plus
branch) for managing a to-do list that contains a Todo
model class (depicted in Figure 2), which includes a couple attribute validations, corresponding model tests, a seeds script for populating the database with some sample Todo
objects, a TodosController
class, a root route, infrastructure for displaying flash notifications (as described in the flash notifications deets), and the routes, controller actions, and view templates for displaying an index page and a show page for Todo
objects.
In addition to the edit-resource page itself, we will add to the app’s index page a link to the edit-resource page for each record in the table, as illustrated in Figure 3.
After a user successfully submits the form, the browser will be redirected to the app’s index page, and a success notification will be displayed, as illustrated in Figure 4.
However, if the user submits invalid data, the form will be redisplayed, with an failure notification and error-message annotations on the invalid form fields, as illustrated in Figure 5.
Step 1 Add an Edit-Resource Route
To add a route for the edit-resource page, we add a route declaration to the config/routes.rb
file, like this:
Rails.application.routes.draw do
root to: redirect('/todos')
get 'todos', to: 'todos#index', as: 'todos'
get 'todos/:id/edit', to: 'todos#edit', as: 'edit_todo'
get 'todos/:id', to: 'todos#show', as: 'todo'
end
Note that this route matches HTTP GET requests with a resource path of todos/:id/edit
(e.g., as in the URL http://localhost:3000/todos/1/edit). As per the to:
argument, the controller action to process such requests is TodosController#edit
. As per the as:
argument, the path and URL helper methods for the edit-resource page will be named edit_todo_path
and edit_todo_url
, respectively.
Important! It does not matter where the edit-resource route declaration (get 'todos/:id/edit'…
) goes in the list of standard resource routes for the Todo model since its URL pattern cannot be confused with any of the other default resource routes. However, to keep things organized, convention dictates it goes before the show route.
Test It!
To confirm that we made this change correctly, we run the following command in the terminal:
rails routes -E
The command should output a long list of routes, and the third one should look like this:
--[ Route 3 ]-------------------------------------------------------------------------------------------------------------------
Prefix | edit_todo
Verb | GET
URI | /todos/:id/edit(.:format)
Controller#Action | todos#edit
If we made an error, something different will be outputted. For example, if we made a syntax error, it would instead produce an error message.
Step 2 Add a Model Controller (if Needed)
Because the base app already has a controller for Todo
model objects, TodosController
, we skip this step.
Step 3 Add a Edit-Resource Controller Action
To create the controller action for the edit-resource page, we perform two substeps: first, we create the controller action code for rendering the view, and then, we add the code for retrieving the model object that will be used by the view form code.
Substep
Create an edit-resource controller action that renders the appropriate view. To complete this substep, we define an edit
method in the TodosController
class (in app/controllers/todos_controller.rb
), like this:
class TodosController < ApplicationController
def index
@todos = Todo.order(:due_date)
render :index
end
def show
@todo = Todo.find(params[:id])
render :show
end
def edit
render :edit
end
end
Note that, so far, the edit
controller action only renders the view template app/views/todos/edit.html.erb
(as specified by the :edit
symbol).
Substep
Make the controller action retrieve the existing model object to be used by the view. To complete this substep, we add a call to the Todo.find
method to the edit
action of the TodosController
class, like this:
class TodosController < ApplicationController
…
def edit
@todo = Todo.find(params[:id])
render :edit
end
end
The find
model class method is used to retrieve a model object from the database by id
. The above call to Todo.find
returns a Todo
object (i.e., the one retrieved from the database), and the @todo
instance variable is assigned a reference to that object. The use of an instance variable (indicated by the @
in @todo
) is important here, because instance variables are accessible within view templates rendered by the class, thus allowing data to be passed from the controller to the view. The params
method is provided by Rails as a way to access the all the data received in the HTTP request to be processed by the controller action. Although params
is technically a method, it can be used like a hash. The above call to params[:id]
returns the ID embedded in the resource path of the HTTP request (recall the 'todos/:id/edit'
part of the edit route). The ID is, in turn, passed to the find
call, effectively telling the call which Todo
object to retrieve from the database.
Test It!
To at least partially confirm that we made this change correctly, we run the web app (as per the steps in the running apps demo), and we open the URL http://localhost:3000/todos/1/edit in our web browser. An error page should be displayed with the message “Template is missing”, because we have not yet created a view template for the edit-resource page. If a different error message appears (e.g., a syntax error), then we made some other mistake, which we would want to fix before proceeding to the next step.
Step 4 Add an Edit-Resource View Template
To print the edit-resource page, we first add the basic HTML skeleton for the page. Then, we fill in the basic logic for the form. Lastly, we add Bootstrap styling to the form.
Substep
Rough in the HTML skeleton. To complete this substep, we create a edit view template file in the folder app/views/todos/
named edit.html.erb
, and we add an <h1>
element to the file, like this:
<h1>Edit To-Do Item</h1>
Substep
Add the form helper to the view. As a first step toward creating the form, we add an embedded Ruby call to the form_with
form helper method, like this:
<h1>Edit To-Do Item</h1>
<%= form_with model: @todo, url: todo_path(@todo), method: :patch, local: true do |f| %>
<% end %>
The form_with
method call takes several key arguments. The model:
option tells the form helper that this form will be for a model class. Recall that the @todo
instance variable was set to reference an existing Todo
model object in the edit
controller action. Thus, by passing the argument model: @todo
, the form helper can infer that the form will be used to modify that Todo
model object. Additionally, this argument is used to populate the form with the model’s existing attribute values or, if the form submission is unsuccessful, the user-entered data. The url: todo_path
specifies that, when the form is submitted, the HTTP should be sent to same resource path as the show route (albeit with a different HTTP request verb). The method: :patch
argument specifies that, when the form is submitted, an HTTP PATCH request should be sent. The local: true
argument specifies that Ajax (a JavaScript-based technology) should not be used to submit form-field data.
Note that the form_with
method also takes a block as an argument (delimited by do |f|
and end
). The block will contain all the form fields, buttons, and other HTML within the form.
Substep Add the form-field helpers to the form. To add the fields to the form, we add embedded Ruby calls to Rails form-field helper methods, like this:
<h1>Edit To-Do Item</h1>
<%= form_with model: @todo, url: todo_path(@todo), method: :patch, local: true do |f| %>
<%= f.text_field :title %>
<%= f.text_area :description %>
<%= f.date_field :due_date %>
<%= f.submit %>
<% end %>
In the above code, the f
variable references a FormBuilder
object that we use to create the form’s input fields and submit button. Depending on the type of attribute, different types of input fields are used. The f.text_field
call generates a single-line text field, whereas the f.text_area
generates a multi-line text field. The f.date_field
call generates a fancy date selector widget. Finally, the f.submit
call generates a button that, when clicked, submits the form data to the server.
If you were to run the server at this point, you would see that the form fields all appear on the edit-resource page, but they lack labels and are poorly styled, as depicted in Figure 6.
We could improve the appearance and usability of this form by adding HTML and CSS to correct the layout and by adding labels using embedded Ruby calls, like this:
<%= f.label :title %>
However, we will instead make use of the bootstrap_form gem to automatically generate these things.
Substep
Add Bootstrap labels and styling to the form. To complete this substep, we change the call to form_with
to bootstrap_form_with
from the bootstrap_form gem, like this:
<h1>Edit To-Do Item</h1>
<%= bootstrap_form_with model: @todo, url: todo_path(@todo), method: :patch, local: true do |f| %>
<%= f.text_field :title %>
<%= f.text_area :description %>
<%= f.date_field :due_date %>
<%= f.submit %>
<% end %>
By switching to the bootstrap_form_with
form helper, the form’s labels, layout, and styling should now appear as in Figure 1. Additionally, bootstrap_form_with
also provides the error-message annotations applied to form fields submitted with invalid values (recall Figure 5).
Test It!
To verify that we completed this step correctly, we run the web app (as per the steps in the running apps demo), and we open the URL http://localhost:3000/todos/1/edit in our web browser. The webpage displayed should look exactly like Figure 1. We should be able to enter data into each form field; however, pressing the “Edit Todo” submit button will produce a “Routing Error” page because “No route matches [PATCH] “/todos/1”. We will add the logic for handling form submissions in the upcoming steps.
Step 5 Add Link(s) to the Edit-Resource Page (if Needed)
To make the form accessible to users of the app, we add to the index page in each record’s row of the table a button that links to that object’s edit form. To complete this step, we will perform two substeps: (1) generating the link(s) in the index view template and (2) styling the link(s) to look like a button.
Substep
Add link(s) from the index page to the edit-resource page. To complete this substep, we add to the app/views/todos/index.html.erb
view template an empty <th>
and corresponding <td>
element containing an embedded-Ruby call to the link_to
view helper inside the loop, like this:
<h1>To-Do Items</h1>
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th>Title</th>
<th>Due Date</th>
<th></th>
</tr>
</thead>
<tbody>
<% @todos.each do |todo| %>
<tr>
<td><%= link_to todo.title, todo_path(todo) %></td>
<td><%= todo.due_date.strftime("%m/%d/%Y") %></td>
<td><%= link_to "Edit", edit_todo_path(todo) %></td>
</tr>
<% end %>
</tbody>
</table>
Substep
Style the link to look like a button. To complete this substep, we add an argument to the call to link_to
that adds Bootstrap button CSS classes to the generated link, like this:
<td><%= link_to "Edit", edit_todo_path(todo), class: 'btn btn-primary' %></td>
Test It!
To verify that we completed this step correctly, we run the web app (as per the steps in the running apps demo), and we open the URL http://localhost:3000/todos in our web browser. A button should now appear next to each to do item that, when clicked, opens the edit-resource page.
Step 6 Add an Update-Resource Route
To add a route for the update-resource controller action that will handle the HTTP PATCH requests generated when users submit the form, we add a patch
route declaration to the config/routes.rb
file, like this:
Rails.application.routes.draw do
root to: redirect('/todos')
get 'todos', to: 'todos#index', as: 'todos'
get 'todos/:id/edit', to: 'todos#edit', as: 'edit_todo'
get 'todos/:id', to: 'todos#show', as: 'todo'
patch 'todos/:id', to: 'todos#update'
end
Note that the patch 'todos/:id'
part of this route corresponds to how we earlier called the bootstrap_form_with
method. In particular, we set the method: patch
and the url: todo_path(@todo)
options, which control the HTTP request that the browser sends when a user clicks the form’s submit button. In particular, these option imply that the browser will send an HTTP PATCH request with the resource path /todos/:id
.
Also, note that this route does not have an as:
argument, because the show route already set one for this resource path ('todo'
); thus, we can use the todo_path
and todo_url
helper methods in relation to either of these get
and patch
routes.
Test It!
To verify that we completed this step correctly, we run the web app (as per the steps in the running apps demo); we open the URL http://localhost:3000/todos/1/edit in our web browser; and we press the “Edit Todo” submit button. An “Unknown action” error page should be displayed because “The action ‘update’ could not be found for TodosController”. This message indicates that our patch
route was matched, but there is no TodosController#update
method, which we will define in the next step.
Step 7 Add an Update-Resource Controller Action
To create the controller action for handling from submissions from the edit-resource page, we perform three substeps: (1) create an empty update-resource controller action, (2) add code to the action that retrieves the model object based on the passed in id
parameter, and (3) add code to the action that updates the model object based on the submitted data, responding to the browser with an HTTP redirect on success and re-rendering the form if updating fails.
Substep
Create an empty update-resource controller action. To complete this substep, we define an update
method in the TodosController
class (in app/controllers/todos_controller.rb
), like this:
class TodosController < ApplicationController
def index
@todos = Todo.order(:due_date)
render :index
end
def show
@todo = Todo.find(params[:id])
render :show
end
def edit
@todo = Todo.find(params[:id])
render :edit
end
def update
end
end
Substep
Retrieve the model object. To complete this substep, we add to the update
method a call to the Todo.find
method, like this:
class TodosController < ApplicationController
…
def update
@todo = Todo.find(params[:id])
end
end
The find
model class method is used to retrieve a model object from the database by id
. The above call to Todo.find
returns a Todo
object (i.e., the one retrieved from the database), and the @todo
instance variable is assigned a reference to that object. The use of an instance variable (indicated by the @
in @todo
) is important here, because instance variables are accessible within view templates rendered by the class, thus allowing data to be passed from the controller to the view. The params
method is provided by Rails as a way to access the all the data received in the HTTP request to be processed by the controller action. Although params
is technically a method, it can be used like a hash. The above call to params[:id]
returns the ID embedded in the resource path of the HTTP request (recall the 'todos/:id'
part of the show route). The ID is, in turn, passed to the find
call, effectively telling the call which Todo
object to retrieve from the database.
Substep
Update the model object, handling both success and failure conditions. To complete this step, we first add to the update
action a call to the update
method wrapped in an if
/else
statement, like this:
class TodosController < ApplicationController
…
def update
@todo = Todo.find(params[:id])
if @todo.update(params.require(:todo).permit(:title, :description, :due_date))
else
end
end
end
This long line of code requires some explanation, so let’s unpack it. The @todo.update()
call then attempts to update the retrieved object’s attributes with the data from the submitted form. Recall from the show pages demo that params
is a method (that can be used like a hash) to access to all the data from the HTTP request to be handled by the controller action.
To understand the above call, params.require(:todo)
, one must understand something about how the params
hash is structured. When using the form_with
(or bootstrap_form_with
) form helper as we did above, the submitted form data will be encoded in the params
hash in the manner shown in this example:
{
"todo" => {
"title" => "Water the garden",
"description" => "Don't forget to water the azaleas!",
"due_date" => "2021-05-08"
}
}
Note that the params
hash has a top-level key, "todo"
, which is a snake_case
version of the model class name, Todo
, and comes from the model: @todo
argument to the form_with
(or bootstrap_form_with
) call. This top-level key maps to a hash containing the user-entered form data. Each key in this inner hash is an attribute name and maps to the value entered into the form by the user.
The call to require(:todo)
is used to help secure the app against malicious data. It basically says that the hash must have a top-level key, "todo"
. If the hash does have such a key, the method returns the value (a hash in this case) to which that key maps. If the hash does not have such a key, the method returns false
, effectively preventing any further processing of the request.
The call to permit(:title, :description, :due_date)
that is applied to the hash returned by require(:todo)
similarly serves a security purpose. This call returns a filtered version of the hash that contains only the keys (and their associated values) that are listed as arguments to the call. Thus, any extraneous (and possibly malicious) key/value pairs that were included in the request are discarded and rendered harmless.
After evaluating the params
-require
-permit
call chain, the call to update
receives as its argument a hash that contains only keys that match the Todo
class attributes, with each of those keys mapping to the value entered by the user into the form.
The call to update
returns true
on success and false
if the update operation failed. Thus, the body of the if
part of the if
/else
statement will run if the call to update
succeeds, and the body of the else
part will run if the call to update
fails. For example, the call to update
would fail if any of the @todo
object’s attribute values violated a validation declared in the Todo
class.
Next, we set the behavior after a successful update, like this:
class TodosController < ApplicationController
…
def update
@todo = Todo.find(params[:id])
if @todo.update(params.require(:todo).permit(:title, :description, :due_date))
flash[:success] = "To-do item successfully updated!"
redirect_to todo_url(@todo)
else
end
end
end
The call to redirect_to
causes the web server to reply to the browser with an HTTP redirect response. In particular, the call, redirect_to todo_url(@todo)
, tells the browser to make a new HTTP GET request to the URL of that todo object’s show page. The flash[:success]
line above that assigns a message string to the flash
hash will cause a flash success notification to appear on the index page when the browser displays it. (See the flash notifications deets for a full explanation of how this works.)
Lastly, we set the behavior for an unsuccessful save, like this:
class TodosController < ApplicationController
…
def update
@todo = Todo.find(params[:id])
if @todo.update(params.require(:todo).permit(:title, :description, :due_date))
flash[:success] = "To-do item successfully updated!"
redirect_to todo_url(@todo)
else
flash.now[:error] = "To-do item update failed"
render :edit
end
end
end
Instead of replying to the browser with an HTTP redirect, like we did for the success case, we re-render the edit-resource page, giving the user a chance to correct the invalid form data they entered. Note that, in this case, we use flash.now
to set the flash notification, because the notification must be included in the current HTTP response to be sent to the browser (and not the next response, as in the case of an HTTP redirect).
Test It!
To verify that we completed this step correctly, we run the web app (as per the steps in the running apps demo). We open the URL http://localhost:3000/todos/1/edit in our web browser, fill out the form with valid data, and we click the “Update Todo” submit button. The browser should be redirected to the show page (http://localhost:3000/todos/1), and the modified attributes should appear. A success notification should also appear at the top of the page.
To verify the form’s error-handling behavior, we follow the link back to the edit-resource page (http://localhost:3000/todos/1/edit), remove the title, and click the “Update Todo” submit button. This time, the form should be redisplayed with an error notification at the top and an error-message annotation on the title field.
Conclusion
Following the above steps, we have now made an edit/update form for modifying instances of an existing model class and saving them to the database.