import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; import { DynamicFormArrayModel, DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, } from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; import { findIndex } from 'lodash'; import { AppState } from '../../app.reducer'; import { FormChangeAction, FormInitAction, FormRemoveAction, FormRemoveErrorAction, FormStatusChangeAction } from './form.actions'; import { FormBuilderService } from './builder/form-builder.service'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util'; import { FormService } from './form.service'; import { formObjectFromIdSelector } from './selectors'; import { FormEntry, FormError } from './form.reducer'; import { isEmpty } from 'lodash'; /** * The default form component. */ @Component({ exportAs: 'formComponent', selector: 'ds-form', styleUrls: ['form.component.scss'], templateUrl: 'form.component.html', }) export class FormComponent implements OnDestroy, OnInit { private formErrors: FormError[] = []; private formValid: boolean; /** * A boolean that indicate if to display form's submit and cancel buttons */ @Input() displaySubmit = true; /** * A boolean that indicate if to emit a form change event */ @Input() emitChange = true; /** * The form unique ID */ @Input() formId: string; /** * An array of DynamicFormControlModel type */ @Input() formModel: DynamicFormControlModel[]; @Input() parentFormModel: DynamicFormGroupModel | DynamicFormGroupModel[]; @Input() formGroup: FormGroup; @Input() formLayout: DynamicFormLayout = null; /* tslint:disable:no-output-rename */ @Output('dfBlur') blur: EventEmitter = new EventEmitter(); @Output('dfChange') change: EventEmitter = new EventEmitter(); @Output('dfFocus') focus: EventEmitter = new EventEmitter(); /* tslint:enable:no-output-rename */ @Output() addArrayItem: EventEmitter = new EventEmitter(); @Output() removeArrayItem: EventEmitter = new EventEmitter(); /** * An event fired when form is valid and submitted . * Event's payload equals to the form content. */ @Output() submit: EventEmitter> = new EventEmitter>(); /** * An object of FormGroup type */ // public formGroup: FormGroup; /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} */ private subs: Subscription[] = []; constructor(private formService: FormService, protected changeDetectorRef: ChangeDetectorRef, private formBuilderService: FormBuilderService, private store: Store) { } /** * Method provided by Angular. Invoked after the view has been initialized. */ /*ngAfterViewChecked(): void { this.subs.push(this.formGroup.valueChanges .filter((formGroup) => this.formGroup.dirty) .subscribe(() => { // Dispatch a FormChangeAction if the user has changed the value in the UI this.store.dispatch(new FormChangeAction(this.formId, this.formGroup.value)); this.formGroup.markAsPristine(); })); }*/ private getFormGroup(): FormGroup { if (!!this.parentFormModel) { return this.formGroup.parent as FormGroup; } return this.formGroup; } private getFormGroupValue() { return this.getFormGroup().value; } private getFormGroupValidStatus() { return this.getFormGroup().valid; } /** * Method provided by Angular. Invoked after the constructor */ ngOnInit() { if (!this.formGroup) { this.formGroup = this.formBuilderService.createFormGroup(this.formModel); } else { this.formModel.forEach((model) => { this.formBuilderService.addFormGroupControl(this.formGroup, this.parentFormModel, model); }); } this.store.dispatch(new FormInitAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel), this.getFormGroupValidStatus())); // TODO: take a look to the following method: // this.keepSync(); this.formValid = this.getFormGroupValidStatus(); this.subs.push(this.formGroup.statusChanges .filter((currentStatus) => this.formValid !== this.getFormGroupValidStatus()) .subscribe((currentStatus) => { // Dispatch a FormStatusChangeAction if the form status has changed this.store.dispatch(new FormStatusChangeAction(this.formId, this.getFormGroupValidStatus())); this.formValid = this.getFormGroupValidStatus(); })); this.subs.push( this.store.select(formObjectFromIdSelector(this.formId)) .filter((formState: FormEntry) => !!formState && (isNotEmpty(formState.errors) || isNotEmpty(this.formErrors))) .map((formState) => formState.errors) .distinctUntilChanged() // .delay(100) // this terrible delay is here to prevent the detection change error .subscribe((errors: FormError[]) => { const {formGroup, formModel} = this; errors .filter((error: FormError) => findIndex(this.formErrors, {fieldId: error.fieldId}) === -1) .forEach((error: FormError) => { const {fieldId} = error; let field: AbstractControl; if (!!this.parentFormModel) { field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as FormGroup, formModel); } else { field = this.formBuilderService.getFormControlById(fieldId, formGroup, formModel); } if (field) { const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel); this.formService.addErrorToField(field, model, error.message); // this.formService.validateAllFormFields(formGroup); this.changeDetectorRef.detectChanges(); } }); this.formErrors .filter((error: FormError) => findIndex(errors, {fieldId: error.fieldId}) === -1) .forEach((error: FormError) => { const {fieldId} = error; let field: AbstractControl; if (!!this.parentFormModel) { field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as FormGroup, formModel); } else { field = this.formBuilderService.getFormControlById(fieldId, formGroup, formModel); } if (field) { const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel); this.formService.removeErrorFromField(field, model, error.message); } }); this.formErrors = errors; this.changeDetectorRef.detectChanges(); }) ); } /** * Method provided by Angular. Invoked when the instance is destroyed */ ngOnDestroy() { this.subs .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); this.store.dispatch(new FormRemoveAction(this.formId)); } /** * Method to check if the form status is valid or not */ public isValid(): Observable { return this.formService.isValid(this.formId) } /** * Method to keep synchronized form controls values with form state */ private keepSync(): void { this.subs.push(this.formService.getFormData(this.formId) .subscribe((stateFormData) => { if (!Object.is(stateFormData, this.formGroup.value) && this.formGroup) { this.formGroup.setValue(stateFormData); } })); } onBlur(event: DynamicFormControlEvent): void { this.blur.emit(event); } onFocus(event: DynamicFormControlEvent): void { this.focus.emit(event); } onChange(event: DynamicFormControlEvent): void { const action: FormChangeAction = new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)); this.store.dispatch(action); this.formGroup.markAsPristine(); if (this.emitChange) { this.change.emit(event); } const control: FormControl = event.control; if (control.valid) { this.store.dispatch(new FormRemoveErrorAction(this.formId, event.model.id)); } } /** * Method called on submit. * Emit a new submit Event whether the form is valid, mark fields with error otherwise */ onSubmit(): void { if (this.getFormGroupValidStatus()) { this.submit.emit(this.formService.getFormData(this.formId)); } else { this.formService.validateAllFormFields(this.formGroup); } } /** * Method to reset form fields */ reset(): void { this.formGroup.reset(); } isItemReadOnly(arrayContext: DynamicFormArrayModel, index: number): boolean { const context = arrayContext.groups[index]; const model = context.group[0] as any; return model.readOnly; } removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; this.removeArrayItem.emit(this.getEvent($event, arrayContext, index, 'remove')); this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext); this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel))); } insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext); this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add')); this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel))); } protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string): DynamicFormControlEvent { const context = arrayContext.groups[index]; const itemGroupModel = context.context; let group = this.formGroup.get(itemGroupModel.id) as FormGroup; if (isNull(group)) { for (const key of Object.keys(this.formGroup.controls)) { group = this.formGroup.controls[key].get(itemGroupModel.id) as FormGroup; if (isNotNull(group)) { break; } } } const model = context.group[0] as DynamicFormControlModel; const control = group.controls[index] as FormControl; return {$event, context, control, group, model, type}; } }