AngularJS is a popular framework used for building single-page applications. One great benefit of using Angular is that it is easy to incorporate automated testing.
I have been using Angular on various projects for a few years now, so naturally I was curious to learn what’s new in Angular 2. In this article, I will be walking through the process of creating a simple Angular 2 application with integrated unit testing.
What We Are Building
For this article we will be building a simple employee directory web application. This will be a straightforward CRUD app that will display a list of our employee data and allow users to add, edit, and remove employees. You can find the full source code here.
Below is our file structure:
app/
employees/
add/
employee-add.component.html
employee-add.component.ts
data/
employee-data.ts
mock-data.ts
edit/
employee-edit.component.html
employee-edit.component.ts
models/
employee.ts
services/
employee.service.ts
app.component.ts
main.ts
test/
employees/
add/
employee-add.component.spec.ts
edit/
employee-edit.component.spec.ts
services/
employee.service.component.spec.ts
employees.component.spec.ts
index.html
karma-test-shim.js
kara.conf.js
package.json
tsconfig.json
typings.json
Our test folder mirrors the structure of our app folder so that it is easy to see what a spec is testing.
Typescript
Most of the current Angular 2 documentation is written in TypeScript, so I will be using it in this project. TypeScript gives the added benefit of ECMAScript 2015 features and strong object types. TypeScript files have a .ts
extension and are compiled to .js
files.
The compiler will give you errors that I’ve found to be useful in debugging as I have put this project together. You can read more about TypeScript at its website.
Test Driven Development
Jasmine and Karma test runner will serve as our tools for unit testing. I will be using the test-driven development approach to build this application.
First we write unit tests that will fail initially and then build the components out until we have passing tests and then refactor as necessary. I’ve found this to be a useful approach as it encourages you to write the minimum amount of code necessary to get tests passing and work in small units.
Let’s get started!
To get the project up and running, I’ve used the 5 Minute QuickStart from the Angular 2 documentation site.
I’ve slightly modified the original package.json
file to add Karma to the devDependencies
. Running the command npm install
will install our dependencies we need to test and run our application. I’ve also added the test
command to scripts
. The command npm test
will run the TypeScript compiler and then run our unit tests. npm start
will start our development server for running our application locally.
package.json:
{
"name": "angular2-quickstart",
"version": "1.0.0",
"scripts": {
"start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
"tsc": "tsc",
"tsc:w": "tsc -w",
"lite": "lite-server",
"typings": "typings",
"test": "tsc && karma start karma.conf.js",
"postinstall": "typings install"
},
"license": "ISC",
"dependencies": {
"angular2": "2.0.0-beta.15",
"systemjs": "0.19.26",
"es6-shim": "^0.35.0",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.2",
"zone.js": "0.6.10"
},
"devDependencies": {
"concurrently": "^2.0.0",
"lite-server": "^2.2.0",
"typescript": "^1.8.10",
"typings": "^0.7.12",
"jasmine-core": "^2.4.1",
"karma-jasmine": "^0.3.8",
"karma-chrome-launcher": "^0.2.3",
"karma-coverage": "^0.5.5",
"karma": "^0.13.22",
"jasmine": "^2.4.1"
}
}
Karma Configuration
There are two files in the source used to configure Karma to run our tests: karma.conf.js
and karma-test-shim.js
. The important part here is having the correct path to our test spec files and all of our app source files. These files can be found in the full source code.
Angular App and Routing Setup
Now that we have our testing configured, we will write the code to initialize our Angular 2 application along with the router for the app. Below is the code for our main app file. You’ll notice imports for the view components that correspond to what used to be controllers in version 1. I will talk about those changes later.
The first thing we do is set up our @RouteConfig
. Each route is given a path, link name, and the view component that will be used. Also notice the useAsDefault
can be set to the route to be loaded as a default on startup.
Next we set up the @Component
. This one is pretty simple. The selector property is the DOM element where the component template will inserted. Our template for this component is pretty simple. Notice backticks can be used to enclose multi-line templates. The router-outlet
element is important here. This is where our view components from our router will go. Also we declare any directives and service providers here as well.
app.component.ts
import {Component} from 'angular2/core';
import {EmployeeService} from './employees/services/employee.service';
import {EmployeesComponent} from './employees/employees.component';
import {EmployeeEditComponent} from './employees/edit/employee-edit.component';
import {EmployeeAddComponent} from './employees/add/employee-add.component';
import {RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS} from 'angular2/router';
@RouteConfig([
{
path: '/employees',
name: 'Employees',
component: EmployeesComponent,
useAsDefault: true
},
{
path: '/edit/:id',
name: 'Edit',
component: EmployeeEditComponent
},
{
path: '/add',
name: 'Add',
component: EmployeeAddComponent
}
])
@Component({
selector: 'directory-app',
template: `
<h1>{{title}}</h1>
<router-outlet></router-outlet>
`,
directives: [
ROUTER_DIRECTIVES
],
providers: [
ROUTER_PROVIDERS,
EmployeeService
]
})
export class AppComponent { }
To kick off the application in our main.ts
file, all we need to do is import the AppComponent
we just built and then pass it to the bootstrap
function to get things started.
main.ts
import {bootstrap} from 'angular2/platform/browser';
import {AppComponent} from './app.component';
bootstrap(AppComponent);
Employee Data Model
Below is the data model we will be using throughout the project. Notice that each property is assigned a data type. If we try to assign a data type that is not expected, the TypeScript compiler will throw an error.
app/employees/data/employee.ts
export class Employee {
id: number;
firstName: string;
lastName: string;
phone: string;
email: string;
createDate: string;
}
Our First Test
Now that we have a foundation for the application in place, it is time to write our first test and build out the employee service. This service will be responsible for interacting with the data source. We will need our service to be able to perform the basic CRUD operations. Setting up the test for the service is straightforward since it has no dependencies that we need to inject for the tests. We simply create a new EmployeeService
and set the data source to some mock data.
test/employees/services/employees.service.spec.ts
import {Employee} from '../../../app/employees/models/employee';
import {EmployeeService} from '../../../app/employees/services/employee.service';
import {MOCK_DATA} from '../../../app/employees/data/mock-data';
describe('Employee Service Tests', () => {
let employeeService = new EmployeeService();
employeeService.data = MOCK_DATA;
});
This is the mock testing data:
app/employees/data/mock-data.ts
export var MOCK_DATA: Employee[] = [
{
"id": 1,
"firstName": "Test1",
"lastName": "Employee1",
"phone": "111-111-1111",
"email": "test1@employee.com",
"createDate": "1/1/1999"
},
{
"id": 2,
"firstName": "Test2",
"lastName": "Employee2",
"phone": "111-111-1112",
"email": "test2@employee.com",
"createDate": "1/1/2002"
},
{
"id": 3,
"firstName": "Test3",
"lastName": "Employee3",
"phone": "111-111-1113",
"email": "test3@employee.com",
"createDate": "1/1/2006"
}
];
Let’s break down the first test case. We pass in a description for the ‘what is being tested.’ Take note of the done =>
parameter used for this test. A test with this parameter is used for testing asynchronous methods. Since our service will be returning a promise, the data will not be available immediately.
In this case, Jasmine will wait for us to call the done()
method to finish executing our test case rather than moving on immediately. First, we write a simple test for getting the list of employee data objects.
it('returns a list of employees', done => {
employeeService.getEmployees()
.then(employees => {
expect(employees.length).toBeDefined();
expect(employees.length).toBe(3);
done();
})
.catch(error => done.fail('Error') );
});
Time to finish out the rest of the service tests here. We would also expect the service to have the ability to get a single employee by ID, add a new employee, and remove an existing employee.
it('returns a single employee by id', done => {
let testEmployee = employee => {
expect(employee).toBeDefined();
expect(employee.firstName).toBe('Test2');
expect(employee.lastName).toBe('Employee2');
done();
};
employeeService.getEmployee(2)
.then(testEmployee)
.catch(error => done.fail('Error') );
});
it('add a new employee', done => {
let newEmployee:Employee = new Employee();
let testNewEmployee = employee => {
expect(employee).toBeDefined();
expect(employee.firstName).toBe('John');
expect(employee.lastName).toBe('Doe');
done();
};
let addEmployeeCallback = () => {
employeeService.getEmployee(222)
.then(testNewEmployee)
.catch(error => done.fail('Error') );
};
newEmployee.id = 222;
newEmployee.firstName = 'John';
newEmployee.lastName = 'Doe';
employeeService.addEmployee(newEmployee)
.then(addEmployeeCallback)
.catch(error => done.fail('Error') );
});
it('remove an employee', done => {
let employeeCount = 0;
let postRemoveCallback = () => {
employeeService.getEmployees()
.then(postEmployees => {
expect(postEmployees.length).toBe(employeeCount - 1);
done();
})
.catch(error => done.fail('Error') );
};
let getEmployeeCallback = employee => {
employeeService.removeEmployee(employee)
.then(postRemoveCallback)
.catch(error => done.fail('Error') );
};
let preRemoveCallback = preEmployees => {
employeeCount = preEmployees.length;
employeeService.getEmployee(1)
.then(getEmployeeCallback)
.catch(error => done.fail('Error') );
}
employeeService.getEmployees()
.then(preRemoveCallback)
.catch(error => done.fail('Error') );
});
Before we can run these service tests, we will need to scaffold out our service.
app/employees/services/employee.service.ts
import {Injectable} from 'angular2/core';
import {Employee} from '../models/employee';
import {EMPLOYEES} from '../data/employee-data'
@Injectable()
export class EmployeeService {
NEW_ID = 16;
data = EMPLOYEES
constructor() {}
getEmployees() {
return Promise.resolve([new Employee()]);
}
getEmployee(id: number) {
return Promise.resolve(new Employee());
}
addEmployee(employee: Employee) {
return Promise.resolve();
}
removeEmployee(employee: Employee) {
return Promise.resolve();
}
}
In Angular 2 @Injectable
is used for creating services. We add this notation to the EmployeeService
class as shown above. We are using JSON data from a file in our project. Normally a service like this would be interacting with a server through AJAX calls for receiving and persisting data. But for the purposes of this article we are keeping things simple.
Our service test is written and the service is scaffolded out. If we run the npm test
command, we should see that our tests are being run, but none of them are passing, which is what would be expected at this point. Let’s flesh out our service methods so we can get those tests passing.
getEmployees() {
return Promise.resolve(this.data);
}
For the getEmployees
we simply return our data. Run our test again and we will have our first passing test case.
No time to celebrate. Let’s get the rest passing.
getEmployee(id: number) {
return Promise.resolve(this.data).then(
employees => employees.filter(employee => employee.id === id)[0]
)
}
addEmployee(employee: Employee) {
let today = new Date();
let month = today.getMonth() + 1;
let date = today.getDate();
let year = today.getFullYear();
if (!employee.id) {
employee.id = this.NEW_ID++;
}
if (!employee.createDate) {
employee.createDate = month + '/' + date + '/' + year;
}
return Promise.resolve(this.data)
.then(employees => employees.push(employee));
}
removeEmployee(employee: Employee) {
let index = this.data.indexOf(employee);
return Promise.resolve(this.data)
.then(employees => employees.splice(index, 1));
}
You’ll notice one of the nice features of using TypeScript is that we can use arrow =>
functions. This is simply a shorthand way of writing functions. We make use of these for our callbacks above. Now if we try running our tests again they should all pass this time. Now we have a fully functioning service and an automated test to let us now if any of our changes in the future break our base functionality. Time for us to move on to our view components next.
Make Components…not Controllers
We are ready to build our views. In version 1 a view would consist of a controller and the $scope
object that would keep track of all the view data/functions. Version 2 does away with all of that. Instead of controllers we will be building components.
Let’s get started by building our test for our first component. This view will be responsible for displaying the list of employee data and will be the page our users initially land on.
test/employees/employees.component.spec.ts
import {provide} from 'angular2/src/core/di/provider';
import {ApplicationRef} from 'angular2/core';
import {RootRouter} from 'angular2/src/router/router';
import {it, describe, expect, inject, beforeEach, beforeEachProviders, MockApplicationRef} from 'angular2/testing';
import {EmployeeService} from '../../app/employees/services/employee.service';
import {EmployeesComponent} from '../../app/employees/employees.component';
import {AppComponent} from '../../app/app.component';
import {Location, Router, ROUTER_PROVIDERS, ROUTER_PRIMARY_COMPONENT, APP_BASE_HREF} from 'angular2/router';
import {SpyLocation} from 'angular2/src/mock/location_mock';
describe('Employee Component Tests', () => {
let employeesComponent: EmployeesComponent;
let location:Location;
beforeEachProviders(() => [
ROUTER_PROVIDERS,
provide(Location, {useClass: SpyLocation}),
provide(Router, {useClass: RootRouter}),
provide(APP_BASE_HREF, {useValue: '/'}),
provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppComponent}),
provide(ApplicationRef, {useClass: MockApplicationRef}),
provide(EmployeeService, {useClass: EmployeeService}),
provide(EmployeesComponent, {useClass: EmployeesComponent})
]);
beforeEach(inject([EmployeesComponent, Location], (ec, l) => {
employeesComponent = ec;
location = l;
}))
});
This is the set up for our employee component test spec. As you can see it is slightly more complicated than the set up for our previous test. This is due to our view components having the Router
as a dependency that we will need to inject for testing purposes. We set up our injector providers in the beforeEachProviders
call. Values are specified for what Angular should be injecting when we create our component in the test. In beforeEach
we use inject to create our EmployeeComponent
for testing. The Location
object will be used for testing if our Router
is taking us to the proper paths. Now we are ready to write test cases for our view. The main functions we want to test on this component are retrieving employee data on initialization and allowing our users to navigate to the edit and add employee views.
it('should fetch the employee list on init', done => {
let testEmployees = () => {
expect(employeesComponent.employees.length).toBe(15);
done();
};
employeesComponent.ngOnInit();
setTimeout(testEmployees);
});
it('should navigate to the edit page', done => {
let testNavigation = () => {
expect(location.path()).toBe('/edit/55');
done();
};
employeesComponent.goToEdit(55);
setTimeout(testNavigation);
});
it('should navigate to the add a new employee page', done => {
let testNavigation = () => {
expect((location as any).path()).toBe('/add');
done();
};
employeesComponent.goToAdd();
setTimeout(testNavigation);
});
Now we will scaffold out our component. A couple of intereating things going on here. The EmployeesComponent
implements the OnInit
class. What this does is when the component is initialized, it will automatically call ngOnInit()
. This is where we will instruct our component to retsieve our data for display on the page. Also, in the constructor Angular’s dependency injection will provide our component with the EmployeeService
that we build previously and the Router
object so we can navigate our users to other views.
app/employees/employees.component.ts
import {Component, OnInit} from 'angular2/core';
import {Employee} from './models/employee';
import {EmployeeService} from './services/employee.service'
import {Router} from 'angular2/router';
@Component({
templateUrl: 'app/employees/employees.component.html',
directives: []
})
export class EmployeesComponent implements OnInit {
title = 'Employee Directory';
employees: Employee[];
constructor(
private _employeeService: EmployeeService,
private _router: Router
) {}
getEmployees() {}
ngOnInit() {}
deleteEmployee(employee: Employee) {}
goToEdit(id: number) {}
goToAdd() {}
}
Let’s fix our failing unit tests be fleshing out our compotent methods:
getEmployees() {
this._employeeService.getEmployees()
.then(employees => this.employees = employees);
}
ngOnInit() {
this.getEmployees();
}
deleteEmployee(employee: Employee) {
this._employeeService.removeEmployee(employee);
}
goToEdit(id: number) {
this._router.navigate(['Edit', { id: id }]);
}
goToAdd() {
this._router.navigate(['Add']);
}
Nothing too complicated going on here. We interact with our EmployeeService
to retrieve or remove Employees and use our Router
to move our users to other views. Something else we should look at is the HTML template for this view:
app/employees/employees.component.html
<div>
<h1>{{title}}</h1>
<table class="table table-striped">
<thead>
<tr>
<td>Name</td>
<td>Phone</td>
<td>Email</td>
<td>Created Date</td>
<td class="text-right">
<button (click)="goToAdd()" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon-plus"></span>
</button>
</td>
</tr>
</thead>
<tbody>
<tr *ngFor="#employee of employees">
<td>{{employee.lastName}}, {{employee.firstName}}</td>
<td>{{employee.phone}}</td>
<td>{{employee.email}}</td>
<td>{{employee.createDate}}</td>
<td class="text-right">
<button (click)="goToEdit(employee.id)" class="btn btn-success btn-sm">
<span class="glyphicon glyphicon-pencil"></span>
</button>
<button (click)="deleteEmployee(employee)" class="btn btn-danger btn-sm">
<span class="glyphicon glyphicon-remove"></span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
If you are familiar with version 1, you’ll notice the markup for event handlers and loops have changed. Rather than ng-click
, a click event is set up using (click)
. A loop is declared using *ngFor="#item of list"
. The brackets {{ }}
for displaying data still work the same way.
Add and Edit Components
For the sake of brevity I will not go through all of the code for the add and edit view components, but the code can be found below. Instead I’ll go over a few specific pieces with parts that haven’t already been covered.
test/employees/add/employee-add.component.spec.ts
describe('Employee Add Component Tests', () => {
let employeeAddComponent: EmployeeAddComponent;
let employeeService: EmployeeService;
let location:Location;
beforeEachProviders(() => [
ROUTER_PROVIDERS,
provide(Location, {useClass: SpyLocation}),
provide(Router, {useClass: RootRouter}),
provide(APP_BASE_HREF, {useValue: '/'}),
provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppComponent}),
provide(ApplicationRef, {useClass: MockApplicationRef}),
provide(EmployeeService, {useClass: EmployeeService}),
provide(EmployeeAddComponent, {useClass: EmployeeAddComponent})
]);
beforeEach(inject([EmployeeAddComponent, EmployeeService, Location], (eac, es, l) => {
employeeAddComponent = eac;
employeeService = es;
location = l;
}))
it('should create a new employee', done => {
let employeeCount:number;
let newEmployee: Employee;
let testNavigation = () => {
expect(location.path()).toBe('/employees');
done();
};
let postAddCallback = employees => {
expect(employees.length).toBe(16);
setTimeout(testNavigation);
employeeService.removeEmployee(newEmployee);
};
let preAddCallback = employees => {
employeeCount = employees.length;
newEmployee = employeeAddComponent.newEmployee;
employeeAddComponent.saveEmployee({});
employeeService.getEmployees()
.then(postAddCallback)
.catch(error => done.fail('Error') );
};
employeeAddComponent.ngOnInit();
employeeService.getEmployees()
.then(preAddCallback)
.catch(error => done.fail('Error') );
});
it('should navigate to the employee list page on cancel', done => {
let testNavigation = () => {
expect(location.path()).toBe('/employees');
done();
};
employeeAddComponent.cancelAdd({});
setTimeout(testNavigation);
});
});
add/employees/add/employee-add.component.ts
import {Component, OnInit} from 'angular2/core';
import {Employee} from '../models/employee';
import {Router} from 'angular2/router';
import {EmployeeService} from '../services/employee.service';
@Component({
templateUrl: 'app/employees/add/employee-add.component.html'
})
export class EmployeeAddComponent implements OnInit {
title = 'Add New Employee'
newEmployee: Employee;
constructor(
private _employeeService: EmployeeService,
private _router: Router
) {}
ngOnInit() {
this.newEmployee = new Employee();
}
saveEmployee(event) {
let _this = this;
this._employeeService.addEmployee(this.newEmployee)
.then(function() {
_this._router.navigate(['Employees']);
});
}
cancelAdd(event) {
this._router.navigate(['Employees']);
}
}
add/employees/add/employee-add.component.html
<h2>{{title}}</h2>
<form class="col-sm-5">
<div class="form-group">
<label for="first-name">First Name</label>
<input [(ngModel)]="newEmployee.firstName" type="text" class="form-control" id="first-name" placeholder="First Name">
</div>
<div class="form-group">
<label for="last-name">Last Name</label>
<input [(ngModel)]="newEmployee.lastName" type="text" class="form-control" id="last-name" placeholder="Last Name">
</div>
<div class="form-group">
<label for="email">Email</label>
<input [(ngModel)]="newEmployee.email" type="text" class="form-control" id="email" placeholder="Email">
</div>
<div class="form-group">
<label for="phone">Phone</label>
<input [(ngModel)]="newEmployee.phone" type="text" class="form-control" id="phone" placeholder="Phone">
</div>
<button (click)="saveEmployee()" type="button" class="btn btn-default">Save</button>
<button (click)="cancelAdd()" type="button" class="btn btn-danger">Cancel</button>
</form>
test/employees/edit/employee-edit.component.spec.ts
describe('Employee Edit Component Tests', () => {
let employeeEditComponent: EmployeeEditComponent;
let location:Location;
beforeEachProviders(() => [
ROUTER_PROVIDERS,
provide(Location, {useClass: SpyLocation}),
provide(Router, {useClass: RootRouter}),
provide(RouteParams, { useValue: new RouteParams({ id: '2' }) }),
provide(APP_BASE_HREF, {useValue: '/'}),
provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppComponent}),
provide(ApplicationRef, {useClass: MockApplicationRef}),
provide(EmployeeService, {useClass: EmployeeService}),
provide(EmployeeEditComponent, {useClass: EmployeeEditComponent})
]);
beforeEach(inject([EmployeeEditComponent, Location], (eec, l) => {
employeeEditComponent = eec;
location = l;
}))
it('should fetch an employee object on init', done => {
let testEmployeePopulated = () => {
expect(employeeEditComponent.employee).toBeDefined();
expect(employeeEditComponent.employee.firstName).toBe('Dwight');
expect(employeeEditComponent.employee.lastName).toBe('Schrute');
done();
};
employeeEditComponent.ngOnInit();
setTimeout(testEmployeePopulated);
});
it('should navigate to the employee list page', done => {
let testNavigation = () => {
expect(location.path()).toBe('/employees');
done();
};
employeeEditComponent.backToDirectory({});
setTimeout(testNavigation);
});
});
app/employees/edit/employee-edit.component.ts
import {Component, OnInit} from 'angular2/core';
import {Employee} from '../models/employee';
import {Router, RouteParams} from 'angular2/router';
import {EmployeeService} from '../services/employee.service';
@Component({
selector: 'employee-detail',
templateUrl: 'app/employees/edit/employee-edit.component.html'
})
export class EmployeeEditComponent implements OnInit {
employee: Employee;
constructor(
private _employeeService: EmployeeService,
private _routeParams: RouteParams,
private _router: Router
) {}
ngOnInit() {
let id = +this._routeParams.get('id');
this._employeeService.getEmployee(id)
.then(employee => this.employee = employee);
}
backToDirectory(event) {
this._router.navigate(['Employees']);
}
}
app/employees/edit/employee-edit.component.html
<div *ngIf="employee">
<h2>Edit Contact Information for {{employee.firstName}} {{employee.lastName}}</h2>
<form class="col-sm-4">
<div class="form-group">
<label for="exampleInputEmail">Email</label>
<input [(ngModel)]="employee.email" class="form-control" id="exampleInputEmail">
</div>
<div class="form-group">
<label for="exampleInputPhone">Phone</label>
<input [(ngModel)]="employee.phone" class="form-control" id="exampleInputPhone">
</div>
<button (click)="backToDirectory()" class="btn btn-default" type="button">Done</button>
</form>
</div>
Route Parameters in Components
The EmployeeEditComponent
makes use of a route parameter. If you recall in our Router setup, we are passing in an id to this view so it’s aware of what employee object needs to be retrieved. In the constructor there is a RouteParams
object. To retrieve parameters from this object this._routeParams.get()
and passing in the key for the value we want to retrieve.
export class EmployeeEditComponent implements OnInit {
employee: Employee;
constructor(
private _employeeService: EmployeeService,
private _routeParams: RouteParams,
private _router: Router
) {}
ngOnInit() {
let id = +this._routeParams.get('id');
this._employeeService.getEmployee(id)
.then(employee => this.employee = employee);
}
}
Databinding: ngModel
Below is piece of the template for the add a new emplove view. The ngModel markup has changed from version 1. Previously you would bind input fields to models using ng-model. Now the syntax has changed to [(ngModel)].
<div class="form-group">
<label for="first-name">First Name</label>
<input [(ngModel)]="newEmployee.firstName" type="text" class="form-control" id="first-name" placeholder="First Name">
</div>
<div class="form-group">
<label for="last-name">Last Name</label>
<input [(ngModel)]="newEmployee.lastName" type="text" class="form-control" id="last-name" placeholder="Last Name">
</div>
<div class="form-group">
<label for="email">Email</label>
<input [(ngModel)]="newEmployee.email" type="text" class="form-control" id="email" placeholder="Email">
</div>
<div class="form-group">
<label for="phone">Phone</label>
<input [(ngModel)]="newEmployee.phone" type="text" class="form-control" id="phone" placeholder="Phone">
</div>
Let’s wrap this up
CodeProject
We’ve built an employee directory using Angular 2 with unit tests, gone over some differences between Angular 2 and version 1, and introduced some of the features of TypeScript. This is obviously just scratching the surface of what Angular 2 can do, but I’ve introduced some of the basics in this article that you can build on. As an developer who uses Angular I will be interested to see how quickly (or slowly) version 2 will be adopted after it moves out of beta.
Let me know if you have any thoughts, comments, or questions!