From a1381c8d85ebbfd42b22eaaacd273367b1c0b29d Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Fri, 7 Mar 2025 19:45:56 +1100 Subject: [PATCH 1/8] async form validator first commit --- src/form/definitions.d.ts | 3 +- src/form/index.html | 49 ++++++++++++++++ src/form/index.ts | 2 + src/form/tp-form-field.ts | 106 ++++++++++++++++++++++++++++++++--- src/form/tp-form-suspense.ts | 5 ++ 5 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 src/form/tp-form-suspense.ts diff --git a/src/form/definitions.d.ts b/src/form/definitions.d.ts index 26032bd..4de7216 100644 --- a/src/form/definitions.d.ts +++ b/src/form/definitions.d.ts @@ -7,8 +7,9 @@ import { TPFormFieldElement } from './tp-form-field'; * Form Validator. */ export interface TPFormValidator { - validate: { ( field: TPFormFieldElement ): boolean }; + validate: { ( field: TPFormFieldElement ): boolean | Promise }; getErrorMessage: { ( field: TPFormFieldElement ): string }; + getSuspenseMessage?: { ( field: TPFormFieldElement ): string }; } /** diff --git a/src/form/index.html b/src/form/index.html index 05cdb02..6f42b0a 100644 --- a/src/form/index.html +++ b/src/form/index.html @@ -29,11 +29,56 @@ width: 100%; } + +
+

Synchronous Form

+ + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Asynchronous Form

@@ -55,6 +100,10 @@ + + + + diff --git a/src/form/index.ts b/src/form/index.ts index 4f9c6de..52d9df2 100644 --- a/src/form/index.ts +++ b/src/form/index.ts @@ -45,6 +45,7 @@ validators.forEach( ( import { TPFormElement } from './tp-form'; import { TPFormFieldElement } from './tp-form-field'; import { TPFormErrorElement } from './tp-form-error'; +import { TPFormSuspenseElement } from './tp-form-suspense'; import { TPFormSubmitElement } from './tp-form-submit'; /** @@ -53,4 +54,5 @@ import { TPFormSubmitElement } from './tp-form-submit'; customElements.define( 'tp-form', TPFormElement ); customElements.define( 'tp-form-field', TPFormFieldElement ); customElements.define( 'tp-form-error', TPFormErrorElement ); +customElements.define( 'tp-form-suspense', TPFormSuspenseElement ); customElements.define( 'tp-form-submit', TPFormSubmitElement ); diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts index 0b7886c..bcb1807 100644 --- a/src/form/tp-form-field.ts +++ b/src/form/tp-form-field.ts @@ -2,6 +2,7 @@ * Internal dependencies. */ import { TPFormErrorElement } from './tp-form-error'; +import { TPFormSuspenseElement } from './tp-form-suspense'; /** * TP Form Field. @@ -39,7 +40,7 @@ export class TPFormFieldElement extends HTMLElement { */ static get observedAttributes(): string[] { // Attributes observed in the TPFormFieldElement web-component. - return [ 'valid', 'error' ]; + return [ 'valid', 'error', 'suspense' ]; } /** @@ -53,7 +54,7 @@ export class TPFormFieldElement extends HTMLElement { // Check if the observed attributes 'valid' or 'error' have changed. // Dispatch a custom 'validate' event. - if ( ( 'valid' === name || 'error' === name ) && oldValue !== newValue ) { + if ( ( 'valid' === name || 'error' === name || 'suspense' === name ) && oldValue !== newValue ) { this.dispatchEvent( new CustomEvent( 'validate', { bubbles: true } ) ); } @@ -70,7 +71,7 @@ export class TPFormFieldElement extends HTMLElement { // Exit the function if validators are not available. if ( ! tpFormValidators ) { - //Early return + // Early return. return; } @@ -83,6 +84,17 @@ export class TPFormFieldElement extends HTMLElement { } else { this.removeErrorMessage(); } + + // Get the 'suspense' attribute value. + const suspense: string = this.getAttribute( 'suspense' ) ?? ''; + + // Check if the suspense exists and has a corresponding suspense message function. + if ( '' !== suspense && suspense in tpFormValidators && 'function' === typeof tpFormValidators[ suspense ].getSuspenseMessage ) { + // this.setSuspenseMessage( tpFormValidators[ suspense ].getSuspenseMessage( this ) ?? '' ); + this.setSuspenseMessage( 'Loading...' ); + } else { + this.removeSuspenseMessage(); + } } /** @@ -118,6 +130,7 @@ export class TPFormFieldElement extends HTMLElement { // Prepare error and valid status. let valid: boolean = true; + let suspense: boolean = false; let error: string = ''; const allAttributes: string[] = this.getAttributeNames(); @@ -126,14 +139,53 @@ export class TPFormFieldElement extends HTMLElement { // Check if the attribute is a validator. if ( attributeName in tpFormValidators && 'function' === typeof tpFormValidators[ attributeName ].validate ) { // We found one, lets validate the field. - const isValid: boolean = tpFormValidators[ attributeName ].validate( this ); + const isValid: boolean | Promise = tpFormValidators[ attributeName ].validate( this ); + error = attributeName; - // Looks like we found an error! - if ( false === isValid ) { + // First check for a Promise. + if ( isValid instanceof Promise ) { + // Yes it is an async validation. + suspense = true; valid = false; - error = attributeName; - // return false; + // Validate it. + isValid + .then( ( suspenseIsValid: boolean ) => { + // Validation is complete. + if ( true === suspenseIsValid ) { + this.setAttribute( 'valid', 'yes' ); + this.removeAttribute( 'error' ); + } else { + this.removeAttribute( 'valid' ); + this.setAttribute( 'error', error ); + } + + // Dispatch a custom 'validation-suspense-success' event. + this.dispatchEvent( new CustomEvent( 'validation-suspense-success' ) ); + } ) + .catch( (): void => { + // There was an error. + this.removeAttribute( 'valid' ); + this.setAttribute( 'error', error ); + + // Dispatch a custom 'validation-suspense-error' event. + this.dispatchEvent( new CustomEvent( 'validation-suspense-error' ) ); + } ) + .finally( (): void => { + // Clean up. + this.removeAttribute( 'suspense' ); + } ); + + // Dispatch a custom 'validation-suspense-start' event. + this.dispatchEvent( new CustomEvent( 'validation-suspense-start' ) ); + + // Return. + return false; + } else if ( false === isValid ) { + // Not a Promise, but looks like we found an error! + valid = false; + + // Return. return false; } } @@ -146,9 +198,18 @@ export class TPFormFieldElement extends HTMLElement { if ( valid ) { this.setAttribute( 'valid', 'yes' ); this.removeAttribute( 'error' ); + this.removeAttribute( 'suspense' ); } else { this.removeAttribute( 'valid' ); - this.setAttribute( 'error', error ); + + // Check for suspense. + if ( suspense ) { + this.setAttribute( 'suspense', error ); + this.removeAttribute( 'error' ); + } else { + this.removeAttribute( 'suspense' ); + this.setAttribute( 'error', error ); + } } // Return validity. @@ -187,4 +248,31 @@ export class TPFormFieldElement extends HTMLElement { // Dispatch a custom 'validation-success' event. this.dispatchEvent( new CustomEvent( 'validation-success' ) ); } + + /** + * Set the suspense message. + * + * @param {string} message Suspense message. + */ + setSuspenseMessage( message: string = '' ): void { + // Look for an existing tp-form-error element. + const suspense: TPFormSuspenseElement | null = this.querySelector( 'tp-form-suspense' ); + + // If found, update its innerHTML with the suspense message. Otherwise, create a new tp-form-suspense element and append it to the component. + if ( suspense ) { + suspense.innerHTML = message; + } else { + const suspenseElement: TPFormSuspenseElement = document.createElement( 'tp-form-suspense' ); + suspenseElement.innerHTML = message; + this.appendChild( suspenseElement ); + } + } + + /** + * Remove the suspense message. + */ + removeSuspenseMessage(): void { + // Find and remove the tp-form-suspense element. + this.querySelector( 'tp-form-suspense' )?.remove(); + } } diff --git a/src/form/tp-form-suspense.ts b/src/form/tp-form-suspense.ts new file mode 100644 index 0000000..90dc398 --- /dev/null +++ b/src/form/tp-form-suspense.ts @@ -0,0 +1,5 @@ +/** + * TP Form Suspense. + */ +export class TPFormSuspenseElement extends HTMLElement { +} From 05bf322d04f4fe07ef62a4a91e17feb418e52f5d Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Fri, 7 Mar 2025 21:15:55 +1100 Subject: [PATCH 2/8] set suspense at a form level --- src/form/index.html | 2 +- src/form/tp-form-field.ts | 76 +++++++++++++++++++++++---------------- src/form/tp-form.ts | 50 ++++++++++++++++++-------- 3 files changed, 82 insertions(+), 46 deletions(-) diff --git a/src/form/index.html b/src/form/index.html index 6f42b0a..f1d518f 100644 --- a/src/form/index.html +++ b/src/form/index.html @@ -36,7 +36,7 @@ return new Promise( (resolve) => { setTimeout( () => { resolve(true); // Resolves true after 2 seconds - }, 2000 ); + }, 5000 ); }); }, getErrorMessage: () => 'There was an error processing your request.', diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts index bcb1807..39efd02 100644 --- a/src/form/tp-form-field.ts +++ b/src/form/tp-form-field.ts @@ -112,7 +112,7 @@ export class TPFormFieldElement extends HTMLElement { * * @return {boolean} Whether this field passed validation. */ - validate(): boolean { + async validate(): Promise { // Retrieve tpFormValidators from the window object. const { tpFormValidators } = window; @@ -130,7 +130,7 @@ export class TPFormFieldElement extends HTMLElement { // Prepare error and valid status. let valid: boolean = true; - let suspense: boolean = false; + let suspense: Promise | null = null; let error: string = ''; const allAttributes: string[] = this.getAttributeNames(); @@ -145,39 +145,47 @@ export class TPFormFieldElement extends HTMLElement { // First check for a Promise. if ( isValid instanceof Promise ) { // Yes it is an async validation. - suspense = true; valid = false; - // Validate it. - isValid - .then( ( suspenseIsValid: boolean ) => { - // Validation is complete. - if ( true === suspenseIsValid ) { - this.setAttribute( 'valid', 'yes' ); - this.removeAttribute( 'error' ); - } else { + // Dispatch a custom 'validation-suspense-start' event. + this.dispatchEvent( new CustomEvent( 'validation-suspense-start' ) ); + + // Create the promise. + suspense = new Promise( ( resolve, reject ): void => { + // Validate it. + isValid + .then( ( suspenseIsValid: boolean ) => { + // Validation is complete. + if ( true === suspenseIsValid ) { + this.setAttribute( 'valid', 'yes' ); + this.removeAttribute( 'error' ); + } else { + this.removeAttribute( 'valid' ); + this.setAttribute( 'error', error ); + } + + // Dispatch a custom 'validation-suspense-success' event. + this.dispatchEvent( new CustomEvent( 'validation-suspense-success' ) ); + + // Resolve the promise. + resolve( true ); + } ) + .catch( (): void => { + // There was an error. this.removeAttribute( 'valid' ); this.setAttribute( 'error', error ); - } - - // Dispatch a custom 'validation-suspense-success' event. - this.dispatchEvent( new CustomEvent( 'validation-suspense-success' ) ); - } ) - .catch( (): void => { - // There was an error. - this.removeAttribute( 'valid' ); - this.setAttribute( 'error', error ); - - // Dispatch a custom 'validation-suspense-error' event. - this.dispatchEvent( new CustomEvent( 'validation-suspense-error' ) ); - } ) - .finally( (): void => { - // Clean up. - this.removeAttribute( 'suspense' ); - } ); - // Dispatch a custom 'validation-suspense-start' event. - this.dispatchEvent( new CustomEvent( 'validation-suspense-start' ) ); + // Dispatch a custom 'validation-suspense-error' event. + this.dispatchEvent( new CustomEvent( 'validation-suspense-error' ) ); + + // Reject the promise. + reject( false ); + } ) + .finally( (): void => { + // Clean up. + this.removeAttribute( 'suspense' ); + } ); + } ); // Return. return false; @@ -212,7 +220,13 @@ export class TPFormFieldElement extends HTMLElement { } } - // Return validity. + // Do we have a suspense? + if ( suspense ) { + // Yes we do, return the promise. + return suspense; + } + + // No we don't, return a resolved promise. return valid; } diff --git a/src/form/tp-form.ts b/src/form/tp-form.ts index b0d2fa9..3942d9f 100644 --- a/src/form/tp-form.ts +++ b/src/form/tp-form.ts @@ -32,16 +32,20 @@ export class TPFormElement extends HTMLElement { * * @param {Event} e Submit event. */ - protected handleFormSubmit( e: SubmitEvent ): void { - // Validate the form. - const formValid: boolean = this.validate(); - - // Prevent form submission if it's invalid. - if ( ! formValid || 'yes' === this.getAttribute( 'prevent-submit' ) ) { - e.preventDefault(); - e.stopImmediatePropagation(); + protected async handleFormSubmit( e: SubmitEvent ): Promise { + // Stop normal form submission. + e.preventDefault(); + e.stopImmediatePropagation(); + + // Ignore a form that currently has suspense. + if ( 'yes' === this.getAttribute( 'suspense' ) ) { + // Bail early. + return; } + // Validate the form. + const formValid: boolean = await this.validate(); + // Get submit button. const submit: TPFormSubmitElement | null = this.querySelector( 'tp-form-submit' ); @@ -57,7 +61,13 @@ export class TPFormElement extends HTMLElement { // If form is valid then dispatch a custom 'submit-validation-success' event. if ( formValid ) { + // Trigger event. this.dispatchEvent( new CustomEvent( 'submit-validation-success', { bubbles: true } ) ); + + // Submit form. + if ( 'yes' !== this.getAttribute( 'prevent-submit' ) ) { + this.form?.submit(); + } } } @@ -66,7 +76,7 @@ export class TPFormElement extends HTMLElement { * * @return {boolean} Whether the form is valid or not. */ - validate(): boolean { + async validate(): Promise { // Dispatch a custom 'validate' event. this.dispatchEvent( new CustomEvent( 'validate', { bubbles: true } ) ); @@ -81,14 +91,26 @@ export class TPFormElement extends HTMLElement { return true; } + // Start by setting the form as suspense. + this.setAttribute( 'suspense', 'yes' ); + // Check if all fields are valid. let formValid: boolean = true; - fields.forEach( ( field: TPFormFieldElement ): void => { - // Validate the field. - if ( ! field.validate() ) { + const validationPromises: Promise[] = Array + .from( fields ) + .map( async ( field: TPFormFieldElement ): Promise => await field.validate() ); + + // Wait for promises to resolve. + await Promise.all( validationPromises ) + .then( ( results: boolean[] ): void => { + // Check if all fields are valid. + formValid = results.every( ( isValid: boolean ) => isValid ); + } ) + .catch( () => { + // There was an error with one or more fields. formValid = false; - } - } ); + } ) + .finally( () => this.removeAttribute( 'suspense' ) ); // If form is valid then dispatch a custom 'validation-success' event else send a custom 'validation-error' event. if ( formValid ) { From 74c115ce912bf4c1aac8d31362e09b70d11c297f Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Fri, 7 Mar 2025 21:30:07 +1100 Subject: [PATCH 3/8] validate field from form --- src/form/tp-form-field.ts | 8 +++++--- src/form/tp-form.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts index 39efd02..157786f 100644 --- a/src/form/tp-form-field.ts +++ b/src/form/tp-form-field.ts @@ -1,6 +1,7 @@ /** * Internal dependencies. */ +import { TPFormElement } from './tp-form'; import { TPFormErrorElement } from './tp-form-error'; import { TPFormSuspenseElement } from './tp-form-suspense'; @@ -29,7 +30,8 @@ export class TPFormFieldElement extends HTMLElement { handleFieldChanged(): void { // Validate the field again if 'valid' or 'error' attribute is present. if ( this.getAttribute( 'valid' ) || this.getAttribute( 'error' ) ) { - this.validate(); + const form: TPFormElement | null = this.closest( 'tp-form' ); + form?.validateField( this ); } } @@ -90,8 +92,8 @@ export class TPFormFieldElement extends HTMLElement { // Check if the suspense exists and has a corresponding suspense message function. if ( '' !== suspense && suspense in tpFormValidators && 'function' === typeof tpFormValidators[ suspense ].getSuspenseMessage ) { - // this.setSuspenseMessage( tpFormValidators[ suspense ].getSuspenseMessage( this ) ?? '' ); - this.setSuspenseMessage( 'Loading...' ); + // @ts-ignore + this.setSuspenseMessage( tpFormValidators[ suspense ]?.getSuspenseMessage( this ) ); } else { this.removeSuspenseMessage(); } diff --git a/src/form/tp-form.ts b/src/form/tp-form.ts index 3942d9f..da0a4a0 100644 --- a/src/form/tp-form.ts +++ b/src/form/tp-form.ts @@ -123,6 +123,21 @@ export class TPFormElement extends HTMLElement { return formValid; } + /** + * Validate one field. + * + * @param {HTMLElement} field Field node. + */ + async validateField( field: TPFormFieldElement ): Promise { + // Set form as suspense, validate and undo suspense. + this.setAttribute( 'suspense', 'yes' ); + const fieldValid: boolean = await field.validate(); + this.removeAttribute( 'suspense' ); + + // Return result. + return fieldValid; + } + /** * Reset form validation. */ From df568ffe89a0a775981ce1bdc614bfa08c06d430 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Fri, 7 Mar 2025 21:38:08 +1100 Subject: [PATCH 4/8] revalidate on change attribute --- src/form/index.html | 2 +- src/form/tp-form-field.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/form/index.html b/src/form/index.html index f1d518f..dd25f99 100644 --- a/src/form/index.html +++ b/src/form/index.html @@ -100,7 +100,7 @@

Asynchronous Form

- + diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts index 157786f..2568365 100644 --- a/src/form/tp-form-field.ts +++ b/src/form/tp-form-field.ts @@ -28,6 +28,12 @@ export class TPFormFieldElement extends HTMLElement { * Update validation when the field has changed. */ handleFieldChanged(): void { + // Check if we want to ignore field revalidations. + if ( 'no' === this.getAttribute( 'revalidate-on-change' ) ) { + // Yes we do, bail! + return; + } + // Validate the field again if 'valid' or 'error' attribute is present. if ( this.getAttribute( 'valid' ) || this.getAttribute( 'error' ) ) { const form: TPFormElement | null = this.closest( 'tp-form' ); From f0f6bf3778a0a258cee4c9f565fa3c6d6456bd0c Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Fri, 7 Mar 2025 21:44:40 +1100 Subject: [PATCH 5/8] update docs and reset --- src/form/README.md | 2 +- src/form/index.html | 4 ++-- src/form/tp-form.ts | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/form/README.md b/src/form/README.md index 294d992..1b5a590 100644 --- a/src/form/README.md +++ b/src/form/README.md @@ -33,7 +33,7 @@ form.resetValidation(); ```html - + <-- If you don't want to revalidate as the value changes diff --git a/src/form/index.html b/src/form/index.html index dd25f99..c70c363 100644 --- a/src/form/index.html +++ b/src/form/index.html @@ -33,9 +33,9 @@