Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ form.resetValidation();
```html
<tp-form prevent-submit="yes">
<form action="#">
<tp-form-field required="yes">
<tp-form-field required="yes" revalidate-on-change="no"> <-- If you don't want to revalidate as the value changes
<label>Field 1</label>
<input type="text" name="field_1">
</tp-form-field>
Expand Down
6 changes: 5 additions & 1 deletion src/form/definitions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { TPFormFieldElement } from './tp-form-field';
* Form Validator.
*/
export interface TPFormValidator {
validate: { ( field: TPFormFieldElement ): boolean };
validate: { ( field: TPFormFieldElement ): boolean | Promise<boolean> };
getErrorMessage: { ( field: TPFormFieldElement ): string };
getSuspenseMessage?: { ( field: TPFormFieldElement ): string };
}

/**
Expand All @@ -23,5 +24,8 @@ declare global {
tpFormErrors: {
[ key: string ]: string;
};
tpFormSuspenseMessages: {
[ key: string ]: string;
};
}
}
49 changes: 49 additions & 0 deletions src/form/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,56 @@
width: 100%;
}
</style>

<script type="module">
window.tpFormValidators['async-validator'] = {
validate: async () => {
return new Promise( ( resolve ) => {
setTimeout( () => {
resolve( true ); // Resolves true after 5 seconds.
}, 5000 );
});
},
getErrorMessage: () => 'There was an error processing your request.',
getSuspenseMessage: () => 'Checking...',
};
</script>
</head>
<body>
<main>
<tp-form prevent-submit="yes">
<form action="#">
<h3>Synchronous Form</h3>
<tp-form-field no-empty-spaces="yes" required="yes">
<label>Field 1</label>
<input type="text" name="field_1">
</tp-form-field>
<tp-form-field required="yes" email="yes">
<label>Field 2</label>
<input type="email" name="field_2">
</tp-form-field>
<tp-form-field required="yes">
<label>Field 3</label>
<select type="text" name="field_3">
<option value="">Select value</option>
<option value="value_1">Value 1</option>
<option value="value_2">Value 2</option>
<option value="value_3">Value 3</option>
</select>
</tp-form-field>
<tp-form-field min-length="4" max-length="8">
<label>Field 4</label>
<textarea name="field_4"></textarea>
</tp-form-field>
<tp-form-submit submitting-text="Submitting...">
<button type="submit">Submit</button>
</tp-form-submit>
</form>
</tp-form>

<tp-form>
<form action="#">
<h3>Asynchronous Form</h3>
<tp-form-field no-empty-spaces="yes" required="yes">
<label>Field 1</label>
<input type="text" name="field_1">
Expand All @@ -55,6 +100,10 @@
<label>Field 4</label>
<textarea name="field_4"></textarea>
</tp-form-field>
<tp-form-field required="yes" email="yes" async-validator="yes" revalidate-on-change="no">
<label>Field 5</label>
<input type="email" name="field_5">
</tp-form-field>
<tp-form-submit submitting-text="Submitting...">
<button type="submit">Submit</button>
</tp-form-submit>
Expand Down
3 changes: 3 additions & 0 deletions src/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const validators = [
*/
window.tpFormValidators = {};
window.tpFormErrors = {};
window.tpFormSuspenseMessages = {};

// Register validators.
validators.forEach( (
Expand All @@ -45,6 +46,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';

/**
Expand All @@ -53,4 +55,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 );
137 changes: 125 additions & 12 deletions src/form/tp-form-field.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/**
* Internal dependencies.
*/
import { TPFormElement } from './tp-form';
import { TPFormErrorElement } from './tp-form-error';
import { TPFormSuspenseElement } from './tp-form-suspense';

/**
* TP Form Field.
Expand All @@ -26,9 +28,16 @@ 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' ) ) {
this.validate();
const form: TPFormElement | null = this.closest( 'tp-form' );
form?.validateField( this );
}
}

Expand All @@ -39,7 +48,7 @@ export class TPFormFieldElement extends HTMLElement {
*/
static get observedAttributes(): string[] {
// Attributes observed in the TPFormFieldElement web-component.
return [ 'valid', 'error' ];
return [ 'valid', 'error', 'suspense' ];
}

/**
Expand All @@ -53,7 +62,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 } ) );
}

Expand All @@ -70,7 +79,7 @@ export class TPFormFieldElement extends HTMLElement {

// Exit the function if validators are not available.
if ( ! tpFormValidators ) {
//Early return
// Early return.
return;
}

Expand All @@ -83,6 +92,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 ) {
// @ts-ignore
this.setSuspenseMessage( tpFormValidators[ suspense ]?.getSuspenseMessage( this ) );
} else {
this.removeSuspenseMessage();
}
}

/**
Expand All @@ -100,7 +120,7 @@ export class TPFormFieldElement extends HTMLElement {
*
* @return {boolean} Whether this field passed validation.
*/
validate(): boolean {
async validate(): Promise<boolean> {
// Retrieve tpFormValidators from the window object.
const { tpFormValidators } = window;

Expand All @@ -118,6 +138,7 @@ export class TPFormFieldElement extends HTMLElement {

// Prepare error and valid status.
let valid: boolean = true;
let suspense: Promise<boolean> | null = null;
let error: string = '';
const allAttributes: string[] = this.getAttributeNames();

Expand All @@ -126,14 +147,64 @@ 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<boolean> = tpFormValidators[ attributeName ].validate( this );
error = attributeName;

// First check for a Promise.
if ( isValid instanceof Promise ) {
// Yes it is an async validation.
valid = false;

// Looks like we found an error!
if ( false === isValid ) {
// 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' );

// Resolve the promise.
resolve( true );
} else {
this.removeAttribute( 'valid' );
this.setAttribute( 'error', error );

// Resolve the promise.
resolve( false );
}

// 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' ) );

// Reject the promise.
reject( false );
} )
.finally( (): void => {
// Clean up.
this.removeAttribute( 'suspense' );
} );
} );

// Return.
return false;
} else if ( false === isValid ) {
// Not a Promise, but looks like we found an error!
valid = false;
error = attributeName;

// return false;
// Return.
return false;
}
}
Expand All @@ -146,12 +217,27 @@ 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.
// 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;
}

Expand Down Expand Up @@ -187,4 +273,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();
}
}
5 changes: 5 additions & 0 deletions src/form/tp-form-suspense.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* TP Form Suspense.
*/
export class TPFormSuspenseElement extends HTMLElement {
}
Loading