close icon
Angular

Migrating an AngularJS App to Angular - Part 1

Learn how to migrate real-world features of an AngularJS 1 application to a fresh Angular 2+ build (Part 1): setup, architecture, and components.

November 07, 2016

Check out the Real-World Angular Series to learn how to build and deploy a full-featured MEAN stack application

, from ideation to production! Start the series here: Real-World Angular Series - Part 1: MEAN Setup and Angular Architecture .

The Branding Guidelines for Angular state that version 1.x should be referred to as AngularJS, whereas all releases from version 2 and up are named Angular. This migration article will continue to use "Angular 1" to refer to AngularJS (1.x) and "Angular 2" to refer to Angular (2 and up) in order to clearly differentiate the frameworks and reduce confusion.

TL;DR: Many AngularJS 1.x developers are interested in Angular 2+, but the major differences between versions 1 and 2+ are daunting when we have so many AngularJS 1 apps already in production or maintenance. Learn how to migrate a real-world AngularJS app to a fresh Angular 2+ build: what's the same, what's similar, and what's completely different. After this tutorial, you should be prepared to tackle your own migrations as well as new Angular 2+ projects. The final code for our Angular 2+ app can be cloned from the ng2-dinos GitHub repo.


AngularJS 1 and Angular 2+

AngularJS 1.x has been a frontrunner among JavaScript frameworks over the past few years. There are thousands of production sites and apps built with Google's "superheroic MVW framework" and many more still in development. In mid-September 2016, Angular 2 was released after a lengthy period of betas and release candidates. Angular developers knew this was coming and that Angular 2 was a full rewrite and platform implementation, not an incremental update.

While Angular developers were and are eager to try Angular 2+, adoption can be challenging. Many of us have Angular 1 apps in development or maintenance and aren't in a position to migrate them to Angular 2 due to tight deadlines, budget constraints, client or management reluctance, etc. Angular 1 is still being maintained under the "AngularJS" moniker and Angular 1 apps are not about to go away.

Note: Angular 2+ uses SemVer (Semantic Versioning). This means that unlike Angular 1, there will no longer be breaking changes in point releases. There will not be an Angular 3; instead, Angular 4 will be the next major release in order to correlate to version 4 of the Angular router.

Migrate vs. Upgrade

Angular 2 is a powerful and attractive platform. Many developers will have their first opportunity to dig in when they tackle migrating an existing Angular 1 app to Angular 2. At this time, upgrading the original codebase is extremely difficult: Angular 2 is not an iteration of Angular 1. Moving between them is more straightforward when migrating to a fresh build that translates the same features on the new platform.

We'll walk through the process of migrating an Angular 1 app to Angular 2. Our Angular 1 project is relatively small but it represents a scalable, real-world Single Page Application. After following this tutorial, you should have a better understanding of how to get started with Angular 2 and how features from Angular 1 translate to Angular 2.

This tutorial assumes you are comfortable developing apps with AngularJS version 1.x. If you're looking to learn Angular 2+ without an Angular 1 comparison, check out resources like Angular 2 Authentication and Getting Started with Angular 2.

Angular 1 "ng1-dinos"

Our Angular 1 app is called ng1-dinos. The code is available at the ng1-dinos GitHub repo. It has the following features:

  • Routing (dinosaurs listing with individual detail pages)
  • Filtering (search for dinosaurs by name)
  • Calls an external Node API to get dinosaur data
  • SCSS and Bootstrap CSS
  • Custom off-canvas navigation
  • Metadata factory to provide dynamic <title>s
  • Gulp build
  • Guided by the Angular 1 Style Guide
  • Scalability

Angular 1 Setup Before Migrating to Angular 2

Let's set up ng1-dinos and get it running locally.

Dependencies

Follow the instructions on the following sites to install these dependencies:

We'll also need to clone sample-nodeserver-dinos. This local Node server will provide the external API for both our ng1-dinos and ng2-dinos apps. Follow the instructions in the sample-nodeserver-dinos README to get it installed and running on http://localhost:3001.

Install and Run "ng1-dinos"

  1. Clone ng1-dinos from GitHub to a local directory of your choosing.
  2. Run npm install from the root directory.
  3. Run gulp to serve the application (runs locally on http://localhost:8000).

Once you have ng1-dinos and the Node API running, the app should look like this in the browser:

Migrating Angular 1 app to Angular 2: Angular 1 app screenshot

Important: Take some time to familiarize with the file structure, code, and features. We won't be making any changes to this application, but it's important to get comfortable with it because everything we do in our Angular 2 app will be a migration of ng1-dinos.

Introducing "ng2-dinos"

Our migrated Angular 2 application will be called ng2-dinos. The full source code for the completed app can be cloned from the ng2-dinos GitHub repo. This app will use the same Node API. From a user's perspective, we want ng2-dinos to be indistinguishable from ng1-dinos. Under the hood, we'll rewrite the app to take advantage of the powerful new features of Angular 2.

Angular 2 brings in several technologies that ng1-dinos does not take advantage of. Instead of a Gulp build, we'll use the Angular CLI to set up and serve ng2-dinos. We're going to write the app using TypeScript and ES6 which will be transpiled by the Angular CLI.

We'll follow the Angular 2 Style Guide for the most part, with a few minor exceptions regarding file structure. For this tutorial, we want to preserve as much of a correlation with ng1-dinos as we can. This will make it easier to follow the migration of features.

Angular 2 Setup for Migrating Angular 1

Let's get started with our ng2-dinos build!

Dependencies

You should have NodeJS with npm installed already.

Next, install the Angular CLI globally with the following command:

$ npm install -g @angular/cli

Initialize ng2-dinos

The first thing we'll do is initialize our new Angular 2 app and get it running. We'll use the Angular CLI to generate a new project with SCSS support using the following command:

$ ng new ng2-dinos --style=scss

Next we can serve the app by running the following command from the root directory of our new app:

$ ng serve

We should be able to view the site in the browser at http://localhost:4200. The app should look like this:

Migrating Angular 1 app to Angular 2: Angular 2 app initialized

Take a look at the file structure for your new ng2-dinos app. You may notice there are test files and configuration, but we won't cover testing in this tutorial. If you'd like to learn more about testing Angular, check out Testing in the Angular docs and articles like Angular 2 Testing In Depth: Services and Three Ways to Test Angular 2 Components.

Linting and Style Guide

The Angular CLI provides code linting with TSLint and Codelyzer. TSLint provides TypeScript linting and Codelyzer provides TSLint rules that adhere to the Angular 2 Style Guide. We can view all of these linting rules at ng2-dinos/tslint.json. We can lint our project using the following command:

$ ng lint

This tutorial follows the Style Guide and adheres to the default rules in the TSLint config file. It's good to lint your project periodically to make sure your code is clean and free of linter errors.

Note: The Angular CLI TSLint "eofline": true rule requires files to end with a newline. This is standard convention. If you want to avoid lots of newline errors when linting, make sure that your files include this.

Customizing Our Angular Project for Migration

Now that we have a working starter project for our ng2-dinos app, we want to restructure it and add some libraries.

Bootstrap CSS

Let's start by adding the Bootstrap CSS CDN to the ng2-dinos/src/index.html file. We can also add a default <title> and some <meta> tags:

<!-- ng2-dinos/src/index.html -->
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>ng2-dinos</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="author" content="Auth0">
  <meta name="description" content="Learn about some popular as well as obscure dinosaurs!">
  <base href="/">
  <!-- Bootstrap CDN stylesheet -->
  <link
    rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
    integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

Note: The code for including the Bootstrap CSS can be found at Bootstrap CDN - Getting Started. We're using version 3.3.7 because it is latest stable at the time of writing. Please note that if you upgrade to version 4.x, there are major changes to be mindful of.

Third Party Libraries

The only third party JavaScript we want is a custom build of Modernizr. We'll be doing quite a bit of copying and pasting from ng1-dinos since we're doing a migration, so it's best to keep your local ng1-dinos project handy.

Our minified, custom Modernizr build can be found at ng1-dinos/src/assets/js/vendor/modernizr.min.js. Create the necessary folder structure in ng2-dinos:

ng2-dinos
  |-src/
    |-assets/
      |-js/
        |-vendor/
          |-modernizr.min.js

Angular CLI uses Webpack to bundle local dependencies, so we won't add Modernizr to our ng2-dinos index file. Instead, we'll add a reference to the .angular-cli.json app's scripts:

// ng2-dinos/.angular-cli.json

{...
  "apps": [
    {...
      "scripts": [
        "assets/js/vendor/modernizr.min.js"
      ],
      ...

Global SCSS

We initialized our project with the --styles=scss flag so SCSS is supported and a global styles.scss file has already been generated. However, it's currently located at the root of the ng2-dinos/src/ folder. To maintain a similar file structure with ng1-dinos, it needs to live in ng2-dinos/src/assets/scss/ instead.

Create a ng2-dinos/src/assets/scss/ folder and move the ng2-dinos/src/styles.scss file into it. Then update the .angular-cli.json app's styles reference:

// ng2-dinos/.angular-cli.json

{...
  "apps": [
    {...
      "styles": [
        "assets/scss/styles.scss"
      ],
      ...

Note: When moving or adding new files, you'll need to stop and restart the Angular CLI server (Ctrl+C, ng serve) to avoid module build errors. Changes within files are watched and live reloaded, but reorganizing the file structure can break this.

Now let's add some global SCSS from ng1-dinos. We'll copy the files and subdirectories from ng1-dinos/src/assets/css/scss/core/ to ng2-dinos/src/assets/scss/.

Note: If you paid close attention, you'll notice we've left off a folder in ng2-dinos. Our Angular 1 ng1-dinos app had a css folder with scss inside it. We don't need the css folder in ng2-dinos because of the Angular CLI Webpack bundling.

When we're done, our ng2-dinos global styles file structure should look like this:

ng2-dinos
  |-src/
    |-assets/
      |-scss/
        |-partials/
          |-_layout.vars.scss
          |-_responsive.partial.scss
        |-_base.scss
        |-_layout.scss
        |-_presentation.scss
        |-styles.scss

Now we'll @import these SCSS files in the ng2-dinos global styles.scss:

/* ng2-dinos/src/assets/scss/styles.scss */

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

// global styles
@import 'base';
@import 'presentation';
@import 'layout';

Restart the Angular CLI server and our app's background color should change to grey. This is a visual indicator that our new global styles are working. If we inspect the page, we should see the global <body> styles applied.

Finally, we'll clean up the _base.scss file. Angular 2 doesn't utilize ng-cloak so we'll remove the ng-cloak ruleset. Afterwards, this is what remains:

/* ng2-dinos/src/assets/scss/_base.scss */

/*--------------------
       BASICS
--------------------*/

/*-- Cursor --*/

a,
input[type=button],
input[type=submit],
button {
  cursor: pointer;
}

/*-- Forms --*/

input[type="text"],
input[type="number"],
input[type="password"],
input[type="date"],
select option,
textarea {
  font-size: 16px;    /* for iOS to prevent autozoom */
}

Update App File Structure

The Angular CLI creates all app files (modules, components, services, pipes, etc.) relative to ng2-dinos/src/app/. Note that the ng2-dinos app has a component (app.component.ts|.html|.scss|.spec.ts) in the root of this folder. This is our app's root component, but we want to move it into a subfolder to keep ng2-dinos organized, scalable, and correlated with ng1-dinos.

Note: Recall that this tutorial won't cover testing. The .spec.ts files have been largely removed from the sample ng2-dinos repo to make it simpler to view. The Angular CLI creates these files automatically when generating new architecture. Feel free to keep them in your project and write tests. For brevity, the rest of the tutorial will no longer mention .spec.ts files. If you're using them, just remember to include them whenever managing files.

Let's move the app.component[.html|.scss|.ts] files to a new folder: ng2-dinos/src/app/core/. The app folder's file structure should now look like this:

ng2-dinos
  |-src/
    |-app/
      |-core/
        |-app.component[.html|.scss|.ts]
      |-app.module.ts

This breaks our build. We can fix it by updating the ng2-dinos/src/app/app.module.ts file. If you have a TypeScript extension enabled in your code editor or IDE, you should see syntax highlighting where TypeScript detects problems. We need to update the path to app.component like so:

// ng2-dinos/src/app/app.module.ts

...
import { AppComponent } from './core/app.component';
...

Note: Always keep in mind that Angular 2 is very interconnected with regard to dependency imports. When we move files, we break references in other places. The CLI tells us where the problems are when we build. TypeScript code hinting in our editor can help too. To address the issue at its root, we can use additional @NgModules to manage dependencies; you can learn more by reading Use @NgModule to Manage Dependencies in your Angular 2 Apps.

That's it for setup! We can officially start migrating ng1-dinos to ng2-dinos.

Angular 2 Root App Component

In the ng1-dinos Angular 1 app, ng-app was on the <html> element. This provided Angular control over the <head>, allowing us to dynamically update the <title> with a custom metadata factory. In Angular 2, our root app component is located inside the <body>. Angular 2 provides a service to manage page <title>s and we shouldn't use an <html>-level app root anymore.

As we saw above, the body of our Angular 2 ng2-dinos index.html file looks like this:

<!-- ng2-dinos/src/index.html -->
...
<body>
  <app-root>Loading...</app-root>
</body>

In comparison, the body of our Angular 1 ng1-dinos index.html file looks like this:

<!-- ng1-dinos/src/index.html -->
...
<body>
  <div class="layout-overflow">
    <div
      class="layout-canvas"
      nav-control
      ng-class="{'nav-open': nav.navOpen, 'nav-closed': !nav.navOpen}">
      <!-- HEADER -->
      <header
        id="header"
        class="header"
        ng-include="'app/header/header.tpl.html'"></header>
      <!-- CONTENT (Angular View) -->
      <div
        id="layout-view"
        class="layout-view"
        ng-view autoscroll="true"></div>
      <!-- FOOTER -->
      <footer
        id="footer"
        class="footer clearfix"
        ng-include="'app/footer/footer.tpl.html'"></footer>
    </div> <!-- /.layout-canvas -->
  </div> <!-- /.layout-overflow -->
  ...
</body>

The layout markup, header, content, and footer children will now move to the ng2-dinos root component app.component (<app-root>).

App Component Template

Let's stub out app.component.html:

<!-- ng2-dinos/src/app/core/app.component.html -->
<div class="layout-overflow">
  <div
    class="layout-canvas"
    [ngClass]="{'nav-open': navOpen, 'nav-closed': !navOpen}">
    <!-- HEADER -->
    <!-- CONTENT -->
    <div id="layout-view" class="layout-view">
        ...content goes here...
    </div>
    <!-- FOOTER -->
  </div> <!-- /.layout-canvas -->
</div> <!-- /.layout-overflow -->

App Component Styles

We already included global SCSS for the site layout and off-canvas nav functionality. Because the styles for the layout, header, and navigation interact with each other, we won't componetize the layout styles in this tutorial. We want to maintain a fairly direct migration path with ng1-dinos, but there will be room for refactoring after the app is migrated. We won't use the app.component.scss file so let's delete it.

App Component TypeScript

Now we'll add the navOpen boolean property we referenced for controlling the .nav-open/.nav-closed classes in the app.component.html above. We also need to remove the reference to app.component.scss since we deleted that file:

// ng2-dinos/src/app/core/app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  navOpen: boolean;

  constructor() { }
}

If we restart the Angular CLI server now and inspect the DOM in the browser, we'll see a .nav-closed class on the <div class="layout-canvas"> element. We can use the inspector to change .nav-closed to .nav-open. If we do this, we should see the page content slide to the right:

Migrating Angular 1 app to Angular 2: Angular 2 app root nav open

Now we're ready to create the header.

Angular 2 Header Component

We can use the Angular CLI's g command (shortcut for generate) to generate new components for our app. Stop the server (Ctrl+C) and let's create a header component:

$ ng g component header

New components are created relative to the ng2-dinos/src/app root. The resulting output should resemble the following:

Migrating Angular 1 app to Angular 2: create Angular 2 component with Angular CLI

We can see from the terminal output that new files were created, but let's also look at the app.module.ts file so we're familiar with everything necessary for adding new components to an Angular 2 app.

app.module.ts is our app's primary @NgModule. It now looks like this:

// ng2-dinos/src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './core/app.component';
import { HeaderComponent } from './header/header.component';

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

As you can see, the HeaderComponent class is imported and has also been added to the @NgModule's declarations array.

Add Header Element to App Component Template

If we open the header.component.ts file, we can see that the @Component's selector is app-header. We generally want custom elements to be hyphenated as per the W3C spec for custom elements. This is also covered by the Angular 2 Style Guide. The Angular CLI generates new component selectors with a prefix. By default, this prefix is app. This way, we won't get conflicts with native elements when calling this component (since <header> already exists in the HTML5 spec).

Let's add <app-header> to our app.component.html:

<!-- ng2-dinos/src/app/core/app.component.html -->
...
    <!-- HEADER -->
    <app-header></app-header>
...

Header Component Template

Let's add our markup to the header component similar to ng1-dinos/src/app/header/header.tpl.html. Open header.component.html and add HTML for the header, off-canvas toggle, and navigation menu:

<!-- ng2-dinos/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 href="/">ng2-dinos</a>
    </h1>
  </div>
  <nav id="nav" class="nav" role="navigation">
    <ul class="nav-list">
      <li>
        <a href>Dinosaurs</a>
      </li>
      <li>
        <a href>About</a>
      </li>
      <li>
        <a href="https://github.com/auth0-blog/sample-nodeserver-dinos">Dino API on GitHub</a>
      </li>
    </ul>
  </nav>
</header>

This is mostly standard markup. The only Angular 2 functionality so far is a (click) binding on the link to toggle the off-canvas menu. We'll add more Angular later once we have multiple views and routing in place.

Header Component Styles

First grab the Angular 1 ng1-dinos/src/assets/css/scss/components/_nav.scss file and copy it into the ng2-dinos header component folder.

Now let's @import it and add SCSS to header.component.scss:

/* ng2-dinos/src/app/header/header.component.scss */

/*--------------------
       HEADER
--------------------*/

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

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

  &-siteTitle {
    font-size: 30px;
    line-height: 50px;
    margin: 0;
    padding: 0 0 0 50px;
    position: absolute;
      top: 0;
    text-align: center;
    width: 100%;

    a {
      color: #fff;
      text-decoration: none;
    }
  }
}

We need to make one modification in the _nav.scss file. We'll change the .nav-open & selector to :host-context(.nav-open) & instead:

/* ng2-dinos/src/app/header/_nav.scss */

...
  :host-context(.nav-open) & {
    span {
      background: transparent;

      &:before,
      &:after { ...

This has to do with how Angular 2 encapsulates DOM node styles. If you've ever used native web components or Google Polymer, you should be familiar with shadow DOM encapsulation in components. Regardless, you may want to read about View Encapsulation in Angular 2.

In a nutshell, Angular 2's default encapsulation mode is Emulated. This means styles are scoped to their components with unique attributes that Angular 2 generates. Having component-isolated styles is often very useful—except for when we want to reach up the DOM tree and have our component styles affected by ancestors.

We don't need to change View Encapsulation in the header component class though. There is only one reference to an ancestor in _nav.scss. We can use special selectors like :host-context() to look up the cascade instead.

Now the component CSS can access the .nav-open class up the DOM tree from the header component.

Note: Recall that the site layout and navigation functionality styles remained global rather than being componetized in app.component.scss (instead we deleted that file). We could have moved the sections of the global _layout.scss into different child components and replaced references to parent styles with :host-context(). We didn't do this because the goal of this tutorial is to demonstrate as close to a 1:1 migration as possible while covering many topics. When we're finished migrating the entire app, I encourage you to refactor where desirable! We'll highlight refactoring suggestions at the end of each part of this tutorial.

Angular 2 Component Interaction

Let's make our header component functional. We need the header to communicate with the root app component to implement the off-canvas navigation.

Header Component TypeScript

Open the header.component.ts file. We'll implement component communication with inputs/outputs and events. Remember that we added a click event binding to our header HTML that looked like this:

<a class="toggle-offcanvas bg-primary" (click)="toggleNav()"><span></span></a>

Note: [], (), and [()] are "binding punctuation" and refer to the direction of data flow. () indicates a binding to an event. You can read more about binding syntax in the Angular 2 docs.

There are a few things we need to do to make this event handler functional.

// ng2-dinos/src/app/header/header.component.ts

import { Component, OnInit, Output, EventEmitter } from '@angular/core';

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

  constructor() { }

  ngOnInit() {
  }

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

}

The header component is a child of the root app component. We need a way to notify the parent when the user clicks the hamburger to open or close the menu. We'll do this by emitting an event that the parent can bind to.

We'll import Output and EventEmitter from @angular/core and then create a new event emitter @Output decorator. We also need a way to track whether the navigation is open or closed, so we'll add a navOpen property that defaults to false.

Note: Notice that we didn't declare a type annotation for navOpen. This is because we initialized the property with a value. The type is inferred from this value. Adding type annotations that can be inferred automatically will result in linting errors.

Now we need to define the click event handler. We already named this function toggleNav() in our header.component.html. The function will toggle the navOpen boolean and emit the navToggled event with the current state of navOpen.

Header Communication with App Component

Next we need to listen for the navToggled event in the parent. Add the following declarative code to app.component.html:

<!-- ng2-dinos/src/app/core/app.component.html -->
...
    <!-- HEADER -->
    <app-header (navToggled)="navToggleHandler($event)"></app-header>
...

Now we'll create the navToggleHandler($event) in app.component.ts:

// ng2-dinos/src/app/core/app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  navOpen: boolean;

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

If we build now, we should be able to open and close the off-canvas navigation by clicking the hamburger icon. When open, the icon should animate into an X and the app should look like this:

Migrating Angular 1 app to Angular 2: Angular 2 app with off-canvas navigation

Everything is working correctly but this doesn't look very good. Let's fix it!

Angular 2 Observables and DOM Properties

In ng1-dinos, all off-canvas nav functionality was handled by ng1-dinos/src/app/core/ui/navControl.dir.js, including menu toggling and layout height. We've migrated the navigation functionality but we're still missing the layout height fix.

We want our minimum page height to be the height of the window no matter how tall the content is. This way, the off-canvas navigation will never look prematurely cut off. To address this, we'll use an RxJS observable and the window.resize event.

Note: In ng1-dinos, we referenced the navControl directive's DOM $element and applied min-height styles with JS. We did this to avoid an additional watcher in Angular 1. However, Angular 2's change detection is vastly improved so we can shift our concerns over watchers to other things instead.

Angular 2 strongly recommends avoiding direct DOM manipulation. There is an ElementRef class that provides access to the native element, but using it is not recommended and is usually avoidable. We'll use property data binding instead.

Add Observable to App Component TypeScript

Our app.component.ts will look like this:

// ng2-dinos/src/app/core/app.component.ts

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/debounceTime';

declare var window: any;

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

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

    this.initWinHeight = window.innerHeight;
    this.resizeFn(null);
  }

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

  private resizeFn(e) {
    let winHeight: number = e ? e.target.innerHeight : this.initWinHeight;
    this.minHeight = `${winHeight}px`;
  }
}

Let's talk about the code above.

First we'll import dependencies. We're going to use the OnInit lifecycle hook from @angular/core to manage the observable and implement initial layout height. Then we need Observable from the RxJS library which is packaged with Angular 2.

In order to avoid TypeScript Name not found errors, we'll declare the type for window to be any.

We're using an RxJS observable to subscribe to the window.resize event and execute a debounced function that sets a min-height. The window.resize event doesn't automatically fire on page load, so we need to trigger the handler manually in ngOnInit().

Note: This tutorial does not cover Reactive Programming (RP) and RxJS in depth. If RP and RxJS are new to you, please read Understanding Reactive Programming and RxJS, or for a more Angular 2-centric approach: Functional Reactive Programming for Angular 2 Developers - RxJs and Observables.

Add DOM Property to App Component Template

We can then bind minHeight to the [style.min-height] property on the layout canvas element in app.component.html:

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

Note: Angular 2 binds to DOM properties, not HTML attributes. This may seem counter-intuitive because we're declaratively adding things like [disabled] or [style.min-height] to our markup, but these refer to properties, not attributes. Please read Binding syntax: An overview to learn more.

Now our app should be the height of the window even if the content is short. If the navigation grows longer than the content, the CSS we imported from ng1-dinos will ensure that it gets a scrollbar. With the menu open, our app should look like this in the browser:

Migrating Angular 1 app to Angular 2: Angular 2 app with off-canvas navigation

We have a header, so let's add the simple footer from ng1-dinos too. Run the $ ng g command to create a new component:

$ ng g component footer

The footer.component.ts should be very simple. There's no dynamic functionality; we just need to create the component and display it. Let's simplify the FooterComponent class:

// ng2-dinos/src/app/footer/footer.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-footer',
  templateUrl: './footer.component.html',
  styleUrls: ['./footer.component.scss']
})
export class FooterComponent {}

We can copy the footer markup from ng1-dinos/src/app/footer/footer.tpl.html to our ng2-dinos footer.component.html file. We just need to update the link so that it references ng2-dinos instead of ng1-dinos:

<!-- ng2-dinos/src/app/footer/footer.component.html -->
<p>
  <small>MIT 2016 | <a href="https://github.com/auth0-blog/ng2-dinos">ng2-dinos @ GitHub</a></small>
</p>

The ng1-dinos footer SCSS comes from ng1-dinos/src/assets/css/scss/components/_footer.scss. We need to add @imports so our Angular 2 component can access global layout variables and responsive mixins. We're also going to change .footer to the special :host selector since .footer no longer exists and we need to style the component's host element:

/* ng2-dinos/src/app/footer/footer.component.scss */

/*--------------------
        FOOTER
--------------------*/

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

:host {
  padding: $padding-screen-small;
  text-align: center;

  @include mq($large) {
    padding: $padding-screen-large;
  }
}

Finally, we'll add the <app-footer> element to the app.component.html:

<!-- ng2-dinos/src/app/core/app.component.html -->
...
    <!-- FOOTER -->
    <app-footer></app-footer>
...

Restart ng serve and we should see the simple footer in our app.

Aside: Refactoring Suggestions

As mentioned before, this is a migration tutorial so one of our goals is to maintain close to 1:1 correlation with ng1-dinos while still implementing Angular 2 best practices. This will be the continued goal in subsequent parts of the tutorial. However, there are refactoring opportunities that we shouldn't ignore.

Note: You may want to wait to refactor until you complete all parts of the tutorial.

Here is my refactoring suggestion from part one of our migration tutorial:

  • Consider componetizing more global SCSS, breaking files like _layout.scss up into respective *.component.scss files and utilizing selectors like :host and :host-context().

Keep an eye out for more refactoring suggestions in the next lessons.

Conclusion

We now have the basic architecture for our ng2-dinos app! We've successfully migrated global styles, custom off-canvas navigation, header, and footer. We've covered Angular 2 setup, components, child-to-parent component communicaton, binding syntax, and even touched on observables. If we run ng lint, our app should be free of linter errors.

In the next parts of the tutorial, we'll enable navigation by creating page components and implementing routing. Then we'll call the API and use HTTP and observables to get and display dinosaur data and detail subpages, create type models, learn about filtering, implement error handling, and show loading states. We'll even address authentication.

Migrating an existing application can be a great way to learn a new framework or technology. We experience familiar and new patterns and implement real-world features. To continue our migration, please join me for Part 2 of Migrating an AngularJS App to Angular!

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon