From 979e0ac74c4d4307f7d422b4eb5545faca920718 Mon Sep 17 00:00:00 2001 From: basher Date: Fri, 5 Apr 2024 11:16:13 +0100 Subject: [PATCH] add webui-form-validate --- ui/src/javascript/ui-init.ts | 30 +++-- .../web-components/webui-disclosure.ts | 4 +- .../webui-form-validate.ts} | 79 +++++------ ui/stories/4. Forms/Form/Form.js | 125 ++---------------- ui/stories/4. Forms/Form/Form.mdx | 8 +- .../4. Forms/Form/FormNoArgs.stories.js | 12 +- .../WebUI Form Validate/WebUIFormValidate.js | 102 ++++++++++++++ .../WebUI Form Validate/WebUIFormValidate.mdx | 13 ++ .../WebUIFormValidate.stories.js | 15 +++ 9 files changed, 212 insertions(+), 176 deletions(-) rename ui/src/javascript/{modules/form-validate.ts => web-components/webui-form-validate.ts} (62%) create mode 100644 ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.js create mode 100644 ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.mdx create mode 100644 ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.stories.js diff --git a/ui/src/javascript/ui-init.ts b/ui/src/javascript/ui-init.ts index e726c30..fc42bca 100644 --- a/ui/src/javascript/ui-init.ts +++ b/ui/src/javascript/ui-init.ts @@ -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'; @@ -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') && @@ -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(); }; diff --git a/ui/src/javascript/web-components/webui-disclosure.ts b/ui/src/javascript/web-components/webui-disclosure.ts index 3487676..e6198d7 100644 --- a/ui/src/javascript/web-components/webui-disclosure.ts +++ b/ui/src/javascript/web-components/webui-disclosure.ts @@ -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', ''); diff --git a/ui/src/javascript/modules/form-validate.ts b/ui/src/javascript/web-components/webui-form-validate.ts similarity index 62% rename from ui/src/javascript/modules/form-validate.ts rename to ui/src/javascript/web-components/webui-form-validate.ts index 2a49554..89c7a05 100644 --- a/ui/src/javascript/modules/form-validate.ts +++ b/ui/src/javascript/web-components/webui-form-validate.ts @@ -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(); @@ -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'); @@ -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); + } + } } diff --git a/ui/stories/4. Forms/Form/Form.js b/ui/stories/4. Forms/Form/Form.js index 9362548..da3044c 100644 --- a/ui/stories/4. Forms/Form/Form.js +++ b/ui/stories/4. Forms/Form/Form.js @@ -151,6 +151,9 @@ export const FormServerValidationHtml = () => ` Telephone input label UK number, between 9 and 13 digits, can include spaces + + Error text... + ` aria-invalid="true" aria-describedby="input-tel-error" /> - - Error text... -
+ + Error text... + - - Error text... -
+ + Error text... +
` Checkbox label
- - Error text... -
Radio legend + + Error text... +
`
- - Error text... - -
- -
- - -
- -`; - -export const FormJSValidationHtml = () => ` -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-
- - Radio legend - -
- - -
-
- - -
-
diff --git a/ui/stories/4. Forms/Form/Form.mdx b/ui/stories/4. Forms/Form/Form.mdx index 7ec718c..252c698 100644 --- a/ui/stories/4. Forms/Form/Form.mdx +++ b/ui/stories/4. Forms/Form/Form.mdx @@ -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'; @@ -25,13 +26,16 @@ import * as FormNoArgs from './FormNoArgs.stories'; ## Server-side validation -- Use the [``](/docs/web-components-or-custom-elements-webui-notify--docs) custom element to add an additional error summary block. +- Uses the [``](/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. ## JavaScript validation +- Uses the [``](/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). - + diff --git a/ui/stories/4. Forms/Form/FormNoArgs.stories.js b/ui/stories/4. Forms/Form/FormNoArgs.stories.js index 9c2d8d1..a43e4ea 100644 --- a/ui/stories/4. Forms/Form/FormNoArgs.stories.js +++ b/ui/stories/4. Forms/Form/FormNoArgs.stories.js @@ -1,7 +1,5 @@ -import { - FormServerValidationHtml, - FormJSValidationHtml -} from './Form'; +import { FormServerValidationHtml } from './Form'; +import { WebUIFormValidateHtml } from '../../6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate'; export default { title: 'Forms/Form', @@ -17,7 +15,7 @@ export const FormServerValidation = { }; FormServerValidation.storyName = 'Form (Server Validation)'; -export const FormJSValidation = { - render: () => FormJSValidationHtml(), +export const WebUIFormValidate = { + render: () => WebUIFormValidateHtml(), }; -FormJSValidation.storyName = 'Form (JavaScript Validation)'; +WebUIFormValidate.storyName = ' JavaScript Validation'; diff --git a/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.js b/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.js new file mode 100644 index 0000000..f52209b --- /dev/null +++ b/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.js @@ -0,0 +1,102 @@ +export const WebUIFormValidateHtml = () => ` + + +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ + Radio legend + +
+ + +
+
+ + +
+
+
+ +
+ + +
+ + +`; diff --git a/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.mdx b/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.mdx new file mode 100644 index 0000000..750624d --- /dev/null +++ b/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.mdx @@ -0,0 +1,13 @@ +import { Meta, Canvas } from '@storybook/blocks'; +import * as WebUIFormValidate from './WebUIFormValidate.stories'; + + + +# `` +- Client-side validation using JavaScript. + +## 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). + + diff --git a/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.stories.js b/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.stories.js new file mode 100644 index 0000000..cd120d4 --- /dev/null +++ b/ui/stories/6. Web Components Or Custom Elements/WebUI Form Validate/WebUIFormValidate.stories.js @@ -0,0 +1,15 @@ +import { WebUIFormValidateHtml } from './WebUIFormValidate'; + +export default { + title: 'Web Components Or Custom Elements/', + parameters: { + status: { + type: 'stable', + }, + }, +}; + +export const WebUIFormValidate = { + render: () => WebUIFormValidateHtml(), +}; +WebUIFormValidate.storyName = '';