The sixth part of this tutorial (Part 6a, Part 6b, and Part 6c) covered reactive forms with custom validation.

The seventh installment in the series covers deleting events, retrieving relational data from MongoDB to list events a user has RSVPed to, and silently renewing authentication tokens.

Angular: Delete Event

Let's pick up right where we left off last time. Our app's administrator can now create and update events. We also need to be able to delete events. We already added a DELETE API route in Part 6. Now let's call this endpoint in our Angular app. We'll do so in our Update Event component.

We want to make deleting an event slightly more involved than simply clicking a button. We also want to avoid showing the user a modal or pop-up message making them confirm their action. To delete an event, we'll have the user confirm the title of the event by entering it into a text field.

Note: This is how GitHub has users confirm deletion of repositories.

We don't want to confuse users about what they should be entering as a title by showing them both the update and delete forms at the same time, so we'll add tabs to the Update Event component.

Add Tabs to Update Event Component Class

The code necessary to add tabs is minimal. There are no initial data calls necessary to delete events, so our tabs for updating events will be less involved compared to the tabs for viewing an event's details vs. displaying RSVPs.

Open the update-event.component.ts file:

// src/app/pages/admin/update-event/update-event.component.ts ... export class UpdateEventComponent implements OnInit, OnDestroy { ... tabSub: Subscription; tab: string; ngOnInit() { ... // Subscribe to query params to watch for tab changes this.tabSub = this.route.queryParams .subscribe(queryParams => { this.tab = queryParams['tab'] || 'edit'; }); } ... ngOnDestroy() { ... this.tabSub.unsubscribe(); } }

We already have all the imports necessary to add tabs to our component class. We'll add properties for a tabSub subscription and tab string to store the name of the current tab.

In ngOnInit() , we'll add a subscription to queryParams to set the local tab property to the contents of the tab query parameter (or 'edit' if no parameter is available).

Finally, we'll unsubscribe from the tabSub in the ngOnDestroy() lifecycle method.

Add Tabs to Update Event Component Template

Let's add the markup necessary to display tabs and dynamic content in our Update Event component template. Open the update-event.component.html file:

<!-- src/app/pages/admin/update-event/update-event.component.html --> ... <ng-template [ngIf]="utils.isLoaded(loading)"> <div *ngIf="event" class="card"> <div class="card-header"> <ul class="nav nav-tabs card-header-tabs"> <li class="nav-item"> <a class="nav-link" [routerLink]="[]" [queryParams]="{tab: 'edit'}" [ngClass]="{'active': utils.tabIs(tab, 'edit')}">Edit</a> </li> <li class="nav-item"> <a class="nav-link" [routerLink]="[]" [queryParams]="{tab: 'delete'}" [ngClass]="{'active': utils.tabIs(tab, 'delete')}">Delete</a> </li> </ul> </div> <div class="card-block"> <!-- Edit event form --> <app-event-form *ngIf="utils.tabIs(tab, 'edit')" [event]="event"></app-event-form> <!-- Delete event --> <app-delete-event *ngIf="utils.tabIs(tab, 'delete')" [event]="event"></app-delete-event> </div> </div> <!-- Error loading event --> ... </ng-template>

We can change our <ng-template [ngIf]="event"> to <div *ngIf="event" class="card"> because this element should now render in the page as a container. Then we'll add the necessary markup to create tabs in a card header element. We'll set up the routerLink s with query parameters and [ngClass] to apply a conditional active class for the current tab. Our two tabs will be called "Edit" and "Delete."

Next, we'll add a .card-block element containing our conditional tab content. We'll show the <app-event-form> component if the active tab is edit . We'll show an <app-delete-event> component if the delete tab is active. We'll also pass the [event] to the Delete Event component, which we'll create next.

Once we have tabs in place, our Update Event component should like this by default:

Create Delete Event Component

Let's generate our Delete Event component:

$ ng g component pages/admin/update-event/delete-event

This is a child component of Update Event and provides the content for the delete tab.

Delete Event Component Class

Open the delete-event.component.ts and let's add some functionality:

// src/app/pages/admin/update-event/delete-event/delete-event.component.ts import { Component, OnDestroy, Input } from '@angular/core'; import { EventModel } from './../../../../core/models/event.model'; import { Subscription } from 'rxjs/Subscription'; import { ApiService } from './../../../../core/api.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-delete-event', templateUrl: './delete-event.component.html', styleUrls: ['./delete-event.component.scss'] }) export class DeleteEventComponent implements OnDestroy { @Input() event: EventModel; confirmDelete: string; deleteSub: Subscription; submitting: boolean; error: boolean; constructor( private api: ApiService, private router: Router) { } removeEvent() { this.submitting = true; // DELETE event by ID this.deleteSub = this.api .deleteEvent$(this.event._id) .subscribe( res => { this.submitting = false; this.error = false; console.log(res.message); // If successfully deleted event, redirect to Admin this.router.navigate(['/admin']); }, err => { console.error(err); this.submitting = false; this.error = true; } ); } ngOnDestroy() { if (this.deleteSub) { this.deleteSub.unsubscribe(); } } }

We'll import OnDestroy and Input , as well as EventModel , Subscription , ApiService , and Router (to redirect after the event has been deleted).

Our parent Update Event component sends the event as an @Input() . We expect this to have the shape EventModel . We'll set a local confirmDelete property to store the string that the user types that needs to match to the event's title to confirm the deletion. We also need a deleteSub subscription, and of course, our standard submitting and error states.

We'll add the API service and Router to the constructor. We actually don't need ngOnInit() in this component, so you'll notice we've removed the method and all references to the OnInit lifecycle hook.

Our removeEvent() method will be called from a button that is only enabled once the user has successfully inputted the event's full title in a text field. We'll send the event's _id to our deleteEvent$() API observable. If the event is successfully deleted, we'll need to redirect to the admin page since there will no longer be any event data available in the Update Event component.

In the ngOnDestroy() method, we'll check if the subscription exists, since it is only created when the user clicks the button to delete the event. If it is present, we'll unsubscribe.

Delete Event Component Template

Now open the delete-event.component.html template and add the following code:

<!-- src/app/pages/admin/update-event/delete-event.component/delete-event.component.html --> <p class="lead"> You are deleting the "<strong [innerHTML]="event.title"></strong>" event. </p> <p class="text-danger"> Deleting this event will also remove all associated RSVPs. Please proceed with caution! </p> <div class="form-group"> <label for="deleteEvent">Confirm event title:</label> <input type="text" id="deleteEvent" class="form-control" name="deleteEvent" [(ngModel)]="confirmDelete"> </div> <!-- Delete button --> <p> <button class="btn btn-danger" (click)="removeEvent()" [disabled]="confirmDelete !== event.title || submitting">Delete Event</button> <app-submitting *ngIf="submitting"></app-submitting> </p> <!-- Error deleting event --> <p *ngIf="error" class="alert alert-danger"> <strong>Oops!</strong> There was an error deleting this event. Please try again. </p>

We'll need to show the name of the event so the admin can confirm the title without too much hassle. Next, we'll display some cautionary information.

Then we'll create an extremely simple form. This form is unlike those we created for RSVPing and creating events. In fact, we don't even need true validation. We can handle everything we need with a simple [(ngModel)] directive and a comparison expression.

Note: If you prefer, you may implement a template-driven form here. You can even create a custom validator. However, for the sake of ease and simplicity, this tutorial won't take that approach.

We'll use [(ngModel)] to set up a two-way binding between the input field and our confirmDelete property. Our delete button will call the removeEvent() method when clicked but will be disabled if the value of confirmDelete is not an exact match to the event.title . As usual, we'll disable the button and display our <app-submitting> loading component if the API call is in progress.

Finally, if something went wrong deleting the event, we'll show an error.

Our Update Event component now looks like this when the Delete tab is active:

Angular: Admin Event Links

Now that the functionality for CRUD (Create Read Update Delete) is complete, let's add a couple more buttons to the Admin page to facilitate access to these features.

Add "Create" Link to Admin Page

Let's add a button to the admin page that links to the Create Event component page. Open the admin.component.html template file and add a paragraph tag with a link:

<!-- src/app/pages/admin/admin.component.html --> ... <ng-template [ngIf]="utils.isLoaded(loading)"> ... <p> <a class="btn btn-success btn-block" routerLink="/admin/event/new">+ Create New Event</a> </p> ...

This link simply leads to the Create Event page.

Add "Edit" and "Delete" Links to Admin Events List

Now let's add a link to each event in the admin component that will take us straight to the Delete tab for that event. In the admin.component.html template file, update the following:

<!-- src/app/pages/admin/admin.component.html --> ... <!-- Events listing --> <section class="list-group"> <div *ngFor="let event of fs.orderByDate(filteredEvents, 'startDatetime')" class="list-group-item list-group-item-action flex-column align-items-start"> ... <p class="mb-1"> <a class="btn btn-info btn-sm" [routerLink]="['/admin/event/update', event._id]">Edit</a> <a class="btn btn-danger btn-sm" [routerLink]="['/admin/event/update', event._id]" [queryParams]="{tab: 'delete'}">Delete</a> </p> </div> </section> ...

Let's add an "Edit" and a "Delete" button. The "Edit" button can lead to the Update Event component on its default tab. The "Delete" link should have [queryParams] set to the delete tab.

Our admin page should now look something like this:

Side Note: Recall that we already added an "Edit" link to our Event Details component. This link should now be active as well.

API: Get Events Users Has RSVPed To

Our application is still missing an important feature: a page where the user can collectively view all their RSVPs for upcoming events.

In order to achieve this, we need to get the list of a user's RSVPs and then find all the events that match the RSVP's eventId . MongoDB is not a relational database, but we can use comparison query operators to do this.

Open the server api.js file and add the following route:

// server/api.js ... /* |-------------------------------------- | API Routes |-------------------------------------- */ ... // GET list of upcoming events user has RSVPed to app.get('/api/events/:userId', jwtCheck, (req, res) => { Rsvp.find({userId: req.params.userId}, 'eventId', (err, rsvps) => { const _eventIdsArr = rsvps.map(rsvp => rsvp.eventId); const _rsvpEventsProjection = 'title startDatetime endDatetime'; let eventsArr = []; if (err) { return res.status(500).send({message: err.message}); } if (rsvps) { Event.find( {_id: {$in: _eventIdsArr}, startDatetime: { $gte: new Date() }}, _rsvpEventsProjection, (err, events) => { if (err) { return res.status(500).send({message: err.message}); } if (events) { events.forEach(event => { eventsArr.push(event); }); } res.send(eventsArr); }); } }); }); ...

We'll first use find() to get all RSVPs with a userId matching the user ID passed as a parameter to the route. We'll send a projection of eventId , which means that the returned results will only contain this single key/value. We'll then create an array of event IDs ( _eventIdsArr ) using the Array .map() method to get just the ID strings. We can then use this array to find() only events that have _id s matching items in the array.

The only properties we'll need for display of the event list in the My RSVPs component are title , startDatetime , and endDatetime . We'll create a projection for these called _rsvpEventsProjection .

After handling errors for retrieving RSVPs, we can then find() events with an _id present in the _eventIdsArr . This is done using the MongoDB $in comparison query operator. We only want upcoming events, so we'll indicate that startDatetime should be greater than or equal to ( $gte ) the current datetime. We'll then pass the _rsvpEventsProjection we just created to get back only the properties we need.

After handling errors for retrieving events, we'll push any results found to an array and send() the array.