Fundamental objects of model-driven forms

FormControl and FormGroup are the two fundamental objects in a model-driven form. FormControl is an input field in the Angular form that encapsulates the value of the input field, its state (is valid), if it has changed (is dirty), or has any errors.

When we build a form, we need to create controls and attach metadata to the controls. We have to attach the Control class to the DOM input element by adding the formControlName attribute, as shown:

<input type="text" formControlName="name" />

FormGroup can be instantiated by FormBuilder. We can also manually construct FormGroup in the components with the default values, as illustrated:

this.bookForm = new FormGroup({
name: new FormControl('book name', Validators.required),
author: new FormControl('author name', Validators.required),
publication: new FormControl('publication name is optional')
});

Let's create an app named ModelDrivenForm in Visual Studio Code (VS Code). The following is the implementation of model-driven forms:

  1. Add the required package and dependency details, and install them using the npm install command:
      {
"name":"model-driven-form",
"version":"1.0.0",
"private":true,
"description":"Model driven form",
"scripts":{
"test:once":"karma start karma.conf.js --single-
run",
"build":"tsc -p src/",
"serve":"lite-server -c=bs-config.json",
"prestart":"npm run build",
"start":"concurrently "npm run build:watch" "npm
run serve"",
"pretest":"npm run build",
"test":"concurrently "npm run build:watch" "karma
start
karma.conf.js"",
"pretest:once":"npm run build",
"build:watch":"tsc -p src/ -w",
"build:upgrade":"tsc",
"serve:upgrade":"http-server",
"build:aot":"ngc -p tsconfig-aot.json && rollup -c
rollup-
config.js",
"serve:aot":"lite-server -c bs-config.aot.json",
"build:babel":"babel src -d src --extensions
".es6" --source-
maps",
"copy-dist-files":"node ./copy-dist-files.js",
"i18n":"ng-xi18n",
"lint":"tslint ./src/**/*.ts -t verbose"
},
"keywords":[
],
"author":"",
"license":"MIT",
"dependencies":{
"@angular/common":"~4.0.0",
"@angular/compiler":"~4.0.0",
"@angular/compiler-cli":"~4.0.0",
"@angular/core":"~4.0.0",
"@angular/forms":"~4.0.0","@angular/http":"~4.0.0",
"@angular/platform-browser":"~4.0.0",
"@angular/platform-browser-dynamic":"~4.0.0",
"@angular/platform-server":"~4.0.0",
"@angular/router":"~4.0.0",
"@angular/tsc-wrapped":"~4.0.0",
"@angular/upgrade":"~4.0.0
",
"angular-in-memory-web-api":"~0.3.1",
"core-js":"^2.4.1",
"rxjs":"5.0.1",
"systemjs":"0.19.39",
"zone.js":"^0.8.4"
},
"devDependencies":{
"@types/angular":"^1.5.16",
"@types/angular-animate":"^1.5.5",
"@types/angular-cookies":"^1.4.2",
"@types/angular-mocks":"^1.5.5",
"@types/angular-resource":"^1.5.6",
"@types/angular-route":"^1.3.2",
"@types/angular-sanitize":"^1.3.3",
"@types/jasmine":"2.5.36",
"@types/node":"^6.0.45",
"babel-cli":"^6.16.0",
"babel-preset-angular2":"^0.0.2",
"babel-preset-es2015":"^6.16.0",
"canonical-path":"0.0.2",
"concurrently":"^3.0.0",
"http-server":"^0.9.0",
"jasmine":"~2.4.1",
"jasmine-core":"~2.4.1",
"karma":"^1.3.0",
"karma-chrome-launcher":"^2.0.0",
"karma-cli":"^1.0.1",
"karma-jasmine":"^1.0.2",
"karma-jasmine-html-reporter":"^0.2.2",
"karma-phantomjs-launcher":"^1.0.2",
"lite-server":"^2.2.2",
"lodash":"^4.16.2",
"phantomjs-prebuilt":"^2.1.7",
"protractor":"~4.0.14",
"rollup":"^0.41.6",
"rollup-plugin-commonjs":"^8.0.2",
"rollup-plugin-node-resolve":"2.0.0",
"rollup-plugin-uglify":"^1.0.1",
"source-map-explorer":"^1.3.2",
"tslint":"^3.15.1",
"typescript":"~2.2.0"
},
"repository":{
}
}
  1. Create a Book class and add the following code snippet:
      export class Book {
constructor(
public id: number,
public name: string,
public author: string,
public publication?: string
) { }
}
  1. Create AppComponent and add the following code:
      import { Component } from '@angular/core';
@Component({
selector: 'first-model-form',
template: '<book-form></book-form>'
})
export class AppComponent { }

This AppComponent shown earlier is the root component of the application that will host the BookFormComponent. AppComponent is decorated with the first-model-form selector and template that has the inline HTML with the <book-form/> special tag. This tag will be updated with the actual template during runtime.

  1. Now, let's add the book-form.component.ts with the following code snippet:
      import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from
'@angular/forms';
import { Book } from './book';
@Component({
selector: 'book-form',
templateUrl: './book-form.component.html'
})
export class BookFormComponent implements OnInit {
bookForm: FormGroup;
public submitted: boolean;
constructor() { }
ngOnInit() {
this.bookForm = new FormGroup({
name: new FormControl('book name',
Validators.required),
author: new FormControl('author name',
Validators.required),
publication: new FormControl('publication name is
optional')
});
}
onSubmit(model: Book, isValid: boolean) {
this.submitted = true;
console.log(model, isValid);
// code to post the data
}
}

Here, note that we have imported FormControl, FormGroup, and Validators from @angular/forms. These are the essential classes to implement a model-driven form. We have also imported Component and OnInit from @angular/core for Component class implementation, and then we have imported Book from book.ts. Book is the data model for this form.

BookFormComponent is decorated with the @Component directive that was imported from @angular/core. Selector value is set to book-form, and the templateUrl is assigned with the template HTML file.

In the BookFormCompoent, we have initialized the form model by instantiating FormGroup with the FormControl properties instantiated and assigned to properties such as name, author, and publication. We have the onSubmit() methods to post the data submitted to API.

  1. Now, let's add the book-form.component.html template file to the following HTML content:
      <div class="container">
<h1>New Book Form</h1>
<form [formGroup]="bookForm" novalidate
(ngSubmit)="onSubmit(bookForm.value,
bookForm.valid)">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control"
formControlName="name">
<small [hidden]="bookForm.controls.name.valid ||
(bookForm.controls.name.pristine && !submitted)"
class="text-
danger">
Name is required.
</small>
</div>
<div class="form-group">
<label for="author">Author</label>
<input type="text" class="form-control"
formControlName="author">
<small [hidden]="bookForm.controls.author.valid ||
(bookForm.controls.author.pristine && !submitted)"
class="text-
danger">
Author is required.
</small>
</div>
<div class="form-group">
<label for="publication">Publication</label>
<input type="text" class="form-control"
formControlName="publication">
</div>
<button type="submit" class="btn btn-
success">Submit</button>
</form>
</div>
<style>
.no-style .ng-valid {
border-left: 1px solid #CCC
}
.no-style .ng-invalid {
border-left: 1px solid #CCC
}
</style>

Similar to the template-driven form, this is a simple form based on model driven that has three input controls to key in the book, author, and publisher name and a submit button to submit the details. In the form tag, we have added the formGroup directive to the forms and assigned bookForm to it. Each input control has a special attribute form ControlName assigned with their respective formControl, such as name, author, and publication respectively.

The onSubmit function from BookFormComponent is assigned to the ngSubmit event of the form. So, when the submit button is clicked on, it will call the onSubmit function in BookFormComponent, passing value and valid property of bookForm.

Note that all the input controls do not have any event-cum-property attribute as in the template-driven form. Here, we can achieve the two-way binding by passing the model value from the bookForm.value property to the onSubmit function and accessing the model from component.

We have the required template and components in place. Now we need to create an AppModule to bootstrap the root component of our application, AppComponent. Create a file named app.module.ts and add the following code snippet:

      import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-
browser';
import { FormsModule, ReactiveFormsModule } from
'@angular/forms';
import { AppComponent } from './app.component';
import { BookFormComponent } from './book-
form.component';
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule
],
declarations: [
AppComponent,
BookFormComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

In the preceding code, note that we have assigned the AppComponent class to bootstrap metadata to inform Angular that AppComponent is the root component of the application. Also, note that we have imported FormsModule and ReactiveFormsModule from @angular/forms.

  1. Now that we have all the required templates and classes in place, we need to bootstrap the module. Let's create a file named main.ts with the following code snippet that bootstraps the module:
      import { platformBrowserDynamic } from 
'@angular/platform-
browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
  1. Finally, add the index.html file with the following content:
      <!DOCTYPE html>
<html>
<head>
<title>Hero Form</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-
scale=1">
<link rel="stylesheet"
href="https://unpkg.com/[email protected]
/dist/css/bootstra p.min.css">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="forms.css">
<!-- Polyfills -->
<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/systemjs/dist/system.src.js">
</script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){
console.error(err);
});
</script>
</head>
<body>
<first-model-form>Loading...</first-model-form>
</body>
</html>

Note that the <first-model-form/> special tag is added in the body. This tag will be updated with the actual template during runtime. Also, note that the required libraries are loaded during runtime using the System.js module loader. The systemjs.config.js file should have the instructions on mapping the npm packages and our application's starting point. Here, our application is bootstrapped in the main.ts, which will be transpiled to main.js after the application is built. The content of the systemjs.config.js is given here:

/**
* System configuration for Angular 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/animations': 'npm:@angular/animations/bundles/animations.umd.js',
'@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
'@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/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.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/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js',
meta: {
'./*.js': {
loader: 'systemjs-angular-loader.js'
}
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
  1. Now, we have all that we need. Let's run the application by pressing F5, and the index page will be rendered with the template powered by BookFormComponent, as follows:
The output of the model-driven form

On clicking on the Submit button by keeping the console window open in the developer tools of the Chrome browser, note that the logging model object is logged with the form valid to false as the author property is missing its value.

Now, let's key in some value in the author property and click on the Submit button by keeping the console window open in the developer tools of the Chrome browser. Note that the model object logged with the form valid to true will all the required properties filled with value, shown as follows:

Inspecting the model-driven form submission

As we have configured the validations in the component using FormGroup, we have loosely coupled the validation logic by moving it from template to component. So, we can write test methods using any test framework to verify the validation logic by asserting the components. Refer to Chapter 8, Testing Angular applications to know how to test an angular application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.81.94