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: