From 03e00a788fd64a17883ef00b3e2c49c3cff2cff0 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 12:15:52 -0400 Subject: [PATCH 01/15] Support alternative HTTP methods This change proposed supporting the use of alternative HTTP methods. For example, now a user can define `http-method=GET` and then component will issue a `GET` request using encoded query params instead. All values other than POST will use this new approach. The default remains POST. --- src/auto-check-element.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index a61dbe6..dc1d3bc 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -176,6 +176,14 @@ export class AutoCheckElement extends HTMLElement { set csrfField(value: string) { this.setAttribute('csrf-field', value) } + + get httpMethod(): string { + return this.getAttribute('http-method') || 'POST' + } + + get isHttpPost(): bool { + return this.httpMethod == 'POST' + } } function setLoadingState(event: Event) { @@ -190,7 +198,7 @@ function setLoadingState(event: Event) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || !csrf || !state) { + if (!src || (isHttpPost && !csrf) || !state) { return } @@ -240,7 +248,7 @@ async function check(autoCheckElement: AutoCheckElement) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || !csrf || !state) { + if (!src || (isHttpPost && !csrf) || !state) { if (autoCheckElement.required) { input.setCustomValidity('') } @@ -255,8 +263,13 @@ async function check(autoCheckElement: AutoCheckElement) { } const body = new FormData() - body.append(csrfField, csrf) - body.append('value', input.value) + if (isHttpPost) { + body.append(csrfField, csrf) + body.append('value', input.value) + } else { + url = new URL(src); + url.search = new URLSearchParams({ value: input.value }).toString(); + } input.dispatchEvent(new AutoCheckSendEvent(body)) @@ -272,7 +285,7 @@ async function check(autoCheckElement: AutoCheckElement) { const response = await fetchWithNetworkEvents(autoCheckElement, src, { credentials: 'same-origin', signal: state.controller.signal, - method: 'POST', + method: httpMethod, body, }) if (response.ok) { From 443a63a54e54c947fb1474fe8d87d9b128d035f7 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 13:27:32 -0400 Subject: [PATCH 02/15] Update src/auto-check-element.ts Co-authored-by: Keith Cirkel --- 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 dc1d3bc..defe2c4 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -198,7 +198,7 @@ function setLoadingState(event: Event) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (isHttpPost && !csrf) || !state) { + if (!src || (this.httpMethod == 'POST' && !csrf) || !state) { return } From af4e0a15ac03d9b810ce4fb65b719bd46bdd4d7f Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 13:27:38 -0400 Subject: [PATCH 03/15] Update src/auto-check-element.ts Co-authored-by: Keith Cirkel --- 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 defe2c4..e94eba8 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -263,7 +263,7 @@ async function check(autoCheckElement: AutoCheckElement) { } const body = new FormData() - if (isHttpPost) { + if (this.httpMethod) { body.append(csrfField, csrf) body.append('value', input.value) } else { From 9482b6b053ce9b2a7e41864207fca4155f622311 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 13:27:44 -0400 Subject: [PATCH 04/15] Update src/auto-check-element.ts Co-authored-by: Keith Cirkel --- 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 e94eba8..58ee755 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -248,7 +248,7 @@ async function check(autoCheckElement: AutoCheckElement) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (isHttpPost && !csrf) || !state) { + if (!src || (this.httpMethod && !csrf) || !state) { if (autoCheckElement.required) { input.setCustomValidity('') } From 8aec52fe52955dffd017e4f06e06cb29f38ec55a Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 13:27:51 -0400 Subject: [PATCH 05/15] Update src/auto-check-element.ts Co-authored-by: Keith Cirkel --- 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 58ee755..9a00257 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -267,7 +267,7 @@ async function check(autoCheckElement: AutoCheckElement) { body.append(csrfField, csrf) body.append('value', input.value) } else { - url = new URL(src); + url = new URL(src, window.location.origin); url.search = new URLSearchParams({ value: input.value }).toString(); } From 1ba5238a74604445eec358f4dd0e8ce1003526c5 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 13:28:15 -0400 Subject: [PATCH 06/15] Update src/auto-check-element.ts --- src/auto-check-element.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index 9a00257..8a1b970 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -180,10 +180,6 @@ export class AutoCheckElement extends HTMLElement { get httpMethod(): string { return this.getAttribute('http-method') || 'POST' } - - get isHttpPost(): bool { - return this.httpMethod == 'POST' - } } function setLoadingState(event: Event) { From 6cde2e890057dc2d2c44cfaf0a3105f2f1cb6eff Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 14:32:58 -0400 Subject: [PATCH 07/15] linting --- 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 8a1b970..08166fb 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -194,7 +194,7 @@ function setLoadingState(event: Event) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (this.httpMethod == 'POST' && !csrf) || !state) { + if (!src || (httpMethod === 'POST' && !csrf) || !state) { return } @@ -244,7 +244,7 @@ async function check(autoCheckElement: AutoCheckElement) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (this.httpMethod && !csrf) || !state) { + if (!src || (httpMethod && !csrf) || !state) { if (autoCheckElement.required) { input.setCustomValidity('') } @@ -259,12 +259,12 @@ async function check(autoCheckElement: AutoCheckElement) { } const body = new FormData() - if (this.httpMethod) { + if (httpMethod === 'POST') { body.append(csrfField, csrf) body.append('value', input.value) } else { - url = new URL(src, window.location.origin); - url.search = new URLSearchParams({ value: input.value }).toString(); + url = new URL(src, window.location.origin) + url.search = new URLSearchParams({value: input.value}).toString() } input.dispatchEvent(new AutoCheckSendEvent(body)) From a8f6b096e1f4d4ca7be01bf4cfc51397b1aca870 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 14:33:38 -0400 Subject: [PATCH 08/15] Update src/auto-check-element.ts --- 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 08166fb..fa659da 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -244,7 +244,7 @@ async function check(autoCheckElement: AutoCheckElement) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (httpMethod && !csrf) || !state) { + if (!src || (httpMethod === 'POST' && !csrf) || !state) { if (autoCheckElement.required) { input.setCustomValidity('') } From 9dc10b4512bf679d1bf7475668a985a4abc26253 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 14:38:46 -0400 Subject: [PATCH 09/15] fix this --- src/auto-check-element.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index fa659da..e023d01 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -194,7 +194,7 @@ function setLoadingState(event: Event) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (httpMethod === 'POST' && !csrf) || !state) { + if (!src || (this.httpMethod === 'POST' && !csrf) || !state) { return } @@ -244,7 +244,7 @@ async function check(autoCheckElement: AutoCheckElement) { const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (httpMethod === 'POST' && !csrf) || !state) { + if (!src || (this.httpMethod === 'POST' && !csrf) || !state) { if (autoCheckElement.required) { input.setCustomValidity('') } @@ -259,12 +259,12 @@ async function check(autoCheckElement: AutoCheckElement) { } const body = new FormData() - if (httpMethod === 'POST') { + if (this.httpMethod === 'POST') { body.append(csrfField, csrf) body.append('value', input.value) } else { - url = new URL(src, window.location.origin) - url.search = new URLSearchParams({value: input.value}).toString() + this.url = new URL(src, window.location.origin) + this.url.search = new URLSearchParams({value: input.value}).toString() } input.dispatchEvent(new AutoCheckSendEvent(body)) @@ -281,7 +281,7 @@ async function check(autoCheckElement: AutoCheckElement) { const response = await fetchWithNetworkEvents(autoCheckElement, src, { credentials: 'same-origin', signal: state.controller.signal, - method: httpMethod, + method: this.httpMethod, body, }) if (response.ok) { From fe7b0a28fea48aabb565bd1133fe06efa570d447 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 18:51:55 +0000 Subject: [PATCH 10/15] fix this.httpMethod --- src/auto-check-element.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index e023d01..4c13fe2 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -191,10 +191,11 @@ function setLoadingState(event: Event) { const src = autoCheckElement.src const csrf = autoCheckElement.csrf + const httpMethod = autoCheckElement.httpMethod const state = states.get(autoCheckElement) // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (this.httpMethod === 'POST' && !csrf) || !state) { + if (!src || (httpMethod === 'POST' && !csrf) || !state) { return } @@ -242,9 +243,10 @@ async function check(autoCheckElement: AutoCheckElement) { const src = autoCheckElement.src const csrf = autoCheckElement.csrf const state = states.get(autoCheckElement) + const httpMethod = autoCheckElement.httpMethod // If some attributes are missing we want to exit early and make sure that the element is valid. - if (!src || (this.httpMethod === 'POST' && !csrf) || !state) { + if (!src || (httpMethod === 'POST' && !csrf) || !state) { if (autoCheckElement.required) { input.setCustomValidity('') } @@ -259,7 +261,7 @@ async function check(autoCheckElement: AutoCheckElement) { } const body = new FormData() - if (this.httpMethod === 'POST') { + if (httpMethod === 'POST') { body.append(csrfField, csrf) body.append('value', input.value) } else { @@ -281,7 +283,7 @@ async function check(autoCheckElement: AutoCheckElement) { const response = await fetchWithNetworkEvents(autoCheckElement, src, { credentials: 'same-origin', signal: state.controller.signal, - method: this.httpMethod, + method: httpMethod, body, }) if (response.ok) { From f65d3b5dd69637e2b67c0423a6e18142f1a27ee0 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 18:54:45 +0000 Subject: [PATCH 11/15] handle url / string properly --- src/auto-check-element.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index 4c13fe2..9b0ea51 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -261,12 +261,12 @@ async function check(autoCheckElement: AutoCheckElement) { } const body = new FormData() + const url = new URL(src, window.location.origin) if (httpMethod === 'POST') { body.append(csrfField, csrf) body.append('value', input.value) } else { - this.url = new URL(src, window.location.origin) - this.url.search = new URLSearchParams({value: input.value}).toString() + url.search = new URLSearchParams({value: input.value}).toString() } input.dispatchEvent(new AutoCheckSendEvent(body)) @@ -280,7 +280,7 @@ async function check(autoCheckElement: AutoCheckElement) { state.controller = makeAbortController() try { - const response = await fetchWithNetworkEvents(autoCheckElement, src, { + const response = await fetchWithNetworkEvents(autoCheckElement, url.toString(), { credentials: 'same-origin', signal: state.controller.signal, method: httpMethod, From c47e47445ec96b542d89659be937229455a7f7ce Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Fri, 24 May 2024 18:57:26 +0000 Subject: [PATCH 12/15] Add docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e45e8d7..2853987 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Note that in the following example the CSRF element is marked with the `data-csr - `csrf` is the [CSRF][] token for the posted form. It's available in the request body as a `authenticity_token` form parameter. - You can also supply the CSRF token via a child element. See [usage](#Usage) example. - `required` is a boolean attribute that requires the validation to succeed before the surrounding form may be submitted. +- `http-method` defaults to `POST` where data is submitted as a POST with form data. You can set `GET` and the HTTP method used will be a get with url encoded params instead. ## Events From 1d5282c24e290d87e19a87a4dfb317d24fe91952 Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Tue, 28 May 2024 17:31:13 +0000 Subject: [PATCH 13/15] Add specs --- src/auto-check-element.ts | 3 +++ test/auto-check.js | 29 +++++++++++++++++++++++++++++ web-test-runner.config.js | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index 9b0ea51..6ee8206 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -219,6 +219,9 @@ function makeAbortController() { } async function fetchWithNetworkEvents(el: Element, url: string, options: RequestInit): Promise { + if (options.method === 'GET') { + delete options.body + } try { const response = await fetch(url, options) el.dispatchEvent(new Event('load')) diff --git a/test/auto-check.js b/test/auto-check.js index 67c12be..611a674 100644 --- a/test/auto-check.js +++ b/test/auto-check.js @@ -114,6 +114,35 @@ describe('auto-check element', function () { }) }) + describe('using HTTP GET', function () { + let checker + let input + + beforeEach(function () { + const container = document.createElement('div') + container.innerHTML = ` + + + ` + document.body.append(container) + + checker = document.querySelector('auto-check') + input = checker.querySelector('input') + }) + + afterEach(function () { + document.body.innerHTML = '' + checker = null + input = null + }) + + it('validates input with successful server response with GET', async function () { + triggerInput(input, 'hub') + await once(input, 'auto-check-complete') + assert.isTrue(input.checkValidity()) + }) + }) + describe('network lifecycle events', function () { let checker let input diff --git a/web-test-runner.config.js b/web-test-runner.config.js index fa0fabd..a68f5cd 100644 --- a/web-test-runner.config.js +++ b/web-test-runner.config.js @@ -19,7 +19,7 @@ export default { middleware: [ async ({request, response}, next) => { const {method, path} = request - if (method === 'POST') { + if (method === 'POST' || method === 'GET') { if (path.startsWith('/fail')) { response.status = 422 // eslint-disable-next-line i18n-text/no-en From e88543f8e1747990ab3960a182932c0977526c7f Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Tue, 28 May 2024 17:31:30 +0000 Subject: [PATCH 14/15] Update auto generate spec --- custom-elements.json | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/custom-elements.json b/custom-elements.json index 3a59cb4..026d746 100644 --- a/custom-elements.json +++ b/custom-elements.json @@ -56,16 +56,8 @@ }, "members": [ { - "kind": "method", + "kind": "field", "name": "setValidity", - "parameters": [ - { - "name": "message", - "type": { - "text": "string" - } - } - ], "inheritedFrom": { "name": "AutoCheckValidationEvent", "module": "src/auto-check-element.ts" @@ -92,16 +84,8 @@ }, "members": [ { - "kind": "method", + "kind": "field", "name": "setValidity", - "parameters": [ - { - "name": "message", - "type": { - "text": "string" - } - } - ], "inheritedFrom": { "name": "AutoCheckValidationEvent", "module": "src/auto-check-element.ts" @@ -206,6 +190,14 @@ "type": { "text": "string" } + }, + { + "kind": "field", + "name": "httpMethod", + "type": { + "text": "string" + }, + "readonly": true } ], "attributes": [ From 059dde3ce3f860cf48f17f6157ddc5cd6a76833b Mon Sep 17 00:00:00 2001 From: Simon Taranto Date: Wed, 29 May 2024 17:13:48 +0000 Subject: [PATCH 15/15] Add type for allowed methods --- src/auto-check-element.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/auto-check-element.ts b/src/auto-check-element.ts index 6ee8206..ce94cc3 100644 --- a/src/auto-check-element.ts +++ b/src/auto-check-element.ts @@ -12,6 +12,11 @@ type State = { controller: Controller | null } +enum AllowedHttpMethods { + GET = 'GET', + POST = 'POST', +} + const states = new WeakMap() class AutoCheckEvent extends Event { @@ -178,7 +183,7 @@ export class AutoCheckElement extends HTMLElement { } get httpMethod(): string { - return this.getAttribute('http-method') || 'POST' + return AllowedHttpMethods[this.getAttribute('http-method') as keyof typeof AllowedHttpMethods] || 'POST' } }