Angular 2 CRUD, modals, animations, pagination, datetimepicker and much more

The source code for this post has been updated to Angular 4 (repository).

Angular 2 and TypeScript have fetched client-side development to the next level but till recently most of web developers hesitated to start a production SPA with those two. The first reason was that Angular was still in development and the second one is that components that are commonly required in a production SPA were not yet available. Which are those components? DateTime pickers, custom Modal popups, animations and much more.. Components and plugins that make websites fluid and user-friendly. But this is old news now, Angular is very close to release its final version and community been familiarized with the new framework has produced a great ammount of such components.

What this post is all about

This post is another step by step walkthrough to build Angular 2 SPAs using TypeScript. I said another one, cause we have already seen such a post before. The difference though is that now we have all the knowledge and tools to create more structured, feature-enhanced and production level SPAs and this is what we will do on this post. The Schedule.SPA application that we are going to build will make use of all the previously mentioned components following the recommended Angular style guide as much as possible. As far as the back-end infrastructure (REST API) that our application will make use of, we have already built it in the previous post Building REST APIs using ASP.NET Core and Entity Framework Core. The source code for the API which was built using .NET Core can be found here where you will also find instructions how to run it. The SPA will display schedules and their related information (who created it, attendees, etc..). It will allow the user to manimulate many aspects of each schedule which means that we are going to see CRUD operations in action. Let’s see the in detail all the features that this SPA will incorporate.

  • HTTP CRUD operations
  • Routing and Navigation using the new Component Router
  • Custom Modal popup windows
  • Angular 2 animations
  • DateTime pickers
  • Notifications
  • Pagination through Request/Response headers
  • Angular Forms validation
  • Angular Directives
  • Angular Pipes

Not bad right..? Before start building it let us see the final product with a .gif (click to view in better quality).
angular-scheduler-spa

Start coding

One decision I ‘ve made for this app is to use Visual Studio Code rich text editor for development. While I used VS 2015 for developing the API, I still find it useless when it comes to TypeScript development. Lots of compile and build errors may make your life misserable. One the other hand, VS Code has a great intellisense features and an awesome integrated command line which allows you to run commands directly from the IDE. You can use though your text editor of your preference. First thing we need to do is configure the Angular – TypeScript application. Create a folder named Scheduler.SPA and open it in your favorite editor. Add the package.json file where we define all the packages we are going to use in our application.

{
  "version": "1.0.0",
  "name": "scheduler",
  "author": "Chris Sakellarios",
  "license": "MIT",
  "repository": "https://github.com/chsakell/angular2-features",
  "private": true,
  "dependencies": {
    "@angular/common": "2.0.0",
    "@angular/compiler": "2.0.0",
    "@angular/core": "2.0.0",
    "@angular/forms": "2.0.0",
    "@angular/http": "2.0.0",
    "@angular/platform-browser": "2.0.0",
    "@angular/platform-browser-dynamic": "2.0.0",
    "@angular/router": "3.0.0",
    "@angular/upgrade": "2.0.0",
    "bootstrap": "^3.3.6",

    "jquery": "^3.0.0",
    "lodash": "^4.13.1",
    "moment": "^2.13.0",
    "ng2-bootstrap": "^1.1.5",
    "ng2-slim-loading-bar": "1.5.1",
    
    "core-js": "^2.4.1",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.12",
    "systemjs": "0.19.27",
    "zone.js": "^0.6.23",
    "angular2-in-memory-web-api": "0.0.20"
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "del": "^2.2.0",
    "gulp": "^3.9.1",
    "gulp-tslint": "^5.0.0",
    "jquery": "^3.0.0",
    "lite-server": "^2.2.0",
    "typescript": "2.0.2",
    "typings": "^1.3.2",
    "tslint": "^3.10.2"
  },
  "scripts": {
    "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite --baseDir ./app --port 8000\" ",
    "lite": "lite-server",
    "postinstall": "typings install",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "typings": "typings"
  }
}

We declared all the Angular required packages (Github repo will always be updated when new version releases) and some others such as ng2-bootstrap which will help us incorporate some cool features in our SPA. When some part of our application make use of that type of packages I will let you know. Next add the systemjs.config.js SystemJS configuration file.

/**
 * System configuration for Angular 2 samples
 * Adjust as necessary for your application needs.
 */
(function (global) {
    System.config({
        paths: {
            // paths serve as alias
            'npm:': 'node_modules/'
        },
        // map tells the System loader where to look for things
        map: {
            // our app is within the app folder
            app: 'app',
            // angular bundles
            '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
            '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
            '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
            '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
            '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
            '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
            '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
            '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
            // other libraries
            'rxjs': 'npm:rxjs',
            'angular2-in-memory-web-api': 'npm:angular2-in-memory-web-api',
            'jquery': 'npm:jquery/',
            'lodash': 'npm:lodash/lodash.js',
            'moment': 'npm:moment/',
            'ng2-bootstrap': 'npm:ng2-bootstrap',
            'symbol-observable': 'npm:symbol-observable'
        },
        // packages tells the System loader how to load when no filename and/or no extension
        packages: {
            app: {
                main: './main.js',
                defaultExtension: 'js'
            },
            rxjs: {
                defaultExtension: 'js'
            },
            'angular2-in-memory-web-api': {
                main: './index.js',
                defaultExtension: 'js'
            },
            'moment': { main: 'moment.js', defaultExtension: 'js' },
            'ng2-bootstrap': { main: 'ng2-bootstrap.js', defaultExtension: 'js' },
            'symbol-observable': { main: 'index.js', defaultExtension: 'js' }
        }
    });
})(this);

I ‘ll make a pause here just to ensure that you understand how SystemJS and the previous two files work together. Suppose that you want to use a DateTime picker in your app. Searching the internet you find an NPM package saying that you need to run the following command to install it.

npm install ng2-bootstrap --save

What this command will do is download the package inside the node_modules folder and add it as a dependency in the package.json. To use that package in your application you need to import the respective module in the app.module.ts (as of angular RC.6 and later). in the component that needs its functionality like this.

import { DatepickerModule } from 'ng2-bootstrap/ng2-bootstrap';

@NgModule({
    imports: [
        BrowserModule,
        DatepickerModule,
        routing,
    ],
// code ommitted

In most cases you will find the import statement on the package documentation. Is that all you need to use the package? No, cause SystemJS will make a request to http://localhost:your_port/ng2-bootstrap which of course doesn’t exist.
Modules are dynamically loaded using SystemJS and the first thing to do is to inform SystemJS where to look when a request to ng2-datetime dispatches the server. This is done through the map object in the systemjs.config.js as follow.

// map tells the System loader where to look for things
 map: 
 {
	// our app is within the app folder
	app: 'app',
	// angular bundles
	'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
	'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
	'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
	'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
	'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
	'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
	'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
	'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
	// other libraries
	'rxjs': 'npm:rxjs',
	'angular2-in-memory-web-api': 'npm:angular2-in-memory-web-api',
	'jquery': 'npm:jquery/',
	'lodash': 'npm:lodash/lodash.js',
	'moment': 'npm:moment/',
	'ng2-bootstrap': 'npm:ng2-bootstrap',
	'symbol-observable': 'npm:symbol-observable'
}

From now on each time a request to ng2-bootstrap reaches the server, SystemJS will map the request to node_modules/ng2-bootstrap which actually exists since we have installed the package. Are we ready yet? No, we still need to inform SystemJS what file name to load and the default extension. This is done using the packages object in the systemjs.config.js.

// packages tells the System loader how to load when no filename and/or no extension
	packages: {
		app: {
			main: './main.js',
			defaultExtension: 'js'
		},
		rxjs: {
			defaultExtension: 'js'
		},
		'angular2-in-memory-web-api': {
			main: './index.js',
			defaultExtension: 'js'
		},
		'moment': { main: 'moment.js', defaultExtension: 'js' },
		'ng2-bootstrap': { main: 'ng2-bootstrap.js', defaultExtension: 'js' },
		'symbol-observable': { main: 'index.js', defaultExtension: 'js' }
	}

Our SPA is a TypeScript application so go ahead and add a tsconfig.json file.

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false,
    "skipLibCheck": true
  }
}

This file will be used by the tsc command when transpiling TypeScript to pure ES5 JavaScript. We also need a typings.json file.

{
  "globalDependencies": {
    "core-js": "registry:dt/core-js#0.0.0+20160725163759",
    "jasmine": "registry:dt/jasmine#2.2.0+20160621224255",
    "node": "registry:dt/node#6.0.0+20160909174046",
    "jquery": "registry:dt/jquery#1.10.0+20160417213236"
  },
  "dependencies": {
    "lodash": "registry:npm/lodash#4.0.0+20160416211519"
  }
}

Those are mostly Angular dependencies plus jquery and lodash that our SPA will make use of. We are going to use some client-side external libraries such as alertify.js and font-awesome. Add a bower.json file and set its contents as follow.

{
  "name": "scheduler.spa",
  "private": true,
  "dependencies": {
    "alertify.js" : "0.3.11",
    "bootstrap": "^3.3.6",
    "font-awesome": "latest"
  }
}

At this point we are all set configuring the SPA so go ahead and run the following commands:

npm install
bower install

npm install will also run typings install as a postinstall event. Before start typing the TypeScript code add the index.html page as well.

<!DOCTYPE html>
<html>
<head>
    <base href="/">
    <meta charset="utf-8" />
    <title>Scheduler</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
    <link href="bower_components/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
    <link href="bower_components/alertify.js/themes/alertify.core.css" rel="stylesheet" />
    <link href="bower_components/alertify.js/themes/alertify.bootstrap.css" rel="stylesheet" />
    <link rel="stylesheet" href="../assets/css/styles.css" />

    <script src="bower_components/jquery/dist/jquery.min.js"></script>
    <script src="node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
    <script src="bower_components/alertify.js/lib/alertify.min.js"></script>
    
    <!-- 1. Load libraries -->
     <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>
    <!-- 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
</head>
<body>
    <scheduler>
        <div class="loader"></div>    
    </scheduler>
</body>
</html>

Angular & TypeScript in action

Add a folder named app at the root of the application and create four subfolders named home, schedules, users and shared. The home folder is responsible to display a landing page, the schedules and users are the basic features in the SPA and the latter shared will contain any component that will be used across the entire app, such as data service or utility services. I will start pasting the code from bottom to top, in other words from the files that bootstrap the application to those that implement certain features. Don’t worry if we haven’t implement all the required components while showing the code, we will during the process. I will however been giving you information regarding any component that haven’t implemented yet.

Bootstrapping the app

Add the main.ts file under app.

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

As of Angular RC.6 and later it is recommended to create at least three basic files to init an Angular 2 app. An app.component.ts to hold the root container of the app, an app.module.ts to hold the app’s NgModule and the previous main.ts to bootstrap the app. Go ahead and create the app.component.ts under the app folder.

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

// Add the RxJS Observable operators we need in this app.
import './rxjs-operators';

@Component({
    selector: 'scheduler',
    templateUrl: 'app/app.component.html'
})
export class AppComponent {

    constructor(private viewContainerRef: ViewContainerRef) {
        // You need this small hack in order to catch application root view container ref
        this.viewContainerRef = viewContainerRef;
    }
}

We need the viewContainerRef mostly for interacting with ng2-bootstrap modals windows. The app.module.ts is one of the most important files in your app. It declares the NgModules that has access to, any Component, directive, pipe or service you need to use accross your app. Add it under app folder as well.

import './rxjs-operators';

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule }    from '@angular/http';

import { PaginationModule } from 'ng2-bootstrap/ng2-bootstrap';
import { DatepickerModule } from 'ng2-bootstrap/ng2-bootstrap';
import { Ng2BootstrapModule } from 'ng2-bootstrap/ng2-bootstrap';
import { ModalModule } from 'ng2-bootstrap/ng2-bootstrap';
import { ProgressbarModule } from 'ng2-bootstrap/ng2-bootstrap';
import { TimepickerModule } from 'ng2-bootstrap/ng2-bootstrap';

import { AppComponent }   from './app.component';
import { DateFormatPipe } from './shared/pipes/date-format.pipe';
import { HighlightDirective } from './shared/directives/highlight.directive';
import { HomeComponent } from './home/home.component';
import { MobileHideDirective } from './shared/directives/mobile-hide.directive';
import { ScheduleEditComponent } from './schedules/schedule-edit.component';
import { ScheduleListComponent } from './schedules/schedule-list.component';
import { UserCardComponent } from './users/user-card.component';
import { UserListComponent } from './users/user-list.component';
import { routing } from './app.routes';

import { DataService } from './shared/services/data.service';
import { ConfigService } from './shared/utils/config.service';
import { ItemsService } from './shared/utils/items.service';
import { MappingService } from './shared/utils/mapping.service';
import { NotificationService } from './shared/utils/notification.service';

@NgModule({
    imports: [
        BrowserModule,
        DatepickerModule,
        FormsModule,
        HttpModule,
        Ng2BootstrapModule,
        ModalModule,
        ProgressbarModule,
        PaginationModule,
        routing,
        TimepickerModule
    ],
    declarations: [
        AppComponent,
        DateFormatPipe,
        HighlightDirective,
        HomeComponent,
        MobileHideDirective,
        ScheduleEditComponent,
        ScheduleListComponent,
        UserCardComponent,
        UserListComponent
    ],
    providers: [
        ConfigService,
        DataService,
        ItemsService,
        MappingService,
        NotificationService
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

We imported Angular’s modules, ng2-bootstrap modules, custom components, directives, pipes and services that we are going to create later on. The DataService holds the CRUD operations for sending HTTP request to the API, the ItemsService defines custom methods for manipulating mostly arrays using the lodash library and last but not least the NotificationService has methods to display notifications to the user. Now let us see the routing in our app. Add the app.routes.ts file as follow.

import { ModuleWithProviders }  from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { UserListComponent } from './users/user-list.component';
import { ScheduleListComponent } from './schedules/schedule-list.component';
import { ScheduleEditComponent } from './schedules/schedule-edit.component';

const appRoutes: Routes = [
    { path: 'users', component: UserListComponent },
    { path: 'schedules', component: ScheduleListComponent },
    { path: 'schedules/:id/edit', component: ScheduleEditComponent },
    { path: '', component: HomeComponent }
];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

This is how we use the new Component Router. At the moment you can understand that http://localhost:your_port/ will activate the HomeComponent and http://localhost:your_port/users the UserListComponent which display all users. RxJS is a huge library and it’ s good practice to import only those modules that you actually need, not all the library cause otherwise you will pay a too slow application startup penalty. We will define any operators that we need in a rxjs-operators.ts file under app folder.

// Statics
import 'rxjs/add/observable/throw';

// Operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/toPromise';

The AppComponent is the root component which means has a router-outlet element on its template where other children components are rendered. Add the app.component.html file under app folder.

<!-- Navigation -->
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" [routerLink]="['/']">
                <i class="fa fa-home fa-3x" aria-hidden="true"></i>
            </a>
        </div>
        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
                <li>
                    <a [routerLink]="['/schedules']"><i class="fa fa-calendar fa-3x" aria-hidden="true"></i></a>
                </li>
                <li>
                    <a [routerLink]="['/users']"><i class="fa fa-users fa-3x" aria-hidden="true"></i></a>
                </li>
                <li>
                    <a href="http://wp.me/p3mRWu-199" target="_blank"><i class="fa fa-info fa-3x" aria-hidden="true"></i></a>
                </li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
                <li>
                    <a href="https://www.facebook.com/chsakells.blog" target="_blank">
                        <i class="fa fa-facebook fa-3x" aria-hidden="true"></i>
                    </a>
                </li>
                <li>
                    <a href="https://twitter.com/chsakellsBlog" target="_blank">
                        <i class="fa fa-twitter fa-3x" aria-hidden="true"></i>
                    </a>
                </li>
                <li>
                    <a href="https://github.com/chsakell" target="_blank">
                        <i class="fa fa-github fa-3x" aria-hidden="true"></i>
                    </a>
                </li>
                <li>
                    <a href="https://chsakell.com" target="_blank">
                        <i class="fa fa-rss-square fa-3x" aria-hidden="true"></i>
                    </a>
                </li>
            </ul>
        </div>
        <!-- /.navbar-collapse -->
    </div>
    <!-- /.container -->
</nav>
<br/>
<!-- Page Content -->
<div class="container">
    <router-outlet></router-outlet>
</div>
<footer class="navbar navbar-fixed-bottom">
    <div class="text-center">
        <h4 class="white">
            <a href="https://chsakell.com/" target="_blank">chsakell's Blog</a>
            <i>Anything around ASP.NET MVC,Web API, WCF, Entity Framework & Angular</i>
        </h4>
    </div>
</footer>

Shared services & interfaces

Before implementing the Users and Schedules features we ‘ll create any service or interface is going to be used across the app. Create a folder named shared under the app and add the interfaces.ts TypeScript file.

export interface IUser {
    id: number;
    name: string;
    avatar: string;
    profession: string;
    schedulesCreated: number;
}

export interface ISchedule {
     id: number;
     title: string;
     description: string;
     timeStart: Date;
     timeEnd: Date;
     location: string;
     type: string;
     status: string;
     dateCreated: Date;
     dateUpdated: Date;
     creator: string;
     creatorId: number;
     attendees: number[];
}

export interface IScheduleDetails {
     id: number;
     title: string;
     description: string;
     timeStart: Date;
     timeEnd: Date;
     location: string;
     type: string;
     status: string;
     dateCreated: Date;
     dateUpdated: Date;
     creator: string;
     creatorId: number;
     attendees: IUser[];
     statuses: string[];
     types: string[];
}

export interface Pagination {
    CurrentPage : number;
    ItemsPerPage : number;
    TotalItems : number;
    TotalPages: number;
}

export class PaginatedResult<T> {
    result :  T;
    pagination : Pagination;
}

export interface Predicate<T> {
    (item: T): boolean
}

In case you have read the Building REST APIs using .NET and Entity Framework Core you will be aware with most of the classes defined on the previous file. They are the TypeScript models that matches the API’s ViewModels. The last interface that I defined is my favorite one. The Predicate interface is a predicate which allows us to pass generic predicates in TypeScript functions. For example we ‘ll see later on the following function.

removeItems<T>(array: Array<T>, predicate: Predicate<T>) {
    _.remove(array, predicate);
}

This is extremely powerfull. What this function can do? It can remove any item from an array that fulfills a certain predicate. Assuming that you have an array of type IUser and you want to remove any user item that has id<0 you would write..

this.itemsService.removeItems<IUser>(this.users, x => x.id < 0);

Add a pipes folder under shared and create a DateTime related pipe.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
    name: 'dateFormat'
})

export class DateFormatPipe implements PipeTransform {
    transform(value: any, args: any[]): any {

        if (args && args[0] === 'local') {
            return new Date(value).toLocaleString();
        }
        else if (value) {
            return new Date(value);
        }
        return value;
    }
}

The pipe simply converts a date to a JavaScript datetime that Angular understands. It can be used either inside an html template..

{{schedule.timeStart | dateFormat | date:'medium'}}

.. or programatically..

this.scheduleDetails.timeStart = new DateFormatPipe().transform(schedule.timeStart, ['local'])

We proceed with the directives. Add a folder named directives under shared. The first one is a simple one that toggles the background color of an element when the mouse enters or leaves. It ‘s very similar to the one described at official’s Angular’s website.

import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({ 
    selector: '[highlight]'
 })
export class HighlightDirective {
    private _defaultColor = 'beige';
    private el: HTMLElement;

    constructor(el: ElementRef) { 
        this.el = el.nativeElement; 
    }

    @Input('highlight') highlightColor: string;
    
    @HostListener('mouseenter') onMouseEnter() {
        this.highlight(this.highlightColor || this._defaultColor);
    }
    @HostListener('mouseleave') onMouseLeave() {
        this.highlight(null);
    }
    
    private highlight(color: string) {
        this.el.style.backgroundColor = color;
    }
}

The second one though is an exciting one. The home page has a carousel with each slide having a font-awesome icon on its left.
angular-crud-modal-animation-02
The thing is that when you reduce the width of the browser the font-image moves on top giving a bad user experience.
angular-crud-modal-animation-03
What I want is the font-awesome icon to hide when the browser reaches a certain width and more over I want this width to be customizable. I believe I have just opened the gates for responsive web design using Angular 2.. Add the following MobileHide directive in a mobile-hide.directive.ts file under shared/directives folder.

import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({ 
    selector: '[mobileHide]',
    host: {
        '(window:resize)': 'onResize($event)'
    }
 })
export class MobileHideDirective {
    private _defaultMaxWidth: number = 768;
    private el: HTMLElement;

    constructor(el: ElementRef) { 
        this.el = el.nativeElement; 
    }

    @Input('mobileHide') mobileHide: number;

    onResize(event:Event) {
        var window : any = event.target;
        var currentWidth = window.innerWidth;
        if(currentWidth < (this.mobileHide || this._defaultMaxWidth))
        {
            this.el.style.display = 'none';
        }
        else
        {
            this.el.style.display = 'block';
        }
    }
}

What this directive does is bind to window.resize event and when triggered check browser’s width: if width is less that the one defined or the default one then hides the element, otherwise shows it. You can apply this directive on the dom like this.

<div mobileHide="772" class="col-md-2 col-sm-2 col-xs-12">
   <span class="fa-stack fa-4x">
    <i class="fa fa-square fa-stack-2x text-primary"></i>
    <i class="fa fa-code fa-stack-1x fa-inverse" style="color:#FFC107"></i>
   </span>
</div>

The div element will be hidden when browser’s width is less than 772px..
angular-scheduler-spa-02
You can extend this directive by creating a new Input parameter which represents a class and instead of hiding the element apply a different class!

Shared services

@Injectable() services that are going to be used across many components in our application will also be placed inside the shared folder. We will separate them though in two different types, core and utilities. Add two folders named services and utils under the shared folder. We will place all core services under services and utilities under utitlities. The most important core service in our SPA is the one responsible to send HTTP requests to the API, the DataService. Add the data.service.ts under the services folder.

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
//Grab everything with import 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

import { IUser, ISchedule, IScheduleDetails, Pagination, PaginatedResult } from '../interfaces';
import { ItemsService } from '../utils/items.service';
import { ConfigService } from '../utils/config.service';

@Injectable()
export class DataService {

    _baseUrl: string = '';

    constructor(private http: Http,
        private itemsService: ItemsService,
        private configService: ConfigService) {
        this._baseUrl = configService.getApiURI();
    }

    getUsers(): Observable<IUser[]> {
        return this.http.get(this._baseUrl + 'users')
            .map((res: Response) => {
                return res.json();
            })
            .catch(this.handleError);
    }

    getUserSchedules(id: number): Observable<ISchedule[]> {
        return this.http.get(this._baseUrl + 'users/' + id + '/schedules')
            .map((res: Response) => {
                return res.json();
            })
            .catch(this.handleError);
    }

    createUser(user: IUser): Observable<IUser> {

        let headers = new Headers();
        headers.append('Content-Type', 'application/json');

        return this.http.post(this._baseUrl + 'users/', JSON.stringify(user), {
            headers: headers
        })
            .map((res: Response) => {
                return res.json();
            })
            .catch(this.handleError);
    }

    updateUser(user: IUser): Observable<void> {

        let headers = new Headers();
        headers.append('Content-Type', 'application/json');

        return this.http.put(this._baseUrl + 'users/' + user.id, JSON.stringify(user), {
            headers: headers
        })
            .map((res: Response) => {
                return;
            })
            .catch(this.handleError);
    }

    deleteUser(id: number): Observable<void> {
        return this.http.delete(this._baseUrl + 'users/' + id)
            .map((res: Response) => {
                return;
            })
            .catch(this.handleError);
    }
    /*
    getSchedules(page?: number, itemsPerPage?: number): Observable<ISchedule[]> {
        let headers = new Headers();
        if (page != null && itemsPerPage != null) {
            headers.append('Pagination', page + ',' + itemsPerPage);
        }

        return this.http.get(this._baseUrl + 'schedules', {
            headers: headers
        })
            .map((res: Response) => {
                return res.json();
            })
            .catch(this.handleError);
    }
    */

    getSchedules(page?: number, itemsPerPage?: number): Observable<PaginatedResult<ISchedule[]>> {
        var peginatedResult: PaginatedResult<ISchedule[]> = new PaginatedResult<ISchedule[]>();

        let headers = new Headers();
        if (page != null && itemsPerPage != null) {
            headers.append('Pagination', page + ',' + itemsPerPage);
        }

        return this.http.get(this._baseUrl + 'schedules', {
            headers: headers
        })
            .map((res: Response) => {
                console.log(res.headers.keys());
                peginatedResult.result = res.json();

                if (res.headers.get("Pagination") != null) {
                    //var pagination = JSON.parse(res.headers.get("Pagination"));
                    var paginationHeader: Pagination = this.itemsService.getSerialized<Pagination>(JSON.parse(res.headers.get("Pagination")));
                    console.log(paginationHeader);
                    peginatedResult.pagination = paginationHeader;
                }
                return peginatedResult;
            })
            .catch(this.handleError);
    }

    getSchedule(id: number): Observable<ISchedule> {
        return this.http.get(this._baseUrl + 'schedules/' + id)
            .map((res: Response) => {
                return res.json();
            })
            .catch(this.handleError);
    }

    getScheduleDetails(id: number): Observable<IScheduleDetails> {
        return this.http.get(this._baseUrl + 'schedules/' + id + '/details')
            .map((res: Response) => {
                return res.json();
            })
            .catch(this.handleError);
    }

    updateSchedule(schedule: ISchedule): Observable<void> {

        let headers = new Headers();
        headers.append('Content-Type', 'application/json');

        return this.http.put(this._baseUrl + 'schedules/' + schedule.id, JSON.stringify(schedule), {
            headers: headers
        })
            .map((res: Response) => {
                return;
            })
            .catch(this.handleError);
    }

    deleteSchedule(id: number): Observable<void> {
        return this.http.delete(this._baseUrl + 'schedules/' + id)
            .map((res: Response) => {
                return;
            })
            .catch(this.handleError);
    }

    deleteScheduleAttendee(id: number, attendee: number) {

        return this.http.delete(this._baseUrl + 'schedules/' + id + '/removeattendee/' + attendee)
            .map((res: Response) => {
                return;
            })
            .catch(this.handleError);
    }

    private handleError(error: any) {
        var applicationError = error.headers.get('Application-Error');
        var serverError = error.json();
        var modelStateErrors: string = '';

        if (!serverError.type) {
            console.log(serverError);
            for (var key in serverError) {
                if (serverError[key])
                    modelStateErrors += serverError[key] + '\n';
            }
        }

        modelStateErrors = modelStateErrors = '' ? null : modelStateErrors;

        return Observable.throw(applicationError || modelStateErrors || 'Server error');
    }
}

The service implements several CRUD operations targeting the API we have built on a previous post. It uses the ConfigService in order to get the API’s URI and the ItemsService to parse JSON objects to typed ones (we ‘ll see it later). Another important function that this service provides is the handleError which can read response errors either from the ModelState or the Application-Error header. The simplest util service is the ConfigService which has only one method to get the API’s URI. Add it under the utils folder.

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

@Injectable()
export class ConfigService {
    
    _apiURI : string;

    constructor() {
        this._apiURI = 'http://localhost:5000/api/';
     }

     getApiURI() {
         return this._apiURI;
     }

     getApiHost() {
         return this._apiURI.replace('api/','');
     }
}

Make sure to change this URI to reflect your back-end API’s URI. It’ s going to be different when you host the API from the console using the dotnet run command and different when you run the application through Visual Studio. The most interesting util service is the ItemsService. I don’t know any client-side application that doesn’t have to deal with array of items and that’s why we need that service. Let’s view the code first. Add it under the utils folder.

import { Injectable } from '@angular/core';
import { Predicate } from '../interfaces'

import * as _ from 'lodash';

@Injectable()
export class ItemsService {

    constructor() { }

    /*
    Removes an item from an array using the lodash library
    */
    removeItemFromArray<T>(array: Array<T>, item: any) {
        _.remove(array, function (current) {
            //console.log(current);
            return JSON.stringify(current) === JSON.stringify(item);
        });
    }

    removeItems<T>(array: Array<T>, predicate: Predicate<T>) {
        _.remove(array, predicate);
    }

    /*
    Finds a specific item in an array using a predicate and repsaces it
    */
    setItem<T>(array: Array<T>, predicate: Predicate<T>, item: T) {
        var _oldItem = _.find(array, predicate);
        if(_oldItem){
            var index = _.indexOf(array, _oldItem);
            array.splice(index, 1, item);
        } else {
            array.push(item);
        }
    }

    /*
    Adds an item to zero index
    */
    addItemToStart<T>(array: Array<T>, item: any) {
        array.splice(0, 0, item);
    }

    /*
    From an array of type T, select all values of type R for property
    */
    getPropertyValues<T, R>(array: Array<T>, property : string) : R
    {
        var result = _.map(array, property);
        return <R><any>result;
    }

    /*
    Util method to serialize a string to a specific Type
    */
    getSerialized<T>(arg: any): T {
        return <T>JSON.parse(JSON.stringify(arg));
    }
}

We can see extensive use of TypeScript in compination with the lodash library. All those functions are used inside the app so you will be able to see how they actually work. Let’s view though some examples right now. The setItem(array: Array, predicate: Predicate, item: T) method can replace a certain item in a typed array of T. For example if there is an array of type IUser that has a user item with id=-1 and you need to replace it with a new IUser, you can simply write..

this.itemsService.setItem<IUser>(this.users, (u) => u.id == -1, _user);

Here we passed the array of IUser, the predicate which is what items to be replaced and the preplacement new item value. Continue by adding the NotificationService and the MappingService which are pretty much self-explanatory, under the utils folder.

import { Injectable } from '@angular/core';
import { Predicate } from '../interfaces'

declare var alertify: any;

@Injectable()
export class NotificationService {
    private _notifier: any = alertify;

    constructor() { }

    /*
    Opens a confirmation dialog using the alertify.js lib
    */
    openConfirmationDialog(message: string, okCallback: () => any) {
        this._notifier.confirm(message, function (e) {
            if (e) {
                okCallback();
            } else {
            }
        });
    }

    /*
    Prints a success message using the alertify.js lib
    */
    printSuccessMessage(message: string) {

        this._notifier.success(message);
    }

    /*
    Prints an error message using the alertify.js lib
    */
    printErrorMessage(message: string) {
        this._notifier.error(message);
    }
}
import { Injectable } from '@angular/core';

import { ISchedule, IScheduleDetails, IUser } from '../interfaces';
import  { ItemsService } from './items.service'

@Injectable()
export class MappingService {

    constructor(private itemsService : ItemsService) { }

    mapScheduleDetailsToSchedule(scheduleDetails: IScheduleDetails): ISchedule {
        var schedule: ISchedule = {
            id: scheduleDetails.id,
            title: scheduleDetails.title,
            description: scheduleDetails.description,
            timeStart: scheduleDetails.timeStart,
            timeEnd: scheduleDetails.timeEnd,
            location: scheduleDetails.location,
            type: scheduleDetails.type,
            status: scheduleDetails.status,
            dateCreated: scheduleDetails.dateCreated,
            dateUpdated: scheduleDetails.dateUpdated,
            creator: scheduleDetails.creator,
            creatorId: scheduleDetails.creatorId,
            attendees: this.itemsService.getPropertyValues<IUser, number[]>(scheduleDetails.attendees, 'id')
        }

        return schedule;
    }

}

Features

Time to implement the SPA’s features starting from the simplest one, the HomeComponent which is responsible to render a landing page. Add a folder named home under app and create the HomeComponent in a home.component.ts file.

import { Component, OnInit, trigger, state, style, animate, transition } from '@angular/core';

declare let componentHandler: any;

@Component({
    moduleId: module.id,
    templateUrl: 'home.component.html',
    animations: [
        trigger('flyInOut', [
            state('in', style({ opacity: 1, transform: 'translateX(0)' })),
            transition('void => *', [
                style({
                    opacity: 0,
                    transform: 'translateX(-100%)'
                }),
                animate('0.6s ease-in')
            ]),
            transition('* => void', [
                animate('0.2s 10 ease-out', style({
                    opacity: 0,
                    transform: 'translateX(100%)'
                }))
            ])
        ])
    ]
})
export class HomeComponent {

    constructor() {

    }

}

Despite that this is the simplest component in our SPA it still make use of some interesting Angular features. The first one is the Angular animations and the second is the the MobileHideDirective directive we created before in order to hide the font-awesome icons when browser’s width is less than 772px. The animation will make the template appear from left to right. Let’s view the template’s code and a preview of what the animation looks like.

<div [@flyInOut]="'in'">

    <div class="container content">
        <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
            <!-- Indicators -->
            <ol class="carousel-indicators">
                <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
                <li data-target="#carousel-example-generic" data-slide-to="1"></li>
                <li data-target="#carousel-example-generic" data-slide-to="2"></li>
            </ol>
            <!-- Wrapper for slides -->
            <div class="carousel-inner">
                <div class="item active">
                    <div class="row">
                        <div class="col-xs-12">
                            <div class="thumbnail adjust1">
                                <div mobileHide="772" class="col-md-2 col-sm-2 col-xs-12">
                                    <span class="fa-stack fa-4x">
                                      <i class="fa fa-square fa-stack-2x text-primary"></i>
                                      <i class="fa fa-html5 fa-stack-1x fa-inverse" style="color:#FFC107"></i>
                                    </span>
                                </div>
                                <div class="col-md-10 col-sm-10 col-xs-12">
                                    <div class="caption">
                                        <p class="text-info lead adjust2">ASP.NET Core</p>
                                        <p><span class="glyphicon glyphicon-thumbs-up"></span> ASP.NET Core is a new open-source
                                            and cross-platform framework for building modern cloud based internet connected
                                            applications, such as web apps, IoT apps and mobile backends.</p>
                                        <blockquote class="adjust2">
                                            <p>Microsoft Corp.</p> <small><cite title="Source Title"><i class="glyphicon glyphicon-globe"></i> https://docs.asp.net/en/latest/</cite></small>                                            </blockquote>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="item">
                    <div class="row">
                        <div class="col-xs-12">
                            <div class="thumbnail adjust1">
                                <div mobileHide="772" class="col-md-2 col-sm-2 col-xs-12">
                                    <span class="fa-stack fa-4x">
                                        <i class="fa fa-square fa-stack-2x text-primary"></i>
                                        <i class="fa fa-code fa-stack-1x fa-inverse" style="color:#FFC107"></i>
                                    </span>
                                </div>
                                <div class="col-md-10 col-sm-10 col-xs-12">
                                    <div class="caption">
                                        <p class="text-info lead adjust2">Angular 2</p>
                                        <p><span class="glyphicon glyphicon-thumbs-up"></span> Learn one way to build applications
                                            with Angular and reuse your code and abilities to build apps for any deployment
                                            target. For web, mobile web, native mobile and native desktop.</p>
                                        <blockquote class="adjust2">
                                            <p>Google</p> <small><cite title="Source Title"><i class="glyphicon glyphicon-globe"></i>https://angular.io/</cite></small>                                            </blockquote>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="item">
                    <div class="row">
                        <div class="col-xs-12">
                            <div class="thumbnail adjust1">
                                <div mobileHide="772" class="col-md-2 col-sm-2 col-xs-12">
                                    <span class="fa-stack fa-4x">
                                      <i class="fa fa-square fa-stack-2x text-primary"></i>
                                      <i class="fa fa-rss fa-stack-1x fa-inverse" style="color:#FFC107"></i>
                                    </span>
                                </div>
                                <div class="col-md-10 col-sm-10 col-xs-12">
                                    <div class="caption">
                                        <p class="text-info lead adjust2">chsakell's Blog</p>
                                        <p><span class="glyphicon glyphicon-thumbs-up"></span> Anything around ASP.NET MVC,Web
                                            API, WCF, Entity Framework & Angular.</p>
                                        <blockquote class="adjust2">
                                            <p>Chris Sakellarios</p> <small><cite title="Source Title"><i class="glyphicon glyphicon-globe"></i> https://chsakell.com</cite></small>                                            </blockquote>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <!-- Controls -->
            <a class="left carousel-control" href="#carousel-example-generic" data-slide="prev"> <span class="glyphicon glyphicon-chevron-left"></span> </a>
            <a class="right carousel-control" href="#carousel-example-generic" data-slide="next"> <span class="glyphicon glyphicon-chevron-right"></span> </a>
        </div>
    </div>

    <hr>
    <!-- Title -->
    <div class="row">
        <div class="col-lg-12">
            <h3>Latest Features</h3>
        </div>
    </div>
    <!-- /.row -->
    <!-- Page Features -->
    <div class="row text-center">
        <div class="col-md-3 col-sm-6 hero-feature">
            <div class="thumbnail">
                <span class="fa-stack fa-5x">
                    <i class="fa fa-square fa-stack-2x text-primary"></i>
                    <i class="fa fa-html5 fa-stack-1x fa-inverse"></i>
                </span>
                <div class="caption">
                    <h3>ASP.NET Core</h3>
                    <p>ASP.NET Core is a significant redesign of ASP.NET.</p>
                    <p>
                        <a href="https://docs.asp.net/en/latest/" target="_blank" class="btn btn-primary">More..</a>
                    </p>
                </div>
            </div>
        </div>
        <div class="col-md-3 col-sm-6 hero-feature">
            <div class="thumbnail">
                <span class="fa-stack fa-5x">
                    <i class="fa fa-square fa-stack-2x text-primary"></i>
                    <i class="fa fa-database fa-stack-1x fa-inverse"></i>
                </span>
                <div class="caption">
                    <h3>EF Core</h3>
                    <p>A cross-platform version of Entity Framework.</p>
                    <p>
                        <a href="https://docs.efproject.net/en/latest/" target="_blank" class="btn btn-primary">More..</a>
                    </p>
                </div>
            </div>
        </div>
        <div class="col-md-3 col-sm-6 hero-feature">
            <div class="thumbnail">
                <span class="fa-stack fa-5x">
                    <i class="fa fa-square fa-stack-2x text-primary"></i>
                    <i class="fa fa-code fa-stack-1x fa-inverse"></i>
                </span>
                <div class="caption">
                    <h3>Angular</h3>
                    <p>Angular is a platform for building mobile and desktop web apps.</p>
                    <p>
                        <a href="https://angular.io/" target="_blank" class="btn btn-primary">More..</a>
                    </p>
                </div>
            </div>
        </div>
        <div class="col-md-3 col-sm-6 hero-feature">
            <div class="thumbnail">
                <span class="fa-stack fa-5x">
                    <i class="fa fa-square fa-stack-2x text-primary"></i>
                    <i class="fa fa-terminal fa-stack-1x fa-inverse"></i>
                </span>
                <div class="caption">
                    <h3>TypeScript</h3>
                    <p>A free and open source programming language.</p>
                    <p>
                        <a href="https://www.typescriptlang.org/" target="_blank" class="btn btn-primary">More..</a>
                    </p>
                </div>
            </div>
        </div>
    </div>
    <!-- /.row -->
    <hr>
    <!-- Footer -->
    <footer>
        <div class="row">
            <div class="col-lg-12">
                <p>Copyright &copy; <a href="https://chsakell.com" target="_blank">chsakell's Blog</a></p>
            </div>
        </div>
    </footer>
</div>

angular-scheduler-spa-03
Add a folder named Schedules. As we declared on the app.routes.ts file schedules will have two distinct routes, one to display all the schedules in a table and another one to edit a specific schedule. The ScheduleListComponent is a quite complex one. Add the schedule-list.component.ts under schedules as well.

import { Component, OnInit, ViewChild, Input, Output,
    trigger,
    state,
    style,
    animate,
    transition } from '@angular/core';

import { ModalDirective } from 'ng2-bootstrap';

import { DataService } from '../shared/services/data.service';
import { DateFormatPipe } from '../shared/pipes/date-format.pipe';
import { ItemsService } from '../shared/utils/items.service';
import { NotificationService } from '../shared/utils/notification.service';
import { ConfigService } from '../shared/utils/config.service';
import { ISchedule, IScheduleDetails, Pagination, PaginatedResult } from '../shared/interfaces';

@Component({
    moduleId: module.id,
    selector: 'app-schedules',
    templateUrl: 'schedule-list.component.html',
    animations: [
        trigger('flyInOut', [
            state('in', style({ opacity: 1, transform: 'translateX(0)' })),
            transition('void => *', [
                style({
                    opacity: 0,
                    transform: 'translateX(-100%)'
                }),
                animate('0.5s ease-in')
            ]),
            transition('* => void', [
                animate('0.2s 10 ease-out', style({
                    opacity: 0,
                    transform: 'translateX(100%)'
                }))
            ])
        ])
    ]
})
export class ScheduleListComponent implements OnInit {
    @ViewChild('childModal') public childModal: ModalDirective;
    schedules: ISchedule[];
    apiHost: string;

    public itemsPerPage: number = 2;
    public totalItems: number = 0;
    public currentPage: number = 1;

    // Modal properties
    @ViewChild('modal')
    modal: any;
    items: string[] = ['item1', 'item2', 'item3'];
    selected: string;
    output: string;
    selectedScheduleId: number;
    scheduleDetails: IScheduleDetails;
    selectedScheduleLoaded: boolean = false;
    index: number = 0;
    backdropOptions = [true, false, 'static'];
    animation: boolean = true;
    keyboard: boolean = true;
    backdrop: string | boolean = true;

    constructor(
        private dataService: DataService,
        private itemsService: ItemsService,
        private notificationService: NotificationService,
        private configService: ConfigService) { }

    ngOnInit() {
        this.apiHost = this.configService.getApiHost();
        this.loadSchedules();
    }

    loadSchedules() {
        //this.loadingBarService.start();

        this.dataService.getSchedules(this.currentPage, this.itemsPerPage)
            .subscribe((res: PaginatedResult<ISchedule[]>) => {
                this.schedules = res.result;// schedules;
                this.totalItems = res.pagination.TotalItems;
                //this.loadingBarService.complete();
            },
            error => {
                //this.loadingBarService.complete();
                this.notificationService.printErrorMessage('Failed to load schedules. ' + error);
            });
    }

    pageChanged(event: any): void {
        this.currentPage = event.page;
        this.loadSchedules();
        //console.log('Page changed to: ' + event.page);
        //console.log('Number items per page: ' + event.itemsPerPage);
    };

    removeSchedule(schedule: ISchedule) {
        this.notificationService.openConfirmationDialog('Are you sure you want to delete this schedule?',
            () => {
                //this.loadingBarService.start();
                this.dataService.deleteSchedule(schedule.id)
                    .subscribe(() => {
                        this.itemsService.removeItemFromArray<ISchedule>(this.schedules, schedule);
                        this.notificationService.printSuccessMessage(schedule.title + ' has been deleted.');
                        //this.loadingBarService.complete();
                    },
                    error => {
                        //this.loadingBarService.complete();
                        this.notificationService.printErrorMessage('Failed to delete ' + schedule.title + ' ' + error);
                    });
            });
    }

    viewScheduleDetails(id: number) {
        this.selectedScheduleId = id;

        this.dataService.getScheduleDetails(this.selectedScheduleId)
            .subscribe((schedule: IScheduleDetails) => {
                this.scheduleDetails = this.itemsService.getSerialized<IScheduleDetails>(schedule);
                // Convert date times to readable format
                this.scheduleDetails.timeStart = new DateFormatPipe().transform(schedule.timeStart, ['local']);
                this.scheduleDetails.timeEnd = new DateFormatPipe().transform(schedule.timeEnd, ['local']);
                //this.slimLoader.complete();
                this.selectedScheduleLoaded = true;
                this.childModal.show();//.open('lg');
            },
            error => {
                //this.slimLoader.complete();
                this.notificationService.printErrorMessage('Failed to load schedule. ' + error);
            });
    }

    public hideChildModal(): void {
        this.childModal.hide();
    }
}

Firstly, the component loads the schedules passing the current page and the number of items per page on the service call. The PaginatedResult response, contains the items plus the pagination information. The component uses PAGINATION_DIRECTIVES and PaginationComponent modules from ng2-bootstrap to render a pagination bar under the schedules table..

 <pagination [boundaryLinks]="true" [totalItems]="totalItems" [itemsPerPage]="itemsPerPage" [(ngModel)]="currentPage" class="pagination-sm"
        previousText="&lsaquo;" nextText="&rsaquo;" firstText="&laquo;" lastText="&raquo;" (pageChanged)="pageChanged($event)"></pagination>

The next important feature on this component is the custom modal popup it uses to display schedule’s details. It makes use of the ModalDirective from ng2-bootstrap. This plugin requires that you place a bsModal directive in your template and bind the model properties you wish to display on its template body. You also need to use the @ViewChild(‘childModal’) for this to work. Let’s view the entire schedule-list.component.html template and a small preview.

<button class="btn btn-primary" type="button" *ngIf="schedules">
   <i class="fa fa-calendar" aria-hidden="true"></i> Schedules  
   <span class="badge">{{totalItems}}</span>
</button>

<hr/>

<div [@flyInOut]="'in'">
    <table class="table table-hover">
        <thead>
            <tr>
                <th><i class="fa fa-text-width fa-2x" aria-hidden="true"></i>Title</th>
                <th><i class="fa fa-user fa-2x" aria-hidden="true"></i>Creator</th>
                <th><i class="fa fa-paragraph fa-2x" aria-hidden="true"></i>Description</th>
                <th><i class="fa fa-map-marker fa-2x" aria-hidden="true"></i></th>
                <th><i class="fa fa-calendar-o fa-2x" aria-hidden="true"></i>Time Start</th>
                <th><i class="fa fa-calendar-o fa-2x" aria-hidden="true"></i>Time End</th>
                <th></th>
                <th></th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr *ngFor="let schedule of schedules">
                <td> {{schedule.title}}</td>
                <td>{{schedule.creator}}</td>
                <td>{{schedule.description}}</td>
                <td>{{schedule.location}}</td>
                <td>{{schedule.timeStart | dateFormat | date:'medium'}}</td>
                <td>{{schedule.timeEnd | dateFormat | date:'medium'}}</td>
                <td><button class="btn btn-primary" (click)="viewScheduleDetails(schedule.id)">
            <i class="fa fa-info-circle" aria-hidden="true"></i>Details</button>
                </td>
                <td><a class="btn btn-primary" [routerLink]="['/schedules',schedule.id,'edit']"><i class="fa fa-pencil-square-o" aria-hidden="true"></i>Edit</a></td>
                <td>
                    <button class="btn btn-danger" (click)="removeSchedule(schedule)"><i class="fa fa-trash" aria-hidden="true"></i>Delete</button>
                </td>
            </tr>
        </tbody>
    </table>

    <pagination [boundaryLinks]="true" [totalItems]="totalItems" [itemsPerPage]="itemsPerPage" [(ngModel)]="currentPage" class="pagination-sm"
        previousText="&lsaquo;" nextText="&rsaquo;" firstText="&laquo;" lastText="&raquo;" (pageChanged)="pageChanged($event)"></pagination>
</div>

<div bsModal #childModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg" *ngIf="selectedScheduleLoaded">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" aria-label="Close" (click)="hideChildModal()">
          <span aria-hidden="true">&times;</span>
        </button>
                <h4>{{scheduleDetails.title}} details</h4>
            </div>
            <div class="modal-body">
                <form ngNoForm method="post">
                    <div class="form-group">
                        <div class="row">
                            <div class="col-md-4">
                                <label class="control-label"><i class="fa fa-user" aria-hidden="true"></i>Creator</label>
                                <input type="text" class="form-control" [(ngModel)]="scheduleDetails.creator" disabled />
                            </div>

                            <div class="col-md-4">
                                <label class="control-label"><i class="fa fa-text-width" aria-hidden="true"></i>Title</label>
                                <input type="text" class="form-control" [(ngModel)]="scheduleDetails.title" disabled />
                            </div>

                            <div class="col-md-4">
                                <label class="control-label"><i class="fa fa-paragraph" aria-hidden="true"></i>Description</label>
                                <input type="text" class="form-control" [(ngModel)]="scheduleDetails.description" disabled />
                            </div>
                        </div>
                    </div>

                    <div class="form-group">
                        <div class="row">
                            <div class="col-xs-6">
                                <label class="control-label"><i class="fa fa-calendar-o" aria-hidden="true"></i>Time Start</label>
                                <input type="text" class="form-control" [(ngModel)]="scheduleDetails.timeStart" disabled />
                            </div>

                            <div class="col-xs-6">
                                <label class="control-label"><i class="fa fa-calendar-check-o" aria-hidden="true"></i>Time End</label>
                                <input type="text" class="form-control" [(ngModel)]="scheduleDetails.timeEnd" disabled />
                            </div>
                        </div>
                    </div>

                    <div class="form-group">
                        <div class="row">
                            <div class="col-md-4">
                                <label class="control-label"><i class="fa fa-map-marker" aria-hidden="true"></i>Location</label>
                                <input type="text" class="form-control" [(ngModel)]="scheduleDetails.location" disabled />
                            </div>

                            <div class="col-md-4 selectContainer">
                                <label class="control-label"><i class="fa fa-spinner" aria-hidden="true"></i>Status</label>
                                <input type="text" class="form-control" [(ngModel)]="scheduleDetails.status" disabled />
                            </div>
                            <div class="col-md-4 selectContainer">
                                <label class="control-label"><i class="fa fa-tag" aria-hidden="true"></i>Type</label>
                                <input type="text" class="form-control" [(ngModel)]="scheduleDetails.type" disabled />
                            </div>
                        </div>
                    </div>
                    <hr/>
                    <div class="panel panel-info">

                        <div class="panel-heading">Attendes</div>

                        <table class="table table-hover">
                            <thead>
                                <tr>
                                    <th></th>
                                    <th><i class="fa fa-user" aria-hidden="true"></i>Name</th>
                                    <th><i class="fa fa-linkedin-square" aria-hidden="true"></i>Profession</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr *ngFor="let attendee of scheduleDetails.attendees">
                                    <td [style.valign]="'middle'">
                                        <img class="img-thumbnail img-small" src="{{apiHost}}images/{{attendee.avatar}}" alt="attendee.name" />
                                    </td>
                                    <td [style.valign]="'middle'">{{attendee.name}}</td>
                                    <td [style.valign]="'middle'">{{attendee.profession}}</td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

angular-scheduler-spa-04
The ScheduleEditComponent is responsible to edit the details of a single Schedule. The interface used for this component is the IScheduleDetails which encapsulates all schedule’s details (creator, attendees, etc..). Add the schedule-edit.component.ts file under the schedules folder.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { NgForm } from '@angular/forms';

import { DataService } from '../shared/services/data.service';
import { ItemsService } from '../shared/utils/items.service';
import { NotificationService } from '../shared/utils/notification.service';
import { ConfigService } from '../shared/utils/config.service';
import { MappingService } from '../shared/utils/mapping.service';
import { ISchedule, IScheduleDetails, IUser } from '../shared/interfaces';
import { DateFormatPipe } from '../shared/pipes/date-format.pipe';

@Component({
    moduleId: module.id,
    selector: 'app-schedule-edit',
    templateUrl: 'schedule-edit.component.html'
})
export class ScheduleEditComponent implements OnInit {
    apiHost: string;
    id: number;
    schedule: IScheduleDetails;
    scheduleLoaded: boolean = false;
    statuses: string[];
    types: string[];
    private sub: any;

    constructor(private route: ActivatedRoute,
        private router: Router,
        private dataService: DataService,
        private itemsService: ItemsService,
        private notificationService: NotificationService,
        private configService: ConfigService,
        private mappingService: MappingService) { }

    ngOnInit() {
        // (+) converts string 'id' to a number
	    this.id = +this.route.snapshot.params['id'];
        this.apiHost = this.configService.getApiHost();
        this.loadScheduleDetails();
    }

    loadScheduleDetails() {
        //this.slimLoader.start();
        this.dataService.getScheduleDetails(this.id)
            .subscribe((schedule: IScheduleDetails) => {
                this.schedule = this.itemsService.getSerialized<IScheduleDetails>(schedule);
                this.scheduleLoaded = true;
                // Convert date times to readable format
                this.schedule.timeStart = new Date(this.schedule.timeStart.toString()); // new DateFormatPipe().transform(schedule.timeStart, ['local']);
                this.schedule.timeEnd = new Date(this.schedule.timeEnd.toString()); //new DateFormatPipe().transform(schedule.timeEnd, ['local']);
                this.statuses = this.schedule.statuses;
                this.types = this.schedule.types;

                //this.slimLoader.complete();
            },
            error => {
                //this.slimLoader.complete();
                this.notificationService.printErrorMessage('Failed to load schedule. ' + error);
            });
    }

    updateSchedule(editScheduleForm: NgForm) {
        console.log(editScheduleForm.value);

        var scheduleMapped = this.mappingService.mapScheduleDetailsToSchedule(this.schedule);

        //this.slimLoader.start();
        this.dataService.updateSchedule(scheduleMapped)
            .subscribe(() => {
                this.notificationService.printSuccessMessage('Schedule has been updated');
                //this.slimLoader.complete();
            },
            error => {
                //this.slimLoader.complete();
                this.notificationService.printErrorMessage('Failed to update schedule. ' + error);
            });
    }

    removeAttendee(attendee: IUser) {
        this.notificationService.openConfirmationDialog('Are you sure you want to remove '
            + attendee.name + ' from this schedule?',
            () => {
                //this.slimLoader.start();
                this.dataService.deleteScheduleAttendee(this.schedule.id, attendee.id)
                    .subscribe(() => {
                        this.itemsService.removeItemFromArray<IUser>(this.schedule.attendees, attendee);
                        this.notificationService.printSuccessMessage(attendee.name + ' will not attend the schedule.');
                        //this.slimLoader.complete();
                    },
                    error => {
                        //this.slimLoader.complete();
                        this.notificationService.printErrorMessage('Failed to remove ' + attendee.name + ' ' + error);
                    });
            });
    }

    back() {
        this.router.navigate(['/schedules']);
    }

}

The interesting part about this component is the validations it carries on the template schedule-edit.component.html.

<form #editScheduleForm="ngForm" *ngIf="scheduleLoaded" novalidate>
    <div class="alert alert-danger" [hidden]="editScheduleForm.form.valid">
        <ul *ngIf="creator.dirty && !creator.valid">
            <li>Creator name is required <i>(5-50 characters)</i></li>
        </ul>
        <ul *ngIf="title.dirty && !title.valid">
            <li *ngIf="title.errors.required">Title is required</li>
            <li *ngIf="title.errors.pattern">Title should have 5-20 characters</li>
        </ul>
        <ul *ngIf="description.dirty && !description.valid">
            <li *ngIf="description.errors.required">Description is required</li>
            <li *ngIf="description.errors.pattern">Description should have at least 10 characters</li>
        </ul>
        <ul *ngIf="location.dirty && !location.valid">
            <li *ngIf="location.errors.required">Location is required</li>
        </ul>
    </div>

    <button type="button" class="btn btn-danger" (click)="back()">
        <i class="fa fa-arrow-circle-left" aria-hidden="true"></i>Back</button>
    <button type="button" [disabled]="!editScheduleForm.form.valid" class="btn btn-default" (click)="updateSchedule(editScheduleForm)">
        <i class="fa fa-pencil-square-o" aria-hidden="true"></i>Update</button>

    <hr/>

    <div class="form-group">
        <div class="row">
            <div class="col-md-4">
                <label class="control-label"><i class="fa fa-user" aria-hidden="true"></i>Creator</label>
                <input type="text" class="form-control" [(ngModel)]="schedule.creator" name="creator" #creator="ngModel" required pattern=".{5,50}"
                    disabled />
            </div>

            <div class="col-md-4">
                <label class="control-label"><i class="fa fa-text-width" aria-hidden="true"></i>Title</label>
                <input type="text" class="form-control" [(ngModel)]="schedule.title" name="title" #title="ngModel" required pattern=".{5,20}"
                />
            </div>

            <div class="col-md-4">
                <label class="control-label"><i class="fa fa-paragraph" aria-hidden="true"></i>Description</label>
                <input type="text" class="form-control" [(ngModel)]="schedule.description" name="description" #description="ngModel" required
                    pattern=".{10,}" />
            </div>
        </div>
    </div>

    <div class="form-group">
        <div class="row">
            <div class="col-xs-6">
                <label class="control-label"><i class="fa fa-calendar-o" aria-hidden="true"></i>Time Start</label>
                <datepicker [(ngModel)]="schedule.timeStart" name="timeStartDate" [showWeeks]="false"></datepicker>
                <timepicker [(ngModel)]="schedule.timeStart" name="timeStartTime" (change)="changed()" [hourStep]="1" [minuteStep]="15" [showMeridian]="true"></timepicker>
            </div>

            <div class="col-xs-6">
                <label class="control-label"><i class="fa fa-calendar-check-o" aria-hidden="true"></i>Time End</label>
                <datepicker [(ngModel)]="schedule.timeEnd" name="timeEndDate" [showWeeks]="false"></datepicker>
                <timepicker [(ngModel)]="schedule.timeEnd" name="timeEndTime" (change)="changed()" [hourStep]="1" [minuteStep]="15" [showMeridian]="true"></timepicker>
            </div>
        </div>
    </div>

    <div class="form-group">
        <div class="row">
            <div class="col-md-4">
                <label class="control-label"><i class="fa fa-map-marker" aria-hidden="true"></i>Location</label>
                <input type="text" class="form-control" [(ngModel)]="schedule.location" name="location" #location="ngModel" required />
            </div>

            <div class="col-md-4 selectContainer">
                <label class="control-label"><i class="fa fa-spinner" aria-hidden="true"></i>Status</label>
                <select class="form-control" [(ngModel)]="schedule.status" name="status">
                    <option *ngFor="let status of statuses" [value]="status">{{status}}</option>
                </select>
            </div>
            <div class="col-md-4 selectContainer">
                <label class="control-label"><i class="fa fa-tag" aria-hidden="true"></i>Type</label>
                <select class="form-control" [(ngModel)]="schedule.type" name="type">
                    <option *ngFor="let type of types" [value]="type">{{type}}</option>
                </select>
            </div>
        </div>
    </div>
    <hr/>
    <div class="panel panel-info">
        <!-- Default panel contents -->
        <div class="panel-heading">Attendes</div>

        <!-- Table -->
        <table class="table table-hover">
            <thead>
                <tr>
                    <th></th>
                    <th><i class="fa fa-user" aria-hidden="true"></i>Name</th>
                    <th><i class="fa fa-linkedin-square" aria-hidden="true"></i>Profession</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                <tr *ngFor="let attendee of schedule.attendees">
                    <td [style.valign]="'middle'">
                        <img class="img-thumbnail img-small" src="{{apiHost}}images/{{attendee.avatar}}" alt="attendee.name" />
                    </td>
                    <td [style.valign]="'middle'">{{attendee.name}}</td>
                    <td [style.valign]="'middle'">{{attendee.profession}}</td>
                    <td [style.valign]="'middle'">
                        <button type="button" class="btn btn-danger btn-sm" (click)="removeAttendee(attendee)"><i class="fa fa-user-times" aria-hidden="true"></i>Remove</button>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</form>

angular-crud-modal-animation-04
Don’t forget that we have also set server-side validations, so if you try to edit a schedule and set the start time to be greater than the end time you should receive an error that was encapsulated by the server in the response message, either on the header or the body.
angular-crud-modal-animation-05
The Users feature is an interesting one as well. I have decided on this one to display each user as a card element instead of using a table. This required to create a user-card custom element which encapsulates all the logic not only for rendering but also manipulating user’s data (CRUD ops..). Add a folder named Users under app and create the UserCardComponent.

import { Component, Input, Output, OnInit, ViewContainerRef, EventEmitter, ViewChild,
    trigger,
    state,
    style,
    animate,
    transition  } from '@angular/core';

import { IUser, ISchedule } from '../shared/interfaces';
import { DataService } from '../shared/services/data.service';
import { ItemsService } from '../shared/utils/items.service';
import { NotificationService } from '../shared/utils/notification.service';
import { ConfigService } from '../shared/utils/config.service';
import { HighlightDirective } from '../shared/directives/highlight.directive';

import { ModalDirective } from 'ng2-bootstrap';

@Component({
    moduleId: module.id,
    selector: 'user-card',
    templateUrl: 'user-card.component.html',
    animations: [
        trigger('flyInOut', [
            state('in', style({ opacity: 1, transform: 'translateX(0)' })),
            transition('void => *', [
                style({
                    opacity: 0,
                    transform: 'translateX(-100%)'
                }),
                animate('0.5s ease-in')
            ]),
            transition('* => void', [
                animate('0.2s 10 ease-out', style({
                    opacity: 0,
                    transform: 'translateX(100%)'
                }))
            ])
        ])
    ]
})
export class UserCardComponent implements OnInit {
    @ViewChild('childModal') public childModal: ModalDirective;
    @Input() user: IUser;
    @Output() removeUser = new EventEmitter();
    @Output() userCreated = new EventEmitter();

    edittedUser: IUser;
    onEdit: boolean = false;
    apiHost: string;
    // Modal properties
    @ViewChild('modal')
    modal: any;
    items: string[] = ['item1', 'item2', 'item3'];
    selected: string;
    output: string;
    userSchedules: ISchedule[];
    userSchedulesLoaded: boolean = false;
    index: number = 0;
    backdropOptions = [true, false, 'static'];
    animation: boolean = true;
    keyboard: boolean = true;
    backdrop: string | boolean = true;

    constructor(private itemsService: ItemsService,
        private notificationService: NotificationService,
        private dataService: DataService,
        private configService: ConfigService) { }

    ngOnInit() {
        this.apiHost = this.configService.getApiHost();
        this.edittedUser = this.itemsService.getSerialized<IUser>(this.user);
        if (this.user.id < 0)
            this.editUser();
    }

    editUser() {
        this.onEdit = !this.onEdit;
        this.edittedUser = this.itemsService.getSerialized<IUser>(this.user);
        // <IUser>JSON.parse(JSON.stringify(this.user)); // todo Utils..
    }

    createUser() {
        //this.slimLoader.start();
        this.dataService.createUser(this.edittedUser)
            .subscribe((userCreated) => {
                this.user = this.itemsService.getSerialized<IUser>(userCreated);
                this.edittedUser = this.itemsService.getSerialized<IUser>(this.user);
                this.onEdit = false;

                this.userCreated.emit({ value: userCreated });
                //this.slimLoader.complete();
            },
            error => {
                this.notificationService.printErrorMessage('Failed to created user');
                this.notificationService.printErrorMessage(error);
                //this.slimLoader.complete();
            });
    }

    updateUser() {
        //this.slimLoader.start();
        this.dataService.updateUser(this.edittedUser)
            .subscribe(() => {
                this.user = this.edittedUser;
                this.onEdit = !this.onEdit;
                this.notificationService.printSuccessMessage(this.user.name + ' has been updated');
                //this.slimLoader.complete();
            },
            error => {
                this.notificationService.printErrorMessage('Failed to edit user');
                this.notificationService.printErrorMessage(error);
                //this.slimLoader.complete();
            });
    }

    openRemoveModal() {
        this.notificationService.openConfirmationDialog('Are you sure you want to remove '
            + this.user.name + '?',
            () => {
                //this.slimLoader.start();
                this.dataService.deleteUser(this.user.id)
                    .subscribe(
                    res => {
                        this.removeUser.emit({
                            value: this.user
                        });
                        //this.slimLoader.complete();
                        //this.slimLoader.complete();
                    }, error => {
                        this.notificationService.printErrorMessage(error);
                        //this.slimLoader.complete();
                    })
            });
    }

    viewSchedules(user: IUser) {
        console.log(user);
        this.dataService.getUserSchedules(this.edittedUser.id)
            .subscribe((schedules: ISchedule[]) => {
                this.userSchedules = schedules;
                console.log(this.userSchedules);
                this.userSchedulesLoaded = true;
                this.childModal.show();
                //this.slimLoader.complete();
            },
            error => {
                //this.slimLoader.complete();
                this.notificationService.printErrorMessage('Failed to load users. ' + error);
            });
        
    }

    public hideChildModal(): void {
        this.childModal.hide();
    }

    opened() {
        //this.slimLoader.start();
        this.dataService.getUserSchedules(this.edittedUser.id)
            .subscribe((schedules: ISchedule[]) => {
                this.userSchedules = schedules;
                console.log(this.userSchedules);
                this.userSchedulesLoaded = true;
                //this.slimLoader.complete();
            },
            error => {
                //this.slimLoader.complete();
                this.notificationService.printErrorMessage('Failed to load users. ' + error);
            });
        this.output = '(opened)';
    }

    isUserValid(): boolean {
        return !(this.edittedUser.name.trim() === "")
            && !(this.edittedUser.profession.trim() === "");
    }

}

The logic about the modal and the animations should be familiar to you at this point. The new feature to notice on this component are the @Input() and @Output() properties. The first one is used so that the host component which is the UserListComponent pass the user item foreach user in a array of IUser items. The two @Output() properties are required so that a user-card can inform the host component that something happend, in our case that a user created or removed. Why? It’s a matter of Separation of Concerns. The list of users is maintained by the UserListComponent and a single UserCardComponent knows nothing about it. That’s why when something happens the UserListComponent needs to be informed and update the user list respectively. Here’s the user-card.component.html.

<div class="panel panel-primary" [ngClass]="{shadowCard: onEdit}" [@flyInOut]="'in'">
    <div class="panel-heading">
        <h3 class="panel-title pull-left" [class.hidden]="onEdit"><i class="fa fa-user" aria-hidden="true"></i>{{edittedUser.name}}</h3>
        <input [(ngModel)]="edittedUser.name" [class.hidden]="!onEdit" [style.color]="'brown'" required class="form-control" />
        <div class="clearfix"></div>
    </div>

    <div highlight="whitesmoke" class="panel-body">
        <div class="">
            <img src="{{apiHost}}images/{{edittedUser.avatar}}" class="img-avatar" alt="">
            <div class="caption">
                <p>
                    <span [class.hidden]="onEdit">{{edittedUser.profession}}</span>
                </p>
                <p [hidden]="!onEdit">
                    <input [(ngModel)]="edittedUser.profession" class="form-control" required />
                </p>
                <p>
                    <button class="btn btn-primary" (click)="viewSchedules(edittedUser)" [disabled]="edittedUser.schedulesCreated === 0">
                    <i class="fa fa-calendar-check-o" aria-hidden="true"></i> Schedules <span class="badge"> 
                        {{edittedUser.schedulesCreated}}</span>
                    </button>
                </p>
            </div>
        </div>
    </div>
    <div class="panel-footer">
        <div [class.hidden]="edittedUser.id < 0">
            <button class="btn btn-default btn-xs" (click)="editUser()">
                <i class="fa fa-pencil" aria-hidden="true"></i>
                    {{onEdit === false ? "Edit" : "Cancel"}}
                </button>
            <button class="btn btn-default btn-xs" [class.hidden]="!onEdit" (click)="updateUser()" [disabled]="!isUserValid()">
                <i class="fa fa-pencil-square-o" aria-hidden="true"></i>Update</button>
            <button class="btn btn-danger btn-xs" (click)="openRemoveModal()">
                <i class="fa fa-times" aria-hidden="true"></i>Remove</button>
        </div>
        <div [class.hidden]="!(edittedUser.id < 0)">
            <button class="btn btn-default btn-xs" [class.hidden]="!onEdit" (click)="createUser()" [disabled]="!isUserValid()">
                <i class="fa fa-plus" aria-hidden="true"></i>Create</button>
        </div>
    </div>
</div>

<div bsModal #childModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg" *ngIf="userSchedulesLoaded">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" aria-label="Close" (click)="hideChildModal()">
          <span aria-hidden="true">&times;</span>
        </button>
                <h4 class="modal-title">{{edittedUser.name}} schedules created</h4>
            </div>
            <div class="modal-body">
                <table class="table table-hover">
                    <thead>
                        <tr>
                            <th>Title</th>
                            <th>Description</th>
                            <th>Place</th>
                            <th>Time Start</th>
                            <th>Time End</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr *ngFor="let schedule of userSchedules">
                            <td> {{schedule.title}}</td>
                            <td>{{schedule.description}}</td>
                            <td>{{schedule.location}}</td>
                            <td>{{schedule.timeStart | dateFormat | date:'medium'}}</td>
                            <td>{{schedule.timeEnd | dateFormat | date:'medium'}}</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>

Also add the user-list.component.ts and notice the usage of the ItemsService for manipulating items.

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

import { DataService } from '../shared/services/data.service';
import { ItemsService } from '../shared/utils/items.service';
import { NotificationService } from '../shared/utils/notification.service';
import { IUser } from '../shared/interfaces';
import { UserCardComponent } from './user-card.component';

@Component({
    moduleId: module.id,
    selector: 'users',
    templateUrl: 'user-list.component.html'
})
export class UserListComponent implements OnInit {

    users: IUser[];
    addingUser: boolean = false;

    constructor(private dataService: DataService,
        private itemsService: ItemsService,
        private notificationService: NotificationService) { }

    ngOnInit() {
        this.dataService.getUsers()
            .subscribe((users: IUser[]) => {
                this.users = users;
            },
            error => {
                this.notificationService.printErrorMessage('Failed to load users. ' + error);
            });
    }

    removeUser(user: any) {
        var _user: IUser = this.itemsService.getSerialized<IUser>(user.value);
        this.itemsService.removeItemFromArray<IUser>(this.users, _user);
        // inform user
        this.notificationService.printSuccessMessage(_user.name + ' has been removed');
    }

    userCreated(user: any) {
        var _user: IUser = this.itemsService.getSerialized<IUser>(user.value);
        this.addingUser = false;
        // inform user
        this.notificationService.printSuccessMessage(_user.name + ' has been created');
        console.log(_user.id);
        this.itemsService.setItem<IUser>(this.users, (u) => u.id == -1, _user);
        // todo fix user with id:-1
    }

    addUser() {
        this.addingUser = true;
        var newUser = { id: -1, name: '', avatar: 'avatar_05.png', profession: '', schedulesCreated: 0 };
        this.itemsService.addItemToStart<IUser>(this.users, newUser);
        //this.users.splice(0, 0, newUser);
    }

    cancelAddUser() {
        this.addingUser = false;
        this.itemsService.removeItems<IUser>(this.users, x => x.id < 0);
    }
}

The removeUser and userCreated are the events triggered from child UserCardComponent components. When those events are triggered, the action has already finished in API/Database level and what remains is to update the client-side list. Here’s the template for the UserListComponent.

<button [class.hidden]="addingUser" class="btn btn-primary" (click)="addUser()">
    <i class="fa fa-user-plus fa-2x" aria-hidden="true"></i>Add</button>
<button [class.hidden]="!addingUser" class="btn btn-danger" (click)="cancelAddUser()">
    <i class="fa fa-ban fa-2x" aria-hidden="true"></i>Cancel</button>

<hr/>

<div class="row text-center">
    <div class="col-md-3 col-sm-6 hero-feature" *ngFor="let user of users">
        <user-card [user]="user" (removeUser)="removeUser($event);" (userCreated)="userCreated($event);"></user-card>
    </div>
</div>

The SPA uses some custom stylesheet styles.css which you can find here. Add it in a new folder named assets/styles under the root of the application. At this point you should be able to run the SPA. Make sure you have set the API first and configure the API’s endpoint in the ConfigService to point it properly. Fire the app by running the following command.

npm start

Conclusion

That’s it we have finished! We have seen many Angular 2 features on this SPA but I believe the more exciting one was how TypeScript can ease client-side development. We saw typed predicates, array manipulation using lodash and last but not least how to install and use 3rd party libraries in our app using SystemJS.

Source Code: You can find the source code for this project here where you will also find instructions on how to run the application.

In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.

Facebook Twitter
.NET Web Application Development by Chris S.
facebook twitter-small


Categories: Angular, ASP.NET, Best practices

Tags:

65 replies

  1. I am just mesmerised by the top quality content on you blog. Just want to ask if there is a road map to follow just to reach a point where you latest blog posts, like the one above, could be better understood by me. I have very basic kind of knowledge about these technologies and need some guidance to get a start.

  2. Chris, thanks a lot for fantastic job you are doing.

  3. Awesome , keep the good work

  4. Chris you are a star, the quality and the way its articulated, really amazing !! Keep up the good work and expecting more…

  5. Thanks, very interesting and useful as usual!

    (small typo – package.son , and maybe select line 10 in code snippet (ng2-datetime)
    “This is done through the map object in the systemjs.config.js as follow.”
    line 11 is selected there 🙂

  6. Thanks Chris… ur Blogs are awesome

  7. Fantastic work. Excelent!

  8. Line 14 of items.service.ts is missing a “(“ and causing an error. It should be..

    removeItemFromArray(array: Array, item: any) {
    // code omitted
    }
    

    Your blogs are REALLY helpful

    • Thanks James, I fixed it. The source code was good though. Actually it’s

      /*
          Removes an item from an array using the lodash library
          */
          removeItemFromArray<T>(array: Array<T>, item: any) {
              _.remove(array, function (current) {
                  //console.log(current);
                  return JSON.stringify(current) === JSON.stringify(item);
              });
          }
      
      • Sorry – I should have said “small typo in the blog” 🙂 since the source code was correct as you stated 🙂

        Thanks again!

  9. great post, turned his fan.

    {
      "ng2-datetime": "^1.1.0",
      "lite-server": "^2.2.0",
      "typescript": "^1.8.10",
      "typings": "^1.0.4",
      "tslint": "^3.10.2"
    }
    

    They are not supported by npm, any tips on what I should do?

  10. Absolutely love the thoroughness and adherence to best practices. You don’t often see tutorials that are this current, complete and in-line with architectural best practices. Just implemented and it’s working fine.

    A couple things:

    1. data.service.ts, line 102, “getSchedule(id: number): Observable”, when I believe it should be “getSchedule(id: number): Observable “. I updated “Schedule” to reference “ISchedule” the interface. GitHub is already updated with this fix, but I think the tutorial needs to be updated as well.

    2. The reference “schedule-edit.ts” should be updated to “schedule-edit.component.ts”

    And finally, a general question.

    There seems to be a fair amount of duplication across client-side and server-side components (e.g. routing, interface definitions/view model, etc.). I don’t think it can be avoided in the current evolution of these technologies, since Angular is operating like a server on the client side (SPA mode). Are you aware of any refactoring opportunities whereby these redundancies can be minimized or eliminated outright? Normally these sort of redundancies are eventually addressed as a technology evolves, but in this case, we’re covering such a wide spectrum of client, server, different vendors (Microsoft, Google, etc…), languages – I’m wondering how they might work together to simplify things a bit more. When I first started looking at developing within these technologies, it was unclear how/where to define the entities/routes. Now I understand it has to be in two places – hence the redundancies. Sorry for the long-winded question – hope it makes sense!

    Fantastic stuff Christos – thank you so much.

  11. Has anyone been able to make this work fully? When I try to edit / update a schedule it seems that the Status and Type properties aren’t being sent over to the API and the update fails. Also, there is an error: “EXCEPTION: Error: Uncaught (in promise): No value accessor for ‘timeStart'” for both timeStart and timeEnd. in the debugger console on Update

    I actually coded the entire tutorial by hand so I would learn as I go – finally I just cloned the source for both the API and ht SPA, but still have the same issues. Thanks!

    • Hi Frank,

      Both issues fixed. The first one was probably the missing parenthesis on Status and Type properties while the second one had to do with some issues that the ng2-datetime plugin was facing.

      Anyway, seems that all work just fine now. Make sure to always sync with the latest version on Github.

      • That did the trick, thanks a lot, Christos! I wanted to hand code it so it would stick with me which was why I didn’t originally sync with GitHub (I later decided to, so I could compare).

        These tutorials (especially part 1) have been extremely helpful! Part 1 was excellent for showing how to separate the different tiers of the application. Thank you again!

  12. Hello Chris,
    Thank you for the excellent post. I have one question: you’re making use of datetime component in the edit screen. how would you include it in the form validation to make it required ?

    • Unfortunately you cannot just use required attribute in the ng2-datetime datetime directive, it will not work.
      Try to understand its behavior by pasting {{schedule.timeStart | json}} somewhere in the schedule-edit.component.html. It will be null only if both [timepicker] and [datepicker] are empty.
      On the other hand you could use FormBuilder, FormGroup, Validators, etc.. from @angular/forms and change the validation logic on that page. Part of that code would look like this:

      import {FORM_DIRECTIVES, FormBuilder, FormGroup, Validators, AbstractControl} from '@angular/forms';
      
      @Component({
        templateUrl: 'schedule-edit.component.html',
        directives: [FORM_DIRECTIVES]
      })
      export class ScheduleEditComponent implements OnInit {
      editForm: FormGroup;
      timeStart: AbstractControl;
      
      constructor(private fb: FormBuilder) { }
      
      ngOnInit() {
          this.editForm= this.fb.group({
            'timeStart': ['', Validators.compose([Validators.required])]
          });
      
          this.timeStart= this.editForm.controls['timeStart'];
        }
      

      You could also create your own custom validators for that field.

      Hope I helped.

  13. Hello, I had to set the version of ng2-slim-loading-bar to 1.2.3 instead of ^1.2.3 because the latest major version (1.4.0) kind of breaks everything.

  14. Hi Christos,

    Do you have any ideas to write blogs about multi tenant app with angular 2 + .net core in future? I am just curious to know. :))

    Thanks
    Jasper

  15. I’m getting an error in the Visual Studio Code editor in regard to the ng2-slim-loading-bar. For example, in the user-card.component.ts:

     import {SlimLoadingBarService} from 'ng2-slim-loading-bar/ng2-slim-loading-bar';
    

    I’m getting the following error:
    Cannot find module ‘ng2-slim-loading-bar’

    The module does exist on my system though in the following directory:
    C:\Projects\ScheduleSpa\node_modules\ng2-slim-loading-bar

    And inside this folder I have the index.js, index.d.ts, index.js.map, package.json, etc files, and bundles, node_modules, src and test folders.

  16. I guess the next step would be to deploy the Web API and Web App to Azure 🙂

  17. Has anyone managed to deploy the Web API to IIS?

  18. Is there an easy way to set http headers for each request?

  19. Please ignore my previous question. I see it in the code:

    createUser(user: IUser): Observable {
    
            let headers = new Headers();
            headers.append('Content-Type', 'application/json');
    
            return this.http.post(this._baseUrl + 'users/', JSON.stringify(user), {
                headers: headers
    
  20. I am getting connection error when I am trying to load the schedules.

  21. do you have any paln to update Angular rc6…
    Thanks

  22. Hi Christos,

    among all the work I have seen with AngularJS, you are doing a great job.

    At the moment, the team that I am working with is evaluating some technologies to develop a small ERP system for particular domain area. Since the ERP is going to be an application on the browser, they are excited about angular2 however I realised that it will require a lot of redundancies between the server and the client side since AngularJS 2 has a lot focus on self-contained applications (SPA).

    ERP systems grow continuously regarding data. Every day you need to add extra objects, fields and relationship between your data and having a framework to facilitate that is imperative. ERP are large CRUD systems with a lot of business logic behind the scenes. I can count approximately 200 tables and more than 5000 fields expected for our application. I can see some challenges using angular:

    – Entities defined on the server side would need to be redefined/replicated on the client side. (For example, your IUser interface is defined twice in the server side and on the client side). Now, imagine keeping in sync more than 200 objects. Every time we change the data structure on the server side, it would require redefining the data in the client.
    – Validating data would need to be done on the server side and the client side. Ideally, a metadata table would be built to avoid that.
    – The volume of client components. I am afraid that angularjs2 would generate a lot of client data and lazy loading them would be hard to manage. For example, we have modules for Contracts, People, Orders, Payment, Invoice. If the browser needs to download the whole app in the startup, I believe it would be slow. I believe there is a way to load them on demand. However, I believe it would be somewhat hard to manage.

    Maybe I should tell the technical team to stick with JSF since everything can be managed on the server side and offering offline capabilities in an ERP-centric application is not a must.

    Please, let me know your thoughts since my view on angularjs2 is just superficial.

    Thanks a lot for your posts.

    • Hi Alisson,
      Because of the fact that ERP systems have dynamic nature you need to adapt this behavior on the client side as well. Instead of defining your models on the client side and have to keep them always synced with the server & vice versa, you should develop your own generic angular framework which contains dynamic, flexible components and templates. For example, if there is an Order entity on the server, the client side needs to get a JSON configuration object that describes that entity, for example which are the columns, the type of each column, the validation logic to apply for each of them, how to render the control in the template and so on..
      It’s difficult but not impossible. The Dynamic Templates in AngularJS post which uses AngularJS 1 version, might help you more on this idea.

      • Hi Christo, thanks for your considerations and reply.

        I will research it a little bit more and I believe that anything is possible in the development world.

        Regarding entities, I believe JSON would help with the part where a declaration for objects in both server and client side. Once client side receives the JSON data, the object can be automatically reconstructed and can be used in the UI. Since it is JavaScript/TypeScript, I imagine that we don’t need to declare the entity statically on the Client side (which is good).

        Thanks for the references! we will read a little bit more before settling on this decision.

  23. Christos, can you please explain how to publish the app on IIS server using Visual Studio Update 3?
    When run app from VS using IIS express application running well. But as it was hosted on the IIS 8.5 app shows below error message in browser
    Oops.
    500 Internal Server Error
    An error occurred while starting the application.
    Can you please explain hosting on IIS Server 8.5 step by step. And changes needed in the configuration files.

  24. Awesome article Christos.
    is it possible to implement adding a user’s schedule? I’m kind of lost with the post service.
    Tnks anyway!

  25. Excellent Post christos.You are rocking.

  26. Bower issue:

    Had to manually install the discontinued distributions for:

    1. jquery 3.*
    2. font-awesome
    3. alertify.js

    The way this was determined was that the page did not load properly, and when compiling got many 404 errors for the “bower_components” folder.

    Maybe Bower was having issues because of the alertify.js discontinuation…

    Took hours to determine this was the issue!

    Awesome blog posts…You should be charging people, but hey thanks!!!!!!

  27. Great post. pls how can use angular-cli to bootstrap this app

  28. Thank-you this was very good indeed. Very educational.

  29. Top quality post.

    There are a few additional considerations I would appreciate your point of view on:

    1. Injecting JavaScript on rendering each component (lazy loading of scripts) to support enterprise grade solutions. It appears that the Angular team allow you to specify the style as an attribute on the component but there is no support for scripts. There must be a good reason for this but not clear why not? I used JavaScript to add scripts to the body after rendering the view. Not sure if there are other approaches one could adopt.
    2. Rendering slide in or out animations based on the level that the routes are nested. I noticed the animation styles are specified for each component. How do you handle styles set in the stylesheets i.e. the old method of handling styles on enter and exit of views (slide in for new views and slide out to return to parent view).
    3. Middle-ware plug-ins e.g. social media authentication.

  30. Excellent and wonderful post

  31. Hi Christos,

    I am beginner to angular js2, trying to do pagination for my grid.

    Are you sending any information in header from service, what purpose you are doing this.
    var paginationHeader: Pagination = this.getSerialized(JSON.parse(res.headers.get(“Pagination”)));
    console.log(paginationHeader);
    peginatedResult.pagination = paginationHeader;

    I am able to display all records in my table, but not recordsperpage records, Where I have to make changes for this.

    Could you please help in this regards.

    Thanks in Advance.

    Regards,
    Ramesh.

  32. Chris, You are lethal champ in Asp.net field. I was looking articles that you posted. Thanks for all of articles.

  33. Chris, I published this application on azure but getting following and some other errors, I also run npm install on azure cmd after deployment. Can you please guide if I am missing somethings.thanks.

    (SystemJS) XHR error (404 Not Found)

  34. Hello,
    How to get github repo for this demo

  35. How can I create new schedules on the client side?
    Thanks in Advance.

  36. I am trying to follow along , obviously I need to create the Web Service first , but since I am running an older laptop , Visual Studio 2013 is my best option , and I never worked with CORE
    So I ll do a Web Api project instead of .NET CORE
    Should I be worryed about anything in particular ???

  37. Can u post the Web service base URL to use in Angular

  38. I have used the Visual Studio 2017 to create the Web API service and Angular 4 CLI to create the Angular project. I has used npm to add alertify and @types/alertify package. All seems work fine.
    The only problem is the “alertify.js” create message and dialog box at the left bottom corner.

  39. Chris, thank you! Your blog is super. And the best one!
    Can you migrate Scheduler.SPA to VS2017?
    I think in VS2017, we can debug TypeScript code.
    Thank you!

  40. Thanks for the great angular/WebApi example!

    Fixed an issue:
    “font-awesome”: “^4.7.0” instead of “font-awesome”: “latest” in bower.json

  41. Great article. You may way to update it to Angular 8 since the new versions are coming very fast.

Leave a reply to Suresh C Cancel reply