From 56c9d51b0ef17dcbe2ee36001f20e8b466e89eff Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Sat, 18 Nov 2023 01:08:02 +1100 Subject: [PATCH 1/8] form components first commit --- src/form/definitions.ts | 16 +++++ src/form/index.html | 65 ++++++++++++++++++ src/form/index.ts | 38 +++++++++++ src/form/style.scss | 0 src/form/tp-form-error.ts | 5 ++ src/form/tp-form-field.ts | 109 ++++++++++++++++++++++++++++++ src/form/tp-form.ts | 55 +++++++++++++++ src/form/validators/min-length.ts | 15 ++++ src/form/validators/required.ts | 12 ++++ webpack.config.js | 1 + 10 files changed, 316 insertions(+) create mode 100644 src/form/definitions.ts create mode 100644 src/form/index.html create mode 100644 src/form/index.ts create mode 100644 src/form/style.scss create mode 100644 src/form/tp-form-error.ts create mode 100644 src/form/tp-form-field.ts create mode 100644 src/form/tp-form.ts create mode 100644 src/form/validators/min-length.ts create mode 100644 src/form/validators/required.ts diff --git a/src/form/definitions.ts b/src/form/definitions.ts new file mode 100644 index 0000000..7dd613f --- /dev/null +++ b/src/form/definitions.ts @@ -0,0 +1,16 @@ +import { TPFormFieldElement } from './tp-form-field'; + +export interface TPFormValidator { + [ key: string ]: { ( field: TPFormFieldElement ): boolean }; +} + +export interface TPFormError { + [ key: string ]: string; +} + +declare global { + interface Window { + tpFormValidators: TPFormValidator; + tpFormErrors: TPFormError; + } +} diff --git a/src/form/index.html b/src/form/index.html new file mode 100644 index 0000000..250e295 --- /dev/null +++ b/src/form/index.html @@ -0,0 +1,65 @@ + + + + + + + Web Component: Form + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + diff --git a/src/form/index.ts b/src/form/index.ts new file mode 100644 index 0000000..71a5369 --- /dev/null +++ b/src/form/index.ts @@ -0,0 +1,38 @@ +/** + * Styles. + */ +import './style.scss'; + +/** + * Validators. + */ +import { RequiredValidator, RequiredValidatorError } from './validators/required'; +import { MinLengthValidator, MinLengthValidatorError } from './validators/min-length'; + +/** + * Register Validators and Errors. + */ +window.tpFormValidators = { + ...RequiredValidator, + ...MinLengthValidator, +}; + +window.tpFormErrors = { + ...RequiredValidatorError, + ...MinLengthValidatorError, +}; + +/** + * Components. + */ +import { TPFormElement } from './tp-form'; +import { TPFormFieldElement } from './tp-form-field'; +import { TPFormErrorElement } from './tp-form-error'; + +/** + * Register Components. + */ +customElements.define( 'tp-form', TPFormElement ); +customElements.define( 'tp-form-field', TPFormFieldElement ); +customElements.define( 'tp-form-error', TPFormErrorElement ); + diff --git a/src/form/style.scss b/src/form/style.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/form/tp-form-error.ts b/src/form/tp-form-error.ts new file mode 100644 index 0000000..917f650 --- /dev/null +++ b/src/form/tp-form-error.ts @@ -0,0 +1,5 @@ +/** + * TP Form Error. + */ +export class TPFormErrorElement extends HTMLElement { +} diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts new file mode 100644 index 0000000..5cca753 --- /dev/null +++ b/src/form/tp-form-field.ts @@ -0,0 +1,109 @@ +import { TPFormErrorElement } from './tp-form-error'; + +/** + * TP Form Field. + */ +export class TPFormFieldElement extends HTMLElement { + connectedCallback(): void { + const field = this.getField(); + field?.addEventListener( 'keyup', this.handleFieldChanged.bind( this ) ); + field?.addEventListener( 'change', this.handleFieldChanged.bind( this ) ); + } + + handleFieldChanged(): void { + if ( this.getAttribute( 'valid' ) || this.getAttribute( 'error' ) ) { + this.validate(); + } + } + + /** + * Get observed attributes. + * + * @return {Array} List of observed attributes. + */ + static get observedAttributes(): string[] { + return [ 'valid', 'error' ]; + } + + /** + * Attribute changed callback. + * + * @param {string} name Attribute name. + * @param {string} oldValue Old value. + * @param {string} newValue New value. + */ + attributeChangedCallback( name: string = '', oldValue: string = '', newValue: string = '' ): void { + if ( ( 'valid' === name || 'error' === name ) && oldValue !== newValue ) { + this.dispatchEvent( new CustomEvent( 'validate', { bubbles: true } ) ); + } + this.update(); + } + + update(): void { + const { tpFormErrors } = window; + if ( ! tpFormErrors ) { + return; + } + + const error: string = this.getAttribute( 'error' ) ?? ''; + if ( '' !== error && error in tpFormErrors && 'string' === typeof tpFormErrors[ error ] ) { + this.setErrorMessage( tpFormErrors[ error ] ); + } else { + this.removeErrorMessage(); + } + } + + getField(): HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null { + return this.querySelector( 'input,select,textarea' ); + } + + validate(): boolean { + const { tpFormValidators } = window; + if ( ! tpFormValidators ) { + return true; + } + + let valid: boolean = true; + let error: string = ''; + const allAttributes: string[] = this.getAttributeNames(); + + allAttributes.every( ( attributeName: string ): boolean => { + if ( attributeName in tpFormValidators && 'function' === typeof tpFormValidators[ attributeName ] ) { + const isValid: boolean = tpFormValidators[ attributeName ]( this ); + + if ( false === isValid ) { + valid = false; + error = attributeName; + return false; + } + } + + return true; + } ); + + if ( valid ) { + this.setAttribute( 'valid', 'yes' ); + this.removeAttribute( 'error' ); + } else { + this.removeAttribute( 'valid' ); + this.setAttribute( 'error', error ); + } + + return valid; + } + + setErrorMessage( message: string = '' ): void { + let error: TPFormErrorElement | null = this.querySelector( 'tp-form-error' ); + if ( error ) { + error.innerHTML = message; + } else { + const errorElement: TPFormErrorElement = document.createElement( 'tp-form-error' ); + errorElement.innerHTML = message; + this.appendChild( errorElement ); + } + } + + removeErrorMessage(): void { + this.querySelector( 'tp-form-error' )?.remove(); + } +} diff --git a/src/form/tp-form.ts b/src/form/tp-form.ts new file mode 100644 index 0000000..c12949e --- /dev/null +++ b/src/form/tp-form.ts @@ -0,0 +1,55 @@ +import { TPFormFieldElement } from './tp-form-field'; + +/** + * TP Form. + */ +export class TPFormElement extends HTMLElement { + /** + * Properties. + */ + protected readonly form: HTMLFormElement | null; + + constructor() { + super(); + this.form = this.querySelector( 'form' ); + } + + connectedCallback(): void { + this.form?.addEventListener( 'submit', this.handleFormSubmit.bind( this ) ); + } + + protected handleFormSubmit( e: SubmitEvent ): void { + const formValid: boolean = this.validate(); + if ( ! formValid || 'yes' === this.getAttribute( 'prevent-submit' ) ) { + e.preventDefault(); + } + } + + validate(): boolean { + const fields: NodeListOf | null = this.querySelectorAll( 'tp-form-field' ); + if ( ! fields ) { + return true; + } + + let formValid: boolean = true; + fields.forEach( ( field: TPFormFieldElement ): void => { + if ( ! field.validate() ) { + formValid = false; + } + } ); + + return formValid; + } + + resetValidation(): void { + const fields: NodeListOf | null = this.querySelectorAll( 'tp-form-field' ); + if ( ! fields ) { + return; + } + + fields.forEach( ( field: TPFormFieldElement ): void => { + field.removeAttribute( 'valid' ); + field.removeAttribute( 'error' ); + } ); + } +} diff --git a/src/form/validators/min-length.ts b/src/form/validators/min-length.ts new file mode 100644 index 0000000..d14081d --- /dev/null +++ b/src/form/validators/min-length.ts @@ -0,0 +1,15 @@ +import { TPFormFieldElement } from '../tp-form-field'; +import { TPFormError, TPFormValidator } from '../definitions'; + +export const MinLengthValidator: TPFormValidator = { + 'min-length': ( field: TPFormFieldElement ): boolean => { + const minLength: number = parseInt( field.getAttribute( 'min-length' ) ?? '0' ); + const value: string = field.getField()?.value ?? ''; + + return '' === value || value.length > minLength; + }, +}; + +export const MinLengthValidatorError: TPFormError = { + 'min-length': 'Must not be less than %1 characters', +}; diff --git a/src/form/validators/required.ts b/src/form/validators/required.ts new file mode 100644 index 0000000..94940c2 --- /dev/null +++ b/src/form/validators/required.ts @@ -0,0 +1,12 @@ +import { TPFormFieldElement } from '../tp-form-field'; +import { TPFormError, TPFormValidator } from '../definitions'; + +export const RequiredValidator: TPFormValidator = { + required: ( field: TPFormFieldElement ): boolean => { + return '' !== field.getField()?.value ?? ''; + }, +}; + +export const RequiredValidatorError: TPFormError = { + required: 'This field is required', +} diff --git a/webpack.config.js b/webpack.config.js index 53e0545..cbf4f32 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -78,6 +78,7 @@ module.exports = ( env ) => { entry: { modal: './src/modal/index.ts', slider: './src/slider/index.ts', + form: './src/form/index.ts', }, module: { rules: [ From a6c2fc25ea8b3c683d348cfebcd4d0e5223c0f17 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Sat, 18 Nov 2023 01:32:25 +1100 Subject: [PATCH 2/8] add max length --- src/form/definitions.ts | 5 ++++- src/form/index.html | 2 +- src/form/index.ts | 3 +++ src/form/tp-form-field.ts | 12 ++++++------ src/form/utility.ts | 12 ++++++++++++ src/form/validators/max-length.ts | 24 ++++++++++++++++++++++++ src/form/validators/min-length.ts | 19 ++++++++++++++----- src/form/validators/required.ts | 8 ++++++-- 8 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 src/form/utility.ts create mode 100644 src/form/validators/max-length.ts diff --git a/src/form/definitions.ts b/src/form/definitions.ts index 7dd613f..c6543c8 100644 --- a/src/form/definitions.ts +++ b/src/form/definitions.ts @@ -1,7 +1,10 @@ import { TPFormFieldElement } from './tp-form-field'; export interface TPFormValidator { - [ key: string ]: { ( field: TPFormFieldElement ): boolean }; + [ key: string ]: { + validate: { ( field: TPFormFieldElement ): boolean }; + getErrorMessage: { ( field: TPFormFieldElement ): string }; + } } export interface TPFormError { diff --git a/src/form/index.html b/src/form/index.html index 250e295..eb48c1e 100644 --- a/src/form/index.html +++ b/src/form/index.html @@ -51,7 +51,7 @@ - + diff --git a/src/form/index.ts b/src/form/index.ts index 71a5369..63623fb 100644 --- a/src/form/index.ts +++ b/src/form/index.ts @@ -8,6 +8,7 @@ import './style.scss'; */ import { RequiredValidator, RequiredValidatorError } from './validators/required'; import { MinLengthValidator, MinLengthValidatorError } from './validators/min-length'; +import { MaxLengthValidator, MaxLengthValidatorError } from './validators/max-length'; /** * Register Validators and Errors. @@ -15,11 +16,13 @@ import { MinLengthValidator, MinLengthValidatorError } from './validators/min-le window.tpFormValidators = { ...RequiredValidator, ...MinLengthValidator, + ...MaxLengthValidator, }; window.tpFormErrors = { ...RequiredValidatorError, ...MinLengthValidatorError, + ...MaxLengthValidatorError, }; /** diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts index 5cca753..b83b9b4 100644 --- a/src/form/tp-form-field.ts +++ b/src/form/tp-form-field.ts @@ -40,14 +40,14 @@ export class TPFormFieldElement extends HTMLElement { } update(): void { - const { tpFormErrors } = window; - if ( ! tpFormErrors ) { + const { tpFormValidators } = window; + if ( ! tpFormValidators ) { return; } const error: string = this.getAttribute( 'error' ) ?? ''; - if ( '' !== error && error in tpFormErrors && 'string' === typeof tpFormErrors[ error ] ) { - this.setErrorMessage( tpFormErrors[ error ] ); + if ( '' !== error && error in tpFormValidators && 'function' === typeof tpFormValidators[ error ]['getErrorMessage'] ) { + this.setErrorMessage( tpFormValidators[ error ]['getErrorMessage']( this ) ); } else { this.removeErrorMessage(); } @@ -68,8 +68,8 @@ export class TPFormFieldElement extends HTMLElement { const allAttributes: string[] = this.getAttributeNames(); allAttributes.every( ( attributeName: string ): boolean => { - if ( attributeName in tpFormValidators && 'function' === typeof tpFormValidators[ attributeName ] ) { - const isValid: boolean = tpFormValidators[ attributeName ]( this ); + if ( attributeName in tpFormValidators && 'function' === typeof tpFormValidators[ attributeName ]['validate'] ) { + const isValid: boolean = tpFormValidators[ attributeName ]['validate']( this ); if ( false === isValid ) { valid = false; diff --git a/src/form/utility.ts b/src/form/utility.ts new file mode 100644 index 0000000..27a3381 --- /dev/null +++ b/src/form/utility.ts @@ -0,0 +1,12 @@ +export const getErrorMessage = ( error: string = '' ): string => { + const { tpFormErrors } = window; + if ( ! tpFormErrors ) { + return ''; + } + + if ( '' !== error && error in tpFormErrors && 'string' === typeof tpFormErrors[ error ] ) { + return tpFormErrors[ error ]; + } + + return ''; +}; diff --git a/src/form/validators/max-length.ts b/src/form/validators/max-length.ts new file mode 100644 index 0000000..63ec620 --- /dev/null +++ b/src/form/validators/max-length.ts @@ -0,0 +1,24 @@ +import { TPFormFieldElement } from '../tp-form-field'; +import { TPFormError, TPFormValidator } from '../definitions'; +import { getErrorMessage } from '../utility'; + +export const MaxLengthValidator: TPFormValidator = { + 'max-length': { + validate: ( field: TPFormFieldElement ): boolean => { + const minLength: number = parseInt( field.getAttribute( 'max-length' ) ?? '0' ); + const value: string = field.getField()?.value ?? ''; + + return '' === value || value.length <= minLength; + }, + getErrorMessage: ( field: TPFormFieldElement ): string => { + const errorMessage: string = getErrorMessage( 'max-length' ); + const minLength: string = field.getAttribute( 'max-length' ) ?? ''; + + return errorMessage.replace( '%1', minLength ); + }, + }, +}; + +export const MaxLengthValidatorError: TPFormError = { + 'max-length': 'Must be less than %1 characters', +}; diff --git a/src/form/validators/min-length.ts b/src/form/validators/min-length.ts index d14081d..41de10c 100644 --- a/src/form/validators/min-length.ts +++ b/src/form/validators/min-length.ts @@ -1,15 +1,24 @@ import { TPFormFieldElement } from '../tp-form-field'; import { TPFormError, TPFormValidator } from '../definitions'; +import { getErrorMessage } from '../utility'; export const MinLengthValidator: TPFormValidator = { - 'min-length': ( field: TPFormFieldElement ): boolean => { - const minLength: number = parseInt( field.getAttribute( 'min-length' ) ?? '0' ); - const value: string = field.getField()?.value ?? ''; + 'min-length': { + validate: ( field: TPFormFieldElement ): boolean => { + const minLength: number = parseInt( field.getAttribute( 'min-length' ) ?? '0' ); + const value: string = field.getField()?.value ?? ''; - return '' === value || value.length > minLength; + return '' === value || value.length >= minLength; + }, + getErrorMessage: ( field: TPFormFieldElement ): string => { + const errorMessage: string = getErrorMessage( 'min-length' ); + const minLength: string = field.getAttribute( 'min-length' ) ?? ''; + + return errorMessage.replace( '%1', minLength ); + }, }, }; export const MinLengthValidatorError: TPFormError = { - 'min-length': 'Must not be less than %1 characters', + 'min-length': 'Must be at least %1 characters', }; diff --git a/src/form/validators/required.ts b/src/form/validators/required.ts index 94940c2..3094b1a 100644 --- a/src/form/validators/required.ts +++ b/src/form/validators/required.ts @@ -1,9 +1,13 @@ import { TPFormFieldElement } from '../tp-form-field'; import { TPFormError, TPFormValidator } from '../definitions'; +import { getErrorMessage } from '../utility'; export const RequiredValidator: TPFormValidator = { - required: ( field: TPFormFieldElement ): boolean => { - return '' !== field.getField()?.value ?? ''; + required: { + validate: ( field: TPFormFieldElement ): boolean => { + return '' !== field.getField()?.value ?? ''; + }, + getErrorMessage: (): string => getErrorMessage( 'required' ), }, }; From cbb03f7769ffc2ff7bdc004108f4da1534b04615 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Sat, 18 Nov 2023 02:06:33 +1100 Subject: [PATCH 3/8] add events --- src/form/{definitions.ts => definitions.d.ts} | 0 src/form/tp-form-field.ts | 3 +++ src/form/tp-form.ts | 9 +++++++++ webpack.config.js | 2 +- 4 files changed, 13 insertions(+), 1 deletion(-) rename src/form/{definitions.ts => definitions.d.ts} (100%) diff --git a/src/form/definitions.ts b/src/form/definitions.d.ts similarity index 100% rename from src/form/definitions.ts rename to src/form/definitions.d.ts diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts index b83b9b4..319a602 100644 --- a/src/form/tp-form-field.ts +++ b/src/form/tp-form-field.ts @@ -101,9 +101,12 @@ export class TPFormFieldElement extends HTMLElement { errorElement.innerHTML = message; this.appendChild( errorElement ); } + + this.dispatchEvent( new CustomEvent( 'validation-error', { bubbles: true } ) ); } removeErrorMessage(): void { this.querySelector( 'tp-form-error' )?.remove(); + this.dispatchEvent( new CustomEvent( 'validation-success', { bubbles: true } ) ); } } diff --git a/src/form/tp-form.ts b/src/form/tp-form.ts index c12949e..534ebcb 100644 --- a/src/form/tp-form.ts +++ b/src/form/tp-form.ts @@ -26,8 +26,11 @@ export class TPFormElement extends HTMLElement { } validate(): boolean { + this.dispatchEvent( new CustomEvent( 'validate', { bubbles: true } ) ); + const fields: NodeListOf | null = this.querySelectorAll( 'tp-form-field' ); if ( ! fields ) { + this.dispatchEvent( new CustomEvent( 'validation-success', { bubbles: true } ) ); return true; } @@ -38,6 +41,12 @@ export class TPFormElement extends HTMLElement { } } ); + if ( formValid ) { + this.dispatchEvent( new CustomEvent( 'validation-success', { bubbles: true } ) ); + } else { + this.dispatchEvent( new CustomEvent( 'validation-error', { bubbles: true } ) ); + } + return formValid; } diff --git a/webpack.config.js b/webpack.config.js index cbf4f32..e738954 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,7 +26,7 @@ class DeclarationBundlerPlugin { const declarationFiles = {}; for ( const filename in assets ) { if ( filename.endsWith( '.d.ts' ) ) { - if ( ! filename.includes( 'index.d.ts' ) ) { + if ( filename.includes( 'tp-' ) ) { declarationFiles[ filename ] = assets[ filename ]; } delete assets[ filename ]; From 258e81db9eb80df57d352b70373b599bebd689f4 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Sat, 18 Nov 2023 02:33:54 +1100 Subject: [PATCH 4/8] refactor validators --- src/form/definitions.d.ts | 18 ++++++++---------- src/form/index.ts | 31 ++++++++++++++++++------------- src/form/validators/max-length.ts | 30 +++++++++++++----------------- src/form/validators/min-length.ts | 30 +++++++++++++----------------- src/form/validators/required.ts | 18 +++++++----------- 5 files changed, 59 insertions(+), 68 deletions(-) diff --git a/src/form/definitions.d.ts b/src/form/definitions.d.ts index c6543c8..7c8b576 100644 --- a/src/form/definitions.d.ts +++ b/src/form/definitions.d.ts @@ -1,19 +1,17 @@ import { TPFormFieldElement } from './tp-form-field'; export interface TPFormValidator { - [ key: string ]: { - validate: { ( field: TPFormFieldElement ): boolean }; - getErrorMessage: { ( field: TPFormFieldElement ): string }; - } -} - -export interface TPFormError { - [ key: string ]: string; + validate: { ( field: TPFormFieldElement ): boolean }; + getErrorMessage: { ( field: TPFormFieldElement ): string }; } declare global { interface Window { - tpFormValidators: TPFormValidator; - tpFormErrors: TPFormError; + tpFormValidators: { + [ key: string ]: TPFormValidator; + } + tpFormErrors: { + [ key: string ]: string; + }; } } diff --git a/src/form/index.ts b/src/form/index.ts index 63623fb..4623d60 100644 --- a/src/form/index.ts +++ b/src/form/index.ts @@ -6,24 +6,29 @@ import './style.scss'; /** * Validators. */ -import { RequiredValidator, RequiredValidatorError } from './validators/required'; -import { MinLengthValidator, MinLengthValidatorError } from './validators/min-length'; -import { MaxLengthValidator, MaxLengthValidatorError } from './validators/max-length'; +import { TPFormValidator } from './definitions'; +import * as required from './validators/required'; +import * as minLength from './validators/min-length'; +import * as maxLength from './validators/max-length'; + +const validators = [ + required, + minLength, + maxLength, +]; /** * Register Validators and Errors. */ -window.tpFormValidators = { - ...RequiredValidator, - ...MinLengthValidator, - ...MaxLengthValidator, -}; +window.tpFormValidators = {}; +window.tpFormErrors = {}; -window.tpFormErrors = { - ...RequiredValidatorError, - ...MinLengthValidatorError, - ...MaxLengthValidatorError, -}; +validators.forEach( ( + { name, validator, errorMessage }: { name: string, validator: TPFormValidator, errorMessage: string } +): void => { + window.tpFormValidators[ name ] = validator; + window.tpFormErrors[ name ] = errorMessage; +} ); /** * Components. diff --git a/src/form/validators/max-length.ts b/src/form/validators/max-length.ts index 63ec620..505a04e 100644 --- a/src/form/validators/max-length.ts +++ b/src/form/validators/max-length.ts @@ -1,24 +1,20 @@ import { TPFormFieldElement } from '../tp-form-field'; -import { TPFormError, TPFormValidator } from '../definitions'; +import { TPFormValidator } from '../definitions'; import { getErrorMessage } from '../utility'; -export const MaxLengthValidator: TPFormValidator = { - 'max-length': { - validate: ( field: TPFormFieldElement ): boolean => { - const minLength: number = parseInt( field.getAttribute( 'max-length' ) ?? '0' ); - const value: string = field.getField()?.value ?? ''; +export const name: string = 'max-length'; +export const errorMessage: string = 'Must be less than %1 characters'; +export const validator: TPFormValidator = { + validate: ( field: TPFormFieldElement ): boolean => { + const minLength: number = parseInt( field.getAttribute( 'max-length' ) ?? '0' ); + const value: string = field.getField()?.value ?? ''; - return '' === value || value.length <= minLength; - }, - getErrorMessage: ( field: TPFormFieldElement ): string => { - const errorMessage: string = getErrorMessage( 'max-length' ); - const minLength: string = field.getAttribute( 'max-length' ) ?? ''; - - return errorMessage.replace( '%1', minLength ); - }, + return '' === value || value.length <= minLength; }, -}; + getErrorMessage: ( field: TPFormFieldElement ): string => { + const errorMessage: string = getErrorMessage( 'max-length' ); + const minLength: string = field.getAttribute( 'max-length' ) ?? ''; -export const MaxLengthValidatorError: TPFormError = { - 'max-length': 'Must be less than %1 characters', + return errorMessage.replace( '%1', minLength ); + }, }; diff --git a/src/form/validators/min-length.ts b/src/form/validators/min-length.ts index 41de10c..6541f29 100644 --- a/src/form/validators/min-length.ts +++ b/src/form/validators/min-length.ts @@ -1,24 +1,20 @@ import { TPFormFieldElement } from '../tp-form-field'; -import { TPFormError, TPFormValidator } from '../definitions'; +import { TPFormValidator } from '../definitions'; import { getErrorMessage } from '../utility'; -export const MinLengthValidator: TPFormValidator = { - 'min-length': { - validate: ( field: TPFormFieldElement ): boolean => { - const minLength: number = parseInt( field.getAttribute( 'min-length' ) ?? '0' ); - const value: string = field.getField()?.value ?? ''; +export const name: string = 'min-length'; +export const errorMessage: string = 'Must be at least %1 characters'; +export const validator: TPFormValidator = { + validate: ( field: TPFormFieldElement ): boolean => { + const minLength: number = parseInt( field.getAttribute( 'min-length' ) ?? '0' ); + const value: string = field.getField()?.value ?? ''; - return '' === value || value.length >= minLength; - }, - getErrorMessage: ( field: TPFormFieldElement ): string => { - const errorMessage: string = getErrorMessage( 'min-length' ); - const minLength: string = field.getAttribute( 'min-length' ) ?? ''; - - return errorMessage.replace( '%1', minLength ); - }, + return '' === value || value.length >= minLength; }, -}; + getErrorMessage: ( field: TPFormFieldElement ): string => { + const errorMessage: string = getErrorMessage( 'min-length' ); + const minLength: string = field.getAttribute( 'min-length' ) ?? ''; -export const MinLengthValidatorError: TPFormError = { - 'min-length': 'Must be at least %1 characters', + return errorMessage.replace( '%1', minLength ); + }, }; diff --git a/src/form/validators/required.ts b/src/form/validators/required.ts index 3094b1a..f510600 100644 --- a/src/form/validators/required.ts +++ b/src/form/validators/required.ts @@ -1,16 +1,12 @@ import { TPFormFieldElement } from '../tp-form-field'; -import { TPFormError, TPFormValidator } from '../definitions'; +import { TPFormValidator } from '../definitions'; import { getErrorMessage } from '../utility'; -export const RequiredValidator: TPFormValidator = { - required: { - validate: ( field: TPFormFieldElement ): boolean => { - return '' !== field.getField()?.value ?? ''; - }, - getErrorMessage: (): string => getErrorMessage( 'required' ), +export const name: string = 'required'; +export const errorMessage: string = 'This field is required'; +export const validator: TPFormValidator = { + validate: ( field: TPFormFieldElement ): boolean => { + return '' !== field.getField()?.value ?? ''; }, + getErrorMessage: (): string => getErrorMessage( 'required' ), }; - -export const RequiredValidatorError: TPFormError = { - required: 'This field is required', -} From 3463b46cdb017b07df25c626f89f3492e2d1a011 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Sat, 18 Nov 2023 02:47:35 +1100 Subject: [PATCH 5/8] add comments --- src/form/definitions.d.ts | 9 +++++++ src/form/tp-form-field.ts | 45 +++++++++++++++++++++++++++---- src/form/tp-form.ts | 22 +++++++++++++++ src/form/utility.ts | 7 +++++ src/form/validators/max-length.ts | 18 +++++++++++-- src/form/validators/min-length.ts | 18 +++++++++++-- src/form/validators/required.ts | 14 ++++++++++ 7 files changed, 124 insertions(+), 9 deletions(-) diff --git a/src/form/definitions.d.ts b/src/form/definitions.d.ts index 7c8b576..34bee29 100644 --- a/src/form/definitions.d.ts +++ b/src/form/definitions.d.ts @@ -1,10 +1,19 @@ +/** + * Internal dependencies. + */ import { TPFormFieldElement } from './tp-form-field'; +/** + * Form Validator. + */ export interface TPFormValidator { validate: { ( field: TPFormFieldElement ): boolean }; getErrorMessage: { ( field: TPFormFieldElement ): string }; } +/** + * Window. + */ declare global { interface Window { tpFormValidators: { diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts index 319a602..5a1f8fc 100644 --- a/src/form/tp-form-field.ts +++ b/src/form/tp-form-field.ts @@ -4,12 +4,18 @@ import { TPFormErrorElement } from './tp-form-error'; * TP Form Field. */ export class TPFormFieldElement extends HTMLElement { + /** + * Connected callback. + */ connectedCallback(): void { const field = this.getField(); field?.addEventListener( 'keyup', this.handleFieldChanged.bind( this ) ); field?.addEventListener( 'change', this.handleFieldChanged.bind( this ) ); } + /** + * Update validation when the field has changed. + */ handleFieldChanged(): void { if ( this.getAttribute( 'valid' ) || this.getAttribute( 'error' ) ) { this.validate(); @@ -39,6 +45,9 @@ export class TPFormFieldElement extends HTMLElement { this.update(); } + /** + * Update component. + */ update(): void { const { tpFormValidators } = window; if ( ! tpFormValidators ) { @@ -46,31 +55,46 @@ export class TPFormFieldElement extends HTMLElement { } const error: string = this.getAttribute( 'error' ) ?? ''; - if ( '' !== error && error in tpFormValidators && 'function' === typeof tpFormValidators[ error ]['getErrorMessage'] ) { - this.setErrorMessage( tpFormValidators[ error ]['getErrorMessage']( this ) ); + if ( '' !== error && error in tpFormValidators && 'function' === typeof tpFormValidators[ error ].getErrorMessage ) { + this.setErrorMessage( tpFormValidators[ error ].getErrorMessage( this ) ); } else { this.removeErrorMessage(); } } + /** + * Get the associated field. + * + * @return {HTMLElement} The associated field for this component. + */ getField(): HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null { return this.querySelector( 'input,select,textarea' ); } + /** + * Validate this field. + * + * @return {boolean} Whether this field passed validation. + */ validate(): boolean { + // Look for validators. const { tpFormValidators } = window; if ( ! tpFormValidators ) { return true; } + // Prepare error and valid status. let valid: boolean = true; let error: string = ''; const allAttributes: string[] = this.getAttributeNames(); + // Traverse all attributes to see if we find a matching validator. allAttributes.every( ( attributeName: string ): boolean => { - if ( attributeName in tpFormValidators && 'function' === typeof tpFormValidators[ attributeName ]['validate'] ) { - const isValid: boolean = tpFormValidators[ attributeName ]['validate']( this ); + if ( attributeName in tpFormValidators && 'function' === typeof tpFormValidators[ attributeName ].validate ) { + // We found one, lets validate the field. + const isValid: boolean = tpFormValidators[ attributeName ].validate( this ); + // Looks like we found an error! if ( false === isValid ) { valid = false; error = attributeName; @@ -78,9 +102,11 @@ export class TPFormFieldElement extends HTMLElement { } } + // No error found, all good. return true; } ); + // Check if the field is valid or not. if ( valid ) { this.setAttribute( 'valid', 'yes' ); this.removeAttribute( 'error' ); @@ -89,11 +115,17 @@ export class TPFormFieldElement extends HTMLElement { this.setAttribute( 'error', error ); } + // Return validity. return valid; } + /** + * Set the error message. + * + * @param {string} message Error message. + */ setErrorMessage( message: string = '' ): void { - let error: TPFormErrorElement | null = this.querySelector( 'tp-form-error' ); + const error: TPFormErrorElement | null = this.querySelector( 'tp-form-error' ); if ( error ) { error.innerHTML = message; } else { @@ -105,6 +137,9 @@ export class TPFormFieldElement extends HTMLElement { this.dispatchEvent( new CustomEvent( 'validation-error', { bubbles: true } ) ); } + /** + * Remove the error message. + */ removeErrorMessage(): void { this.querySelector( 'tp-form-error' )?.remove(); this.dispatchEvent( new CustomEvent( 'validation-success', { bubbles: true } ) ); diff --git a/src/form/tp-form.ts b/src/form/tp-form.ts index 534ebcb..4a901ab 100644 --- a/src/form/tp-form.ts +++ b/src/form/tp-form.ts @@ -1,3 +1,6 @@ +/** + * Internal dependencies. + */ import { TPFormFieldElement } from './tp-form-field'; /** @@ -9,15 +12,26 @@ export class TPFormElement extends HTMLElement { */ protected readonly form: HTMLFormElement | null; + /** + * Constructor. + */ constructor() { super(); this.form = this.querySelector( 'form' ); } + /** + * Connected callback. + */ connectedCallback(): void { this.form?.addEventListener( 'submit', this.handleFormSubmit.bind( this ) ); } + /** + * Handle form submission. + * + * @param {Event} e Submit event. + */ protected handleFormSubmit( e: SubmitEvent ): void { const formValid: boolean = this.validate(); if ( ! formValid || 'yes' === this.getAttribute( 'prevent-submit' ) ) { @@ -25,6 +39,11 @@ export class TPFormElement extends HTMLElement { } } + /** + * Validate the form. + * + * @return {boolean} Whether the form is valid or not. + */ validate(): boolean { this.dispatchEvent( new CustomEvent( 'validate', { bubbles: true } ) ); @@ -50,6 +69,9 @@ export class TPFormElement extends HTMLElement { return formValid; } + /** + * Reset form validation. + */ resetValidation(): void { const fields: NodeListOf | null = this.querySelectorAll( 'tp-form-field' ); if ( ! fields ) { diff --git a/src/form/utility.ts b/src/form/utility.ts index 27a3381..7e367a8 100644 --- a/src/form/utility.ts +++ b/src/form/utility.ts @@ -1,3 +1,10 @@ +/** + * Get the error message based on its code. + * + * @param {string} error Error code. + * + * @return {string} The error message. + */ export const getErrorMessage = ( error: string = '' ): string => { const { tpFormErrors } = window; if ( ! tpFormErrors ) { diff --git a/src/form/validators/max-length.ts b/src/form/validators/max-length.ts index 505a04e..0fcbbb4 100644 --- a/src/form/validators/max-length.ts +++ b/src/form/validators/max-length.ts @@ -1,9 +1,23 @@ +/** + * Internal dependencies. + */ import { TPFormFieldElement } from '../tp-form-field'; import { TPFormValidator } from '../definitions'; import { getErrorMessage } from '../utility'; +/** + * Name. + */ export const name: string = 'max-length'; + +/** + * Error message. + */ export const errorMessage: string = 'Must be less than %1 characters'; + +/** + * Validator. + */ export const validator: TPFormValidator = { validate: ( field: TPFormFieldElement ): boolean => { const minLength: number = parseInt( field.getAttribute( 'max-length' ) ?? '0' ); @@ -12,9 +26,9 @@ export const validator: TPFormValidator = { return '' === value || value.length <= minLength; }, getErrorMessage: ( field: TPFormFieldElement ): string => { - const errorMessage: string = getErrorMessage( 'max-length' ); + const error: string = getErrorMessage( 'max-length' ); const minLength: string = field.getAttribute( 'max-length' ) ?? ''; - return errorMessage.replace( '%1', minLength ); + return error.replace( '%1', minLength ); }, }; diff --git a/src/form/validators/min-length.ts b/src/form/validators/min-length.ts index 6541f29..9d20739 100644 --- a/src/form/validators/min-length.ts +++ b/src/form/validators/min-length.ts @@ -1,9 +1,23 @@ +/** + * Internal dependencies. + */ import { TPFormFieldElement } from '../tp-form-field'; import { TPFormValidator } from '../definitions'; import { getErrorMessage } from '../utility'; +/** + * Name. + */ export const name: string = 'min-length'; + +/** + * Error message. + */ export const errorMessage: string = 'Must be at least %1 characters'; + +/** + * Validator. + */ export const validator: TPFormValidator = { validate: ( field: TPFormFieldElement ): boolean => { const minLength: number = parseInt( field.getAttribute( 'min-length' ) ?? '0' ); @@ -12,9 +26,9 @@ export const validator: TPFormValidator = { return '' === value || value.length >= minLength; }, getErrorMessage: ( field: TPFormFieldElement ): string => { - const errorMessage: string = getErrorMessage( 'min-length' ); + const error: string = getErrorMessage( 'min-length' ); const minLength: string = field.getAttribute( 'min-length' ) ?? ''; - return errorMessage.replace( '%1', minLength ); + return error.replace( '%1', minLength ); }, }; diff --git a/src/form/validators/required.ts b/src/form/validators/required.ts index f510600..9e169fa 100644 --- a/src/form/validators/required.ts +++ b/src/form/validators/required.ts @@ -1,9 +1,23 @@ +/** + * Internal dependencies. + */ import { TPFormFieldElement } from '../tp-form-field'; import { TPFormValidator } from '../definitions'; import { getErrorMessage } from '../utility'; +/** + * Name. + */ export const name: string = 'required'; + +/** + * Error message. + */ export const errorMessage: string = 'This field is required'; + +/** + * Validator. + */ export const validator: TPFormValidator = { validate: ( field: TPFormFieldElement ): boolean => { return '' !== field.getField()?.value ?? ''; From 0e9b6e837788765050fe1aae667d0532c36b5c78 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Sat, 18 Nov 2023 02:53:13 +1100 Subject: [PATCH 6/8] email validation --- src/form/index.ts | 2 ++ src/form/validators/email.ts | 26 ++++++++++++++++++++++++++ src/form/validators/max-length.ts | 6 +++--- src/form/validators/min-length.ts | 2 +- src/form/validators/required.ts | 2 +- 5 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/form/validators/email.ts diff --git a/src/form/index.ts b/src/form/index.ts index 4623d60..d6b5450 100644 --- a/src/form/index.ts +++ b/src/form/index.ts @@ -8,11 +8,13 @@ import './style.scss'; */ import { TPFormValidator } from './definitions'; import * as required from './validators/required'; +import * as email from './validators/email'; import * as minLength from './validators/min-length'; import * as maxLength from './validators/max-length'; const validators = [ required, + email, minLength, maxLength, ]; diff --git a/src/form/validators/email.ts b/src/form/validators/email.ts new file mode 100644 index 0000000..4d216f5 --- /dev/null +++ b/src/form/validators/email.ts @@ -0,0 +1,26 @@ +/** + * Internal dependencies. + */ +import { TPFormFieldElement } from '../tp-form-field'; +import { TPFormValidator } from '../definitions'; +import { getErrorMessage } from '../utility'; + +/** + * Name. + */ +export const name: string = 'email'; + +/** + * Error message. + */ +export const errorMessage: string = 'Please enter a valid email address'; + +/** + * Validator. + */ +export const validator: TPFormValidator = { + validate: ( field: TPFormFieldElement ): boolean => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( field.getField()?.value ?? '' ); + }, + getErrorMessage: (): string => getErrorMessage( name ), +}; diff --git a/src/form/validators/max-length.ts b/src/form/validators/max-length.ts index 0fcbbb4..6098d25 100644 --- a/src/form/validators/max-length.ts +++ b/src/form/validators/max-length.ts @@ -26,9 +26,9 @@ export const validator: TPFormValidator = { return '' === value || value.length <= minLength; }, getErrorMessage: ( field: TPFormFieldElement ): string => { - const error: string = getErrorMessage( 'max-length' ); - const minLength: string = field.getAttribute( 'max-length' ) ?? ''; + const error: string = getErrorMessage( name ); + const maxLength: string = field.getAttribute( 'max-length' ) ?? ''; - return error.replace( '%1', minLength ); + return error.replace( '%1', maxLength ); }, }; diff --git a/src/form/validators/min-length.ts b/src/form/validators/min-length.ts index 9d20739..a06b592 100644 --- a/src/form/validators/min-length.ts +++ b/src/form/validators/min-length.ts @@ -26,7 +26,7 @@ export const validator: TPFormValidator = { return '' === value || value.length >= minLength; }, getErrorMessage: ( field: TPFormFieldElement ): string => { - const error: string = getErrorMessage( 'min-length' ); + const error: string = getErrorMessage( name ); const minLength: string = field.getAttribute( 'min-length' ) ?? ''; return error.replace( '%1', minLength ); diff --git a/src/form/validators/required.ts b/src/form/validators/required.ts index 9e169fa..9ee93eb 100644 --- a/src/form/validators/required.ts +++ b/src/form/validators/required.ts @@ -22,5 +22,5 @@ export const validator: TPFormValidator = { validate: ( field: TPFormFieldElement ): boolean => { return '' !== field.getField()?.value ?? ''; }, - getErrorMessage: (): string => getErrorMessage( 'required' ), + getErrorMessage: (): string => getErrorMessage( name ), }; From 976c7b52612672ea6936de16f3d510ba39108b70 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Mon, 20 Nov 2023 09:47:49 +1100 Subject: [PATCH 7/8] update submit button --- src/form/index.html | 4 +-- src/form/index.ts | 2 ++ src/form/tp-form-submit.ts | 53 ++++++++++++++++++++++++++++++++++++++ src/form/tp-form.ts | 13 ++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/form/tp-form-submit.ts diff --git a/src/form/index.html b/src/form/index.html index eb48c1e..f459034 100644 --- a/src/form/index.html +++ b/src/form/index.html @@ -55,9 +55,9 @@ - + - + diff --git a/src/form/index.ts b/src/form/index.ts index d6b5450..0a83b11 100644 --- a/src/form/index.ts +++ b/src/form/index.ts @@ -38,6 +38,7 @@ validators.forEach( ( import { TPFormElement } from './tp-form'; import { TPFormFieldElement } from './tp-form-field'; import { TPFormErrorElement } from './tp-form-error'; +import { TPFormSubmitElement } from './tp-form-submit'; /** * Register Components. @@ -45,4 +46,5 @@ import { TPFormErrorElement } from './tp-form-error'; customElements.define( 'tp-form', TPFormElement ); customElements.define( 'tp-form-field', TPFormFieldElement ); customElements.define( 'tp-form-error', TPFormErrorElement ); +customElements.define( 'tp-form-submit', TPFormSubmitElement ); diff --git a/src/form/tp-form-submit.ts b/src/form/tp-form-submit.ts new file mode 100644 index 0000000..5b04d2a --- /dev/null +++ b/src/form/tp-form-submit.ts @@ -0,0 +1,53 @@ +/** + * TP Form Submit. + */ +export class TPFormSubmitElement extends HTMLElement { + /** + * Get observed attributes. + * + * @return {Array} List of observed attributes. + */ + static get observedAttributes(): string[] { + return [ 'submitting-text', 'original-text', 'submitting' ]; + } + + /** + * Attribute changed callback. + * + * @param {string} _name Attribute name. + * @param {string} oldValue Old value. + * @param {string} newValue New value. + */ + attributeChangedCallback( _name: string = '', oldValue: string = '', newValue: string = '' ): void { + if ( oldValue !== newValue ) { + this.update(); + } + } + + /** + * Update this component. + */ + update(): void { + // Get submit button. + const submitButton: HTMLButtonElement | null = this.querySelector( 'button[type="submit"]' ); + if ( ! submitButton ) { + return; + } + + // Prepare submit button text. + const submittingText: string = this.getAttribute( 'submitting-text' ) ?? ''; + const originalText: string = this.getAttribute( 'original-text' ) ?? submitButton.innerHTML; + + // Check if we are submitting. + if ( 'yes' === this.getAttribute( 'submitting' ) ) { + submitButton.setAttribute( 'disabled', 'disabled' ); + this.setAttribute( 'original-text', originalText ); + submitButton.innerHTML = submittingText; + } else { + submitButton.removeAttribute( 'disabled' ); + this.removeAttribute( 'submitting' ); + this.removeAttribute( 'original-text' ); + submitButton.innerHTML = originalText; + } + } +} diff --git a/src/form/tp-form.ts b/src/form/tp-form.ts index 4a901ab..9e488bf 100644 --- a/src/form/tp-form.ts +++ b/src/form/tp-form.ts @@ -2,6 +2,7 @@ * Internal dependencies. */ import { TPFormFieldElement } from './tp-form-field'; +import { TPFormSubmitElement } from './tp-form-submit'; /** * TP Form. @@ -37,6 +38,15 @@ export class TPFormElement extends HTMLElement { if ( ! formValid || 'yes' === this.getAttribute( 'prevent-submit' ) ) { e.preventDefault(); } + + const submit: TPFormSubmitElement | null = this.querySelector( 'tp-form-submit' ); + if ( submit ) { + if ( formValid ) { + submit.setAttribute( 'submitting', 'yes' ); + } else { + submit.removeAttribute( 'submitting' ); + } + } } /** @@ -82,5 +92,8 @@ export class TPFormElement extends HTMLElement { field.removeAttribute( 'valid' ); field.removeAttribute( 'error' ); } ); + + const submit: TPFormSubmitElement | null = this.querySelector( 'tp-form-submit' ); + submit?.removeAttribute( 'submitting' ); } } From 376799055b72f8588d744fc52de5584ff3c6923e Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Mon, 20 Nov 2023 09:54:05 +1100 Subject: [PATCH 8/8] add comment --- src/form/tp-form-field.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/form/tp-form-field.ts b/src/form/tp-form-field.ts index 5a1f8fc..67961bb 100644 --- a/src/form/tp-form-field.ts +++ b/src/form/tp-form-field.ts @@ -1,3 +1,6 @@ +/** + * Internal dependencies. + */ import { TPFormErrorElement } from './tp-form-error'; /**