Introduction
In this blog I will go through creating a real life example of validating a form using Angular2. The form mimics creating a new blog. In this blog I will go through the following concepts of Angular2:
- Navigation
- Components communication
- Form builder
- Custom validation
- Using external libraries with Angular2
What are we building exactly?
We will build two views. One being the dashboard
where you see the list of created blogs and the second view is create new blog form.
Project structure
Image 1 shows the project structure.
Running the application
Pull the latest source code to a directory and cd
in that directory then run the following
npm install
npm run build
npm run serve
Open a browser windows and navigate to http://localhost:8080
The npm task serve
will run the webpack-dev-server which will run in the background watching any changes to reload the browser. webpack-dev-server will also recreate the bundle for our app on the fly.
Webpack configuration
I am using webpack to bundle this application. All I am doing in the npm build
task is running the command line tool for webpack. webpack command line tool uses a config file called webpack.config.js
. Listing 1 shows the content of webpack.config.js
.
var webpack = require("webpack");
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');
module.exports = {
entry: {
"vendor": "./app/vendor",
"app": "./app/app"
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].bundle.js"
},
resolve: {
extensions: ['', '.ts', '.js']
},
devtool: 'source-map',
devServer: {
colors: true,
historyApiFallback: true,
inline: true
},
module: {
loaders: [
{
test: /\.ts/,
loaders: ['ts-loader'],
exclude: /node_modules/
}
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.bundle.js"),
new HtmlWebpackPlugin({
path: path.resolve(__dirname, "dist"),
filename: 'index.html',
template: 'template.html'
})
]
}
Here is an explanation about this config entries:
- entry: This lists the entry point for every bundle we want to build. webpack will traverse all the imports in the entry file and build a bundle.
- output: That's where the bundles will be located
- resolve: The files extensions we want webpack to resolve
- devtool: enhance debugging options, here I am adding
source-map
so webpack will emit source maps for the bundles created. - devServer: These are options for the webpack-dev-server
- module: This is were you specify extra options for webpack, for example I am addiing the ts-loader to load TypeScript files for webpack.
- plugins: Optional plugins to add some extra options to webpack, here I am adding html-webpack-plugin to create an html file from a template and copy it to the
dist
folder.CommonsChunkPlugin
identifies common modules and put them into a commons chunk.
The app folder
The app folder is where the application lives. It is divided by features, services and shared components.
Features in this application are the views which are available to navigate to namely: dashboard and create blog.
Services contains application wide functionalities. Here we have datastore and logger. Datastore
contains methods to save and retrieve blogs. Logger
allow us to display messages to the user.
Shared components contains self contained UI components which can be used in other components. Here I have 2 Components namely: validationMessage
and markdownPreview
In the following sections I will go through every folder in more details.
The services
As discussed before, this application is utilising 2 services namely: Datastore
and Logger
.
Like in Angular1, Services are used in Angular2 for cross functionalities tasks like fetching data from an http service or log an error message.
Listing 1 shows the Datastore
source code
import {Injectable} from '@angular/core';
import {Blog} from './../blogs/blog'
@Injectable()
export class Datastore {
blogs: Blog[]
constructor(){
this.blogs = [];
}
createBlog (blog: Blog): Promise {
return new Promise((resolve,reject) => {
this.blogs.push(blog);
resolve(true);
});
}
getBlogs():Blog[]{
return this.blogs;
}
}
We need to import Injectable
so we can decorate our service. This will allow us to inject this service when we need to use it.
Note that there is nothing specific about this file that tells you it is a service unlike in Angular1 where you have to create a service using a certain syntax i.e
Angular.factory
Without the @Injectable
annotation, this is just a JavaScript class. This is one of the main goals of Angular2. That is, create your classes as you usually do then you can easily port them if you want to Angular2.
This service is using browser's memory to store a new blog. As you can see, it is returning a Promise
which when resolved returns true
to indicate that the blog was created.
getBlogs
just returns the array of blog
objects which we are maintaining in memory.
Listing 2 shows the Logger
service.
import * as humane from 'humane';
import {Injectable} from "@angular/core";
@Injectable()
export class Logger {
error(message):void {
humane.default.log(message, {addnCls: 'humane-libnotify-error'});
}
info(message):void {
humane.default.log(message, {addnCls: 'humane-libnotify-info'});
}
}
Beside importing @Injectable
, we are also importing humane-js which is the third part library I am using to show messages on the UI. What is interesting about this library is that it has nothing to do with Angular2. This Logger service wraps the functionality of this library and made it available as a service.
The days were you have to wait for everything to be Angularised before you can use it in an Angular application has gone because of the new Angular2 design as explained.
I have changed the type difinotion file for humane-js as it was not exporting a module which created TypeScript errors. The edited definition file is in typings
folder.
The Shared components
In this application we have 2 shared Component. The first one is the markdownPreview
. This component is responsible for showing an html for a given markdown. Listing 3 shows component markdownPreview
source code
import {Component, Input} from '@angular/core'
import * as marked from 'marked';
@Component({
selector: 'markdown-preview',
template: `<label>Blog text preview</label>
<p [innerHTML]="convertToHtml()"></p>`
})
export class MarkdownPreview {
@Input() markdownInput:string;
convertToHtml(){
return marked.parse(this.markdownInput);
}
}
This component needs some input to work with and the result will be the rendered component's template.
For this component, the input is markdown text which will be converted to html. I am using marked
to convert markdown to html.
The validationMessage
component is used to format the error message for a given form control. Listing 4 shows the source code for this component.
import {Component, Input} from '@angular/core'
import {AbstractControl} from "@angular/forms";
@Component({
selector: 'validation-message',
host: {
class: ''
},
template: `<div class="ui error small message error-message-container" [class.visible]="validationControl.hasError(validationName) && validationControl.touched">{{errorMessage}}</div>`
})
export class ValidationMessage {
@Input() validationControl: AbstractControl
@Input() validationName: string = ''
@Input() errorMessage: string = ''
}
This component takes in 3 inputs:
- validationControl: The form control we want to display the validation message for
- validationName: The validation rule we are checking like
required
or any other custom validation rule. This name will be stored in a collection in the control itself thats why we can check if it exist on the control itself by using thehasError
method - errorMessage: the error message to display if the control is invalid
The blogs feature
The blogs folder contains the blog
class which is a plain object with properties representing a blog object. Listing 5 shows the source code for blog
export class Blog {
constructor(public title: string,
public tags: string[],
public markdownText: string,
public htmlText: string) {
}
}
The main component in this application is createBlog.component
which contains the main form to create blogs. The idea of this component is to make use of other componenets to do its business. This is the beauty of Angular2, you create small components then you build from these small components a bigger one and so on.
I will decompose this component to 3 parts header, markup and class.
Listing 6 shows the header of createBlog.componenet
import {Component} from '@angular/core'
import {
FormBuilder,
FormGroup,
Validators,
AbstractControl
} from '@angular/forms';
import * as _ from 'underscore';
import 'rxjs/add/operator/debounceTime';
import {Datastore} from './../services/datastore'
import {Blog} from "./blog";
import {Logger} from "../services/logger";
I am importing the bits that I will need in building this component like underscore
and the components I explained earlier as well as the datastore
service.
Note that I am importing
Logger
andDatastore
for intellisense/semantics purposes not to inject them into the component. Injecting is made possible because we define these providers when we bootstrap the application as you will see later.
Listing 7 shows part of the markup for this component.
@Component({
selector: 'create-blog',
template: `<h3>Create blog</h3>
<form class="ui form" #createBlogForm="ngForm">
<div class="field" [class.error]="!titleControl.valid && titleControl.touched">
<label>Title</label>
<input type="text" placeholder="blog title" [formControl]="titleControl" />
<validation-message [validationControl]="titleControl" [validationName]="'required'"
[errorMessage]="'Title is required'"></validation-message>
</div>
<div class="field" [class.error]="!tagsControl.valid > 0 && tagsControl.touched">
<label>Tags</label>
<div class="tags-container">
<a class="ui orange label" *ngFor="let tag of tags">{{tag}}<i (click)="removeTag(tag)" class="delete icon"></i></a>
</div>
<input type="text" placeholder="tags" [formControl]="tagsControl" (keyup)="onKey($event)" />
<validation-message [validationControl]="tagsControl" [validationName]="'tagsInvalid'"
[errorMessage]="'Tags is required'"></validation-message>
</div>
<div class="field" [class.error]="!blogTextControl.valid && blogTextControl.touched">
<label>Blog Text</label>
<textarea [formControl]="blogTextControl"></textarea>
<validation-message [validationControl]="blogTextControl" [validationName]="'required'"
[errorMessage]="'Blog text is required'"></validation-message>
</div>
<div class="field">
<markdown-preview [markdownInput]="blogTextControl.value"></markdown-preview>
</div>
<button class="ui blue button" type="button" [disabled]="isFormValid()" (click)="createBlog()">
Create Blog
</button>
</form>
`,
})
The title
field is a required text box. The tags
field is a text box as well but its values are stored in an array outside the text box. As such the validation for this field is different from the title
field. The tags require a custom validator which I will explain later in the class
listing. The blog text is where you input the blog body. As you can see the markdown-preview
component is used to display the html equivalent of your markdown.
Note how the validation-message
component is used to display the validation error.
Listing 8 shows the rest of the createBlog.componenet
export class CreateBlogComponent {
tags: string[];
createBlogForm: FormGroup;
titleControl: AbstractControl;
tagsControl: AbstractControl;
blogTextControl: AbstractControl;
constructor(private formBuilder: FormBuilder, private datastore: Datastore, private loggerService: Logger) {
this.tagsValidator = this.tagsValidator.bind(this);
this.tags = [];
this.createBlogForm = formBuilder.group({
'title': ['', Validators.required],
'tags': ['', this.tagsValidator],
'blogText': ['', Validators.required]
});
this.titleControl = this.createBlogForm.controls['title'];
this.tagsControl = this.createBlogForm.controls['tags'];
this.blogTextControl = this.createBlogForm.controls['blogText'];
}
onKey(event: any) {
if (event.keyCode === 13 && !this.tagExist(this.tagsControl.value)) {
this.tags.push(event.target.value);
(<AbstractControl>this.tagsControl).patchValue('');
}
}
removeTag(tagToRemove: string) {
this.tags = _.reject(this.tags, (tag) => tagToRemove === tag);
this.tagsControl.updateValueAndValidity();
}
tagExist(tag: string): boolean {
let exist = _.contains(this.tags, tag);
if (exist) {
this.loggerService.error(`tag ${tag} already exist`);
return true;
}
return false;
}
tagsValidator(control: AbstractControl): any {
if (this.tags && this.tags.length > 0) {
return null;
}
return {'tagsInvalid': true};
}
isFormValid(){
return !this.createBlogForm.valid;
}
createBlog() {
let blog = new Blog(this.titleControl.value, this.tags, this.blogTextControl.value, this.blogTextControl.value);
this.datastore.createBlog(blog).then((result) => this.loggerService.info(`blog ${this.titleControl.value} was created`));
}
}
The constructor takes some dependencies namely: formBuilder
which is an Angular2 class that is responsible for creating form controls, datastore
which is used to create and retrieve blogs and loggerService
for logging.
I am creating the form controls then I am taking an instance for each control so I can use it for validation. This makes the code more readable but you have to type more!
The tagsValidator
is the custom validator for the tags
field that I mentioned earlier.
this.tagsValidator = this.tagsValidator.bind(this);
This line will make this
in the tagsValidator
points to createBlog.componenet
class instance. This is important because tagsValidator
needs to access the tags
array in order to validate it.
You insert tags by typing in the tags
field and hitting return. That's where onKey
function comes into play. A new tag will be added if it is not already added. removeTag
will remove a tag from the tags
array and tagExist
will check if a tag is already there in the tags
array.
tagsValidator
is our custom validator. This function is hooked into the Angular2 validation system and it will called automatically by the framework. As you can see, this function receives a control to validate and you guessed it right, this is our tags
text input field. Returning null
means there is no error but if there is an error we return an error name with a boolean value. This error name will be used to look for this particular validation error in the form.
isFormValid
returns a boolean to indicate if a form is valid or not. This is used to enable or disable the submit button (create blog) depending whether the form is valid or not. When we wrap an html property on an html element with brackets [], it means that their value will be dynamically retreived from our code as compared to being static, i.e. Angular will execute the right hand side value. In this example it will call the isFormValid
function.
Finally the createBlog
function uses datastore
to create a blog and the loggerService
to display a message when the blog is saved.
Image 2 shows createBlog.component
view
The Dashboard
The dashboard view contains a table which lists the blogs you created. Image 3 show the dashboard view.
listing 9 shows the full details of the dashboard component.
import { Component } from '@angular/core';
import {Datastore} from './../services/datastore'
import {Blog} from './../blogs/blog'
@Component({
selector:'dashboard',
template:`<h3>Dashboard</h3>
<table class="ui orange table">
<thead>
<tr>
<th>Title</th>
<th>Tags</th>
</tr>
<tr *ngFor="let blog of blogs">
<td>{{blog.title}}</td>
<td>{{blog.tags}}</td>
</tr>
</thead>
</table>`
})
export class DashboardComponent {
blogs : Blog[];
constructor(private datastore:Datastore){
this.blogs = this.datastore.getBlogs();
}
}
The dashboard uses the datastore
service to get all the created blogs and display them using *ngFor
. The constructor retrieves the blogs and store them in blogs
array which the *ngFor
loops over.
Application shell
The application shell is the component that holds the navigation and contains a place holder for other components to load in. Listing 10 shows the app.component
class.
import {Component} from '@angular/core';
@Component({
selector: 'my-app',
template: `
<div class="ui container main-container">
<div class="ui two item green menu">
<a class="item" routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
<a class="item" routerLink="/add" routerLinkActive="active">Add new Blog</a>
</div>
<div>
<router-outlet></router-outlet>
</div>
</div>`
})
export class AppComponent {
}
This components holds the routerLink
to other components. The selected component will be loaded in the router-outlet
place holder. I will discuss routing next. The routerLinkActive
directive will add the class active
to the anchor tag if it is the current component you are navigating to.
Bootstrap and Navigation
As you may noticed this application uses navigation to move from the dashboard to create blog and vice versa. Navigation is accomplished using Angular2 router. The configuration for the application bootstrap and routing is happening in app
module. Listing 11 shows the implementation for app
module.
import { NgModule } from "@angular/core";
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { RouterModule, Routes } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from "@angular/platform-browser";
import { AppComponent } from './app.component'
import { DashboardComponent } from './dashboard/dashboard.component';
import { CreateBlogComponent } from './blogs/createBlog.component'
import { ValidationMessage } from './shared/validationMessage';
import { MarkdownPreview } from './shared/markdownPreview';
import { Datastore } from './services/datastore';
import { Logger } from './services/logger'
const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'add', component: CreateBlogComponent },
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
];
@NgModule({
declarations: [
DashboardComponent,
CreateBlogComponent,
ValidationMessage,
MarkdownPreview,
AppComponent
],
imports: [
BrowserModule,
RouterModule.forRoot(routes),
FormsModule,
ReactiveFormsModule
],
bootstrap: [AppComponent],
providers: [
Datastore, Logger
]
})
export class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule)
.then(success => console.log(`Bootstrap success`))
.catch(error => console.log(error));
Note that this is a module not a component. A module contains a list of component and also it can use other modules. For example in this module I am including the components which will form this module in the declarations
part. This will make these components available in the rest of the module. For example: The CreateBlogComponent
uses the MarkdownPreview
and ValidationMessage
components directly by using their selector
.
The imports
part is where you declare which modules you want to import in your module, i.e. using their functionality in your module. In this module we are importing FormsModule
for example because we will be working with forms.
I defined the routes for this application and add them to the RouterModule
in the imports
section. This will enable routing in this application. The routes are self explanatory apart from the empty route. The empty route will redirect any route that is not inlcuded in the routes
array to the default routes which is defined here as the dashboard /dashboard.
The providers
section is where you add your services, hence I am adding the Datastore
and the Logger
services. The bootstrap
section is where you define your shell component. In our example this will be the AppComponent
.
We call the platformBrowserDynamic
to glue everything together and bootstrap the module.
Conclusion
In this blog I have explained how to create an input form and how to validate it in Angular2. There were some complex validation requirement as well like the live preview for markdown. We created components, services and module to achieve our goal. We also added navigation to switch between the 2 views that we created. Hopefully this blog post will give you a good head start on creating Angular2 forms.