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.
Join the DZone community and get the full member experience.
Join For FreeI 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...
— Scott Davis (@scottdavis99) August 10, 2016
/cc #angular2#rc5https://t.co/WmNalTYgTN
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
- About 30-40 minutes.
- A favorite text editor or IDE. I recommend IntelliJ IDEA and its Angular 2 TypeScript Live Templates plugin.
- Node.js and npm installed. I recommend using nvm.
- Angular CLI installed. If you don't have Angular CLI installed, install it using
npm install -g angular-cli@latest
.
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.
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.
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.
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.
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.
Published at DZone with permission of Matt Raible, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments