Angular 2 Tutorial: Create a CRUD App with Angular CLI

June 27, 2016 0 Comments angular 2, cli

Angular 2 Tutorial: Create a CRUD App with Angular CLI


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

Angular 2 is an open source framework for building mobile and desktop applications.

Rather than a successor of AngularJS 1.x, Angular 2 can be considered an entirely new framework built on learnings 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.

In this article we'll be building an Angular 2 Todo web application that allows users to:

  • quickly create new todo's using an input field and hitting the enter key
  • toggle todo's as complete or incomplete
  • remove todo's that are no longer needed

Here is a live demo of the application we will be building.

All code is publicly available right here so feel free to fork and play with the code yourself.

Let's get started!

The Angular CLI

One of the easiest ways start a new Angular 2 application is to use the brand new Angular command-line interface (CLI) that allows you to:

  • generate boilerplate code for new Angular 2 applications
  • add features (components, directives, services, pipes, etc) to existing Angular 2 applications

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:

which should display the version you have installed.

You can visit the official installation notes for more information if needed.

Generating Our Todo Application

Now that we have Angular CLI installed, we can use it to generate our Todo application:

$ ng new angular2-todo-app 

This will create a new directory for us with everything we need to get started:

 ├── angular-cli-build.js ├── angular-cli.json ├── config │   ├── environment.dev.ts │   ├── environment.js │   ├── environment.prod.ts │   ├── karma.conf.js │   ├── karma-test-shim.js │   └── protractor.conf.js ├── e2e │   ├── app.e2e-spec.ts │   ├── app.po.ts │   ├── tsconfig.json │   └── typings.d.ts ├── package.json ├── public ├── README.md ├── src │   ├── app │   │   ├── app.component.css │   │   ├── app.component.html │   │   ├── app.component.spec.ts │   │   ├── app.component.ts │   │   ├── environment.ts │   │   ├── index.ts │   │   └── shared │   │   └── index.ts │   ├── favicon.ico │   ├── index.html │   ├── main.ts │   ├── system-config.ts │   ├── tsconfig.json │   └── typings.d.ts ├── tslint.json ├── typings │   └── ... └── typings.json 

You can now:

# enter new directory the CLI created for you $ cd angular2-todo-app # start the development server $ ng serve 

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

The application will automatically reload when a source file has changed.

How convenient is that!

Angular Ingredients

Angular CLI already generated the entire Angular 2 application boilerplate for us when we used the ng new command. But it doesn't stop there. It can also help us add ingredients to our existing Angular application using the ng generate command:

# Generate a new component $ ng generate component my-new-component # Generate a new directive $ ng generate directive my-new-directive # Generate a new pipe $ ng generate pipe my-new-pipe # Generate a new service $ ng generate service my-new-service # Generate a new class $ ng generate class my-new-class # Generate a new interface $ ng generate interface my-new-interface # Generate a new enum $ ng generate enum my-new-enum 

If you are not familiar with the basic building blocks of an Angular 2 application, it is highly recommended that you read the Angular 2 QuickStart first.

To meet the needs of our Todo application, we will need:

  • a Todo class to represent individual todo's
  • a TodoService to create, update and remove todo's
  • a TodoApp component to display the user interface

So let's add these ingredients one by one.

Creating the Todo Class

Because we use TypeScript, 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 

which will create:

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

Let's open up src/app/todo.ts and replace its contents with:

export class Todo { id: number; title: string = ''; complete: boolean = false; constructor(values: Object = {}) { Object.assign(this, values); } } 

Each Todo item has 3 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

The constructor logic allows us to specify property values during instantiation:

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

In fact, Angular CLI generated src/app/todo.spec.ts for us so let's add a unit test to make sure the constructor logic works as expected:

import { beforeEach, beforeEachProviders, describe, xdescribe, expect, it, xit, async, inject } from '@angular/core/testing'; 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 the unit tests:

which will execute Karma to run all your unit tests.

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

Now that we have a Todo class, let's create a Todo service to manage all todo items for us.

Creating the TodoService

The TodoService will be responsible for managing our Todo items.

In a future article we will see how we can 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 Todo 

which will create:

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

We can now add our todo management logic to our TodoService in src/app/todo.service.ts:

import {Injectable} from '@angular/core'; import {Todo} from './todo'; @Injectable() export class TodoService { // 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): TodoService { if (!todo.id) { todo.id = ++this.lastId; } this.todos.push(todo); return this; } // Simulate DELETE /todos/:id deleteTodoById(id: number): TodoService { 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 our logic works as expected, let's add unit tests to src/app/todo.service.spec.ts which was already generated by Angular CLI.

Because Angular CLI already generates the boilerplate code for us, we only have to worry about implementing the tests:

import { beforeEach, beforeEachProviders, describe, xdescribe, expect, it, xit, async, inject } from '@angular/core/testing'; import {Todo} from './todo'; import {TodoService} from './todo.service'; describe('Todo Service', () => { beforeEachProviders(() => [TodoService]); describe('#getAllTodos()', () => { it('should return an empty array by default', inject([TodoService], (service: TodoService) => { expect(service.getAllTodos()).toEqual([]); })); it('should return all todos', inject([TodoService], (service: TodoService) => { 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([TodoService], (service: TodoService) => { 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([TodoService], (service: TodoService) => { 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([TodoService], (service: TodoService) => { 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([TodoService], (service: TodoService) => { 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([TodoService], (service: TodoService) => { 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([TodoService], (service: TodoService) => { 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.

To check whether our business logic is valid, we run our unit tests again:

Ok, now that we have a working TodoService, it's time to implement the interface part of the application.

In Angular 2, parts of the interface are represented by components.

Creating the TodoApp Component

Again, let's use Angular CLI to generate the component for us:

$ ng generate component TodoApp 

which will create:

src/app/todo-app/todo-app.component.css src/app/todo-app/todo-app.component.html src/app/todo-app/todo-app.component.spec.ts src/app/todo-app/todo-app.component.ts src/app/todo-app/index.ts 

Template and styles can also be specified inline inside the script file. Angular CLI creates separate files by default, so we will use separate files in this article.

Let's start by adding the component's view to src/app/todo-app/todo-app.component.html:

<section class="todoapp"> <header class="header"> <h1>Todos</h1> <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()"> </header> <section class="main" *ngIf="todos.length > 0"> <ul class="todo-list"> <li *ngFor="let todo of todos" [class.completed]="todo.complete"> <div class="view"> <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete"> <label>{{todo.title}}</label> <button class="destroy" (click)="removeTodo(todo)"></button> </div> </li> </ul> </section> <footer class="footer" *ngIf="todos.length > 0"> <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span> </footer> </section> 

Here is a super short primer on Angular's template syntax in case you have never seen it yet:

  • [property]="expression": set property to result of expression
  • (event)="statement": execute statement when event occurred
  • [(property)]="expression": create two-way binding with expression
  • [class.special]="expression": add special CSS class to element when expression is truthy
  • [style.color]="expression": set color CSS property to result of expression

If you're not familiar with Angular's template syntax, you should definitely read the official template syntax documentation.

Let's see what that means for our view. At the top there is an input to create a new todo:

  • [(ngModel)]="newTodo.title": adds a two-way binding between the input value and newTodo.title
  • (keyup.enter)="addTodo()": tells Angular to execute addTodo() when the enter key was pressed while typing in the input element

Don't worry about where newTodo or addTodo() come frome yet, we will get there shortly. Just try to understand the semantics of the view for now.

Next there is a section to display the todo's:

    *ngIf="todos.length > 0": only show the section element and all its children when there is at least 1 todo

Within that section, we ask Angular to generate an li element for each todo:

  • *ngFor="let todo of todos": loop over all todo's and assign current todo to a variable called todo for each iteration
  • [class.completed]="todo.complete": apply CSS class complete to li element when todo.complete is truthy

and finally we display todo details for each todo within the ngFor loop:

  • (click)="toggleTodoComplete(todo)": execute toggleTodoComplete(todo) when checkbox is clicked
  • [checked]="todo.complete": assign the value of todo.complete to the property checked of the element
  • (click)="removeTodo(todo)": execute removeTodo(todo) when destroy button is clicked

Ok, let's breathe. That was quite a bit of syntax we went through.

If you want to learn every detail about Angular's template syntax, make sure to read the official template documentation.

You may wonder how expressions like addTodo() and newTodo.title can be evaluated. We haven't defined them yet, so how can Angular know what we mean?

That's exactly where the expression context comes in. The expression context of a component is the component instance. And the component instance is an instantiation of the component class.

The component class of our TodoAppComponent is defined in src/app/todo-app/todo-app.component.ts.

Angular CLI already created the TodoAppComponent class boilerplate for us:

import { Component } from '@angular/core'; @Component({ moduleId: module.id, selector: 'app-todo-app', templateUrl: 'todo-app.component.html', styleUrls: ['todo-app.component.css'] }) export class TodoAppComponent { constructor() {} } 

so we can immediately start adding our custom logic.

We will be needing the TodoService instance, so let's start by injecting it in our component.

First we import the TodoService class and specify it in the providers array of the Component decoration:

// Import class so we can register it as dependency injection token import {TodoService} from '../todo.service'; @Component({ // ... providers: [TodoService] }) export class TodoAppComponent { // ... } 

The TodoAppComponent's dependency injector will now recognize the TodoService class as a dependency injection token and return a single instance of TodoService when we ask for it.

Angular's dependency injection system accepts a variety of dependency injection recipes. The syntax above is a shorthand notation for the Class provider recipe that provides dependencies using the singleton pattern. Check out Angular's dependency injection documentation for more details.

Now that the component's dependency injector knows what it needs to provide, we ask it to inject the TodoService instance in our component by specifying the dependency in the TodoAppComponent constructor:

// Import class so we can use it as dependency injection token in the constructor import {TodoService} from '../todo.service'; @Component({ // ... }) export class TodoAppComponent { // Ask Angular DI system to inject the dependency // associated with the dependency injection token `TodoService` // and assign it to a property called `todoService` constructor(private todoService: TodoService) { } // Service is now available as this.todoService toggleTodoComplete(todo) { this.todoService.toggleTodoComplete(todo); } } 

We can now implement all logic we need in our view by adding properties and methods to our TodoAppComponent class:

import {Component} from '@angular/core'; import {Todo} from '../todo'; import {TodoService} from '../todo.service'; @Component({ moduleId: module.id, selector: 'todo-app', templateUrl: 'todo-app.component.html', styleUrls: ['todo-app.component.css'], providers: [TodoService] }) export class TodoAppComponent { newTodo: Todo = new Todo(); constructor(private todoService: TodoService) { } addTodo() { this.todoService.addTodo(this.newTodo); this.newTodo = new Todo(); } toggleTodoComplete(todo) { this.todoService.toggleTodoComplete(todo); } removeTodo(todo) { this.todoService.deleteTodoById(todo.id); } get todos() { return this.todoService.getAllTodos(); } } 

We first instantiate a newTodo property and assign a new Todo() when the component class is instantiated. This is the newTodo we added a two-way binding to in our view:

Whenever the input value changes in the view, the value in the component instance is updated. And whenever the value in the component instance changes, the value in the input element in the view changes.

Next we implement all methods we used in our view.

Their implementation is very short and should be self-explanatory as we delegate all business logic to the todoService.

Delegating business logic to a service is a good programming practice as it allows us to centrally manage and test the business logic.

Feel free to play around with the live demo to see what the result looks like.

Before we wrap up this tutorial, let's have a look at one last really cool feature of Angular CLI.

Deploying to GitHub Pages

Angular CLI makes it super simple to deploy our application to GitHub Pages with a single command like this:

$ ng github-pages:deploy --message 'deploy(dist): deploy on GitHub pages' 

The github-pages:deploy command tells Angular CLI to build a static version of our Angular application and push it to the gh-pages branch of our GitHub repository:

$ ng github-pages:deploy --message 'deploy(dist): deploy on GitHub pages' Built project successfully. Stored in "dist/". Deployed! Visit https://sitepoint-editors.github.io/angular2-todo-app/ Github pages might take a few minutes to show the deployed site. 

Our application is now available at https://sitepoint-editors.github.io/angular2-todo-app/.

How awesome is that!

Summary

Angular 2 is a beast, no doubt. A very powerful beast!

We've covered a lot so let's recap what we have learned in this article:

  • We learned how to install Angular CLI and how much time it saves us when creating new applications or adding features to existing applications.
  • We learned how to implement business logic in an Angular service and how to test our business logic using unit tests.
  • We learned how to use a component to interact with the user and how to delegate logic to a service using dependency injection.
  • We learned the basics of Angular template syntax and briefly touched on how Angular dependency injection works.
  • Finally, we learned how to quickly deploy our application to GitHub Pages.

There is a lot more to learn about Angular 2 that we hope to cover in future articles, such as how to:

  • communicate with a REST API backend using Angular 2's HTTP service
  • filter todo's using Angular pipes
  • implement routing to make it a multi-page application

and much, much more.

So stay tuned for more about this wonderful world of Angular 2.

Have you built anything with Angular 2 yet? Are you planning to upgrade your Angular 1.x applications? Please get in touch via the comments and let us know what you think!

Angular 2 Tutorial: Create a CRUD App with Angular CLI


Tag cloud