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

Real-World Angular Series - Part 1b: MEAN Setup and Angular Architecture

DZone's Guide to

Real-World Angular Series - Part 1b: MEAN Setup and Angular Architecture

In the second part of this article, we finish setting up the basics of our Angular web application, by setting up Home and Global components.

· Web Dev Zone
Free Resource

Add user login and MFA to your next project in minutes. Create a free Okta developer account, drop in one of our SDKs to your application and get back to building.

If you missed the first half of this article, check out Part 1a here

Angular: Add a Home Component

Let's generate a component for our app's homepage. We'll run the following Angular CLI command from the root of our project:

$ ng g component pages/home

Now let's add our Home component to routing. Open the app-routing.module.ts file and import the HomeComponent:

// src/app/app-routing.module.ts
...
import { HomeComponent } from './pages/home/home.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  }
];
...

We'll set the '' path to use our new Home component. Now when we view our app in the browser, we should see our Home component rendered:

Angular app home works

Use Title Service

Let's add a <title> to our home component. We'll do this by using the Title service. We already provided Title in Angular App Setup, so we can use it immediately.

Open the home.component.ts file:

// src/app/pages/home/home.component.ts
...
import { Title } from '@angular/platform-browser';

...
export class HomeComponent implements OnInit {
  pageTitle = 'Events';

  constructor(private title: Title) { }

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

}

We'll import the Title service and then add a property called pageTitle with a value of Events. Then we'll pass the Title service to the constructor and in the ngOnInit() lifecycle method, we'll use the title.setTitle() method to change the document title to the value of our local pageTitle. By storing this title in a property, we can also use it in our component's template to set a heading. Let's do that now.

Open home.component.html and add:

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

The document title and heading should now show up in the browser. We have routing and a home component in place, so now we can get started on the global layout of our Angular app.

Angular: Layout and Global Components

Next, we need to set up our Angular app's layout and global elements, such as header, navigation, and footer. We want our app to work in any size browser, so we'll implement off-canvas navigation. To do so, we need to add some markup and functionality to our root app component as well as create a header and footer.

Let's generate the components for our app's global header and footer:

$ ng g component header
$ ng g component footer

Note: For the sake of brevity in this tutorial, we're going to ignore .spec.ts files, as we won't cover testing. However, feel free to write your own tests during development. If you wish to generate components without creating test files, you can add the --no-specflag to your g commands.

Header Component

Let's start with the Header component we just generated. Open the header.component.ts file and add:

// src/app/header/header.component.ts
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import 'rxjs/add/operator/filter';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
  @Output() navToggled = new EventEmitter();
  navOpen = false;

  constructor(private router: Router) { }

  ngOnInit() {
    // If nav is open after routing, close it
    this.router.events
      .filter(event => event instanceof NavigationStart && this.navOpen)
      .subscribe(event => this.toggleNav());
  }

  toggleNav() {
    this.navOpen = !this.navOpen;
    this.navToggled.emit(this.navOpen);
  }
}

This component handles interaction with the hamburger menu toggle and route changes from the navigation links. There are many ways that Angular can tackle component interaction. In this case, we want a parent to listen for a child event (App listens for an event from Header). We'll use the @Output decorator to create an EventEmitter to notify the parent component when the user has clicked on the hamburger menu toggle to open or close the nav panel.

The navOpen property defaults to false (the off-canvas navigation panel is closed on load). We need access to the Router to determine when navigation has taken place, so we'll add it to the constructor.

When the component initializes, we'll use the router events observable to check if navOpen is true on NavigationStart. If this is the case, we want to close the panel so it's not still open when we arrive at a new route.

The toggleNav() method is executed when the user clicks the hamburger toggle button in the template. It emits a navToggledevent that the parent component can then listen for and handle. When the user clicks the toggle, we'll change the value of navOpen and then emit the event with this new value.

This click event is implemented in the header.component.html template:

<!-- src/app/header/header.component.html -->
<header id="header" class="header">
  <div class="header-page bg-primary">
    <a class="toggle-offcanvas bg-primary" (click)="toggleNav()"><span></span></a>
    <h1 class="header-page-siteTitle">
      <a routerLink="/">RSVP</a>
    </h1>
  </div>

  <nav id="nav" class="nav" role="navigation">
    <ul class="nav-list">
      <li>
        <a
          routerLink="/"
          routerLinkActive="active"
          [routerLinkActiveOptions]="{ exact: true }">Events</a>
      </li>
    </ul>
  </nav>
</header>

Note: Please take a look at Angular's binding syntax. Parentheses () indicate event binding and square brackets [] indicate expression binding.

The <nav> element contains an unordered list of our routes. For now, we only have the "Events" link (which routes to our Home component). We'll use the routerLink and routerLinkActive directives on an anchor tag to handle links to our routes. We'll specify the route we want to navigate to and the class to be applied when that route is active.

We need to add [routerLinkActiveOptions] to ensure an exact match to the / path in the case of the root route. Otherwise, this link will receive the active class whenever any route containing / is active.

Our header.component.scss contains the styles for our hamburger menu toggle, site title, and navigation list. Open this file and add the following:

/* src/app/header/header.component.scss */
/*--------------------
       HEADER
--------------------*/

@import '../../assets/scss/partials/layout.vars';

/*-- Navigation --*/

.nav {
  background: #eee;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  box-shadow: inset -8px 0 8px -6px rgba(0,0,0,0.2);
  display: none; /* deal with FOUC */
  height: 100%;
  overflow-y: auto;
  padding: $padding-screen-small;
  position: absolute;
    top: 0;
  transform: translate3d(-100%,0,0);
  width: 270px;

  :host-context(.nav-closed) &,
  :host-context(.nav-open) & {
    display: block; /* deal with FOUC */
  }
  .active {
    font-weight: bold;
  }
  &-list {
    list-style: none;
    margin-bottom: 0;
    padding-left: 0;

    a {
      display: block;
      padding: 6px;

      &:hover,
      &:active,
      &:focus {
        text-decoration: none;
      }
    }
  }
}

/*-- Hamburger toggle --*/

.toggle-offcanvas {
  border-right: 1px solid rgba(255,255,255,.5);
  display: inline-block;
  height: 50px;
  padding: 23.5px 13px;
  position: relative;
  text-align: center;
  width: 50px;
  z-index: 100;

  span,
  span:before,
  span:after {
    background: #fff;
    border-radius: 1px;
    content: '';
    display: block;
    height: 3px;
    position: absolute;
    transition: all 250ms ease-in-out;
    width: 24px;
  }
  span {
    &:before {
      top: -9px;
    }
    &:after {
      bottom: -9px;
    }
  }
  :host-context(.nav-open) & {
    span {
      background: transparent;

      &:before,
      &:after {
        top: 0;
      }
      &:before {
        transform: rotate(45deg);
      }
      &:after {
        transform: rotate(-45deg);
      }
    }
  }
}

/*-- Header and title --*/

.header-page {
  color: #fff;
  height: 50px;
  margin-bottom: 10px;
  position: relative;

  &-siteTitle {
    font-size: 30px;
    line-height: 50px;
    margin: 0;
    padding: 0 0 0 60px;
    position: absolute;
      top: 0;
    width: 100%;
  }
  a {
    color: #fff;
    text-decoration: none;
  }
}

Note: If we need access to partials (such as _layout.vars.scss) in our encapsulated component styles, we need to import those files.

This file provides styles for the nav and header as well as CSS to animate the hamburger icon into an X and back. When accessing classes outside the current component, we can use the special selector :host-context(.ancestor-class)to reach out of the component's encapsulation and up the tree.

Footer Component

Our Footer component is very simple: just a static paragraph with a link to the source repo. Open the footer.component.html template and add:

<!-- src/app/footer/footer.component.html -->
<p class="text-center">
  MIT 2017
</p>

Note: You could put something fancier here if you wish.

Add the following styles to footer.component.scss:

/* src/app/footer/footer.component.scss */
/*--------------------
       FOOTER
--------------------*/

:host {
  display: block;
  padding-bottom: 10px;
}
p {
  font-size: 12px;
  margin-bottom: 0;
}

We'll shift the bottom margin/padding to the host element so the paragraph margin doesn't interfere with our calculation of window height in the next step.

App Component

Now we'll add the Header and Footer components to our App component, along with markup and logic to enable off-canvas navigation.

Open the app.component.ts file:

// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  navOpen: boolean;
  minHeight: string;
  private _initWinHeight = 0;

  constructor() {}

  ngOnInit() {
    Observable.fromEvent(window, 'resize')
      .debounceTime(200)
      .subscribe((event) => this._resizeFn(event));

    this._initWinHeight = window.innerHeight;
    this._resizeFn(null);
  }

  navToggledHandler(e: boolean) {
    this.navOpen = e;
  }

  private _resizeFn(e) {
    const winHeight: number = e ? e.target.innerHeight : this._initWinHeight;
    this.minHeight = `${winHeight}px`;
  }
}

We'll create a navOpen property to sync the state of the navigation panel with our Header component. This is where we'll store the event data that the Header component sends when the navToggled event is emitted. We'll use a navToggledHandler() method with an $event argument to react to this event.

We'll use an observable fromEvent to subscribe to the window resize event. We can run a _resizeFn() handler that ensures that the height of the layout canvas matches the height of the browser viewport.

Note: We could also achieve close to the same thing by setting height: 100vh on the layout canvas element in our CSS, but we're going with a JS option due to inconsistencies with vh in mobile browsers.

Open the app.component.html file and add:

<!-- src/app/app.component.html -->
<div class="layout-overflow">
  <div
    class="layout-canvas"
    [ngClass]="{'nav-open': navOpen, 'nav-closed': !navOpen}"
    [style.min-height]="minHeight">

    <!-- HEADER -->
    <app-header (navToggled)="navToggledHandler($event)"></app-header>

    <!-- CONTENT -->
    <div id="layout-view" class="layout-view">
      <router-outlet></router-outlet>
    </div>

    <!-- FOOTER -->
    <app-footer></app-footer>

  </div> <!-- /.layout-canvas -->
</div> <!-- /.layout-overflow -->

We have several layout containers to manage the off-canvas panel that slides in and out from the left side of the screen, pushing the rest of the content over. We also use the navOpen property to apply or remove .nav-open and .nav-closed classes on the <div class="layout-canvas"> element with the ngClass directive.

Note: These are the classes that we used :host-context() to access in the child Header component.

We'll also apply the calculated minHeight using a [style.min-height]property.

Note: This is a DOM property, not an HTML attribute. It's important to note the difference. Make sure to read through Binding Syntax: HTML attribute vs. DOM property to learn about this new mental model.

The <app-header> component listens for the navToggled event and then handles it with our navToggledHandler() method. We then have a container element with the router-outlet directive inside it. This is where all of our routed components will render. Finally, we have our <app-footer> component.

The styles for our app.component.scss should look like this:

/* src/app/app.component.scss */
/*--------------------
    APP COMPONENT
--------------------*/

@import '../assets/scss/partials/layout.vars';
@import '../assets/scss/partials/responsive.partial';

.layout-overflow {
  overflow: hidden; /* necessary to handle offcanvas scrollbar behavior */
}
.layout-canvas {
  background: #fff;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden; /* Safari: http://caniuse.com/#search=css3%203d */
  position: relative;
    left: 0;
  transition: transform 250ms ease;
  transform: translate3d(0,0,0);
  width: 100%;

  &.nav-open {
    transform: translate3d(270px,0,0);
  }
}
.layout-view {
  padding: $padding-screen-small;

  @include mq($large) {
    margin: 0 auto;
    max-width: 960px;
    padding: $padding-screen-large;
  }
}

When the navigation is open, the layout canvas should slide over. We also have a few styles for the container for routed components (layout view).

That's it for the layout and global navigation! The app should look like this in the browser:

animated Angular app with global off-canvas navigation

Now that we have our structure and global components in place, we're ready to start developing features next time.

Aside: Linting

The Angular CLI uses Codelyzer to lint Angular projects and raises warnings when the developer has used practices that do not adhere to the Angular Style Guide. Now might be a good time to run $ ng lint to lint our project and make sure there are no errors.

Summary

We've covered setup and dependencies for the software and tools needed for our MEAN stack application. We've also established the basic layout and architecture of our Angular front-end. In the next part of the Real-World Angular Series, we'll tackle authentication and authorization, feature planning, and data modeling.

Launch your application faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
web dev ,angular ,typescript ,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 }}