Adding built-in and custom validations

Validations are an important part of forms, and reactive forms make it easier to manage them. Let's start by adding some validations to our basic information form. 

First, we will add a required validation to our Product Name input field. In reactive forms, you will be adding the validations to the component class instead of the template:

import { FormBuilder, FormGroup, Validators } from '@angular/forms';
...
export class ProductComponent implements OnInit {
...
constructor(private fb: FormBuilder) {
this.productForm = fb.group({
basic: fb.group({
name: ['', Validators.required],
description: '',
active: false,
features: fb.array([
fb.control('')
])
}),
expiration: fb.group({
expirationDate: null,
})
});
}
}

To display an error, we can use the clr-control-error component that's provided by Clarity:

<!-- Input field for Product Name -->
<clr-input-container>
<label for="product-name">Product Name *</label>
<input clrInput type="text" id="product-name" placeholder="IPhone X..."
formControlName="name">
<clr-control-error>This Field is Required!</clr-control-error>
</clr-input-container>

If we weren't using Clarity's clr-control-error component, we would have checked for any errors and whether the field was pristine using the form's status:

<div *ngIf="productForm.get('basic.name').invalid && productForm.get('basic.name').dirty">
This Field is Required!
</div>

Whenever such big logic is used in the templates, it is always recommended to create a get property in the class and use it in the template:

class ProductComponent {
get basicFieldInvalid() {
return this.productForm.get('basic.name').invalid &&
this.productForm.get('basic.name').dirty;

}
}

Then, we need to use the basicFieldInvalid property in the template. This way, we can keep the logic in the component class and also use the property in multiple places in the template or component class, if required.

Now, let's add some validations to our expiration date. We will be composing two validations for the expiry date: one is the required validation, while the other will state that the expiration should be after today's date, which would be a custom validation that's not built into the Angular framework, such as minlength, maxlength, and so on:

import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl } from '@angular/forms';

function minDateValidation(date: Date): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
const forbidden = new Date(control.value) < date;
return forbidden ? {minDateValidation: {value: control.value}}
: null;
};

}
...
export class ProductComponent implements OnInit {
...
constructor(private fb: FormBuilder) {
this.productForm = fb.group({
basic: fb.group({
name: ['', Validators.Required],
description: '',
active: false,
features: fb.array([
fb.control('')
])
}),
expiration: fb.group({
expirationDate: [null,
Validators.compose([Validators.required,
minDateValidation(new Date())])
,
})
});
}

get expirationError() {
if (this.productForm.get('expiration.expirationDate').hasError('required')) {
return 'This Field is Required!';
}
if (this.productForm.get('expiration.expirationDate').hasError('minDateValidation')) {
return 'Expiration should be after today's date';
}

}
}

In our template, we'll show the respective error by using the following code:

<clr-input-container formGroupName="expiration">
<input type="date" clrInput clrDate formControlName="expirationDate">
<clr-control-error>{{expirationError}}</clr-control-error>
</clr-input-container>

Now, let's make sure that the user doesn't go to the next step if the current step is invalid. We can do that by overriding the buttons on the basic information and expiration steps. Let's begin with the basic information step:

<clr-wizard-page>
<ng-template clrPageTitle>Basic Info</ng-template>
...
<ng-template clrPageButtons>
<clr-wizard-button [type]="'cancel'">Cancel</clr-wizard-button>
<clr-wizard-button [type]="'previous'">Back</clr-wizard-button>
<clr-wizard-button [clrWizardButtonDisabled]="isBasicInvalid"
[
type]="'next'">Next</clr-wizard-button>
</ng-template
>
</clr-wizard-page>

Let's create a get property, isBasicInvalid, in the component class:

get isBasicInvalid(): boolean {
return this.productForm.get('basic').invalid;
}

This will make sure that if the basic information step is invalid, we cannot go to the next step.

Let's do the same thing for the expiration information step:

<clr-wizard-page>
<ng-template clrPageTile>Expiration Info</ng-template>
...
<
ng-template clrPageButtons>
<clr-wizard-button [clrWizardButtonDisabled]="isExpirationInvalid"
(
click)="handleFinish()" [type]="'finish'">Finish</clr-wizard-
button
>
</ng-template
>
</clr-wizard-page>

Now, let's create the isExpirationInvalid property, as well as the handleFinish and handleCancel methods:

import { ClrWizard } from '@clr/angular';
...
class ProductComponent {
...

@Output() finish = new EventEmitter();
@ViewChild('productWizard', { static: false }) productWizard:
ClrWizard;

...


get isExpirationInvalid(): boolean {
return this.productForm.get('expiration').invalid;
}

handleClose() {
this.finish.emit();
this.close();
}


close() {
this.productForm.reset();
this.deviceType = 'tablet';

this.productWizard.goTo(this.productWizard.pageCollection.pages.first.id);
this.productWizard.reset();
}


handleFinish() {
this.finish.emit({
product: {
type: this.deviceType,
...this.productForm.get('basic').value,
...this.productForm.get('expiration').value,
}

});
this.close();
}
}

Here, we use EventEmitter to send all the form values that we enter on the three steps as a product. Then, we call close, which resets the form and the wizard, and also takes us to the first step of the wizard on close. On handleClose, we emit nothing in finish and call close.

Now, in our products component, we will listen to the finish event and save the information:

<app-product
*ngIf="productOpen"
[product]="selectedProduct"
(finish)="handleFinish($event)"
></app-product>

Now, we will implement the handleFinish, addProduct, and onEdit methods in our ProductsComponent class:

class ProductsComponent {
productOpen;
selectedProduct: IProduct;

addProduct() {
this.productOpen = true;
this.selectedProduct = undefined;
}


onEdit(product) {
this.productOpen = true;
this.selectedProduct = product;
}

handleFinish(event) {
if (event && event.product) {
if (this.selectedProduct) {
// Edit Flow
this.productsService.editProduct(this.selectedProduct.id,
event
.product);

} else {
// Save New
this.productsService.addProduct(event.product);
}
}
this.productOpen = false;
}
}

Now, we can add the editProduct and addProduct methods to our ProductService:

...

@Injectable({
providedIn: 'root'
})
export class ProductsService {
...

addProduct(product) {
this.products = [
{
id: generateId(),
...product,
},

...this.products,
];

this.products$.next(this.products);
}


editProduct(id, product) {
const index = this.products.findIndex(p => p.id === id);
this.products = [
...this.products.slice(0, index),
{

id,
...product,
},

...this.products.slice(index + 1),
];

this.products$.next(this.products);
}

}

Now that adding products works, we need to make sure that the wizard shows the values of the product that's being edited. We will have to set the values to the form in ProductComponent. We can do this by checking whether the product is available by making changes to the product component and setting the values.

We will be using the pick function from Lodash to do this, so let's install lodash and its types first using npm:

> npm install lodash @types/lodash --save

We will use this to pick values and put them in the proper formGroup in productForm:

import * as _ from 'lodash';
...
class ProductComponent implements OnInit, OnChanges {
ngOnInit
() {
if (this.product) {
this.productForm.setValue({
basic: {
..._.pick(this.product, ['name', 'description',
'active']),
features: this.product.features || [''],
},
expiration: {
..._.pick(this.product, ['expirationDate']),
}
});
this.deviceType = this.product.type;
}
}

ngOnChanges() {
this.ngOnInit();
}
}

We are now able to add new products to the inventory, which completes our implementation of the project. In the next section, we will try to optimize the bundle of the application to improve its performance by using tools to calculate the size of the production bundles.

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

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