Comments in Angular - Building Nested Comments with Replies
In this post we will implement comments inside Angular as a separate module. And actually the comment feature is really difficult. This is exactly why I want to show you how to do that inside Angular correctly.
Generated project
So here I already generated for us an Angular application. And it is completely empty. And the only thing that is external here is a styles.css file where all global classes for our module are written.
/* src/styles.css */
.comments {
margin-top: 20px;
}
.comments-title {
font-size: 30px;
margin-bottom: 20px;
}
.comments-container {
margin-top: 40px;
}
.comment-form-title {
font-size: 22px;
}
.comment-form-textarea {
width: 100%;
height: 80px;
margin-bottom: 8px;
margin-top: 8px;
border: 1px solid rgb(107, 114, 12);
}
.comment-form-button {
font-size: 16px;
padding: 8px 16px;
background: rgb(59, 130, 246);
border-radius: 8px;
color: white;
}
.comment-form-button:hover:enabled {
cursor: pointer;
background: rgb(37, 99, 235);
}
.comment-form-button:disabled {
opacity: 0.7;
cursor: default;
}
.comment-form-cancel-button {
margin-left: 10px;
}
.comment {
display: flex;
margin-bottom: 28px;
}
.comment-image-container {
margin-right: 12px;
}
.comment-image-container img {
border-radius: 50px;
}
.comment-right-part {
width: 100%;
}
.comment-content {
display: flex;
}
.comment-author {
margin-right: 8px;
font-size: 20px;
color: rgb(59, 130, 246);
}
.comment-text {
font-size: 18px;
}
.comment-actions {
display: flex;
font-size: 12px;
color: rgb(51, 51, 51);
cursor: pointer;
margin-top: 8px;
}
.comment-action {
margin-right: 8px;
}
.comment-action:hover {
text-decoration: underline;
}
.replies {
margin-top: 20px;
}
This is why you can just take this styles so you should not retype them and we can fully focus on Angular.
Planning
So how we will implement this feature? We will create an additional module for the whole comments. Why is that? Because actually we can reuse comments on every single page. We can write comments for the post, profile or other entity.
Which means that it must be completely sharable and configurable.
This is what I want to do here is to create a new folder comments
inside src/app
.
Binding API
Also I want to make our implementation as realistic as possible. For this we will use a tool which is called json-server. It allows us to create a fake API in seconds. We just need to install globally json-server
package with npm and create db.json file inside our project.
{
"comments": [
{
"id": "1",
"body": "First comment",
"username": "Jack",
"userId": "1",
"parentId": null,
"createdAt": "2021-08-16T23:00:33.010+02:00"
},
{
"id": "2",
"body": "Second comment",
"username": "John",
"userId": "2",
"parentId": null,
"createdAt": "2021-08-16T23:00:33.010+02:00"
},
{
"id": "3",
"body": "First comment first child",
"username": "John",
"userId": "2",
"parentId": "1",
"createdAt": "2021-08-16T23:00:33.010+02:00"
},
{
"id": "4",
"body": "Second comment second child",
"username": "John",
"userId": "2",
"parentId": "2",
"createdAt": "2021-08-16T23:00:33.010+02:00"
}
]
}
Here we created a new key comments
were we wrote some default comments to work with. Now we need to install this package and start API.
npm install -g json-server
json-server --watch db.json
As you can see inside http://localhost:300/comments we get our comments that we created back. Additionally all typical requests like POST, DELETE, GET, UPDATE and PUT will work out of the box and make changes to db.json file.
Which means our API is ready and our Angular application can work with this API.
Creating module
Now let's start with creating our comments module.
// src/app/comments/comments.module.ts
@NgModule({
})
export class CommentsModule {}
Here is our new empty module. But we also should not forget to register it in app.module.ts
@NgModule({
...
imports: [
...
CommentsModule
],
})
export class AppModule {}
The next step is to create our comments. And for this application we plan 3 components:
- Component for the list of comments
- Component for the single comment
- Component for comment form (create comment / reply comment)
Let's create an empty list of comments, single comment and comment form.
// src/app/comments/components/comments/comments.component.ts
@Component({
selector: 'comments',
templateUrl: './comments.component.html',
})
export class CommentsComponent {}
// src/app/comments/components/comment/comment.component.ts
@Component({
selector: 'comment',
templateUrl: './comment.component.html',
})
export class CommentComponent {}
// src/app/comments/components/commentForm/commentForm.component.ts
@Component({
selector: 'comment-form',
templateUrl: './commentForm.component.html',
})
export class CommentFormComponent {}
We also should not forget to register our components in comments.module.ts
.
@NgModule({
declarations: [CommentsComponent, CommentComponent, CommentFormComponent]
})
export class CommentsModule {}
Now let's render our comments
component in app
.
<!-- app.components.html -->
<comments></comments>
Current user ID
The next thing that I want to talk about is our current user. We must provide currentUserId
in our comments section. Why is that? Actually in production application we have logged in user. And typically when we are logged in we can create and update comments. Inside our current comments implementation we won't do anything about current user. This is why I want to pass inside our comments
component currentUserId
just like a string. This is exactly how we will do it in real application.
<!-- app.components.html -->
<comments currentUserId="1"></comments>
In this case our comments component won't know anything about current user implementation and will just get ID as an input.
// src/app/comments/components/comments/comments.component.ts
@Component({
selector: 'comments',
templateUrl: './comments.component.html',
})
export class CommentsComponent {
@Input() currentUserId: string | undefined;
}
So here we provided currentUserId
as a string. The important point is that our currentUserId might be not always provided from outside. This is why we must mark it as undefined.
Creating interface for comment
The next thing that we need is to create an interface for our comment. We must leverage Typescript as we want to make our application as string as possible.
// src/app/comments/types/comment.interface.ts
export interface CommentInterface {
id: string;
body: string;
username: string;
userId: string;
parentId: null | string;
createdAt: string;
}
Here we provided all needed fields for comment. As you can see parentId
can be either null or a string. Our root comments won't have a parent this is why parentId will be null there. All our nested comments will have a parentId
so we know were this comment belongs.
Creating service
The next thing that we want to create is our service. Because we want to fetch comments from our API.
// src/app/comments/services/comments.service.ts
@Injectable()
export class CommentsService {
constructor(private httpClient: HttpClient) {}
getComments(): Observable<CommentInterface[]> {
return this.httpClient.get<CommentInterface[]>(
'http://localhost:3000/comments'
);
}
}
We created our CommentsService
and the first method inside is getComments
which will fetch a list of comments from API. Back we get a stream of CommentInterface[]
. This is why it is important to create interfaces for our entities. In this case we can type everything correctly.
Inside we used httpClient which comes from Angular to fetch data. To make it working we must also add HttpClientModule in app.module.ts
. Also we should not forget to register our service in comments module.
@NgModule({
...
providers: [CommentsService]
})
export class AppModule {}
Now we can use our new service inside component and fetch data.
// src/app/comments/components/comments/comments.component.ts
export class CommentsComponent implements OnInit {
@Input() currentUserId!: string;
comments: CommentInterface[] = [];
constructor(private commentsService: CommentsService) {}
ngOnInit(): void {
this.commentsService.getComments().subscribe((comments) => {
this.comments = comments;
});
}
}
So here we injected CommentsService
in our comments component and called getComments
inside ngOnInit
. As it is a stream we subscribed to it and wrote the result inside comments property.
As you can see we successfully fetched our comments from API as in real project.
Rendering comments
Now let's render our list of comments without creating additional component.
<!-- src/app/comments/components/comments/comments.component.html -->
<div class="comments">
<h3 class="comments-title">Comments</h3>
<div class="comment-form-title">Write comment</div>
COMMENT FORM
<div class="comments-container">
<div *ngFor="let comment of comments">
{{comment.body}}
</div>
</div>
</div>
So this is just simple markup where we render comments inside a div.
Now let's render instead of div
a comment
component where we will pass comment inside
.
// src/app/comments/components/comment/comment.component.ts
export class CommentComponent {
@Input() comment!: CommentInterface;
}
<!-- src/app/comments/components/comment/comment.component.html -->
{{comment.body}}
We can use comment component instead of div.
<!-- src/app/comments/components/comments/comments.component.html -->
...
<div class="comments-container">
<comment
*ngFor="let comment of comments"
[comment]="comment"
>
</comment>
</div>
...
As you can see we successfully rendered comments with additional comment
component. But obviously our component markup doesn't look good and we must fix it.
Markup for the comment component
<!-- src/app/comments/components/comment/comment.component.html -->
<div class="comment">
<div class="comment-image-container">
<img src="/assets/user-icon.png" />
</div>
<div class="comment-right-part">
<div class="comment-content">
<div class="comment-author">{{ comment.username }}</div>
</div>
<div class="comment-text">{{ comment.body }}</div>
</div>
</div>
Here is just markup of our comment with property from our comment
object.
Creating comment form
Now I want to continue with comment form because it will help us to create new comments. What inputs do we need in our comments form?
- Initial text (for editing form)
- Cancel button (to cancel reply or edit)
- Submit label (label on button "add", "reply", "update")
// src/app/comments/components/commentForm/commentForm.component.ts
export class CommentFormComponent implements OnInit {
@Input() submitLabel!: string;
@Input() hasCancelButton: boolean = false;
@Input() initialText: string = '';
form!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.form = this.fb.group({
title: [this.initialText, Validators.required],
});
}
onSubmit(): void {
console.log('onSubmit', this.form.value)
}
}
Here we passed 3 new inputs inside our CommentFormComponent
. We also injected FormBuilder inside we create new reactive form. As we have just a single title property we created it and passed initialText
as default value inside.
Now it's time to add our markup.
<!-- src/app/comments/components/commentForm/commentForm.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<textarea class="comment-form-textarea" formControlName="title"></textarea>
<button class="comment-form-button" type="submit" [disabled]="form.invalid">
{{ submitLabel }}
</button>
<button
*ngIf="hasCancelButton"
type="button"
class="comment-form-button comment-form-cancel-button"
>
Cancel
</button>
</form>
Now we can use our CommentFormComponent
inside our CommentsComponent
.
<!-- src/app/comments/components/comments/comments.component.html -->
...
<h3 class="comments-title">Comments</h3>
<div class="comment-form-title">Write comment</div>
<comment-form
submitLabel="Write"
>
</comment-form>
...
This is how our form is looking now.
Submitting a form
Now we want to implement form submitting. For this we must create an output which will give our comment title outside.
// src/app/comments/components/commentForm/commentForm.component.ts
export class CommentFormComponent implements OnInit {
...
@Output()
handleSubmit = new EventEmitter<string>();
...
onSubmit(): void {
this.handleSubmit.emit(this.form.value.title);
this.form.reset();
}
Here we added handleSubmit
output and emitted it with title from the form outside. We also used form.reset()
to return our form to initial state.
Now we can handle it outside.
<!-- src/app/comments/components/comments/comments.component.html -->
...
<h3 class="comments-title">Comments</h3>
<div class="comment-form-title">Write comment</div>
<comment-form
submitLabel="Write"
(handleSubmit)="addComment({text: $event, parentId: null})"
>
</comment-form>
...
Here we added a method addComment
with specific notation. We don't only want to provide text from the comment form inside but the object with parentId
. We do that because when we will send data to API we must provide a valid parentId or null.
// src/app/comments/components/comments/comments.component.ts
export class CommentsComponent implements OnInit {
...
addComment({
text,
parentId,
}: {
text: string;
parentId: string | null;
}): void {
console.log('addComment', text, parentId)
}
}
As you can see in browser our comment is logged and it is working.
Saving a comment
Now it's time to save our comment that we created. This is why we want to add new method to our CommentsService
.
export class CommentsService {
constructor(private httpClient: HttpClient) {}
...
createComment(
text: string,
parentId: string | null = null
): Observable<CommentInterface> {
return this.httpClient.post<CommentInterface>(
'http://localhost:3000/comments',
{
body: text,
parentId,
// Should not be set here
createdAt: new Date().toISOString(),
userId: '1',
username: 'John',
}
);
}
}
Here we created createComment
which gets text and parentId. It return stream with created comment and we make POST request inside. The important thing here is that in real application we would provide only body and parentId inside and backend will add createdAt, userId, username on it's own. We use a fake API here which won't return such fields for us this is why we set there here.
createdAt, userId, username are here ONLY because of fake API
Now we can use our API method in comments component.
// src/app/comments/components/comments/comments.component.ts
...
addComment({
text,
parentId,
}: {
text: string;
parentId: string | null;
}): void {
this.commentsService
.createComment(text, parentId)
.subscribe((createdComment) => {
this.comments = [...this.comments, createdComment];
});
}
Here we used our method from commentsService
and got createdComment
back. After this we updated array with our comments by using spread operator.
As you can see in browser we successfully created a comment and rendered it on the page.
Actions for comment
We want to implement create, update, delete buttons now. First of all let's add them to the markup.
<!-- src/app/comments/components/comment/comment.component.html -->
<div class="comment">
<div class="comment-image-container">
<img src="/assets/user-icon.png" />
</div>
<div class="comment-right-part">
<div class="comment-content">
<div class="comment-author">{{ comment.username }}</div>
<div>{{ createdAt }}</div>
</div>
<div class="comment-text">{{ comment.body }}</div>
COMMENT FORM EDITING
<div class="comment-actions">
<div
*ngIf="canReply"
class="comment-action"
>
Reply
</div>
<div
*ngIf="canEdit"
class="comment-action"
>
Edit
</div>
<div
*ngIf="canDelete"
class="comment-action"
>
Delete
</div>
</div>
COMMENT FORM REPLYING & REPLIES
</div>
</div>
As you see I used canReply
, canEdit
, canDelete
that we must implement now
// src/app/comments/components/comment/comment.component.js
export class CommentComponent implements OnInit {
@Input() currentUserId!: string;
createdAt: string = '';
canReply: boolean = false;
canEdit: boolean = false;
canDelete: boolean = false;
ngOnInit(): void {
const fiveMinutes = 300000;
const timePassed =
new Date().getMilliseconds() -
new Date(this.comment.createdAt).getMilliseconds() >
fiveMinutes;
this.canReply = Boolean(this.currentUserId);
this.canEdit = this.currentUserId === this.comment.userId && !timePassed;
this.canDelete =
this.currentUserId === this.comment.userId &&
this.replies.length === 0 &&
!timePassed;
}
This is a lot of code!
- First of all we created
canReply
,canEdit
,canDelete
and setted them to false. - We can reply to the comment when we are logged in. This is why we check if we have currentUserId in
canReply
. But in order to get it we must providecurrentUserId
in the input. - We want to allow
canEdit
andcanDelete
only first 5 minutes this is why we createdfiveMinutes
andtimePassed
property - We also don't want to allow deleting a comment if it has replies. But in order to do this we must implement replies which we will do in a second
Now we must get replies as an input for the comment. We also must write logic to calculate them in our CommentsComponent
.
// src/app/comments/components/comment/comment.component.js
export class CommentComponent implements OnInit {
...
@Input() replies: CommentInterface[]
}
Now we must provide our replies from outside.
<!-- src/app/comments/components/comments/comments.component.html -->
...
<div class="comments-container">
<comment
*ngFor="let comment of comments"
[comment]="comment"
[currentUserId]="currentUserId"
[replies]="getReplies(comment.id)"
>
</comment>
</div>
We call a getReplies
function where we provide parentId inside. Let's create this function.
src/app/comments/components/comments/comments.component.ts
...
getReplies(commentId: string): CommentInterface[] {
return this.comments
.filter((comment) => comment.parentId === commentId)
.sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
}
...
We just took our this.comments
and filtered them by commentId
. So we find all child comments here. We also sorted them in ascending order by createdAt
property.
As you can see in browser our action buttons are there.
Active comment
Now we need to think about active comment. And actually when we click reply button or edit button we need to render our comment form in this specific place. So actually we want to activate showing form for specific comment. This is why we need to store activeComment
. This will give us enough information to render a comment form.
For this I want to create additional interface and an enum.
// src/app/comments/types/activeCommentType.enum.ts
export enum ActiveCommentTypeEnum {
replying = 'replying',
editing = 'editing',
}
// src/app/comments/types/activeComment.interface.ts
import { ActiveCommentTypeEnum } from './activeCommentType.enum';
export interface ActiveCommentInterface {
id: string;
type: ActiveCommentTypeEnum;
}
In our active comment we need to know commentId
and type which can be 'replying' or 'editing'. It is a good practice to move such constants to enum in Typescript.
Now we can create activeComment
inside our CommentsComponent
.
// src/app/comments/components/comments/comments.component.ts
...
export class CommentsComponent implements OnInit {
...
activeComment: ActiveCommentInterface | null = null;
....
}
By default no comment is selected so it's null. After we select a comment to must provide an ID and type inside.
Now we must attach events to our action buttons.
<!-- src/app/comments/components/comment/comment.component.html -->
<div class="comment-actions">
<div
*ngIf="canReply"
class="comment-action"
(click)="
setActiveComment.emit({
id: comment.id,
type: activeCommentType.replying
})
"
>
Reply
</div>
<div
*ngIf="canEdit"
class="comment-action"
(click)="
setActiveComment.emit({
id: comment.id,
type: activeCommentType.editing
})
"
>
Edit
</div>
...
</div>
Here we added setActiveComment
output in our CommentsComponent
. We provided inside id and type exactly like we need per definition.
// src/app/comments/components/comment/comment.component.ts
export class CommentComponent implements OnInit {
...
@Output()
setActiveComment = new EventEmitter<ActiveCommentInterface | null>();
activeCommentType = ActiveCommentTypeEnum;
}
We registered setActiveComment
emitter and activeCommentType
because it is the only way to use our enum in html.
Now we must handle setActiveComment
in our CommentsComponent
.
<!-- src/app/comments/components/comments/comments.component.html -->
...
<div class="comments-container">
<comment
...
(setActiveComment)="setActiveComment($event)"
>
</comment>
</div>
and we also must assign inside our activeComment
.
// src/app/comments/components/comments/comments.component.ts
export class CommentsComponent implements OnInit {
...
setActiveComment(activeComment: ActiveCommentInterface | null): void {
this.activeComment = activeComment;
}
}
Reply form
It is time to create our reply form in a single comment.
<!-- src/app/comments/components/comment/comment.component.html -->
<div class="comment-actions">
...
</div>
<comment-form
*ngIf="isReplying()"
submitLabel="Reply"
(handleSubmit)="addComment.emit({ text: $event, parentId: replyId })"
></comment-form>
We used our comment-form
here which we will render only when we have isReplying
true. We also provided submitLabel
inside and added function to handleSubmit
. Which means that we must create addComment
output in our CommentComponent
.
// src/app/comments/components/comment/comment.component.ts
export class CommentComponent implements OnInit {
...
@Output()
addComment = new EventEmitter<{ text: string; parentId: string | null }>();
isReplying(): boolean {
if (!this.activeComment) {
return false;
}
return (
this.activeComment.id === this.comment.id &&
this.activeComment.type === this.activeCommentType.replying
);
}
}
Here we created addComment
output which has exactly the same signature like our addComment
method in CommentsComponent
. Inside isReplying
we check if our active comment id and type equals our comment id and type in CommentComponent
.
Now we need to talk about replyId
. We provided it inside addComment.emit
but it doesn't exist yet in our component. So replyId
is the commentId where we will attach our reply. But we don't want to render nested comments indefinitely because it is really slow. This is why we will render only 2 levels of comments: root level and 1 child level. If you try to reply to a reply we will attach it as a child of root comment.
This is why here is a logic how to calculate our replyId.
// src/app/comments/components/comment/comment.component.ts
export class CommentComponent implements OnInit {
...
@Input() parentId!: string | null;
replyId: string | null = null;
ngOnInit(): void {
...
this.replyId = this.parentId ? this.parentId : this.comment.id;
}
}
First we can get from outside parentId
. And we will provide it inside ONLY if our comment is a reply and not a root comment. If our comment has a parentId
we store it inside our reply. So instead of attaching a reply to current comment we will attach it to the parent. If we don't have a parent so we are inside root comment we use this.comment.id
.
And the last thing that we must do here is create event emitter addComment
.
// src/app/comments/components/comment/comment.component.ts
export class CommentComponent implements OnInit {
...
@Output()
addComment = new EventEmitter<{ text: string; parentId: string | null }>();
...
}
Now we need to provide needed data in CommentsComponent
.
<!-- src/app/comments/components/comments/comments.component.html -->
...
<div class="comments-container">
<comment
...
(addComment)="addComment($event)"
>
</comment>
</div>
We don't need to provide parentId inside as it is a root comment.
Which means we successfully implemented reply form and adding of child comment.
One small problem that we have is that our reply form is not closing when we hit "Reply". We can easily achieve this if we set activeComment
in null again.
// src/app/comments/components/comments/comments.component.ts
...
addComment({
text,
parentId,
}: {
text: string;
parentId: string | null;
}): void {
this.commentsService
.createComment(text, parentId)
.subscribe((createdComment) => {
this.comments = [...this.comments, createdComment];
this.activeComment = null;
});
}
Now after we hit reply our comment form is closed again.
Rendering replies
Now it's time to render our replies. For this we first need to add markup of our replies.
<!-- src/app/comments/components/comment/comment.component.html -->
<comment-form
*ngIf="isReplying()"
submitLabel="Reply"
(handleSubmit)="addComment.emit({ text: $event, parentId: replyId })"
></comment-form>
<div class="replies" *ngIf="replies.length > 0">
<comment
*ngFor="let reply of replies"
[comment]="reply"
(setActiveComment)="setActiveComment.emit($event)"
[activeComment]="activeComment"
(deleteComment)="deleteComment.emit($event)"
(addComment)="addComment.emit($event)"
[parentId]="comment.id"
[replies]="[]"
[currentUserId]="currentUserId"
></comment>
Here we looped through replies of the comment because they are already available for us through the input. Inside we recursively render comment component. It is important to provide every single input and outside inside it just like we did in CommentsComponent
. The only difference is that child component will never have replies so we provide an empty array inside.
As you can see in browser, we successfully rendered the list of replies for every comment.
Edit form
Now it's time to implement our edit form. For this we need isEditing
property just like we made isReplying
.
// src/app/comments/components/comment/comment.component.ts
export class CommentComponent implements OnInit {
...
isEditing(): boolean {
if (!this.activeComment) {
return false;
}
return (
this.activeComment.id === this.comment.id &&
this.activeComment.type === this.activeCommentType.editing
);
}
}
Is is fully copy paste of isReplying
but with editing activeCommentType
inside. Let's render our editing form now.
<!-- src/app/comments/components/comment/comment.component.html -->
<div class="comment-text" *ngIf="!isEditing()">{{ comment.body }}</div>
<comment-form
*ngIf="isEditing()"
submitLabel="Update"
[hasCancelButton]="true"
[initialText]="comment.body"
(handleSubmit)="
updateComment.emit({ text: $event, commentId: comment.id })
"
></comment-form>
<div class="comment-actions">
Here we put if condition to render our comment body only if it is not in editing state. We also used our comment form component to render editing form. The only thing which is missing is updateComment
output that we must add.
// src/app/comments/components/comment/comment.component.ts
export class CommentComponent implements OnInit {
...
@Output()
updateComment = new EventEmitter<{ text: string; commentId: string }>();
...
}
Now we must provide updateComment not only in root comment but also in child comments.
<!-- src/app/comments/components/comments/comments.component.html -->
...
<div class="comments-container">
<comment
...
(updateComment)="updateComment($event)"
>
</comment>
</div>
<!-- src/app/comments/components/comment/comment.component.html -->
<comment
*ngFor="let reply of replies"
...
(updateComment)="updateComment.emit($event)"
></comment>
We still didn't add updateComment
method to our CommentsComponent
. It should make update request and then modify comment on frontend.
// src/app/comments/components/comments/comments.component.ts
...
updateComment({
text,
commentId,
}: {
text: string;
commentId: string;
}): void {
this.commentsService
.updateComment(commentId, text)
.subscribe((updatedComment) => {
this.comments = this.comments.map((comment) => {
if (comment.id === commentId) {
return updatedComment;
}
return comment;
});
this.activeComment = null;
});
}
...
The only thing that is missing now is updateComment
method inside our service.
// src/app/comments/services/comments.service.ts
@Injectable()
export class CommentsService {
constructor(private httpClient: HttpClient) {}
updateComment(id: string, text: string): Observable<CommentInterface> {
return this.httpClient.patch<CommentInterface>(
`http://localhost:3000/comments/${id}`,
{
body: text,
}
);
}
}
We get id and text as arguments and make a patch request to update our body.
As you can see now we can update any comment.
Cancel button
But the next problem is that our cancel button logic is comment form is still not implemented. We need an emit in our form.
<!-- src/app/comments/components/commentForm/commentForm.component.html -->
...
<button
*ngIf="hasCancelButton"
type="button"
class="comment-form-button comment-form-cancel-button"
(click)="handleCancel.emit()"
>
Cancel
</button>
...
And we must add an output for this
// src/app/comments/components/commentForm/commentForm.component.ts
export class CommentFormComponent implements OnInit {
@Output()
handleCancel = new EventEmitter<void>();
....
}
Now in our edit form we can just set active comment back to null.
<!-- src/app/comments/components/comment/comment.component.html -->
<div class="comment-text" *ngIf="!isEditing()">{{ comment.body }}</div>
<comment-form
*ngIf="isEditing()"
...
(handleSubmit)="
updateComment.emit({ text: $event, commentId: comment.id })
"
(handleCancel)="setActiveComment.emit(null)"
></comment-form>
<div class="comment-actions">
Our cancel button is working now.
Deleting comment
Our last feature is deleting of the comment. It is exactly the same like update but we just need a comment id that we want to delete.
// src/app/comments/services/comments.service.ts
@Injectable()
export class CommentsService {
constructor(private httpClient: HttpClient) {}
deleteComment(id: string): Observable<{}> {
return this.httpClient.delete(`http://localhost:3000/comments/${id}`);
}
}
Here is our deleteComment
method inside a service which removes comment by ID. Now we need a click handler and an output.
<!-- src/app/comments/components/comment/comment.component.html -->
<div
*ngIf="canDelete"
class="comment-action"
(click)="deleteComment.emit(comment.id)"
>
Delete
</div>
</div>
And we must add an output.
// src/app/comments/components/comment/comment.component.ts
export class CommentComponent implements OnInit {
...
@Output()
deleteComment = new EventEmitter<string>();
...
}
Now we need to handle this action in root comment and in child comment.
<!-- src/app/comments/components/comments/comments.component.html -->
...
<div class="comments-container">
<comment
...
(deleteComment)="deleteComment($event)"
>
</comment>
</div>
<!-- src/app/comments/components/comment/comment.component.html -->
<comment
*ngFor="let reply of replies"
...
(deleteComment)="deleteComment.emit($event)"
></comment>
And last but not least is to create a deleteComment
function inside CommentsComponent
.
// src/app/comments/components/comments/comments.component.ts
...
deleteComment(commentId: string): void {
this.commentsService.deleteComment(commentId).subscribe(() => {
this.comments = this.comments.filter(
(comment) => comment.id !== commentId
);
});
}
...
As you can see in browser now we can remove comments. So we successfully implemented nested comments feature.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done![](https://cdn.monsterlessons-academy.com/mla-website/author-in-course.png)