File

src/app/components/common-forms/common-forms.component.ts

Implements

OnChanges OnDestroy AfterViewInit

Metadata

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor(formBuilder: FormBuilder, commonUtilService: CommonUtilService)
Parameters :
Name Type Optional
formBuilder FormBuilder No
commonUtilService CommonUtilService No

Inputs

config
Type : any
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
Returns : void
Private initializeForm
initializeForm()
Returns : void
isOptionsArray
isOptionsArray(options: any)
Parameters :
Name Type Optional
options any No
Returns : any
isOptionsClosure
isOptionsClosure(options: any)
Parameters :
Name Type Optional
options any No
Returns : boolean
ngAfterViewInit
ngAfterViewInit()
Returns : void
ngOnChanges
ngOnChanges(changes: SimpleChanges)
Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
onNestedFormFinalize
onNestedFormFinalize(nestedFormGroup: FormGroup, fieldConfig: FieldConfig<any>)
Parameters :
Name Type Optional
nestedFormGroup FormGroup No
fieldConfig FieldConfig<any> No
Returns : void
onNestedFormInitialize
onNestedFormInitialize(nestedFormGroup: FormGroup, fieldConfig: FieldConfig<any>)
Parameters :
Name Type Optional
nestedFormGroup FormGroup No
fieldConfig FieldConfig<any> No
Returns : void
Private prepareFormValidationData
prepareFormValidationData(element: FieldConfig<any>, index)
Parameters :
Name Type Optional
element FieldConfig<any> No
index No
Returns : {}

Properties

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">&nbsp;*</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">&nbsp;*</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>

./common-forms.component.scss

@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
Component
Html element with directive

results matching ""

    No results matching ""