File
Implements
Index
Properties
|
|
Methods
|
|
Inputs
|
|
Outputs
|
|
dataLoadStatusDelegate
|
Type : any
|
Default value : new Subject<'LOADING' | 'LOADED'>()
|
|
Outputs
dataLoadStatus
|
Type : EventEmitter
|
|
finalize
|
Type : EventEmitter
|
|
initialize
|
Type : EventEmitter
|
|
statusChanges
|
Type : EventEmitter
|
|
valueChanges
|
Type : EventEmitter
|
|
Methods
handleLinkClick
|
handleLinkClick(event: MouseEvent)
|
|
Parameters :
Name |
Type |
Optional |
event |
MouseEvent
|
No
|
|
Private
initializeForm
|
initializeForm()
|
|
|
isOptionsArray
|
isOptionsArray(options: any)
|
|
Parameters :
Name |
Type |
Optional |
options |
any
|
No
|
|
isOptionsClosure
|
isOptionsClosure(options: any)
|
|
Parameters :
Name |
Type |
Optional |
options |
any
|
No
|
|
ngAfterViewInit
|
ngAfterViewInit()
|
|
|
ngOnChanges
|
ngOnChanges(changes: SimpleChanges)
|
|
Parameters :
Name |
Type |
Optional |
changes |
SimpleChanges
|
No
|
|
ngOnDestroy
|
ngOnDestroy()
|
|
|
onNestedFormFinalize
|
onNestedFormFinalize(nestedFormGroup: FormGroup, fieldConfig: FieldConfig<any>)
|
|
|
onNestedFormInitialize
|
onNestedFormInitialize(nestedFormGroup: FormGroup, fieldConfig: FieldConfig<any>)
|
|
|
Private
prepareFormValidationData
|
prepareFormValidationData(element: FieldConfig<any>, index)
|
|
Returns : {}
|
Private
dataLoadStatusSinkSubscription
|
Type : Subscription
|
|
FieldConfigInputType
|
Default value : FieldConfigInputType
|
|
formGroup
|
Type : FormGroup
|
|
optionsMap$
|
Type : literal type
|
Default value : {}
|
|
requiredFieldsMap
|
Type : literal type
|
Default value : {}
|
|
Private
statusChangesSubscription
|
Type : Subscription
|
|
validationTriggers
|
Type : QueryList<HTMLElement>
|
Decorators :
@ViewChildren('validationTrigger')
|
|
Private
valueChangesSubscription
|
Type : Subscription
|
|
ValueComparator
|
Default value : ValueComparator
|
|
import {
AfterViewInit, Component, EventEmitter, Input,
OnChanges, OnDestroy, Output, QueryList, SimpleChanges, ViewChildren
} from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { CommonUtilService } from '@app/services/common-util.service';
import { Observable, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, map, scan, tap } from 'rxjs/operators';
import {
FieldConfig, FieldConfigInputType, FieldConfigOption,
FieldConfigOptionsBuilder, FieldConfigValidationType
} from './field-config';
import { ValueComparator } from './value-comparator';
@Component({
selector: 'app-common-forms',
templateUrl: './common-forms.component.html',
styleUrls: ['./common-forms.component.scss'],
})
export class CommonFormsComponent implements OnChanges, OnDestroy, AfterViewInit {
@Output() initialize = new EventEmitter();
@Output() finalize = new EventEmitter();
@Output() valueChanges = new EventEmitter();
@Output() statusChanges = new EventEmitter();
@Output() dataLoadStatus = new EventEmitter<'LOADING' | 'LOADED'>();
@Input() config;
@Input() dataLoadStatusDelegate = new Subject<'LOADING' | 'LOADED'>();
@ViewChildren('validationTrigger') validationTriggers: QueryList<HTMLElement>;
formGroup: FormGroup;
FieldConfigInputType = FieldConfigInputType;
ValueComparator = ValueComparator;
optionsMap$: { [code: string]: Observable<FieldConfigOption<any>[]> } = {};
requiredFieldsMap: { [code: string]: boolean } = {};
private statusChangesSubscription: Subscription;
private valueChangesSubscription: Subscription;
private dataLoadStatusSinkSubscription: Subscription;
constructor(
private formBuilder: FormBuilder,
private commonUtilService: CommonUtilService
) { }
ngOnDestroy(): void {
this.finalize.emit();
if (this.statusChangesSubscription) {
this.statusChangesSubscription.unsubscribe();
}
if (this.valueChangesSubscription) {
this.valueChangesSubscription.unsubscribe();
}
if (this.dataLoadStatusSinkSubscription) {
this.dataLoadStatusSinkSubscription.unsubscribe();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['config']) {
if ((changes['config'].currentValue && changes['config'].firstChange) || changes['config'].previousValue !== changes['config'].currentValue) {
this.initializeForm();
changes['config'].currentValue.forEach((config: FieldConfig<any>) => {
if (config.validations && config.validations.length) {
this.requiredFieldsMap[config.code] = !!config.validations.find(val => val.type === FieldConfigValidationType.REQUIRED);
}
if (!config.templateOptions) {
return;
}
if (!config.templateOptions.options) {
config.templateOptions.options = [];
}
if (this.isOptionsClosure(config.templateOptions.options)) {
this.optionsMap$[config.code] = (config.templateOptions.options as FieldConfigOptionsBuilder<any>)(
this.formGroup.get(config.code) as FormControl,
this.formGroup.get(config.context) as FormControl,
() => this.dataLoadStatusDelegate.next('LOADING'),
() => this.dataLoadStatusDelegate.next('LOADED')
) as any;
}
});
}
}
if (this.dataLoadStatusSinkSubscription) {
this.dataLoadStatusSinkSubscription.unsubscribe();
}
if (this.statusChangesSubscription) {
this.statusChangesSubscription.unsubscribe();
}
if (this.valueChangesSubscription) {
this.valueChangesSubscription.unsubscribe();
}
this.dataLoadStatusSinkSubscription = this.dataLoadStatusDelegate.pipe(
scan<'LOADING' | 'LOADED', { loadingCount: 0, loadedCount: 0 }>((acc, event) => {
if (event === 'LOADED') {
acc.loadedCount++;
} else {
acc.loadingCount++;
}
return acc;
}, { loadingCount: 0, loadedCount: 0 }),
map<{ loadingCount: 0, loadedCount: 0 }, 'LOADING' | 'LOADED'>((aggregates) => {
if (aggregates.loadingCount !== aggregates.loadedCount) {
return 'LOADING';
}
return 'LOADED';
}),
distinctUntilChanged(),
tap((result) => {
if (result === 'LOADING') {
this.dataLoadStatus.emit('LOADING');
} else {
this.dataLoadStatus.emit('LOADED');
}
})
).subscribe();
this.statusChangesSubscription = this.formGroup.statusChanges.pipe(
tap((v) => {
this.statusChanges.emit({
isPristine: this.formGroup.pristine,
isDirty: this.formGroup.dirty,
isInvalid: this.formGroup.invalid,
isValid: this.formGroup.valid
});
})
).subscribe();
this.valueChangesSubscription = this.formGroup.valueChanges.pipe(
tap((v) => {
this.valueChanges.emit(v);
})
).subscribe();
}
ngAfterViewInit() {
this.config.forEach(element => {
if (element.asyncValidation && element.asyncValidation.asyncValidatorFactory && this.formGroup.get(element.code)) {
this.formGroup.get(element.code).setAsyncValidators(element.asyncValidation.asyncValidatorFactory(
element.asyncValidation.marker,
this.validationTriggers
));
}
});
}
onNestedFormFinalize(nestedFormGroup: FormGroup, fieldConfig: FieldConfig<any>) {
if (!this.formGroup.get('children') || !this.formGroup.get(`children.${fieldConfig.code}`)) {
return;
}
(this.formGroup.get('children') as FormGroup).removeControl(fieldConfig.code);
if (!Object.keys((this.formGroup.get('children') as FormGroup).controls).length) {
this.formGroup.removeControl('children');
}
}
onNestedFormInitialize(nestedFormGroup: FormGroup, fieldConfig: FieldConfig<any>) {
if (!this.formGroup.get('children')) {
this.formGroup.addControl('children', new FormGroup({}));
}
(this.formGroup.get('children') as FormGroup).addControl(fieldConfig.code, nestedFormGroup);
}
private initializeForm() {
if (!this.config.length) {
console.error('FORM LIST IS EMPTY');
return;
}
const formGroupData = {};
this.config.forEach((element: any, index) => {
if (element.type !== FieldConfigInputType.LABEL) {
const formValueList = this.prepareFormValidationData(element, index);
formGroupData[element.code] = formValueList;
}
});
this.formGroup = this.formBuilder.group(formGroupData);
this.initialize.emit(this.formGroup);
}
private prepareFormValidationData(element: FieldConfig<any>, index) {
const formValueList = [];
const validationList = [];
let defaultVal: any = '';
switch (element.type) {
case FieldConfigInputType.INPUT:
defaultVal = element.templateOptions.type === 'number' ?
(element.default && Number.isInteger(element.default) ? element.default : 0) :
(element.default && (typeof element.default) === 'string' ? element.default : '');
break;
case FieldConfigInputType.SELECT:
case FieldConfigInputType.NESTED_SELECT:
defaultVal = element.templateOptions.multiple ?
(element.default && Array.isArray(element.default) ? element.default : []) : (element.default || null);
break;
case FieldConfigInputType.CHECKBOX:
defaultVal = false || !!element.default;
break;
}
formValueList.push(defaultVal);
if (element.validations && element.validations.length) {
element.validations.forEach((data, i) => {
switch (data.type) {
case FieldConfigValidationType.REQUIRED:
if (element.type === FieldConfigInputType.CHECKBOX) {
validationList.push(Validators.requiredTrue);
} else if (element.type === FieldConfigInputType.SELECT || element.type === FieldConfigInputType.NESTED_SELECT) {
validationList.push((c) => {
if (element.templateOptions.multiple) {
return c.value && c.value.length ? null : 'error';
}
return !!c.value ? null : 'error';
});
} else {
validationList.push(Validators.required);
}
break;
case FieldConfigValidationType.PATTERN:
validationList.push(Validators.pattern(element.validations[i].value as string));
break;
case FieldConfigValidationType.MINLENGTH:
validationList.push(Validators.minLength(element.validations[i].value as number));
break;
case FieldConfigValidationType.MAXLENGTH:
validationList.push(Validators.maxLength(element.validations[i].value as number));
break;
}
});
}
formValueList.push(Validators.compose(validationList));
return formValueList;
}
isOptionsArray(options: any) {
return Array.isArray(options);
}
isOptionsClosure(options: any) {
return typeof options === 'function';
}
handleLinkClick(event: MouseEvent) {
if (event.target && event.target['hasAttribute'] && (event.target as HTMLAnchorElement).hasAttribute('href')) {
this.commonUtilService.openLink((event.target as HTMLAnchorElement).getAttribute('href'));
}
}
}
<div [formGroup]="formGroup" *ngIf="formGroup">
<ng-container *ngFor="let field of config; let index = i">
<div *ngIf="field.type === FieldConfigInputType.SELECT || field.type === FieldConfigInputType.NESTED_SELECT"
[hidden]="field.templateOptions?.hidden || null">
<div class="sb-dropdown">
<ion-item class="input-item">
<ion-label position="stacked" class="label-font align-text">
{{ field.templateOptions?.label | translate }}
<ion-text *ngIf="field.templateOptions?.label && requiredFieldsMap[field.code]">
<span class="required-star"> *</span>
</ion-text>
</ion-label>
<ion-select [formControl]="formGroup.get(field.code)" [multiple]="false"
[interfaceOptions]="{
header: field.templateOptions?.label,
cssClass: 'select-box',
animated: false
}"
[disabled]="field.disabled || (field.context && formGroup.get(field.context).invalid)"
[compareWith]="ValueComparator.valueComparator"
placeholder="{{ field.templateOptions?.placeholder | translate }}"
okText="{{'BTN_SUBMIT' | translate}}" cancelText="{{'CANCEL' | translate}}">
<ng-container *ngIf="isOptionsArray(field.templateOptions?.options)">
<ion-select-option *ngFor="let option of field.templateOptions?.options" [value]="option?.value">
{{option?.label}}
</ion-select-option>
</ng-container>
<ng-container *ngIf="isOptionsClosure(field.templateOptions?.options) && optionsMap$[field.code]">
<ion-select-option *ngFor="let option of (optionsMap$[field.code]) | async" [value]="option?.value">
{{option?.label}}
</ion-select-option>
</ng-container>
</ion-select>
</ion-item>
</div>
</div>
<div *ngIf="field.type === FieldConfigInputType.INPUT" [hidden]="field.templateOptions?.hidden || null">
<ng-container *ngIf="formGroup.get(field.code); let formControl">
<ion-item class="input-item cf-input-primary">
<ion-label position="stacked" class="label-font align-text ion-text-capitalize">
{{ field.templateOptions?.label | translate }}
<ion-text *ngIf="field.templateOptions?.label && requiredFieldsMap[field.code]">
<span class="required-star"> *</span>
</ion-text>
</ion-label>
<div class="W100 merged-input-container MT16" style="text-align:start"
[ngClass]="{'': (!formControl.dirty || !formControl.touched) && !formControl.errors ,'cf-input-error': (formControl.dirty || formControl.touched) && formControl.errors }">
<span class="prefix" *ngIf="field.templateOptions?.prefix">{{field.templateOptions?.prefix}}</span>
<ion-input formControlName="{{field.code}}"
placeholder="{{ field.templateOptions?.placeholder | translate}}" class="form-control custom">
</ion-input>
<span class="otp-validator" *ngIf="field.asyncValidation">
<img *ngIf="formControl.value && formControl.status === 'VALID'" src="assets/imgs/green_tick.svg" alt="verification success">
<img *ngIf="formControl.value && formControl.status !== 'VALID'" src="assets/imgs/red_exclamation.svg" alt="verification failure">
<img *ngIf="!formControl.value" src="assets/imgs/empty_circle.svg" alt="empty field">
</span>
</div>
<ng-container *ngFor="let validation of field.validations">
<div class="cf-error"
*ngIf="(validation.type && (validation.type).toLowerCase && validation.message && formControl.errors && formControl.errors[(validation.type).toLowerCase()] && (formControl.dirty || formControl.touched))">
{{ validation.message | translate }}
</div>
</ng-container>
</ion-item>
<ng-container *ngIf="field.asyncValidation?.trigger">
<div class="async-validator" [hidden]="formControl.status === 'VALID' || formControl.status !== 'PENDING' || !formControl.value">
<div class="cf-error" *ngIf="field.asyncValidation?.message">
{{ field.asyncValidation.message | translate }}
</div>
<div class="verification-btn">
<ion-button class="ion-text-capitalize"
shape="round"
#validationTrigger
[attr.data-marker]="field.asyncValidation.marker">
{{field.asyncValidation.trigger}}
</ion-button>
</div>
</div>
</ng-container>
</ng-container>
</div>
<div class="flex-container M16" *ngIf="field.type === FieldConfigInputType.CHECKBOX"
[hidden]="field.templateOptions?.hidden || null">
<div>
<ion-checkbox formControlName="{{field.code}}"></ion-checkbox>
</div>
<ng-container *ngIf="field.templateOptions?.label">
<span>{{ field.templateOptions?.label | translate}}</span>
</ng-container>
<ng-container *ngIf="field.templateOptions?.labelHtml">
<div [innerHTML]="field.templateOptions?.labelHtml | translateHtml" (click)="handleLinkClick($event)"></div>
</ng-container>
</div>
<div class="M16" *ngIf="field.type === FieldConfigInputType.LABEL">
{{field.templateOptions?.label | translate}}
</div>
<app-common-forms *ngIf="field.type === FieldConfigInputType.NESTED_GROUP"
(initialize)="onNestedFormInitialize($event, field)" (finalize)="onNestedFormFinalize($event, field)"
[dataLoadStatusDelegate]="dataLoadStatusDelegate" [config]="field.children">
</app-common-forms>
</ng-container>
</div>
@import "src/assets/styles/_variables.scss";
:host {
.label-font {
color: map-get($colors, primary_black) !important;
font-family: "Noto Sans", sans-serif !important;
font-size: 1.25rem !important;
line-height: 1.375rem !important;
font-weight: normal;
}
.select-text:first-letter {
text-transform: capitalize;
}
.custom-footer-background .toolbar-background-ios,
.custom-footer-background .toolbar-background-md {
background: unset !important;
}
.padding-12 {
padding: 12px !important;
}
.item-select-disabled {
.label-md {
color: map-get($colors, medium_gray);
opacity: 1;
}
ion-select {
border-color: map-get($colors, primary_black);
}
}
.item-label-stacked ion-select {
border: 1px solid map-get($colors, primary);
border-radius: 5px;
margin-top: 16px;
padding-left: 8px;
padding-right: 16px !important;
font-size: $font-size-base;
font-family: "Noto Sans", sans-serif;
.select-icon {
.select-icon-inner {
border: solid blue;
border-width: 0 2px 2px 0;
display: inline-block;
padding: 4px;
transform: rotate(45deg);
animation: upDownAnimate 5s linear infinite;
animation-duration: 0.9s;
}
}
}
.item-label-stacked.item-select-disabled {
ion-label {
color: map-get($colors, primary_black);
}
ion-select {
border-color: map-get($colors, primary_black);
.select-icon {
.select-icon-inner {
border-color: map-get($colors, primary_black);
animation: none;
}
}
.select-placeholder {
color: map-get($colors, primary_black);
}
}
}
ion-item {
--border-color: var(--ion-color-danger, #f1453d);
}
ion-button{
--background: #{$blue} !important;
}
.item-label-stacked ion-input {
border-radius: 5px;
padding-left: 16px !important;
padding-right: 16px !important;
font-size: $font-size-base;
}
.cf-input-primary ion-input{
font-weight: bold;
border: none;
--placeholder-opacity: 0.3 !important;
}
.cf-input-error{
border: 1px solid red;
}
ion-item.item-label-stacked {
--border-width: 0;
--highlight-background: transparent;
}
.item-select ion-label {
color: map-get($colors, primary);
}
ion-label {
color: map-get($colors, primary_black);
font-family: "Noto Sans", sans-serif;
font-size: $font-size-base;
letter-spacing: 0;
line-height: 1.188rem;
}
}
.cf-title{
font-size: 1rem;
font-weight: bold;
color: $blue;
margin: 16px 16px 0;
}
.cf-input-title{
color: map-get($colors, primary_black);
font-family: "Noto Sans", sans-serif;
font-size: $font-size-base;
letter-spacing: 0;
line-height: 1.188rem;
margin: 16px 0 8px;
}
.cf-input-box{
border: 0.5px solid map-get($colors, primary_black);
border-radius: 2px;
ion-input{
margin: 0 8px;
}
}
.cf-error{
margin-top: 8px;
display: block;
font-size: 0.75rem;
color: red;
line-height: 0.625rem;
}
.input-item{
padding-top: 8px;
}
.cf-tnc-text{
padding: 0 8px;
}
.sb-new-btn {
background-color: $blue;
color: map-get($colors, white);
width: 100%;
height: 2.5rem;
box-shadow: 0 2px 7px 0 rgba(0, 0, 0, 0.25);
font-family: "Noto Sans", sans-serif;
font-size: 1rem;
}
.sb-new-btn-outline {
background-color: map-get($colors, white);
color: $blue;
width: 100%;
height: 2.5rem;
border: 1px solid $blue;
box-shadow: 0 2px 7px 0 rgba(0, 0, 0, 0.25);
font-family: "Noto Sans", sans-serif;
font-size: 1rem;
}
.blur-btn {
opacity: 0.5;
}
.tnc-link {
color: $blue;
text-decoration: underline;
}
ion-checkbox {
margin-right: 8px;
--background-checked: #{$blue} !important;
--border-color-checked: #{$blue} !important;
}
.merged-input-container {
border: 1px solid map-get($colors, primary);
display: flex;
flex: 1 1 auto;
border-radius: 6px;
.decorator {
display: inline-block;
max-width: 3.125rem;
}
.custom {
display: inline-block;
}
ion-input{
border: none;
}
span{
font-size: $font-size-base;
opacity: 0.7;
margin: auto;
}
}
.verification-btn{
text-align: center;
ion-button {
--background: #008840 !important;
}
}
.otp-validator{
padding-left: 8px;
padding-right: 8px;
}
.prefix{
padding-left: 8px;
}
.required-star{
color: red;
}
.async-validator{
margin: 0 16px;
}
Legend
Html element with directive