From ed275fc349bbc3436c25468e9cf70a0ade186065 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 20 Apr 2023 11:40:33 +0100 Subject: [PATCH 1/5] use regular Event constructors for simple events --- src/auto-check-element.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index db3b976..5702256 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -151,13 +151,13 @@ function makeAbortController() { async function fetchWithNetworkEvents(el: Element, url: string, options: RequestInit): Promise { try { const response = await fetch(url, options) - el.dispatchEvent(new CustomEvent('load')) - el.dispatchEvent(new CustomEvent('loadend')) + el.dispatchEvent(new Event('load')) + el.dispatchEvent(new Event('loadend')) return response } catch (error) { if ((error as Error).name !== 'AbortError') { - el.dispatchEvent(new CustomEvent('error')) - el.dispatchEvent(new CustomEvent('loadend')) + el.dispatchEvent(new Event('error')) + el.dispatchEvent(new Event('loadend')) } throw error } @@ -203,7 +203,7 @@ async function check(autoCheckElement: AutoCheckElement) { if (state.controller) { state.controller.abort() } else { - autoCheckElement.dispatchEvent(new CustomEvent('loadstart')) + autoCheckElement.dispatchEvent(new Event('loadstart')) } state.controller = makeAbortController() From c82bbaf20df008730f1d6a21b4eacf1eb4214749 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 20 Apr 2023 11:40:47 +0100 Subject: [PATCH 2/5] use AutoCheckCompleteEvent for completion events --- src/auto-check-element.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index 5702256..d32cd50 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -14,6 +14,12 @@ type State = { const states = new WeakMap() +export class AutoCheckCompleteEvent extends Event { + constructor() { + super('auto-check-complete', {bubbles: true}) + } +} + export class AutoCheckElement extends HTMLElement { static define(tag = 'auto-check', registry = customElements) { registry.define(tag, this) @@ -221,11 +227,11 @@ async function check(autoCheckElement: AutoCheckElement) { processFailure(response, input, autoCheckElement.required) } state.controller = null - input.dispatchEvent(new CustomEvent('auto-check-complete', {bubbles: true})) + input.dispatchEvent(new AutoCheckCompleteEvent()) } catch (error) { if ((error as Error).name !== 'AbortError') { state.controller = null - input.dispatchEvent(new CustomEvent('auto-check-complete', {bubbles: true})) + input.dispatchEvent(new AutoCheckCompleteEvent()) } } } From 14cd6d37748b022d2aa7ce58cb81f9e127321094 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 20 Apr 2023 12:06:57 +0100 Subject: [PATCH 3/5] rewrite auto-check events to be classes --- custom-elements.json | 172 ++++++++++++++++++++++++++++++++++++++ src/auto-check-element.ts | 119 ++++++++++++++------------ 2 files changed, 238 insertions(+), 53 deletions(-) diff --git a/custom-elements.json b/custom-elements.json index 64bb3e4..0fe96a5 100644 --- a/custom-elements.json +++ b/custom-elements.json @@ -6,6 +6,138 @@ "kind": "javascript-module", "path": "src/auto-check-element.ts", "declarations": [ + { + "kind": "class", + "description": "", + "name": "AutoCheckCompleteEvent", + "superclass": { + "name": "AutoCheckEvent", + "module": "src/auto-check-element.ts" + }, + "members": [ + { + "kind": "field", + "name": "detail", + "readonly": true, + "inheritedFrom": { + "name": "AutoCheckEvent", + "module": "src/auto-check-element.ts" + } + } + ] + }, + { + "kind": "class", + "description": "", + "name": "AutoCheckSuccessEvent", + "superclass": { + "name": "AutoCheckEvent", + "module": "src/auto-check-element.ts" + }, + "members": [ + { + "kind": "field", + "name": "detail", + "readonly": true, + "inheritedFrom": { + "name": "AutoCheckEvent", + "module": "src/auto-check-element.ts" + } + } + ] + }, + { + "kind": "class", + "description": "", + "name": "AutoCheckStartEvent", + "superclass": { + "name": "AutoCheckValidationEvent", + "module": "src/auto-check-element.ts" + }, + "members": [ + { + "kind": "method", + "name": "setValidity", + "parameters": [ + { + "name": "message", + "type": { + "text": "string" + } + } + ], + "inheritedFrom": { + "name": "AutoCheckValidationEvent", + "module": "src/auto-check-element.ts" + } + }, + { + "kind": "field", + "name": "detail", + "readonly": true, + "inheritedFrom": { + "name": "AutoCheckEvent", + "module": "src/auto-check-element.ts" + } + } + ] + }, + { + "kind": "class", + "description": "", + "name": "AutoCheckErrorEvent", + "superclass": { + "name": "AutoCheckValidationEvent", + "module": "src/auto-check-element.ts" + }, + "members": [ + { + "kind": "method", + "name": "setValidity", + "parameters": [ + { + "name": "message", + "type": { + "text": "string" + } + } + ], + "inheritedFrom": { + "name": "AutoCheckValidationEvent", + "module": "src/auto-check-element.ts" + } + }, + { + "kind": "field", + "name": "detail", + "readonly": true, + "inheritedFrom": { + "name": "AutoCheckEvent", + "module": "src/auto-check-element.ts" + } + } + ] + }, + { + "kind": "class", + "description": "", + "name": "AutoCheckSendEvent", + "superclass": { + "name": "AutoCheckEvent", + "module": "src/auto-check-element.ts" + }, + "members": [ + { + "kind": "field", + "name": "detail", + "readonly": true, + "inheritedFrom": { + "name": "AutoCheckEvent", + "module": "src/auto-check-element.ts" + } + } + ] + }, { "kind": "class", "description": "", @@ -75,6 +207,46 @@ } ], "exports": [ + { + "kind": "js", + "name": "AutoCheckCompleteEvent", + "declaration": { + "name": "AutoCheckCompleteEvent", + "module": "src/auto-check-element.ts" + } + }, + { + "kind": "js", + "name": "AutoCheckSuccessEvent", + "declaration": { + "name": "AutoCheckSuccessEvent", + "module": "src/auto-check-element.ts" + } + }, + { + "kind": "js", + "name": "AutoCheckStartEvent", + "declaration": { + "name": "AutoCheckStartEvent", + "module": "src/auto-check-element.ts" + } + }, + { + "kind": "js", + "name": "AutoCheckErrorEvent", + "declaration": { + "name": "AutoCheckErrorEvent", + "module": "src/auto-check-element.ts" + } + }, + { + "kind": "js", + "name": "AutoCheckSendEvent", + "declaration": { + "name": "AutoCheckSendEvent", + "module": "src/auto-check-element.ts" + } + }, { "kind": "js", "name": "AutoCheckElement", diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index d32cd50..bf3d56c 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -14,9 +14,60 @@ type State = { const states = new WeakMap() -export class AutoCheckCompleteEvent extends Event { +class AutoCheckEvent extends Event { + constructor(public readonly phase: string) { + super(`auto-check-${phase}`, {bubbles: true}) + } + + // Backwards compatibiltiy with `CustomEvent` + get detail() { + return this + } +} + +class AutoCheckValidationEvent extends AutoCheckEvent { + constructor(public readonly phase: string, public message = '') { + super(phase) + } + + setValidity(message: string) { + this.message = message + } +} + +// eslint-disable-next-line custom-elements/no-exports-with-element +export class AutoCheckCompleteEvent extends AutoCheckEvent { + constructor() { + super('complete') + } +} + +// eslint-disable-next-line custom-elements/no-exports-with-element +export class AutoCheckSuccessEvent extends AutoCheckEvent { + constructor(public readonly response: Response) { + super('success') + } +} + +// eslint-disable-next-line custom-elements/no-exports-with-element +export class AutoCheckStartEvent extends AutoCheckValidationEvent { constructor() { - super('auto-check-complete', {bubbles: true}) + super('start', 'Verifying…') + } +} + +// eslint-disable-next-line custom-elements/no-exports-with-element +export class AutoCheckErrorEvent extends AutoCheckValidationEvent { + constructor(public readonly response: Response) { + // eslint-disable-next-line i18n-text/no-en + super('error', 'Validation failed') + } +} + +// eslint-disable-next-line custom-elements/no-exports-with-element +export class AutoCheckSendEvent extends AutoCheckEvent { + constructor(public readonly body: FormData) { + super('send') } } @@ -128,17 +179,10 @@ function setLoadingState(event: Event) { return } - let message = 'Verifying…' - const setValidity = (text: string) => (message = text) - input.dispatchEvent( - new CustomEvent('auto-check-start', { - bubbles: true, - detail: {setValidity}, - }), - ) - + const startEvent = new AutoCheckStartEvent() + input.dispatchEvent(startEvent) if (autoCheckElement.required) { - input.setCustomValidity(message) + input.setCustomValidity(startEvent.message) } } @@ -199,12 +243,7 @@ async function check(autoCheckElement: AutoCheckElement) { body.append(csrfField, csrf) body.append('value', input.value) - input.dispatchEvent( - new CustomEvent('auto-check-send', { - bubbles: true, - detail: {body}, - }), - ) + input.dispatchEvent(new AutoCheckSendEvent(body)) if (state.controller) { state.controller.abort() @@ -222,9 +261,16 @@ async function check(autoCheckElement: AutoCheckElement) { body, }) if (response.ok) { - processSuccess(response, input, autoCheckElement.required) + if (autoCheckElement.required) { + input.setCustomValidity('') + } + input.dispatchEvent(new AutoCheckSuccessEvent(response.clone())) } else { - processFailure(response, input, autoCheckElement.required) + const event = new AutoCheckErrorEvent(response.clone()) + input.dispatchEvent(event) + if (autoCheckElement.required) { + input.setCustomValidity(event.message) + } } state.controller = null input.dispatchEvent(new AutoCheckCompleteEvent()) @@ -236,37 +282,4 @@ async function check(autoCheckElement: AutoCheckElement) { } } -function processSuccess(response: Response, input: HTMLInputElement, required: boolean) { - if (required) { - input.setCustomValidity('') - } - input.dispatchEvent( - new CustomEvent('auto-check-success', { - bubbles: true, - detail: { - response: response.clone(), - }, - }), - ) -} - -function processFailure(response: Response, input: HTMLInputElement, required: boolean) { - // eslint-disable-next-line i18n-text/no-en - let message = 'Validation failed' - const setValidity = (text: string) => (message = text) - input.dispatchEvent( - new CustomEvent('auto-check-error', { - bubbles: true, - detail: { - response: response.clone(), - setValidity, - }, - }), - ) - - if (required) { - input.setCustomValidity(message) - } -} - export default AutoCheckElement From 759c82cb5f8f9804186edd146f5c82f91b9dca7d Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 20 Apr 2023 12:13:36 +0100 Subject: [PATCH 4/5] add on get/set for all events --- custom-elements.json | 13 +++++++++++++ src/auto-check-element.ts | 15 +++++++++++++++ test/auto-check.js | 17 ++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/custom-elements.json b/custom-elements.json index 0fe96a5..3a59cb4 100644 --- a/custom-elements.json +++ b/custom-elements.json @@ -158,6 +158,19 @@ } ] }, + { + "kind": "field", + "name": "#onloadend", + "privacy": "private", + "type": { + "text": "((event: Event) => void) | null" + }, + "default": "null" + }, + { + "kind": "field", + "name": "onloadend" + }, { "kind": "field", "name": "input", diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index bf3d56c..22d1ed9 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -77,6 +77,21 @@ export class AutoCheckElement extends HTMLElement { return this } + #onloadend: ((event: Event) => void) | null = null + get onloadend() { + return this.#onloadend + } + + set onloadend(listener: ((event: Event) => void) | null) { + if (this.#onloadend) { + this.removeEventListener('loadend', this.#onloadend as unknown as EventListenerOrEventListenerObject) + } + this.#onloadend = typeof listener === 'object' || typeof listener === 'function' ? listener : null + if (typeof listener === 'function') { + this.addEventListener('loadend', listener as unknown as EventListenerOrEventListenerObject) + } + } + connectedCallback(): void { const input = this.input if (!input) return diff --git a/test/auto-check.js b/test/auto-check.js index 4dc9fcc..67c12be 100644 --- a/test/auto-check.js +++ b/test/auto-check.js @@ -151,6 +151,22 @@ describe('auto-check element', function () { assert.deepEqual(['loadstart', 'load', 'loadend'], events) }) + + it('can use setters', async function () { + const events = [] + const track = event => events.push(event.type) + + checker.onloadstart = track + checker.onload = track + checker.onerror = track + checker.onloadend = track + + const completed = Promise.all([once(checker, 'loadstart'), once(checker, 'load'), once(checker, 'loadend')]) + triggerInput(input, 'hub') + await completed + + assert.deepEqual(['loadstart', 'load', 'loadend'], events) + }) }) describe('auto-check lifecycle events', function () { @@ -235,7 +251,6 @@ describe('auto-check element', function () { input.value = 'hub' input.dispatchEvent(new InputEvent('input')) - input.dispatchEvent(new InputEvent('input')) }) it('do not emit if essential attributes are missing', async function () { From 1c8813f851a4627510426d5aa7d7a761feb71014 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 20 Apr 2023 20:05:39 +0100 Subject: [PATCH 5/5] fix typo Co-authored-by: Rez --- src/auto-check-element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index 22d1ed9..a1d6f91 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -19,7 +19,7 @@ class AutoCheckEvent extends Event { super(`auto-check-${phase}`, {bubbles: true}) } - // Backwards compatibiltiy with `CustomEvent` + // Backwards compatibility with `CustomEvent` get detail() { return this }