Tuesday, December 20, 2016

Angular 2 Tutorial: Create a CRUD App with Angular CLI

Illustration of a monitor covered with post-it notes and to-do lists

This article is by guest authors Todd Motto and Jurgen Van de Moere. SitePoint guest posts aim to bring you engaging content from prominent writers and speakers of the JavaScript community.


2016.12.20: This article has been revised in response to reader feedback, and to take account of the current release version of Angular 2.


[special]This is the first article in a 4-part series on how to write a Todo application in Angular 2:[/special]

  1. Part 1— Getting our first version of the Todo application up and running
  2. Part 2— Creating separate components to display a list of todo's and a single todo
  3. Part 3— Update the Todo service to communicate with a REST API
  4. Part 4— Use Component Router to route to different components

In each article we'll refine the underlying architecture of the application and we make sure we have a working version of the application that looks like this:

Animated GIF of Finished Todo Application

By the end of this series, our application architecture will look like this:

Application Architecture of Finished Todo Application

[author_more]

The items that are marked with a red border are discussed in this article, while items that are not marked with a red border will be discussed in follow-up articles within this series.

In this first part, you will learn how to:

  • initialize your Todo application using Angular CLI
  • create a Todo class to represent individual todo's
  • create a TodoDataService service to create, update and remove todo's
  • use the AppComponent component to display the user interface
  • deploy your application to GitHub pages

So let's get started!

Rather than a successor of AngularJS 1.x, Angular 2 can be considered an entirely new framework built on lessons from AngularJS 1.x. Hence the name change where Angular is used to denote Angular 2 and AngularJS refers to AngularJS 1.x. In this article we will use Angular and Angular 2 interchangeably but they both refer to Angular 2.

Initialize Your Todo Application Using Angular CLI

One of the easiest ways to start a new Angular 2 application is to use Angular's command-line interface (CLI).

To install Angular CLI, run:

$ npm install -g angular-cli

which will install the ng command globally on your system.

To verify whether your installation completed successfully, you can run:

$  ng version

which should display the version you have installed:

angular-cli: 1.0.0-beta.21
node: 6.1.0
os: darwin x64

Now that you have Angular CLI installed, you can use it to generate your Todo application:

$ ng new todo-app

This creates a new directory with all files you need to get started:

todo-app
├── README.md
├── angular-cli.json
├── e2e
│   ├── app.e2e-spec.ts
│   ├── app.po.ts
│   └── tsconfig.json
├── karma.conf.js
├── package.json
├── protractor.conf.js
├── src
│   ├── app
│   │   ├── app.component.css
│   │   ├── app.component.html
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   └── index.ts
│   ├── assets
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.css
│   ├── test.ts
│   ├── tsconfig.json
│   └── typings.d.ts
└── tslint.json

If you are not familiar with the Angular CLI yet, make sure you check out The Ultimate Angular CLI Reference.

You can now navigate to the new directory:

$ cd todo-app

and start the Angular CLI development server:

$ ng serve

which will start a local development server that you can navigate to in your browser on http://localhost:4200/.

The Angular CLI development server includes LiveReload support, so your browser automatically reloads the application when a source file changes.

How convenient is that!

Creating the Todo Class

Because Angular CLI generates TypeScript files, we can use a class to represent Todo items.

So let's use Angular CLI to generate a Todo class for us:

$ ng generate class Todo --spec

which will create:

src/app/todo.spec.ts
src/app/todo.ts

Let's open up src/app/todo.ts:

export class Todo {
}

and add the logic we need:

export class Todo {
  id: number;
  title: string = '';
  complete: boolean = false;

  constructor(values: Object = {}) {
    Object.assign(this, values);
  }
}

In this Todo class definition, we specify that each Todo instance will have three properties:

  • id: number, unique ID of the todo item
  • title: string, title of the todo item
  • complete: boolean, whether or not the todo item is complete

We also provide constructor logic that lets us specify property values during instantiation so we can easily create new Todo instances like this:

let todo = new Todo({
  title: 'Read SitePoint article',
  complete: false
});

While we are at it, let's add a unit test to make sure our constructor logic works as expected.

When generating the Todo class, we used the --spec option. This told Angular CLI to also generate src/app/todo.spec.ts for us with a basic unit test:

import {Todo} from './todo';

describe('Todo', () => {
  it('should create an instance', () => {
    expect(new Todo()).toBeTruthy();
  });
});

Let's add an additional unit test to make sure the constructor logic works as expected:

import {Todo} from './todo';

describe('Todo', () => {
  it('should create an instance', () => {
    expect(new Todo()).toBeTruthy();
  });

  it('should accept values in the constructor', () => {
    let todo = new Todo({
      title: 'hello',
      complete: true
    });
    expect(todo.title).toEqual('hello');
    expect(todo.complete).toEqual(true);
  });
});

To verify whether our code works as expected, we can now run:

$ ng test

to execute the Karma test runner and run all our unit tests. This should output:

[karma]: No captured browser, open http://localhost:9876/
[karma]: Karma v1.2.0 server started at http://localhost:9876/
[launcher]: Launching browser Chrome with unlimited concurrency
[launcher]: Starting browser Chrome
[Chrome 54.0.2840 (Mac OS X 10.12.0)]: Connected on socket /#ALCo3r1JmW2bvt_fAAAA with id 84083656
Chrome 54.0.2840 (Mac OS X 10.12.0): Executed 5 of 5 SUCCESS (0.159 secs / 0.154 secs)

If your unit tests are failing, you can compare your code to the working code on GitHub.

Now that we have a working Todo class to represent an individual todo, let's create a TodoDataService service to manage all todo's.

Creating the TodoDataService Service

The TodoDataService will be responsible for managing our Todo items.

In another part of this series you will learn how to communicate with a REST API, but for now we will store all data in memory.

Let's use Angular CLI again to generate the service for us:

$ ng generate service TodoData

which outputs:

installing service
  create src/app/todo-data.service.spec.ts
  create src/app/todo-data.service.ts
  WARNING Service is generated but not provided, it must be provided to be used

When generating a service, Angular CLI also generates a unit test by default so we don't have to explicitly use the --spec option.

Angular CLI has generated the following code for our TodoDataService in src/app/todo-data.service.ts:

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

@Injectable()
export class TodoDataService {

  constructor() { }

}

and a corresponding unit test in src/app/todo-data.service.spec.ts:

/* tslint:disable:no-unused-variable */

import { TestBed, async, inject } from '@angular/core/testing';
import { TodoDataService } from './todo-data.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [TodoDataService]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));
});

Let's open up src/app/todo-data.service.ts and add our todo management logic to the TodoDataService:

import {Injectable} from '@angular/core';
import {Todo} from './todo';

@Injectable()
export class TodoDataService {

  // Placeholder for last id so we can simulate
  // automatic incrementing of id's
  lastId: number = 0;

  // Placeholder for todo's
  todos: Todo[] = [];

  constructor() {
  }

  // Simulate POST /todos
  addTodo(todo: Todo): TodoDataService {
    if (!todo.id) {
      todo.id = ++this.lastId;
    }
    this.todos.push(todo);
    return this;
  }

  // Simulate DELETE /todos/:id
  deleteTodoById(id: number): TodoDataService {
    this.todos = this.todos
      .filter(todo => todo.id !== id);
    return this;
  }

  // Simulate PUT /todos/:id
  updateTodoById(id: number, values: Object = {}): Todo {
    let todo = this.getTodoById(id);
    if (!todo) {
      return null;
    }
    Object.assign(todo, values);
    return todo;
  }

  // Simulate GET /todos
  getAllTodos(): Todo[] {
    return this.todos;
  }

  // Simulate GET /todos/:id
  getTodoById(id: number): Todo {
    return this.todos
      .filter(todo => todo.id === id)
      .pop();
  }

  // Toggle todo complete
  toggleTodoComplete(todo: Todo){
    let updatedTodo = this.updateTodoById(todo.id, {
      complete: !todo.complete
    });
    return updatedTodo;
  }

}

The actual implementation details of the methods are not essential for the purpose of this article. The main takeaway is that we centralize the business logic in a service.

To make sure the business logic in our TodoDataService service works as expected, we also add some additional unit tests in src/app/todo.service.spec.ts:

import {TestBed, async, inject} from '@angular/core/testing';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [TodoDataService]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

  describe('#getAllTodos()', () => {

    it('should return an empty array by default', inject([TodoDataService], (service: TodoDataService) => {
      expect(service.getAllTodos()).toEqual([]);
    }));

    it('should return all todos', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
    }));

  });

  describe('#save(todo)', () => {

    it('should automatically assign an incrementing id', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getTodoById(1)).toEqual(todo1);
      expect(service.getTodoById(2)).toEqual(todo2);
    }));

  });

  describe('#deleteTodoById(id)', () => {

    it('should remove todo with the corresponding id', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
      service.deleteTodoById(1);
      expect(service.getAllTodos()).toEqual([todo2]);
      service.deleteTodoById(2);
      expect(service.getAllTodos()).toEqual([]);
    }));

    it('should not removing anything if todo with corresponding id is not found', inject([TodoDataService], (service: TodoDataService) => {
      let todo1 = new Todo({title: 'Hello 1', complete: false});
      let todo2 = new Todo({title: 'Hello 2', complete: true});
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
      service.deleteTodoById(3);
      expect(service.getAllTodos()).toEqual([todo1, todo2]);
    }));

  });

  describe('#updateTodoById(id, values)', () => {

    it('should return todo with the corresponding id and updated data', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.updateTodoById(1, {
        title: 'new title'
      });
      expect(updatedTodo.title).toEqual('new title');
    }));

    it('should return null if todo is not found', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.updateTodoById(2, {
        title: 'new title'
      });
      expect(updatedTodo).toEqual(null);
    }));

  });

  describe('#toggleTodoComplete(todo)', () => {

    it('should return the updated todo with inverse complete status', inject([TodoDataService], (service: TodoDataService) => {
      let todo = new Todo({title: 'Hello 1', complete: false});
      service.addTodo(todo);
      let updatedTodo = service.toggleTodoComplete(todo);
      expect(updatedTodo.complete).toEqual(true);
      service.toggleTodoComplete(todo);
      expect(updatedTodo.complete).toEqual(false);
    }));

  });

});

Karma comes pre-configured with Jasmine. You can read the Jasmine documentation to learn more about the Jasmine syntax.

Let's zoom in on some of the parts in the unit tests above:

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [TodoDataService]
  });
});

First of all what is TestBed?

TestBed is a utility provided by @angular/core/testing to configure and create an Angular testing module in which we want to run our unit tests.

We use the TestBed.configureTestingModule() method to configure and create a new Angular testing module. We can configure the testing module to our liking by passing in a configuration object. This configuration object can have most of the properties of a normal Angular module.

In this case we use the providers property to configure the testing module to use the real TodoDataService when running the tests.

In part 3 of this series we will let the TodoDataService communicate with a real REST API and we will see how we can inject a mock service in our test module to prevent the tests from communicating with the real API.

Next, we use the inject function provided by @angular/core/testing to inject the correct service from the TestBed injector in our test function:

Continue reading %Angular 2 Tutorial: Create a CRUD App with Angular CLI%


by Todd Motto via SitePoint

No comments:

Post a Comment