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

Getting Started and Testing With Angular CLI and Angular 2 (RC5) — Part 1

DZone's Guide to

Getting Started and Testing With Angular CLI and Angular 2 (RC5) — Part 1

Check out part one of this in-depth tutorial on using Angular CLI and Angular 2 with testing. Author Matt Raible offers a nice introduction with best practices and setup.

· Web Dev Zone
Free Resource

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

I started creating Angular 2 applications when it was in beta (back in March). To keep up with Angular 2's changes, I wrote a tutorial about developing with RC1 in June. Earlier this month, RC5 was released and many things changed once again. I think Scott Davis sums it up nicely in a tweet.

They keep saying "Release Candidate", but I don't think it means what they think it means...

/cc #angular2#rc5https://t.co/WmNalTYgTN

— Scott Davis (@scottdavis99) August 10, 2016

To keep up with the rapid pace of change in Angular 2, I decided to write another tutorial, this time using Angular CLI. The biggest change I found since writing the last tutorial is testing infrastructure changes. Since Angular's Testing documentation hasn't been updated recently, hopefully this tutorial will help.

Below is a table of contents in case you want to skip right to a particular section.

  • What you'll build
  • What you'll need
  • Create your project
  • Run the application
  • Add a search feature
    • The Basics
    • The Backend
  • Add an edit feature
  • Testing and CI (to be covered in the next post)

What You'll Build

You'll build a simple web application with Angular CLI, a new tool for Angular 2 development. You'll create an application with search and edit features.

What You'll Need

The latest release of Angular CLI (beta 10) uses Angular 2 RC4. Because of this, I used the master branch of Angular CLI to create this tutorial. To do this, clone angular-cli and run npm link in the directory you cloned it into. If you have issues, see #1733.

Angular Augury is a Google Chrome Dev Tools extension for debugging Angular 2 applications. I haven't needed it much myself, but I can see how it might come in handy.

Create Your Project

Create a new project using the ng new command:

ng new ng2-demo


This will create a ng2-demo project and run npm install in it. It takes about a minute to complete, but will vary based on your internet connection speed.

[mraible:~/dev] 45s $ ng new ng2-demo
installing ng2
  create .editorconfig
  create README.md
  create src/app/app.component.css
  create src/app/app.component.html
  create src/app/app.component.spec.ts
  create src/app/app.component.ts
  create src/app/environment.ts
  create src/app/index.ts
  create src/app/shared/index.ts
  create src/favicon.ico
  create src/index.html
  create src/main.ts
  create src/system-config.ts
  create src/tsconfig.json
  create src/typings.d.ts
  create angular-cli-build.js
  create angular-cli.json
  create config/environment.dev.ts
  create config/environment.js
  create config/environment.prod.ts
  create config/karma-test-shim.js
  create config/karma.conf.js
  create config/protractor.conf.js
  create e2e/app.e2e-spec.ts
  create e2e/app.po.ts
  create e2e/tsconfig.json
  create e2e/typings.d.ts
  create .gitignore
  create package.json
  create public/.npmignore
  create tslint.json
  create typings.json
Successfully initialized git.
- Installing packages for tooling via npm
  -- es6-shim (global)
  -- angular-protractor (global dev)
  -- jasmine (global dev)
  -- selenium-webdriver (global dev)

Installed packages for tooling via npm.
[mraible:~/dev] 1m5s $


You can see the what version of Angular CLI you're using with ng --version.

$ ng --version
angular-cli: local (v1.0.0-beta.11-webpack.2, branch: master)
node: 4.4.7
os: darwin x64


Run the Application

The project is configured with a simple web server for development. To start it, run:

ng serve


You should see a screen like the one below at http://localhost:4200.

Default Homepage

You can make sure your new project's tests pass, run ng test:

$ ng test
Built project successfully. Stored in "dist/".
...
Chrome 52.0.2743 (Mac OS X 10.11.6): Executed 2 of 2 SUCCESS (0.039 secs / 0.012 secs)


Add a Search Feature

To add a search feature, open the project in an IDE or your favorite text editor. For IntelliJ IDEA, use File > New Project > Static Web and point to the ng2-demo directory.


The Basics

In a terminal window, cd into your project's directory and run the following command. This will create a search component.

$ ng g component search
installing component
  create src/app/search/search.component.css
  create src/app/search/search.component.html
  create src/app/search/search.component.spec.ts
  create src/app/search/search.component.ts
  create src/app/search/index.ts


Adding a Search Route
In previous versions of CLI, you could generate a route and a component. However, since beta 8, route generation has been disabled. This will likely be re-enabled in a future release.

The Router documentation for Angular 2 RC5 provides the information you need to setup a route to the SearchComponent you just generated. Here's a quick summary:

Create src/app/app.routing.ts to define your routes.

import { Routes, RouterModule } from '@angular/router';
import { SearchComponent } from './search/index';

const appRoutes: Routes = [
  { path: 'search', component: SearchComponent },
  { path: '', redirectTo: '/search', pathMatch: 'full' }
];

export const appRoutingProviders: any[] = [];

export const routing = RouterModule.forRoot(appRoutes);


Without the last path to redirect, there's a Cannot match any routes: '' console error.

In src/app/app.module.ts, import the two constants you exported and configure them in @NgModule:

import { routing, appRoutingProviders } from './app.routing';

import { SearchComponent } from './search/search.component';

@NgModule({
  ...
  imports: [
    ...
    routing
  ],
  providers: [appRoutingProviders],
  ...
})
export class AppModule { }


In src/app/app.component.html, add a RouterOutlet to display routes.

<!-- Routed views go here -->
<router-outlet></router-outlet>


Now that you have routing setup, you can continue writing the search feature.

To allow navigation to the SearchComponent, you can add a link in src/app/app.component.html.

<nav>
  <a routerLink="/search" routerLinkActive="active">Search</a>
</nav>


Open src/app/search/search.component.html and replace its default HTML with the following:

<h2>Search</h2>
<form>
  <input type="search" name="query" [(ngModel)]="query" (keyup.enter)="search()">
  <button type="button" (click)="search()">Search</button>
</form>
<pre>{{searchResults | json}}</pre>


If you still have ng serve running, your browser should refresh automatically. If not, navigate to http://localhost:4200, and you should see the search form.

Search component

If you want to add CSS for this components, open src/app/search/search.component.css and add some CSS. For example:

:host {
  display: block;
  padding: 0 20px;
}


This section has shown you how to generate a new component to a basic Angular 2 application with Angular CLI. The next section shows you how to create a use a JSON file and localStorage to create a fake API.

The Backend

To get search results, create a SearchService that makes HTTP requests to a JSON file. Start by generating a new service.

ng g service search


Move the generated search.service.ts and its test to app/shared/search. You will likely need to create this directory.

Then, create src/app/shared/search/data/people.json to hold your data.

[
  {
    "id": 1,
    "name": "Peyton Manning",
    "phone": "(303) 567-8910",
    "address": {
      "street": "1234 Main Street",
      "city": "Greenwood Village",
      "state": "CO",
      "zip": "80111"
    }
  },
  {
    "id": 2,
    "name": "Demaryius Thomas",
    "phone": "(720) 213-9876",
    "address": {
      "street": "5555 Marion Street",
      "city": "Denver",
      "state": "CO",
      "zip": "80202"
    }
  },
  {
    "id": 3,
    "name": "Von Miller",
    "phone": "(917) 323-2333",
    "address": {
      "street": "14 Mountain Way",
      "city": "Vail",
      "state": "CO",
      "zip": "81657"
    }
  }
]


Modify src/app/shared/search/search.service.ts and provide Http as a dependency in its constructor. In this same file, create a getAll() method to gather all the people. Also, define the Address and Person classes that JSON will be marshalled to.

import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';

@Injectable()
export class SearchService {
  constructor(private http: Http) {}

  getAll() {
    return this.http.get('app/shared/search/data/people.json').map((res: Response) => res.json());
  }
}

export class Address {
  street: string;
  city: string;
  state: string;
  zip: string;

  constructor(obj?: any) {
    this.street = obj && obj.street || null;
    this.city = obj && obj.city || null;
    this.state = obj && obj.state || null;
    this.zip = obj && obj.zip || null;
  }
}

export class Person {
  id: number;
  name: string;
  phone: string;
  address: Address;

  constructor(obj?: any) {
    this.id = obj && Number(obj.id) || null;
    this.name = obj && obj.name || null;
    this.phone = obj && obj.phone || null;
    this.address = obj && obj.address || null;
  }
}


To make these classes available for consumption by your components, edit src/app/shared/index.ts and add the following:

export * from './search/search.service';


In search.component.ts, add imports for these classes.

import { Person, SearchService } from '../shared/index';


You can now add query and searchResults variables. While you're there, modify the constructor to inject the SearchService.

export class SearchComponent implements OnInit {
  query: string;
  searchResults: Array<Person>;

  constructor(private searchService: SearchService) {}


Then implement the search() method to call the service's getAll() method.

search(): void {
  this.searchService.getAll().subscribe(
    data => { this.searchResults = data; },
    error => console.log(error)
  );
}


At this point, you'll likely see the following message in your browser's console.

ORIGINAL EXCEPTION: No provider for SearchService!


To fix the "No provider" error from above, update app.component.ts to import the SearchService and add the service to the list of providers.

import { SearchService } from './shared/index';

@Component({
  ...
  styleUrls: ['app.component.css'],
  viewProviders: [SearchService]
})


Now clicking the search button should work. To make the results look better, remove the <pre> tag and replace it with a <table>.

<table *ngIf="searchResults">
  <thead>
  <tr>
    <th>Name</th>
    <th>Phone</th>
    <th>Address</th>
  </tr>
  </thead>
  <tbody>
  <tr *ngFor="let person of searchResults; let i=index">
    <td>{{person.name}}</td>
    <td>{{person.phone}}</td>
    <td>{{person.address.street}}<br/>
      {{person.address.city}}, {{person.address.state}} {{person.address.zip}}
    </td>
  </tr>
  </tbody>
</table>


Then add some additional CSS to improve its table layout.

table {
  margin-top: 10px;
  border-collapse: collapse;
}

th {
  text-align: left;
  border-bottom: 2px solid #ddd;
  padding: 8px;
}

td {
  border-top: 1px solid #ddd;
  padding: 8px;
}


Now the search results look better.

Search Results

But wait, we still don't have search functionality! To add a search feature, add a search() method to SearchService.

search(q: string) {
  if (!q || q === '*') {
    q = '';
  } else {
    q = q.toLowerCase();
  }
  return this.getAll().map(data => {
    let results: any = [];
    data.map(item => {
      if (JSON.stringify(item).toLowerCase().includes(q)) {
        results.push(item);
      }
    });
    return results;
  });
}


Then refactor SearchComponent to call this method with its query variable.

search(): void {
  this.searchService.search(this.query).subscribe(
    data => { this.searchResults = data; },
    error => console.log(error)
  );
}


Now, search results will be filtered by the query value you type in.

This section showed you how to fetch and display search results. The next section builds on this and shows how to edit and save a record.

Add an Edit Feature

Modify search.component.html to add a click handler for editing a person.

 <td><a (click)="onSelect(person)">{person.name}</a></td> 


In previous versions of Angular 2, you could embed a link with parameters directly into the HTML. For example:

<a [routerLink]="['/edit', person.id]">


Unfortunately, this doesn't work with RC5. Another issue is adding href="" causes the page to refresh. Without href, the link doesn't look like a link. If you know of a solution to this problem, please send me a pull request.

Then add onSelect(person) to search.component.ts. You'll need to import Router and set it as a local variable to make this work.

import { Router } from '@angular/router';
...
export class SearchComponent implements OnInit {
  ...

  constructor(private searchService: SearchService, private router: Router) { }

  ...

  onSelect(person: Person) {
    this.router.navigate(['/edit', person.id]);
  }
}

Run the following command to generate an EditComponent.

$ ng g component edit
installing component
  create src/app/edit/edit.component.css
  create src/app/edit/edit.component.html
  create src/app/edit/edit.component.spec.ts
  create src/app/edit/edit.component.ts
  create src/app/edit/index.ts


Add a route for this component in app.routing.ts:

import { EditComponent } from './edit/index';

const appRoutes: Routes = [
  { path: 'search', component: SearchComponent },
  { path: 'edit/:id', component: EditComponent },
  { path: '', redirectTo: '/search', pathMatch: 'full' }
];


Update src/app/edit/edit.component.html to display an editable form. You might notice I've added id attributes to most elements. This is to make things easier when writing integration tests with Protractor.

<div *ngIf="person">
  <h3>{{editName}}</h3>
  <div>
    <label>Id:</label>
    {{person.id}}
  </div>
  <div>
    <label>Name:</label>
    <input [(ngModel)]="editName" name="name" id="name" placeholder="name"/>
  </div>
  <div>
    <label>Phone:</label>
    <input [(ngModel)]="editPhone" name="phone" id="phone" placeholder="Phone"/>
  </div>
  <fieldset>
    <legend>Address:</legend>
    <address>
      <input [(ngModel)]="editAddress.street" id="street"><br/>
      <input [(ngModel)]="editAddress.city" id="city">,
      <input [(ngModel)]="editAddress.state" id="state" size="2">
      <input [(ngModel)]="editAddress.zip" id="zip" size="5">
    </address>
  </fieldset>
  <button (click)="save()" id="save">Save</button>
  <button (click)="cancel()" id="cancel">Cancel</button>
</div>


Modify EditComponent to import model and service classes and to use the SearchService to get data.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Address, Person, SearchService } from '../shared/index';
import { Subscription } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-edit',
  templateUrl: 'edit.component.html',
  styleUrls: ['edit.component.css']
})
export class EditComponent implements OnInit, OnDestroy {
  person: Person;
  editName: string;
  editPhone: string;
  editAddress: Address;

  sub: Subscription;

  constructor(private route: ActivatedRoute,
              private router: Router,
              private service: SearchService) {
  }

  ngOnInit() {
    this.sub = this.route.params.subscribe(params => {
      let id = + params['id']; // (+) converts string 'id' to a number
      this.service.get(id).subscribe(person => {
        if (person) {
          this.editName = person.name;
          this.editPhone = person.phone;
          this.editAddress = person.address;
          this.person = person;
        } else {
          this.gotoList();
        }
      });
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }

  cancel() {
    this.router.navigate(['/search']);
  }

  save() {
    this.person.name = this.editName;
    this.person.phone = this.editPhone;
    this.person.address = this.editAddress;
    this.service.save(this.person);
    this.gotoList();
  }

  gotoList() {
    if (this.person) {
      this.router.navigate(['/search', {term: this.person.name} ]);
    } else {
      this.router.navigate(['/search']);
    }
  }
}


Modify SearchService to contain functions for finding a person by their id, and saving them. While you're in there, modify the search() method to be aware of updated objects in localStorage.

search(q: string) {
  if (!q || q === '*') {
    q = '';
  } else {
    q = q.toLowerCase();
  }
  return this.getAll().map(data => {
    let results: any = [];
    data.map(item => {
      // check for item in localStorage
      if (localStorage['person' + item.id]) {
        item = JSON.parse(localStorage['person' + item.id]);
      }
      if (JSON.stringify(item).toLowerCase().includes(q)) {
        results.push(item);
      }
    });
    return results;
  });
}

get(id: number) {
  return this.getAll().map(all => {
    if (localStorage['person' + id]) {
      return JSON.parse(localStorage['person' + id]);
    }
    return all.find(e => e.id === id);
  });
}

save(person: Person) {
  localStorage['person' + person.id] = JSON.stringify(person);
}


You can add CSS to src/app/edit/edit.component.css if you want to make the form look a bit better.

:host {
  display: block;
  padding: 0 20px;
}

button {
  margin-top: 10px;
}


At this point, you should be able to search for a person and update their information.

Edit form

The <form> in src/app/edit/edit.component.html calls a save() function to update a person's data. You already implemented this above. The function calls a gotoList() function that appends the person's name to the URL when sending the user back to the search screen.

gotoList() {
  if (this.person) {
    this.router.navigate(['/search', {term: this.person.name} ]);
  } else {
    this.router.navigate(['/search']);
  }
}


Since the SearchComponent doesn't execute a search automatically when you execute this URL, add the following logic to do so in its constructor.

import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
...
  sub: Subscription;

  constructor(private searchService: SearchService, private router: Router, private route: ActivatedRoute) {
    this.sub = this.route.params.subscribe(params => {
      if (params['term']) {
        this.query = decodeURIComponent(params['term']);
        this.search();
      }
    });
  }


You'll want to implement OnDestroy and define the ngOnDestroy method to clean up this subscription.

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

export class SearchComponent implements OnInit, OnDestroy {
...
  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}


After making all these changes, you should be able to search/edit/update a person's information. If it works - nice job!

Conclusion

Wait, we're not done yet! Stay tuned for part 2 where we'll continue by going over automated testing and continuous integration in Angular 2.

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
angular ,javascript ,web developement ,html5 ,continuous integration

Published at DZone with permission of Matt Raible, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

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

{{ parent.tldr }}

{{ parent.urlSource.name }}