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

Real-World Angular Series, Part 4b: Access Management, Admin, and Detail Pages

DZone's Guide to

Real-World Angular Series, Part 4b: Access Management, Admin, and Detail Pages

We continue our deep dive into creating Angular web applications by exploring how to use Typescript, SCSS, and HTML to create a details page and basic authentication.

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

Welcome back! If you missed the first part of this article, check it out here

Angular: Event Component

In our Home and Admin components, we linked individual events by their IDs. Now it's time to create the event detail page that these links lead to.

$ ng g component pages/event

Our Event component is going to have two tabs with child components: an Event Detail component and an RSVP component. The routed Event component will provide data to the child components via input binding, so we'll manage tab navigation with route parameters.

Note: The other alternative would be to use Resolve in the parent route to fetch API data and then use child routes that observe the parent's resolve data. However, we are avoiding route resolves for reasons cited earlier in this tutorial, such as the appearance of sluggish navigation. However, feel free to explore this approach on your own if you prefer.

Use the Angular CLI to generate components for the child components like so:

$ ng g component pages/event/event-detail
$ ng g component pages/event/rsvp

Add Tab Utility to Service

We want to be able to support tabs in our application. Let's add a small tab-checking utility to our utils.service.ts:

// src/app/pages/event/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-event',
  templateUrl: './event.component.html',
  styleUrls: ['./event.component.scss']
})
export class EventComponent implements OnInit, OnDestroy {
  pageTitle: string;
  id: string;
  routeSub: Subscription;
  tabSub: Subscription;
  eventSub: Subscription;
  event: EventModel;
  loading: boolean;
  error: boolean;
  tab: string;
  eventPast: boolean;

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

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

    // Subscribe to query params to watch for tab changes
    this.tabSub = this.route.queryParams
      .subscribe(queryParams => {
        this.tab = queryParams['tab'] || 'details';
      });
  }

  private _getEvent() {
    this.loading = true;
    // GET event by ID
    this.eventSub = this.api
      .getEventById$(this.id)
      .subscribe(
        res => {
          this.event = res;
          this._setPageTitle(this.event.title);
          this.loading = false;
          this.eventPast = this.utils.eventPast(this.event.endDatetime);
        },
        err => {
          console.error(err);
          this.loading = false;
          this.error = true;
          this._setPageTitle('Event Details');
        }
      );
  }

  private _setPageTitle(title: string) {
    this.pageTitle = title;
    this.title.setTitle(title);
  }

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

}

We'll add a tabIs() method to return a boolean if the current tab matches another tab name. This is how we'll implement logic to apply classes and show or hide tab-dependent content.

Event Component Class

Open the event.component.ts file and add the following code:

// src/app/pages/event/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-event',
  templateUrl: './event.component.html',
  styleUrls: ['./event.component.scss']
})
export class EventComponent implements OnInit, OnDestroy {
  pageTitle: string;
  id: string;
  routeSub: Subscription;
  tabSub: Subscription;
  eventSub: Subscription;
  event: EventModel;
  loading: boolean;
  error: boolean;
  tab: string;
  eventPast: boolean;

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

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

    // Subscribe to query params to watch for tab changes
    this.tabSub = this.route.queryParams
      .subscribe(queryParams => {
        this.tab = queryParams['tab'] || 'details';
      });
  }

  private _getEvent() {
    this.loading = true;
    // GET event by ID
    this.eventSub = this.api
      .getEventById$(this.id)
      .subscribe(
        res => {
          this.event = res;
          this._setPageTitle(this.event.title);
          this.loading = false;
          this.eventPast = this.utils.eventPast(this.event.endDatetime);
        },
        err => {
          console.error(err);
          this.loading = false;
          this.error = true;
          this._setPageTitle('Event Details');
        }
      );
  }

  private _setPageTitle(title: string) {
    this.pageTitle = title;
    this.title.setTitle(title);
  }

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

}

As always, first, we'll add our imports. Let's dive right into the class, covering the imports as we go through the code.

This time, we won't set a pageTitle immediately. We first need to retrieve the event data from the API. We'll also grab the event's id by subscribing to the ActivatedRoute route parameters observable. We'll subscribe to the route's query parameters observable to set the tab. As usual, we'll get our event data from the API service, annotating results with the EventModel type. For this component and its children, we also want to know if the event has already ended so we'll use an eventPast property to track this with the eventPast()method we added to our UtilsService in the Angular: Create a Utility Service section of Part 3.

Note: Users should not RSVP to events in the past.

In our ngOnInit() method, we'll subscribe to the route params, set the local id property, and execute the method that fetches the event from the API (_getEvent()). Then we'll subscribe to the query params to set the tab. If there is no query parameter present, we'll default to the details tab.

In our _getEvent() method, we'll also set the pageTitle with the title of the retrieved event using a _setPageTitle() method. We'll also check to see if the event is in the past. If an error occurs, we'll set the page title to Event Details.

Finally, we'll unsubscribe from all three subscriptions when the component is destroyed.

Event Component Template

Next, let's build out the event.component.html template file:

<!-- src/app/pages/event/event.component.html -->
<app-loading *ngIf="loading"></app-loading>

<ng-template [ngIf]="utils.isLoaded(loading)">
  <h1 class="text-center">{{pageTitle}}</h1>
  <!-- Event -->
  <ng-template [ngIf]="event">
    <!-- Event is over -->
    <p *ngIf="eventPast" class="alert alert-danger">
      <strong>This event is over.</strong>
    </p>

    <div class="card">
      <!-- Event tab navigation -->
      <div class="card-header">
        <ul class="nav nav-tabs card-header-tabs">
          <li class="nav-item">
            <a
              class="nav-link"
              [routerLink]="[]"
              [queryParams]="{tab: 'details'}"
              [ngClass]="{'active': utils.tabIs(tab, 'details')}">Details</a>
          </li>
          <li class="nav-item">
            <a
              class="nav-link"
              [routerLink]="[]"
              [queryParams]="{tab: 'rsvp'}"
              [ngClass]="{'active': utils.tabIs(tab, 'rsvp')}">RSVP</a>
          </li>
        </ul>
      </div>

      <!-- Event detail tab -->
      <app-event-detail
        *ngIf="utils.tabIs(tab, 'details')"
        [event]="event"></app-event-detail>

      <!-- Event RSVP tab -->
      <app-rsvp
        *ngIf="utils.tabIs(tab, 'rsvp')"
        [eventId]="event._id"
        [eventPast]="eventPast"></app-rsvp>
    </div>
  </ng-template>

  <!-- Error loading events -->
  <p *ngIf="error" class="alert alert-danger">
    <strong>Oops!</strong> There was an error retrieving information for this event. 
  </p>
</ng-template>

Once the API call has been made and an event has been retrieved, we'll show an alert if the event is in the past. We'll then display our tabs in a Bootstrap card component. We'll use the RouterLink directive with [queryParams] to set the tab and update the active class accordingly.

Below the tab navigation, we'll load the Event Detail or RSVP component conditionally, passing in necessary data: the full [event] for Event Detail and [eventId] and [eventPast] for RSVP.

Event Component Styles

In preparation for both our Event Detail and RSVP components, let's add a ruleset to our Event component's styles to fix an issue with the way <strong>tags in Bootstrap list groups are displayed.

Open the event.component.scss file:

/* src/app/pages/event/event.component.scss */
/*--------------------
    EVENT COMPONENT
--------------------*/

:host /deep/ .list-group-item > strong {
  padding-right: 5px;
}

Let's discuss this briefly. We've already talked about the :host special selector in the past: it provides styles for the component's custom element. The /deep/(or alternatively, >>>) selector is another Angular component special selector. Normally, styles only apply to their own component. This means any styles applied to the Event component would not be inherited by the Event Detail or RSVP child components. The /deep/ selector forces a style down through all child elements. Since both of our child components will have list groups with <strong> tags in them, we can use :host /deep/ to propagate a ruleset. We might not want this style throughout our entire app, so we won't add it to our global _base.scss, but we'll need it in multiple children here.

If you visit the Event component in the browser now, you should be able to switch between the two tabs.

Note: The Event component is accessible by clicking any of the events in the Home or Admin component lists.

Aside: "Private" Events

Some of our events are set to viewPublic: false. If you recall, all this means is that these events don't show up in a public listing. They still appear in the Admin component listing and can also be direct-linked. If you're an admin and you have an event you'd like to share only with specific people, you can access the page through the Admin listing and email invitees the direct link, which might look something like this: /event/590a642ef36d281a3dc29522.

Note: It's important to understand there is no security conferred here. All events are still accessible to any authenticated user, we're just making it slightly more difficult for people to discover certain events without a direct link. If you'd like to implement security to ensure that users need a real invitation code in order to view and/or RSVP to events, that would be a great feature to work through on your own after completing this tutorial series. Keep in mind this must be implemented both on the client and server for proper security.

Add Tab Support to Auth Redirection

Now that we have working tabs, we need to update our AuthService to support redirection with query parameters. In Angular: Access Management, we added support to redirect the user to a previous route after logging in. However, at that time, we did not set this up in a way that supported query parameters.

Let's update the auth.service.ts file to do so now:

// src/app/core/auth/auth.service.ts
...
export class AuthService {
  ...
  private _getProfile(authResult) {
    // Use access token to retrieve user's profile and set session
    this._auth0.client.userInfo(authResult.accessToken, (err, profile) => {
      ...
      this._redirect();
      this._clearRedirect();
    });
  }

  ...

  private _redirect() {
    // Redirect with or without 'tab' query parameter
    // Note: does not support additional params besides 'tab'
    const fullRedirect = decodeURI(localStorage.getItem('authRedirect'));
    const redirectArr = fullRedirect.split('?tab=');
    const navArr = [redirectArr[0] || '/'];
    const tabObj = redirectArr[1] ? { queryParams: { tab: redirectArr[1] }} : null;

    if (!tabObj) {
      this.router.navigate(navArr);
    } else {
      this.router.navigate(navArr, tabObj);
    }
  }

...

Let's create a private function called _redirect(). This will assess the authRedirect string stored in local storage and split it into the appropriate Angular path and query parameters, if necessary. Then it will navigate to the route.

Now we're ready to implement our Event Detail and RSVP child components.

Angular: Event Detail Component

Our Event Detail component is the tab that will display the event information.

Event Detail Component Class

Let's open the event-detail.component.ts that we created recently:

// src/app/pages/event/event-detail/event-detail.component.ts
import { Component, Input } from '@angular/core';
import { AuthService } from './../../../auth/auth.service';
import { UtilsService } from './../../../core/utils.service';
import { EventModel } from './../../../core/models/event.model';

@Component({
  selector: 'app-event-detail',
  templateUrl: './event-detail.component.html',
  styleUrls: ['./event-detail.component.scss']
})
export class EventDetailComponent {
  @Input() event: EventModel;

  constructor(
    public utils: UtilsService,
    public auth: AuthService) { }

}

This component class only has a few simple responsibilities. It needs to accept the [event] input passed in from the parent using the @Input decorator. This data is one-way bound to the child component and can be used like any locally-declared property. We also need to make methods from UtilsService and AuthService available to the template. We do this by passing them as publicto the constructor method.

Event Detail Component Template

Let's add our template now in event-detail.component.html:

<!-- src/app/pages/event/event-detail/event-detail.component.html -->
<div class="card-block">
  <h2 class="card-title text-center">Event Details</h2>
</div>

<ul class="list-group list-group-flush">
  <li class="list-group-item">
    <strong>When:</strong>{{utils.eventDatesTimes(event.startDatetime, event.endDatetime)}}
  </li>
  <li class="list-group-item">
    <strong>Where:</strong>{{event.location}} (<a href="https://www.google.com/maps/dir//{{event.location}}" target="_blank">get directions</a>)
  </li>
</ul>

<p
  *ngIf="event.description"
  class="card-block lead"
  [innerHTML]="event.description"></p>

<div *ngIf="auth.isAdmin" class="card-footer text-right small">
  <a [routerLink]="['/admin/event/update', event._id]">Edit</a>
</div>

This child component lives inside the Event component and all it needs to do is show event information. In a Bootstrap list group, we'll display the event dates and times and the location. In order to show the dates/times in a reader-friendly way, we'll use the eventDatesTimes() method from our utility service. The location should also be followed by a link to Google Maps so the user can get directions if needed. This can open in a new tab.

The event description is set with the [innerHTML] DOM property directive so that, after automatic sanitization, it will render safe markup if any is present.

Last, we'll add a link to edit the event if the user is an admin.

Note: Right now, this "Edit" link won't go anywhere since we haven't created the Update Event component or route. We'll add the component later and then the link will function.

We're now finished with our Event Detail tab component! It should look something like this in the browser:

Angular MEAN app - event detail component

Now that we have our event details, we're ready to implement the logic to manage RSVPs next time.

Summary

In Part 4 of our Real-World Angular Series, we've covered access management with Angular, displaying admin data, and setting up detail pages with tabs. In the next part of the tutorial series, we'll tackle simple animation as well as creating and updating data with a template-driven form.

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.

Topics:
web dev ,web application development ,angular ,typescript

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}