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
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