Forráskód Böngészése

Latest changes imported from AMP - inc semi-realtime validation

Richard Knight 5 éve
szülő
commit
0587fea44d

+ 51 - 16
src/app/dynaform/components/_abstract/native-input.component.ts

@@ -1,9 +1,10 @@
-import { OnInit, Input, Output, ChangeDetectorRef, EventEmitter } from '@angular/core';
+import { OnInit, OnDestroy, Input, Output, ChangeDetectorRef, EventEmitter } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { FriendlyValidationErrorsService } from './../../services/friendly-validation-errors.service';
-// import { Subject } from 'rxjs';
+import { Subject, Subscription } from 'rxjs';
+import { debounceTime } from 'rxjs/operators';
 
-export abstract class NativeInputComponent implements OnInit {
+export abstract class NativeInputComponent implements OnInit, OnDestroy {
 
 	@Input()
 	control: FormControl;
@@ -22,8 +23,15 @@ export abstract class NativeInputComponent implements OnInit {
 	_meta: StringMap<any>;
 
 	pendingValidation: boolean;
+	hasFocus: boolean = false;
+	waitForFirstChange: boolean = false;
+	keyUp$: Subject<string> = new Subject();
 
-	// firstFailureMsg: Subject<string> = new Subject();
+	valueSBX: Subscription;
+	statusSBX: Subscription;
+	keyUpSBX: Subscription;
+
+	keyUpValidationDelay: number = 2000; // Validate after 2 seconds IF the control still has focus
 
 	constructor(
 		protected valErrsService: FriendlyValidationErrorsService,
@@ -34,8 +42,8 @@ export abstract class NativeInputComponent implements OnInit {
 		// this.control is not an instance of FormControl when it's a Checkbutton in a Checkbutton group (but is when it's a solo Checkbutton) - BUT WHY? WHY!!!
 		// Hence the 'if' statement
 		if (this.control instanceof FormControl) {
-			this.control.valueChanges.subscribe(this.markForCheck.bind(this));
-			this.control.statusChanges.subscribe(status => {
+			this.valueSBX = this.control.valueChanges.subscribe(this.markForCheck.bind(this));
+			this.statusSBX = this.control.statusChanges.subscribe(status => {
 				if (status === 'PENDING') {
 					this.pendingValidation = true;
 				} else if (this.pendingValidation) {
@@ -44,6 +52,27 @@ export abstract class NativeInputComponent implements OnInit {
 				}
 			});
 		}
+		this.keyUpSBX = this.keyUp$.pipe(debounceTime(this.keyUpValidationDelay)).subscribe(val => {
+			if (this.hasFocus) {
+				// THE ORDER OF THE NEXT THREE LINES IS IMPORTANT!
+				// We MUST make the control as dirty before setting the value to avoid corrupting the initialValue recorded in Dynaform's makeAsyncTest utility
+				this.control.markAsTouched();
+				this.control.markAsDirty();
+				this.control.setValue(val);
+			}
+		});
+	}
+
+	ngOnDestroy() {
+		if (this.valueSBX) {
+			this.valueSBX.unsubscribe();
+		}
+		if (this.statusSBX) {
+			this.statusSBX.unsubscribe();
+		}
+		if (this.keyUpSBX) {
+			this.keyUpSBX.unsubscribe();
+		}
 	}
 
 	exposeForTemplate() {
@@ -79,17 +108,23 @@ export abstract class NativeInputComponent implements OnInit {
 		return this._meta.valFailureMsgs[key] || this.valErrsService.getFriendly(key, this.control.errors[key]);
 	}
 
-	/*
-	getFirstFailureMsg(): void {
-		if (!this.control.errors || this.control.errors === {}) {
-			this.firstFailureMsg.next('');
-		}
-		console.log(this.control.errors);
-		const key = Object.keys(this.control.errors)[0];
-		const err = this._meta.valFailureMsgs[key] || this.valErrsService.getFriendly(key, this.control.errors[key]);
-		this.firstFailureMsg.next(err);
+	gainFocus(): void {
+		this.hasFocus = true;
+		this.waitForFirstChange = true;
+	}
+
+	loseFocus(): void {
+		this.hasFocus = false;
 	}
-	*/
 
+	handleKeyup(currentFieldValue: string): void {
+		this.keyUp$.next(currentFieldValue);
+		if (this.control.value && this.waitForFirstChange) {
+			// Hide any validation errors if the control has a previous value (set after Angular's first updateOn event) and its value has changed
+			// NOTE that this.control.value may lag behind currentFieldValue depending on the updateOn startegy chosen
+			this.control.markAsPending();
+			this.waitForFirstChange = false;
+		}
+	}
 
 }

+ 6 - 1
src/app/dynaform/components/clarity/datepicker/datepicker.component.html

@@ -1,3 +1,8 @@
 <clr-date-container [ngClass]="extraClass">
-	<input clrDate [formControl]="control">
+	<input clrDate
+		[formControl]="control"
+		(focus)="gainFocus()"
+		(blur)="loseFocus()"
+		(keyup)="handleKeyup($event.target.value)"
+	>
 </clr-date-container>

+ 7 - 1
src/app/dynaform/components/clarity/password/clr-password.component.html

@@ -1,5 +1,11 @@
 <clr-password-container>
 	<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
-	<input clrPassword [formControl]="control" [placeholder]="placeholder" />
+	<input clrPassword
+		[formControl]="control"
+		[placeholder]="placeholder"
+		(focus)="gainFocus()"
+		(blur)="loseFocus()"
+		(keyup)="handleKeyup($event.target.value)"
+	>
 	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </clr-password-container>

+ 17 - 2
src/app/dynaform/components/clarity/text/clr-text.component.html

@@ -1,6 +1,13 @@
 <clr-input-container *ngIf="!link; else fieldWithLink">
 	<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
-	<input clrInput type="text" [formControl]="control" [placeholder]="placeholder" spellcheck="false">
+	<input clrInput type="text"
+		[formControl]="control"
+		[placeholder]="placeholder"
+		(focus)="gainFocus()"
+		(blur)="loseFocus()"
+		(keyup)="handleKeyup($event.target.value)"
+		spellcheck="false"
+	>
 	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </clr-input-container>
 
@@ -8,7 +15,15 @@
 <ng-template #fieldWithLink>
 	<clr-input-container>
 		<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
-		<input clrInput type="text" #field [formControl]="control" [placeholder]="placeholder" spellcheck="false">
+		<input clrInput type="text"
+			#field
+			[formControl]="control"
+			[placeholder]="placeholder"
+			(focus)="gainFocus()"
+			(blur)="loseFocus()"
+			(keyup)="handleKeyup($event.target.value)"
+			spellcheck="false"
+		>
 		<div class="input-group-append">
 			<button class="btn btn-outline" type="button"
 				[routerLink]="[ link.route, field.value ]">{{ link.label || 'Details' }}</button>

+ 3 - 0
src/app/dynaform/components/clarity/textarea/clr-textarea.component.html

@@ -3,6 +3,9 @@
 	<textarea clrTextarea
 		[formControl]="control"
 		[placeholder]="placeholder"
+		(focus)="gainFocus()"
+		(blur)="loseFocus()"
+		(keyup)="handleKeyup($event.target.value)"
 		rows="5"
 	></textarea>
 	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>

+ 4 - 2
src/app/dynaform/services/_formdata-utils.ts

@@ -404,7 +404,7 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 	// The main function - builds FormGroups containing other FormGroups, FormArrays and FormControls
 	const buildFormGroup = metaG => {
 		// Ensure that we have Field-Specific Metadata, not raw Objects
-		const metaWithNameKeys = addMissingNames(metaG); // <!--- DO WE REALLY HAVE TO CALL addMissingManes again here - it should have been done already?
+		const metaWithNameKeys = addMissingNames(metaG); // <!--- DO WE REALLY HAVE TO CALL addMissingNames again here - it should have been done already?
 		// MAYBE only run this if first entry isn't right, for reasons of efficiency
 		// const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
 		const fieldModeledMeta = metaWithNameKeys;
@@ -477,6 +477,7 @@ const resetValidityRecursive = (group: FormGroup | FormArray): void => {
 	}
 };
 
+/*
 const advanceUpdateStategyOfInvalidControlsRecursive = (group: FormGroup | FormArray): void => {
 	// For all invalid text / select / password control, change the updateOn startegy from 'blur' to 'change'
 	// For as-you-type feedback if validation has failed once
@@ -498,6 +499,7 @@ const advanceUpdateStategyOfInvalidControlsRecursive = (group: FormGroup | FormA
 		}
 	}
 };
+*/
 
 
 // ---------------------------------------------------------------------------------------------------------------------
@@ -716,6 +718,6 @@ export {
 	buildFieldSpecificMetaInClosure, extractFieldMappings,
 	buildFormGroupFunctionFactory,
 	resetAsyncValidatorsRecursive,
-	touchAndUpdateValidityRecursive, resetValidityRecursive, advanceUpdateStategyOfInvalidControlsRecursive,
+	touchAndUpdateValidityRecursive, resetValidityRecursive,
 	generateNewModel, updateMeta,
 };

+ 7 - 2
src/app/dynaform/services/dynaform.service.ts

@@ -107,7 +107,7 @@ import {
 	buildFieldSpecificMetaInClosure, extractFieldMappings,
 	buildFormGroupFunctionFactory,
 	resetAsyncValidatorsRecursive,
-	touchAndUpdateValidityRecursive, resetValidityRecursive, advanceUpdateStategyOfInvalidControlsRecursive,
+	touchAndUpdateValidityRecursive, resetValidityRecursive, // advanceUpdateStategyOfInvalidControlsRecursive,
 	generateNewModel, updateMeta
 } from './_formdata-utils';
 
@@ -190,6 +190,7 @@ export class DynaformService {
 		const mapping = extractFieldMappings(meta || this.meta); // Memoize
 		const mappedModel = this.modelMapper.reverseMap(newModel, mapping);
 		(form || this.form).patchValue(mappedModel);
+		(form || this.form).updateValueAndValidity();
 	}
 
 	updateMeta(newMeta: StringMap<any>, path: string = '/', meta?: StringMap<any>): StringMap<any> {
@@ -233,9 +234,13 @@ export class DynaformService {
 		resetValidityRecursive(form || this.form);
 	}
 
+	/*
 	goRealTime(form?: FormGroup): void {
-		advanceUpdateStategyOfInvalidControlsRecursive(form || this.form);
+		// See https://medium.com/p/7acc7f93f357/responses/show INCLUDING FIRST COMMENT
+		// AND _setUpdateStrategy in Angular 8 source https://github.com/angular/angular/blob/8.2.0/packages/forms/src/model.ts#L879
+		// advanceUpdateStategyOfInvalidControlsRecursive(form || this.form);
 	}
+	*/
 
 	// -----------------------------------------------------------------------------------------------------------------
 	// Convenience methods combining several steps

+ 4 - 4
src/app/dynaform/utils.ts

@@ -1,10 +1,10 @@
 // Utility functions for Dyynaform consumers
 
-import { FormControl, ValidationErrors } from '@angular/forms';
+import { FormControl, FormGroup, ValidationErrors } from '@angular/forms';
 import { ValueTransformer } from './interfaces';
 import { Observable, of, merge } from 'rxjs';
 import { filter, map, mapTo, switchMap, } from 'rxjs/internal/operators';
-import { FindValueOperator } from 'rxjs/internal/operators/find';
+
 
 // Dropdown Modified Input - Starts With / Contains / Matches
 const standardModifiers = ['Starts with', 'Contains', 'Matches'];
@@ -61,9 +61,9 @@ const excludeFields = (obj: StringMap<any>, fieldsToExclude: string | string[])
 // Higher order function that takes a bound test function and failure flag
 // and returns an AsyncValidator-compatible function
 // that in turn returns an Observable of ValidationErrors
-const makeAsyncTest = (boundFnName: string, failureFlag: string): (fc: FormControl) => Observable<ValidationErrors> => {
+const makeAsyncTest = (boundFnName: string, failureFlag: string): (fc: FormControl | FormGroup) => Observable<ValidationErrors> => {
 	let initialValue;
-	return function(fc: FormControl | 'RESET'): Observable<ValidationErrors> {
+	return function(fc: FormControl | FormGroup | 'RESET'): Observable<ValidationErrors> {
 		if (fc === 'RESET') {
 			// Special value that resets the initial value
 			initialValue = '';