Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Real-World Angular Series, Part 6a: Reactive Forms and Custom Validation

DZone's Guide to

Real-World Angular Series, Part 6a: Reactive Forms and Custom Validation

We continue our exploration of Angular app development, and learn how to add events to our RSVP app and add admin endpoints to an API service.

· Web Dev Zone
Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

The fifth part of this tutorial (Part 5a and Part 5b) covered simple animation and using a template-driven form to add and update data.

The sixth installment in the series covers posting data with reactive forms and implementing custom validation in Angular.

Let's pick up right where we left off last time. Let's add the API endpoints our app's administrator needs in order to create, update, and delete events.

POST a New Event

In order to add a new event in our RSVP app, we'll create a new /api/event/new endpoint in the Node API.

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

// server/api.js
...
/*
 |--------------------------------------
 | API Routes
 |--------------------------------------
 */
  ...
  // POST a new event
  app.post('/api/event/new', jwtCheck, adminCheck, (req, res) => {
    Event.findOne({
      title: req.body.title,
      location: req.body.location,
      startDatetime: req.body.startDatetime}, (err, existingEvent) => {
      if (err) {
        return res.status(500).send({message: err.message});
      }
      if (existingEvent) {
        return res.status(409).send({message: 'You have already created an event with this title, location, and start date/time.'});
      }
      const event = new Event({
        title: req.body.title,
        location: req.body.location,
        startDatetime: req.body.startDatetime,
        endDatetime: req.body.endDatetime,
        description: req.body.description,
        viewPublic: req.body.viewPublic
      });
      event.save((err) => {
        if (err) {
          return res.status(500).send({message: err.message});
        }
        res.send(event);
      });
    });
  });

  ...

Only admin users should be able to add, update, or delete events. This endpoint needs jwtCheckandadminCheck middleware functions. Then we'll use the find() method to look for an event with the request's title, location, and startDatetime. If an event exists that matches all these fields, it's safe to say that we're trying to create a duplicate of an existing event and we should send an error.

If no existingEvent can be found, then we can create a new Event() with the data from the request body and save() it to MongoDB, handling errors if necessary and sending the new event data back in the response.

PUT (Edit) Existing Event

Now add the following PUT route to edit events: /api/event/:id.

// server/api.js
...
  // PUT (edit) an existing event
  app.put('/api/event/:id', jwtCheck, adminCheck, (req, res) => {
    Event.findById(req.params.id, (err, event) => {
      if (err) {
        return res.status(500).send({message: err.message});
      }
      if (!event) {
        return res.status(400).send({message: 'Event not found.'});
      }
      event.title = req.body.title;
      event.location = req.body.location;
      event.startDatetime = req.body.startDatetime;
      event.endDatetime = req.body.endDatetime;
      event.viewPublic = req.body.viewPublic;
      event.description = req.body.description;

      event.save(err => {
        if (err) {
          return res.status(500).send({message: err.message});
        }
        res.send(event);
      });
    });
  });

  ...

Again, we need the authentication and admin middleware to secure our route. We'll pass the event ID as a route parameter and use it to fetch the event with findById(). We'll handle errors, then update this event's properties with data sent with the PUT request. After updating, we'll save() our changes and handle any errors, sending the updated event data back in the response.

DELETE an Event and Its RSVPs

Our final events operation will be to delete events. In doing so, we'll also delete all the RSVPs associated with that event. There's no point in keeping them around if the event is gone.

Add the following /api/event/:idDELETE API route to the api.js file:

// server/api.js
...
  // DELETE an event and all associated RSVPs
  app.delete('/api/event/:id', jwtCheck, adminCheck, (req, res) => {
    Event.findById(req.params.id, (err, event) => {
      if (err) {
        return res.status(500).send({message: err.message});
      }
      if (!event) {
        return res.status(400).send({message: 'Event not found.'});
      }
      Rsvp.find({eventId: req.params.id}, (err, rsvps) => {
        if (rsvps) {
          rsvps.forEach(rsvp => {
            rsvp.remove();
          });
        }
        event.remove(err => {
          if (err) {
            return res.status(500).send({message: err.message});
          }
          res.status(200).send({message: 'Event and RSVPs successfully deleted.'});
        });
      });
    });
  });

  ...

We'll pass the event ID as a route parameter, verify the user is authenticated and an admin, then findById() to fetch the event. If the event is found, we'll find() all RSVPs with an eventId property matching the event being deleted. We'll remove these associated RSVPs, and then remove the event and handle any errors. On successful deletion, a simple confirmation message is sent in the response.

Angular: Add Event Admin Endpoints to API Service

We'll now add the corresponding methods to our ApiService to call the new API endpoints we just added.

Open the api.service.ts file and add these three methods:

// src/app/core/api.service.ts
...
  // POST new event (admin only)
  postEvent$(event: EventModel): Observable<EventModel> {
    return this.authHttp
      .post(`${ENV.BASE_API}event/new`, event)
      .map(this._handleSuccess)
      .catch(this._handleError);
  }

  // PUT existing event (admin only)
  editEvent$(id: string, event: EventModel): Observable<EventModel> {
    return this.authHttp
      .put(`${ENV.BASE_API}event/${id}`, event)
      .map(this._handleSuccess)
      .catch(this._handleError);
  }

  // DELETE existing event and all associated RSVPs (admin only)
  deleteEvent$(id: string): Observable<any> {
    return this.authHttp
      .delete(`${ENV.BASE_API}event/${id}`)
      .map(this._handleSuccess)
      .catch(this._handleError);
  }

  ...

The POST and PUT methods should be type Observable<EventModel>, since the stream will return the new or updated event. The DELETE method returns a JSON message, so we'll add a type annotation of Observable<any>. We'll then pass the appropriate event object and/or event ID as parameters to the call the API endpoints.

Now we're ready to make events admin API calls in our Angular application!

Angular: Event Admin Components

We'll create three new components to support our event administration: a page to create events, a page to update or delete existing events, and an events form.

Create Event Admin Components

Let's scaffold these components with the Angular CLI now:

$ ng g component pages/admin/create-event
$ ng g component pages/admin/update-event
$ ng g component pages/admin/event-form

Note: Since only admins can create and update events, these components will live in the src/app/pages/admin folder.

Add Event Admin Routes

Next, open the app-routing.module.ts file and add the following child routes like so:

// src/app/app-routing.module.ts
...
import { CreateEventComponent } from './pages/admin/create-event/create-event.component';
import { UpdateEventComponent } from './pages/admin/update-event/update-event.component';

const routes: Routes = [
  ...,
  {
    path: 'admin',
    ...,
    children: [
      ...,
      {
        path: 'event/new',
        component: CreateEventComponent
      },
      {
        path: 'event/update/:id',
        component: UpdateEventComponent
      }
    ]
  },
  ...

We'll import the Create Event and Update Event components, then add them as children of the admin route. Their full paths will then be /admin/event/newand /admin/event/update/:id.

Create Event Component

Our Create Event component is a simple container for the event form.

Open the create-event.component.ts file and add:

// src/app/pages/admin/create-event/create-event.component.ts
...
import { Title } from '@angular/platform-browser';
...
export class CreateEventComponent implements OnInit {
  pageTitle = 'Create Event';

  constructor(private title: Title) { }

  ngOnInit() {
    this.title.setTitle(this.pageTitle);
  }

}

All we need to do in the class is set the title, which should feel quite familiar by now.

Now open the create-event.component.html template:

<!-- src/app/pages/admin/create-event/create-event.component.html -->
<h1 class="text-center">{{pageTitle}}</h1>

<app-event-form></app-event-form>

We'll set an <h1> title and then show the event form component.

Update Event Component

We'll write some code for the Update Event component to get the appropriate event so we can pass it to the event form.

Open the update-event.component.ts and let's get started:

// src/app/pages/admin/update-event/update-event.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { AuthService } from './../../../auth/auth.service';
import { ApiService } from './../../../core/api.service';
import { UtilsService } from './../../../core/utils.service';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { EventModel } from './../../../core/models/event.model';

@Component({
  selector: 'app-update-event',
  templateUrl: './update-event.component.html',
  styleUrls: ['./update-event.component.scss']
})
export class UpdateEventComponent implements OnInit, OnDestroy {
  pageTitle = 'Update Event';
  routeSub: Subscription;
  eventSub: Subscription;
  event: EventModel;
  loading: boolean;
  error: boolean;
  private _id: string;

  constructor(
    private route: ActivatedRoute,
    public auth: AuthService,
    private api: ApiService,
    public utils: UtilsService,
    private title: Title) { }

  ngOnInit() {
    this.title.setTitle(this.pageTitle);

    // Set event ID from route params and subscribe
    this.routeSub = this.route.params
      .subscribe(params => {
        this._id = params['id'];
        this._getEvent();
      });
  }

  private _getEvent() {
    this.loading = true;
    // GET event by ID
    this.eventSub = this.api
      .getEventById$(this._id)
      .subscribe(
        res => {
          this.event = res;
          this.loading = false;
        },
        err => {
          console.error(err);
          this.loading = false;
          this.error = true;
        }
      );
  }

  ngOnDestroy() {
    this.routeSub.unsubscribe();
    this.eventSub.unsubscribe();
  }

}

The Update Event component functions similarly to the Event component, using a route parameter to get the intended event ID. We'll import the usual for API subscriptions (OnDestroy, ApiService, Subscription, EventModel) and general component imports (Title, AuthService, UtilsService). We'll also need ActivatedRoute to get the event ID from the URL.

We'll subscribe to the ActivatedRoute's params to get the event ID and set it as a local private property called _id. We can then use this ID to fetch the event from the API, managing the subscription the same way we have in previous components and unsubscribing in ngOnDestroy().

Open the update-event.component.html template next and add the following:

<!-- src/app/pages/admin/update-event/update-event.component.html -->
<h1 class="text-center">{{pageTitle}}</h1>

<app-loading *ngIf="loading"></app-loading>

<ng-template [ngIf]="utils.isLoaded(loading)">
  <ng-template [ngIf]="event">
    <!-- Event form -->
    <app-event-form [event]="event"></app-event-form>
  </ng-template>

  <!-- Error loading event -->
  <p *ngIf="error" class="alert alert-danger">
    <strong>Error:</strong> Event data could not be retrieved. View <a routerLink="/admin" class="alert-link">Admin Events</a>. 
  </p>
</ng-template>

If the event API call succeeded, we'll show the event form component, passing in the [event] data to prefill our edit form. If an error occurred, we'll show a message with a link back to the Admin page.

Add Link to Header Component Template

Let's add a link to create a new event in the header.component.html template:

<!-- src/app/header/header.component.html -->
...
      <li>
        <a
          *ngIf="auth.loggedIn && auth.isAdmin"
          routerLink="/admin/event/new"
          routerLinkActive="active">Create Event</a>
      </li>
...

Like the "Admin" link, the "Create Event" link should only show when the user is logged in and an admin.

Tune in next time when we'll cover Reactive Event Form Setup and creating a Custom Form Validation. 

Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

Topics:
web dev ,angular ,web application development

Published at DZone with permission of Kim Maida, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}