(OK) Angular 2 CRUD, modals, animations, pagination, datetimepicker and much more
来源:互联网 发布:excel数据有效性 编辑:程序博客网 时间:2024/06/06 00:35
https://chsakell.com/2016/06/27/angular-2-crud-modals-animations-pagination-datetimepicker/
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 apost 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. TheSchedule.SPA application that we are going to build will make use of all the previously mentioned components following the recommendedAngular 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 postBuilding REST APIs using ASP.NET Core and Entity Framework Core. The source code for the API which was built using.NET Core can be foundhere where you will also find instructions how to run it. The SPA will displayschedules 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 seeCRUD 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).
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 theAngular – TypeScript application. Create a folder namedScheduler.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 asng2-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 thesystemjs.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 aDateTime 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 thepackage.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 tohttp://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 tong2-datetime dispatches the server. This is done through the map object in thesystemjs.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 tonode_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 thepackages 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 atsconfig.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 atypings.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 asalertify.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 theindex.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 namedhome, schedules, users and shared. The home folder is responsible to display a landing page, theschedules and users are the basic features in the SPA and the lattershared will contain any component that will be used across the entire app, such asdata 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. Anapp.component.ts to hold the root container of the app, anapp.module.ts to hold the app’s NgModule and the previous main.ts to bootstrap the app. Go ahead and create theapp.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. Theapp.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 underapp 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. TheDataService holds the CRUD operations for sending HTTP request to the API, theItemsService defines custom methods for manipulating mostly arrays using thelodash 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 theapp.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 theHomeComponent and http://localhost:your_port/users theUserListComponent 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 underapp 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 arouter-outlet element on its template where other children components are rendered. Add theapp.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 namedshared 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’sViewModels. 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 typeIUser and you want to remove any user item that hasid<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 undershared. 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.
The thing is that when you reduce the width of the browser the font-image moves on top giving a bad user experience.
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 followingMobileHide directive in a mobile-hide.directive.ts file undershared/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..
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 theshared folder. We will separate them though in two different types, core and utilities. Add two folders namedservices and utils under the shared folder. We will place all core services underservices and utilities under utitlities. The most important core service in our SPA is the one responsible to sendHTTP requests to the API, the DataService. Add thedata.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 theConfigService in order to get the API’s URI and theItemsService to parse JSON objects to typed ones(we ‘ll see it later). Another important function that this service provides is thehandleError which can read response errors either from theModelState 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 theutils 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 thedotnet run command and different when you run the application through Visual Studio. The most interesting util service is theItemsService. 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 theutils 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. ThesetItem(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 typeIUser that has a user item with id=-1 and you need to replace it with a newIUser, 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 theNotificationService 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 namedhome under app and create the HomeComponent in ahome.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 theAngular 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 © <
a
href
=
"https://chsakell.com"
target
=
"_blank"
>chsakell's Blog</
a
></
p
>
</
div
>
</
div
>
</
footer
>
</
div
>
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. TheScheduleListComponent is a quite complex one. Add theschedule-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. ThePaginatedResult response, contains the items plus the pagination information. The component usesPAGINATION_DIRECTIVES and PaginationComponent modules fromng2-bootstrap to render a pagination bar under the schedules table..
<
pagination
[boundaryLinks]="true" [totalItems]="totalItems" [itemsPerPage]="itemsPerPage" [(ngModel)]="currentPage"
class
=
"pagination-sm"
previousText
=
"‹"
nextText
=
"›"
firstText
=
"«"
lastText
=
"»"
(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 theModalDirective 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 entireschedule-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
=
"‹"
nextText
=
"›"
firstText
=
"«"
lastText
=
"»"
(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"
>×</
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
>
The ScheduleEditComponent is responsible to edit the details of a single Schedule. The interface used for this component is theIScheduleDetails which encapsulates all schedule’s details(creator, attendees, etc..). Add the schedule-edit.component.ts file under theschedules 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 templateschedule-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
>
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.
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 auser-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 theUserCardComponent.
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 theUserListComponent 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 ofSeparation 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 theUserListComponent needs to be informed and update the user list respectively. Here’s theuser-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"
>×</
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 childUserCardComponent 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 theConfigService 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 typedpredicates, array manipulation using lodash and last but not least how to install and use3rd party libraries in our app using SystemJS.
Source Code: You can find the source code for this projecthere 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 followchsakell’s Blog on its Facebook or Twitter accounts.
- (OK) Angular 2 CRUD, modals, animations, pagination, datetimepicker and much more
- (OK) Table component with sorting and pagination for Angular2
- WLAN Scan with NDIS Miniport and Much More - CodeProject
- Angular 2 Tutorial: Create a CRUD App with Angular CLI and TypeScript
- angular实例分页【pagination】
- Generics Tutorial – Example Class, Interface, Methods, Wildcards and much more
- Java Reflection Tutorial for Classes, Methods, Fields, Constructors, Annotations and much more
- Parajumpers Herre much more beautiful
- Angular 2 CRUD application using Nodejs
- angular分页插件tm.pagination
- Angular练习之animations动画
- 【Angular】angular-animations 动画 BrowserAnimationsModule 详解
- with sand making machine much more effective
- WPF Animations and Performance
- View layers and animations
- can not find module @angular/animations/browser
- angular学习(十七)——-Animations
- angular4 安装@angular/animations等问题
- Trie模板
- js执行顺序
- Docker镜像以及CMD与ENTRYPOINT指令的比较
- Atitit 编程语言知识点tech tree v2 attilax大总结
- 在MySQL中阻止UPDATE, DELETE 语句的执行,在没有添加WHERE条件
- (OK) Angular 2 CRUD, modals, animations, pagination, datetimepicker and much more
- fgetc_fputc函数
- 《甜蜜蜜》
- window下用taskkill杀死进程
- List<Map<String, String>> 合并map的字段数据问题
- Linux-Shell脚本执行方式
- 协议森林08 不放弃 (TCP协议与流通信)
- 算法:分治法求凸包上的点以及由凸包所构成的多边形的面积
- sqlalchemy 包实践小结