Skip to content

Commit

Permalink
add webui-form-validate
Browse files Browse the repository at this point in the history
  • Loading branch information
basher committed Apr 5, 2024
1 parent 4f86361 commit 979e0ac
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 176 deletions.
30 changes: 16 additions & 14 deletions ui/src/javascript/ui-init.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
// Dependencies.
// 1. Modules.
import FormValidate from './modules/form-validate';
import Search from './modules/search';
import Slider from './modules/slider';

// 2. For DEMO purposes only.
import demoAjaxFetchHTML from './modules/demo-ajax-fetch-html';

// 3. Web components.
// Web components.
import WebUIDisclosure from './web-components/webui-disclosure';
import WebUIFormValidate from './web-components/webui-form-validate';
import WebUIMakeClickable from './web-components/webui-make-clickable';
import WebUIModal from './web-components/webui-modal';
import WebUINotify from './web-components/webui-notify';
Expand All @@ -19,17 +13,19 @@ import WebUIToggle from './web-components/webui-toggle';
import WebUITabs from './web-components/webui-tabs';
import WebUIVideoPlayer from './web-components/webui-video-player';

export const uiInit = (): void => {
FormValidate.start();
Search.start();
Slider.start();
// Modules.
import Search from './modules/search';
import Slider from './modules/slider';

// For DEMO purposes only.
demoAjaxFetchHTML.start();
// For DEMO purposes only.
import demoAjaxFetchHTML from './modules/demo-ajax-fetch-html';

export const uiInit = (): void => {
// Define Web Components
!customElements.get('webui-disclosure') &&
customElements.define('webui-disclosure', WebUIDisclosure);
!customElements.get('webui-form-validate') &&
customElements.define('webui-form-validate', WebUIFormValidate);
!customElements.get('webui-make-clickable') &&
customElements.define('webui-make-clickable', WebUIMakeClickable);
!customElements.get('webui-modal') &&
Expand All @@ -48,4 +44,10 @@ export const uiInit = (): void => {
customElements.define('webui-toggle', WebUIToggle);
!customElements.get('webui-video-player') &&
customElements.define('webui-video-player', WebUIVideoPlayer);

// For DEMO purposes only.
demoAjaxFetchHTML.start();

Search.start();
Slider.start();
};
4 changes: 2 additions & 2 deletions ui/src/javascript/web-components/webui-disclosure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ export default class WebUIDisclosure extends HTMLElement {

if (!this.trigger || !this.content) return;

this.a11ySetup();
this.setupA11y();

this.trigger?.addEventListener('click', this);
}

private a11ySetup(): void {
private setupA11y(): void {
this.trigger?.removeAttribute('hidden');
this.trigger?.setAttribute('aria-expanded', 'false');
this.content?.setAttribute('hidden', '');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,36 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

export default class FormValidate {
private form: HTMLFormElement;
export default class FormValidate extends HTMLElement {
private form: HTMLFormElement | null;
private errorFieldClass: string;
private errorMsgClass: string;

constructor(form: HTMLFormElement) {
this.form = form;
constructor() {
super();

this.form = this.querySelector('form');
this.errorFieldClass = 'form__field--has-error';
this.errorMsgClass = 'form__error';

this.init();
}

public static start(): void {
const forms = document.querySelectorAll(
'[data-module="form-validate"]',
);

[...(forms as any)].forEach((form) => {
const instance = new FormValidate(form);
return instance;
});
}
if (!this.form) return;

private init(): void {
this.initFormValidate();
this.form.addEventListener('submit', (e: Event) =>
this.handleSubmit(e),
);
this.form.addEventListener(
'blur',
(e: Event) => {
this.handleBlur(e);
},
true,
);
}

private initFormValidate(): void {
// Prevent native HTML5 validation.
this.form.noValidate = true;

this.form.addEventListener('submit', this);
this.form.addEventListener('blur', this, true);
}

private handleSubmit(e: Event): void {
if (!this.form.checkValidity()) {
if (!this.form?.checkValidity()) {
e.preventDefault();

[...(this.form.elements as any)].forEach((field) => {
[...(this.form?.elements as any)].forEach((field) => {
if (!field.checkValidity()) {
this.showError(field);
}
});

// Focus on 1st error.
const firstError = this.form.querySelector(
const firstError = this.form?.querySelector(
'[aria-invalid]',
) as HTMLElement;
firstError?.focus();
Expand Down Expand Up @@ -84,9 +60,25 @@ export default class FormValidate {
errorMsg.textContent = field.validationMessage;

fieldWrapper?.classList.add(this.errorFieldClass);

// Only add 1 error msg per field (e.g. a group of radio buttons).
if (!fieldWrapper?.querySelector(`#${errorMsg.id}`)) {
fieldWrapper?.appendChild(errorMsg);
let target;

if (field.type === 'checkbox') {
target = fieldWrapper.querySelector('.checkbox');
fieldWrapper?.insertBefore(errorMsg, target);
return;
}

if (field.type === 'radio') {
const fieldset = fieldWrapper.querySelector('fieldset');
target = fieldWrapper.querySelector('.radio');
fieldset?.insertBefore(errorMsg, target);
return;
}

fieldWrapper?.insertBefore(errorMsg, field);
}

field.setAttribute('aria-invalid', 'true');
Expand All @@ -102,4 +94,15 @@ export default class FormValidate {
fieldWrapper?.classList.remove(this.errorFieldClass);
errorMsg?.remove();
}

// Handle constructor() event listeners.
handleEvent(e: MouseEvent) {
if (e.type === 'submit') {
this.handleSubmit(e);
}

if (e.type === 'blur') {
this.handleBlur(e);
}
}
}
125 changes: 12 additions & 113 deletions ui/stories/4. Forms/Form/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ export const FormServerValidationHtml = () => `
Telephone input label
<span class="label__hint">UK number, between 9 and 13 digits, can include spaces</span>
</label>
<span class="form__error" id="input-tel-error">
Error text...
</span>
<input
type="tel"
id="input-tel"
Expand All @@ -161,14 +164,14 @@ export const FormServerValidationHtml = () => `
aria-invalid="true"
aria-describedby="input-tel-error"
/>
<span class="form__error" id="input-tel-error">
Error text...
</span>
</div>
<div class="form__field form__field--has-error">
<label for="select" class="label">
Select label
</label>
<span class="form__error" id="select-error">
Error text...
</span>
<select
class="select"
id="select"
Expand All @@ -181,11 +184,11 @@ export const FormServerValidationHtml = () => `
<option value="2">option 2</option>
<option value="3">option 3</option>
</select>
<span class="form__error" id="select-error">
Error text...
</span>
</div>
<div class="form__field form__field--has-error">
<span class="form__error" id="checkbox-1-error">
Error text...
</span>
<div class="checkbox">
<input
type="checkbox"
Expand All @@ -199,15 +202,15 @@ export const FormServerValidationHtml = () => `
Checkbox label
</label>
</div>
<span class="form__error" id="checkbox-1-error">
Error text...
</span>
</div>
<div class="form__field form__field--has-error">
<fieldset class="fieldset">
<legend class="legend">
Radio legend
</legend>
<span class="form__error" id="radio_group-error">
Error text...
</span>
<div class="radio">
<input
type="radio"
Expand All @@ -233,110 +236,6 @@ export const FormServerValidationHtml = () => `
<label for="radio-2" class="radio__label">Radio 2 label</label>
</div>
</fieldset>
<span class="form__error" id="radio_group-error">
Error text...
</span>
</div>
<div class="button-group">
<button
type="submit"
class="button button--text button--positive"
>
Submit
</button>
<button
type="reset"
class="button button--text"
>
Reset
</button>
</div>
</form>
`;

export const FormJSValidationHtml = () => `
<form class="form" action="#" data-module="form-validate">
<div class="form__field">
<label for="input-text1" class="label">
Text input 1 label
</label>
<input
type="text"
id="input-text1"
class="input"
placeholder="placeholder"
required
/>
</div>
<div class="form__field">
<label for="input-tel" class="label">
Telephone input label
<span class="label__hint">UK number, between 9 and 13 digits, can include spaces</span>
</label>
<input
type="tel"
id="input-tel"
class="input"
pattern="^\\d{3,5}\\s?\\d{3,4}\\s?\\d{3,4}$"
autocomplete="tel"
required
/>
</div>
<div class="form__field">
<label for="select" class="label">
Select label
</label>
<select
class="select"
id="select"
required
>
<option value="">choose...</option>
<option value="1">option 1</option>
<option value="2">option 2</option>
<option value="3">option 3</option>
</select>
</div>
<div class="form__field">
<div class="checkbox">
<input
type="checkbox"
id="checkbox-1"
class="checkbox__input"
required
/>
<label for="checkbox-1" class="checkbox__label">
Checkbox label
</label>
</div>
</div>
<div class="form__field">
<fieldset class="fieldset">
<legend class="legend">
Radio legend
</legend>
<div class="radio">
<input
type="radio"
id="radio-1"
class="radio__input"
name="radio_group"
required
/>
<label for="radio-1" class="radio__label">Radio 1 label</label>
</div>
<div class="radio">
<input
type="radio"
id="radio-2"
class="radio__input"
name="radio_group"
required
/>
<label for="radio-2" class="radio__label">Radio 2 label</label>
</div>
</fieldset>
</div>
<div class="button-group">
Expand Down
8 changes: 6 additions & 2 deletions ui/stories/4. Forms/Form/Form.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Meta, Canvas, Controls } from '@storybook/blocks';
import * as Form from './Form.stories';
import * as FormNoArgs from './FormNoArgs.stories';
import * as WebUIFormValidate from '../../6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.stories';

<Meta of={Form} />

Expand All @@ -25,13 +26,16 @@ import * as FormNoArgs from './FormNoArgs.stories';
<Controls of={Form.Form} />

## Server-side validation
- Use the [`<webui-notify>`](/docs/web-components-or-custom-elements-webui-notify--docs) custom element to add an additional error summary block.
- Uses the [`<webui-notify>`](/docs/web-components-or-custom-elements-webui-notify--docs) custom element to add an additional error summary block at the top of the form.
- List all the form errors, with anchors to each form field that has an error.

<Canvas of={FormNoArgs.FormServerValidation} />

## JavaScript validation
- Uses the [`<webui-form-validate>`](/docs/web-components-or-custom-elements-webui-form-validate--docs) custom element.

## Additional reading
- See a [better approach to form validation](https://adamsilver.io/blog/the-problem-with-live-validation-and-what-to-do-instead/) that uses form `submit` rather than live inline validation.
- Uses the [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#validating_forms_using_javascript).

<Canvas of={FormNoArgs.FormJSValidation} />
<Canvas of={WebUIFormValidate.WebUIFormValidate} />
Loading

0 comments on commit 979e0ac

Please sign in to comment.