Skip to content

Commit

Permalink
Merge pull request #3 from Travelopia/feature/form
Browse files Browse the repository at this point in the history
Form Component
  • Loading branch information
junaidbhura authored Nov 19, 2023
2 parents be91ed9 + 3767990 commit 255a9f4
Show file tree
Hide file tree
Showing 14 changed files with 589 additions and 1 deletion.
26 changes: 26 additions & 0 deletions src/form/definitions.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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: {
[ key: string ]: TPFormValidator;
}
tpFormErrors: {
[ key: string ]: string;
};
}
}
65 changes: 65 additions & 0 deletions src/form/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Web Component: Form</title>

<link rel="stylesheet" href="../../dist/form/style.css" media="all">
<script type="module" src="../../dist/form/index.js"></script>

<style>
form {
display: flex;
flex-direction: column;
gap: 10px;
max-width: 600px;
margin: 40px auto;
}

tp-form-field,
label {
display: block;
}

input,
select,
textarea {
width: 100%;
}
</style>
</head>
<body>
<main>
<tp-form prevent-submit="yes">
<form action="#">
<tp-form-field 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>
</main>
</body>
</html>
50 changes: 50 additions & 0 deletions src/form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Styles.
*/
import './style.scss';

/**
* Validators.
*/
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,
];

/**
* Register Validators and Errors.
*/
window.tpFormValidators = {};
window.tpFormErrors = {};

validators.forEach( (
{ name, validator, errorMessage }: { name: string, validator: TPFormValidator, errorMessage: string }
): void => {
window.tpFormValidators[ name ] = validator;
window.tpFormErrors[ name ] = errorMessage;
} );

/**
* Components.
*/
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.
*/
customElements.define( 'tp-form', TPFormElement );
customElements.define( 'tp-form-field', TPFormFieldElement );
customElements.define( 'tp-form-error', TPFormErrorElement );
customElements.define( 'tp-form-submit', TPFormSubmitElement );

Empty file added src/form/style.scss
Empty file.
5 changes: 5 additions & 0 deletions src/form/tp-form-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* TP Form Error.
*/
export class TPFormErrorElement extends HTMLElement {
}
150 changes: 150 additions & 0 deletions src/form/tp-form-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* Internal dependencies.
*/
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();
}
}

/**
* 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 component.
*/
update(): void {
const { tpFormValidators } = window;
if ( ! tpFormValidators ) {
return;
}

const error: string = this.getAttribute( 'error' ) ?? '';
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 ) {
// 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;
return false;
}
}

// No error found, all good.
return true;
} );

// Check if the field is valid or not.
if ( valid ) {
this.setAttribute( 'valid', 'yes' );
this.removeAttribute( 'error' );
} else {
this.removeAttribute( 'valid' );
this.setAttribute( 'error', error );
}

// Return validity.
return valid;
}

/**
* Set the error message.
*
* @param {string} message Error message.
*/
setErrorMessage( message: string = '' ): void {
const 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 );
}

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 } ) );
}
}
53 changes: 53 additions & 0 deletions src/form/tp-form-submit.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading

0 comments on commit 255a9f4

Please sign in to comment.