Features
In this chapter, we'll add a feature to Display and Add Comments on the
Task's show page. We are going to make the following changes in our
application:
-
Both assignee and the owner can add comments on their tasks.
-
All added comments will be displayed in descending order of the time at which
they were added. i.e. the most recently added comment will be shown at the
top.
-
The comments list will include the date and time at which the comment was
posted.
Here is how the feature will look like when implemented:
Technical design
The implementation of this feature is fairly straightforward:
-
We will be creating a new model, named Comment, having these fields: a
non-empty text content, a reference to the User who made the comment, and a
reference to its parent Task.
-
We will open an API route for creating comments.
-
We don't need a separate API for listing comments. We can include the comments
in Task's show action.
-
We will update the Jbuilder for show action in TasksController to
include the comments data in tasks JSON.
-
In frontend, we will add a new component, named Comments, to display the
comments section. This will include a textarea to enter comment content, a
button to post it via API and a list to display the previously created
comments.
-
We will include the Comments component at the bottom of the Show component
as per the design.
Let's now implement the feature.
We'll create a new model, Comment. Run the following command in the terminal:
Here we have generated the migration script as well Comment model.
By passing argument content:text, our migration file will be pre-populated
with the script to add content to the comments table having data type as
text.
Now if we observe, every comment would belong to a Task and as well to the
User who is adding the comment. Passing task:references and
user:references as the arguments to the generate model command automatically
adds the required associations and foreign key constraints.
This is how the comment.rb file would look after the required associations are
added:
Now open the last migration file under the db/migrate folder. It should be as
follows:
t.references :user adds column user_id to the comments table and by passing
option foreign_key: true, a foreign key constraint is added.
Run the migration using rails db:migrate for the changes to take effect:
Now, let's first add a constant for the maximum length of the content. Then we
will add the presence and length validations to the content field the same way
we did in Task's title field.
We can do so by adding the following lines of code in the Comment model:
Now similarly we have to make changes in the Task and User models to
introduce associations for comments. Note that each task can have many comments:
dependent: :destroy is a callback which makes sure that when a task is
deleted, all the comments added to it are deleted as well. Similarly, the same
callback is passed in the User model, which would delete all the comments by a
user when the user is deleted.
To test comments, we can make use of factory-bot as we did in the previous
chapter. First, we need factories to generate tasks and comments.
Create test/factories/task.rb and populate it with the following content:
The dummy records that we are creating using factory-bot are based on the
database models in our application. Models can also contain associations with
other models. For thorough testing of the application we also need to create the
associated records.
For example, in this case, while creating a dummy task record, we also need to
create the associated assigned_user and task_owner records. We can do so
using the factory-bot by adding the association name and specifying the
factory name which should be used to create the associated records.
Hence, in the task factory we have added the association names and specified
the user factory to be used to create those records.
Note that, we are only selecting the first 50 characters generated by the
Faker bot for task title so that the validation for title length holds true.
Also, create test/factories/comment.rb and populate it with the following
code:
You might have noticed that we are not assigning values for some fields. Also,
keen readers might have realized that these fields are model associations. And
you are right. This is how we define associations in a factory.
FactoryBot is capable of automatically generating associations for entities. So
if we generate an instance of comment, an instance of user and task will
be auto-generated, given their factories are defined. Therefore we don't need to
manually define users and tasks and assign them to it dynamically.
Now, populate test/models/comment_test.rb with the following content:
Let's test whether our comment validations are correct or not:
Now since we have tested the validations, let's make sure that a valid comment
is getting created. For such test scenarios where we need to check whether the
DB data has been created/updated, we can make use of assert_difference, where
we check the difference between previous count and current count of items in
the table:
The above test validates that after saving the comment count gets increased
by 1.
Now let's test whether the association between User and Comment are properly
validated or not:
And similarly, association between Task and Comment:
Now let's add routes for create action of Comments:
Here only: :create specifies to create only one route which would be for the
create action. Hence, a route of the format /comments is created, to which we
can send a POST request and Rails will redirect it to the create action of
the comments controller.
Let's add a controller for comments:
We'll only be adding the create action in our controller as we have defined a
route only for this action:
We do not need to send a json or a success message along with the response when
a comment is created successfully.
We'll make a slight change to the show action in the Tasks controller to load
all the comments of the loaded task so that we can render those comments on the
task's show page. Modify show action in tasks controller as follows:
Here, we are using order('created_at DESC') since we need to display the
latest comments first.
We need to update the Jbuilder for show action to include comments along with
other task details.
To do so update app/views/tasks/show.json.jbuilder view with the following
lines of code:
Now let's add React code for the Comment section.
Let's create the comments API first:
Open the file and add the following code to it:
To list the comments, add an index.js file:
Inside Comments/index.jsx add the following contents:
Now, fully replace Show.jsx with the following content:
Now the comments can be seen in the task show page.
Inside the Comments component we are using textarea which is an HTML form
element to allow a user to add a comment. HTML input elements such as input,
textarea and select maintain a state of their own which updates based on the
user input.
Whereas in React, the state is a property of the components. Hence it is better
to combine the state of textarea with the React state which will make React's
state the single source of truth.
This way, React handles the responsibility of rendering the textarea element
and controlling its value thus making textarea a controlled component.
To do so, we have passed the value of newComment from showTask component's
state to textarea element's value prop and setNewComment function as a
callback function to the onChange prop of textarea element.
Now, any change in the textarea input will invoke the setNewComment function
which will update Show component's state and set the value of newComment.
Updated value of newComment will be reflected in the textarea input.
Once the submit button is clicked, handleSubmit function inside ShowTask.jsx
will handle the comment post request.
The post request will be sent to the comments_path and on that route
comments_controller's create action will handle that request.
Now a user can comment on the particular task.
delete vs destroy methods
delete will only delete the current object record from the database but not
its associated records from the database.
destroy will delete the current object record along with its associated
records from the database.
Let's understand more, by doing an example from the Rails console. We will be
using Rails sandbox console so that all changes we do in the example rolls back
when we exit the console.
Open Rails sandbox console using the below command:
First let's create a new task:
We have taken id of the first user in user_id variable and used it while
creating the task.
Now create a new comment for this task. Remember while creating the comment we
are setting task_id as the id of the above created task, which we can access
through task.id:
Let's try to delete this task using delete method:
Running the above command will throw the following error:
We are getting the above error because the task we have created above is
associated with the comment. delete will not delete associated records from
the database.
If we want to delete this task then first we have to delete its associated
comment from the comments table.
Here we can use the destroy method since it will also delete the associated
records from the database.
Once the above command is run, the task will be deleted successfully along with
its associated comment.
The usage of delete or destroy matters, based on the use case and context.
If multiple parent objects share common children object, then calling destroy on
specific parent object will destroy the children objects which are shared among
all parents.
So use the delete method when no association is there for the entity to be
deleted. In other cases where we have to destroy the associations too, use the
destroy method.
destroy also runs callbacks on the model while delete doesn't. For example
before_destroy and after_destroy callbacks.
Similarly we have delete_all and destroy_all methods. These methods behave
the same as delete and destroy methods but the only difference is that they
execute on all records matching conditions instead of one record.
Let's run the following command to exit from the console:
Let's commit changes made in this chapter: