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.

The remaining steps are concerned with making the update-resource action for processing form submissions.

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.

A form for editing a to do item.

Figure 1. A form for editing a to-do item.

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.

A class diagram depicting a Todo model class with the following attributes: a title string, description text, and a due date

Figure 2. The Todo model class.

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.

A screenshot of the right side of the index page that shows buttons with the text Edit.

Figure 3. Buttons on the right side of the index page that link to the edit-resource page.

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.

A screenshot of the top of the show page that shows a notification that says To-do item successfully updated!

Figure 4. A success notification that is displayed after a successful submission of the form.

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.

A screenshot of the edit-resource page that shows an error notification at the top of the page and form fields annotated with error messages like title cannot be blank.

Figure 5. The edit-resource page after an unsuccessful form submission. The page includes a failure notification at the top and error-message annotations on the form fields that contain invalid values.

Base App Code

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 1 Changeset

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 3 Changeset

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.

A form for editing a to do item.

Figure 6. An unlabeled and unstyled form for editing a to-do item.

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 4 Changeset

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 5 Changeset

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 6 Changeset

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.

Step 7 Changeset

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.

Demo App Code