Skip to content

Commit c486c28

Browse files
Merge pull request #103 from Travelopia/feature/form-async-validator
Async Form Validators
2 parents d4c1b53 + 16acc22 commit c486c28

File tree

7 files changed

+247
-40
lines changed

7 files changed

+247
-40
lines changed

src/form/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ form.resetValidation();
3333
```html
3434
<tp-form prevent-submit="yes">
3535
<form action="#">
36-
<tp-form-field required="yes">
36+
<tp-form-field required="yes" revalidate-on-change="no"> <-- If you don't want to revalidate as the value changes
3737
<label>Field 1</label>
3838
<input type="text" name="field_1">
3939
</tp-form-field>

src/form/definitions.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { TPFormFieldElement } from './tp-form-field';
77
* Form Validator.
88
*/
99
export interface TPFormValidator {
10-
validate: { ( field: TPFormFieldElement ): boolean };
10+
validate: { ( field: TPFormFieldElement ): boolean | Promise<boolean> };
1111
getErrorMessage: { ( field: TPFormFieldElement ): string };
12+
getSuspenseMessage?: { ( field: TPFormFieldElement ): string };
1213
}
1314

1415
/**
@@ -23,5 +24,8 @@ declare global {
2324
tpFormErrors: {
2425
[ key: string ]: string;
2526
};
27+
tpFormSuspenseMessages: {
28+
[ key: string ]: string;
29+
};
2630
}
2731
}

src/form/index.html

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,56 @@
2929
width: 100%;
3030
}
3131
</style>
32+
33+
<script type="module">
34+
window.tpFormValidators['async-validator'] = {
35+
validate: async () => {
36+
return new Promise( ( resolve ) => {
37+
setTimeout( () => {
38+
resolve( true ); // Resolves true after 5 seconds.
39+
}, 5000 );
40+
});
41+
},
42+
getErrorMessage: () => 'There was an error processing your request.',
43+
getSuspenseMessage: () => 'Checking...',
44+
};
45+
</script>
3246
</head>
3347
<body>
3448
<main>
3549
<tp-form prevent-submit="yes">
3650
<form action="#">
51+
<h3>Synchronous Form</h3>
52+
<tp-form-field no-empty-spaces="yes" required="yes">
53+
<label>Field 1</label>
54+
<input type="text" name="field_1">
55+
</tp-form-field>
56+
<tp-form-field required="yes" email="yes">
57+
<label>Field 2</label>
58+
<input type="email" name="field_2">
59+
</tp-form-field>
60+
<tp-form-field required="yes">
61+
<label>Field 3</label>
62+
<select type="text" name="field_3">
63+
<option value="">Select value</option>
64+
<option value="value_1">Value 1</option>
65+
<option value="value_2">Value 2</option>
66+
<option value="value_3">Value 3</option>
67+
</select>
68+
</tp-form-field>
69+
<tp-form-field min-length="4" max-length="8">
70+
<label>Field 4</label>
71+
<textarea name="field_4"></textarea>
72+
</tp-form-field>
73+
<tp-form-submit submitting-text="Submitting...">
74+
<button type="submit">Submit</button>
75+
</tp-form-submit>
76+
</form>
77+
</tp-form>
78+
79+
<tp-form>
80+
<form action="#">
81+
<h3>Asynchronous Form</h3>
3782
<tp-form-field no-empty-spaces="yes" required="yes">
3883
<label>Field 1</label>
3984
<input type="text" name="field_1">
@@ -55,6 +100,10 @@
55100
<label>Field 4</label>
56101
<textarea name="field_4"></textarea>
57102
</tp-form-field>
103+
<tp-form-field required="yes" email="yes" async-validator="yes" revalidate-on-change="no">
104+
<label>Field 5</label>
105+
<input type="email" name="field_5">
106+
</tp-form-field>
58107
<tp-form-submit submitting-text="Submitting...">
59108
<button type="submit">Submit</button>
60109
</tp-form-submit>

src/form/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const validators = [
2929
*/
3030
window.tpFormValidators = {};
3131
window.tpFormErrors = {};
32+
window.tpFormSuspenseMessages = {};
3233

3334
// Register validators.
3435
validators.forEach( (
@@ -45,6 +46,7 @@ validators.forEach( (
4546
import { TPFormElement } from './tp-form';
4647
import { TPFormFieldElement } from './tp-form-field';
4748
import { TPFormErrorElement } from './tp-form-error';
49+
import { TPFormSuspenseElement } from './tp-form-suspense';
4850
import { TPFormSubmitElement } from './tp-form-submit';
4951

5052
/**
@@ -53,4 +55,5 @@ import { TPFormSubmitElement } from './tp-form-submit';
5355
customElements.define( 'tp-form', TPFormElement );
5456
customElements.define( 'tp-form-field', TPFormFieldElement );
5557
customElements.define( 'tp-form-error', TPFormErrorElement );
58+
customElements.define( 'tp-form-suspense', TPFormSuspenseElement );
5659
customElements.define( 'tp-form-submit', TPFormSubmitElement );

src/form/tp-form-field.ts

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/**
22
* Internal dependencies.
33
*/
4+
import { TPFormElement } from './tp-form';
45
import { TPFormErrorElement } from './tp-form-error';
6+
import { TPFormSuspenseElement } from './tp-form-suspense';
57

68
/**
79
* TP Form Field.
@@ -26,9 +28,16 @@ export class TPFormFieldElement extends HTMLElement {
2628
* Update validation when the field has changed.
2729
*/
2830
handleFieldChanged(): void {
31+
// Check if we want to ignore field revalidations.
32+
if ( 'no' === this.getAttribute( 'revalidate-on-change' ) ) {
33+
// Yes we do, bail!
34+
return;
35+
}
36+
2937
// Validate the field again if 'valid' or 'error' attribute is present.
3038
if ( this.getAttribute( 'valid' ) || this.getAttribute( 'error' ) ) {
31-
this.validate();
39+
const form: TPFormElement | null = this.closest( 'tp-form' );
40+
form?.validateField( this );
3241
}
3342
}
3443

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

4554
/**
@@ -53,7 +62,7 @@ export class TPFormFieldElement extends HTMLElement {
5362
// Check if the observed attributes 'valid' or 'error' have changed.
5463

5564
// Dispatch a custom 'validate' event.
56-
if ( ( 'valid' === name || 'error' === name ) && oldValue !== newValue ) {
65+
if ( ( 'valid' === name || 'error' === name || 'suspense' === name ) && oldValue !== newValue ) {
5766
this.dispatchEvent( new CustomEvent( 'validate', { bubbles: true } ) );
5867
}
5968

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

7180
// Exit the function if validators are not available.
7281
if ( ! tpFormValidators ) {
73-
//Early return
82+
// Early return.
7483
return;
7584
}
7685

@@ -83,6 +92,17 @@ export class TPFormFieldElement extends HTMLElement {
8392
} else {
8493
this.removeErrorMessage();
8594
}
95+
96+
// Get the 'suspense' attribute value.
97+
const suspense: string = this.getAttribute( 'suspense' ) ?? '';
98+
99+
// Check if the suspense exists and has a corresponding suspense message function.
100+
if ( '' !== suspense && suspense in tpFormValidators && 'function' === typeof tpFormValidators[ suspense ].getSuspenseMessage ) {
101+
// @ts-ignore
102+
this.setSuspenseMessage( tpFormValidators[ suspense ]?.getSuspenseMessage( this ) );
103+
} else {
104+
this.removeSuspenseMessage();
105+
}
86106
}
87107

88108
/**
@@ -100,7 +120,7 @@ export class TPFormFieldElement extends HTMLElement {
100120
*
101121
* @return {boolean} Whether this field passed validation.
102122
*/
103-
validate(): boolean {
123+
async validate(): Promise<boolean> {
104124
// Retrieve tpFormValidators from the window object.
105125
const { tpFormValidators } = window;
106126

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

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

@@ -126,14 +147,64 @@ export class TPFormFieldElement extends HTMLElement {
126147
// Check if the attribute is a validator.
127148
if ( attributeName in tpFormValidators && 'function' === typeof tpFormValidators[ attributeName ].validate ) {
128149
// We found one, lets validate the field.
129-
const isValid: boolean = tpFormValidators[ attributeName ].validate( this );
150+
const isValid: boolean | Promise<boolean> = tpFormValidators[ attributeName ].validate( this );
151+
error = attributeName;
152+
153+
// First check for a Promise.
154+
if ( isValid instanceof Promise ) {
155+
// Yes it is an async validation.
156+
valid = false;
130157

131-
// Looks like we found an error!
132-
if ( false === isValid ) {
158+
// Dispatch a custom 'validation-suspense-start' event.
159+
this.dispatchEvent( new CustomEvent( 'validation-suspense-start' ) );
160+
161+
// Create the promise.
162+
suspense = new Promise( ( resolve, reject ): void => {
163+
// Validate it.
164+
isValid
165+
.then( ( suspenseIsValid: boolean ) => {
166+
// Validation is complete.
167+
if ( true === suspenseIsValid ) {
168+
this.setAttribute( 'valid', 'yes' );
169+
this.removeAttribute( 'error' );
170+
171+
// Resolve the promise.
172+
resolve( true );
173+
} else {
174+
this.removeAttribute( 'valid' );
175+
this.setAttribute( 'error', error );
176+
177+
// Resolve the promise.
178+
resolve( false );
179+
}
180+
181+
// Dispatch a custom 'validation-suspense-success' event.
182+
this.dispatchEvent( new CustomEvent( 'validation-suspense-success' ) );
183+
} )
184+
.catch( (): void => {
185+
// There was an error.
186+
this.removeAttribute( 'valid' );
187+
this.setAttribute( 'error', error );
188+
189+
// Dispatch a custom 'validation-suspense-error' event.
190+
this.dispatchEvent( new CustomEvent( 'validation-suspense-error' ) );
191+
192+
// Reject the promise.
193+
reject( false );
194+
} )
195+
.finally( (): void => {
196+
// Clean up.
197+
this.removeAttribute( 'suspense' );
198+
} );
199+
} );
200+
201+
// Return.
202+
return false;
203+
} else if ( false === isValid ) {
204+
// Not a Promise, but looks like we found an error!
133205
valid = false;
134-
error = attributeName;
135206

136-
// return false;
207+
// Return.
137208
return false;
138209
}
139210
}
@@ -146,12 +217,27 @@ export class TPFormFieldElement extends HTMLElement {
146217
if ( valid ) {
147218
this.setAttribute( 'valid', 'yes' );
148219
this.removeAttribute( 'error' );
220+
this.removeAttribute( 'suspense' );
149221
} else {
150222
this.removeAttribute( 'valid' );
151-
this.setAttribute( 'error', error );
223+
224+
// Check for suspense.
225+
if ( suspense ) {
226+
this.setAttribute( 'suspense', error );
227+
this.removeAttribute( 'error' );
228+
} else {
229+
this.removeAttribute( 'suspense' );
230+
this.setAttribute( 'error', error );
231+
}
152232
}
153233

154-
// Return validity.
234+
// Do we have a suspense?
235+
if ( suspense ) {
236+
// Yes we do, return the promise.
237+
return suspense;
238+
}
239+
240+
// No we don't, return a resolved promise.
155241
return valid;
156242
}
157243

@@ -187,4 +273,31 @@ export class TPFormFieldElement extends HTMLElement {
187273
// Dispatch a custom 'validation-success' event.
188274
this.dispatchEvent( new CustomEvent( 'validation-success' ) );
189275
}
276+
277+
/**
278+
* Set the suspense message.
279+
*
280+
* @param {string} message Suspense message.
281+
*/
282+
setSuspenseMessage( message: string = '' ): void {
283+
// Look for an existing tp-form-error element.
284+
const suspense: TPFormSuspenseElement | null = this.querySelector( 'tp-form-suspense' );
285+
286+
// If found, update its innerHTML with the suspense message. Otherwise, create a new tp-form-suspense element and append it to the component.
287+
if ( suspense ) {
288+
suspense.innerHTML = message;
289+
} else {
290+
const suspenseElement: TPFormSuspenseElement = document.createElement( 'tp-form-suspense' );
291+
suspenseElement.innerHTML = message;
292+
this.appendChild( suspenseElement );
293+
}
294+
}
295+
296+
/**
297+
* Remove the suspense message.
298+
*/
299+
removeSuspenseMessage(): void {
300+
// Find and remove the tp-form-suspense element.
301+
this.querySelector( 'tp-form-suspense' )?.remove();
302+
}
190303
}

src/form/tp-form-suspense.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* TP Form Suspense.
3+
*/
4+
export class TPFormSuspenseElement extends HTMLElement {
5+
}

0 commit comments

Comments
 (0)