Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support alternative HTTP methods #72

Merged
merged 15 commits into from
May 29, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 10 additions & 18 deletions custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -206,6 +190,14 @@
"type": {
"text": "string"
}
},
{
"kind": "field",
"name": "httpMethod",
"type": {
"text": "string"
},
"readonly": true
}
],
"attributes": [
Expand Down
31 changes: 25 additions & 6 deletions src/auto-check-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ type State = {
controller: Controller | null
}

enum AllowedHttpMethods {
GET = 'GET',
POST = 'POST',
}

const states = new WeakMap<AutoCheckElement, State>()

class AutoCheckEvent extends Event {
Expand Down Expand Up @@ -176,6 +181,10 @@ export class AutoCheckElement extends HTMLElement {
set csrfField(value: string) {
this.setAttribute('csrf-field', value)
}

get httpMethod(): string {
return AllowedHttpMethods[this.getAttribute('http-method') as keyof typeof AllowedHttpMethods] || 'POST'
}
}

function setLoadingState(event: Event) {
Expand All @@ -187,10 +196,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 || !csrf || !state) {
if (!src || (httpMethod === 'POST' && !csrf) || !state) {
return
}

Expand All @@ -214,6 +224,9 @@ function makeAbortController() {
}

async function fetchWithNetworkEvents(el: Element, url: string, options: RequestInit): Promise<Response> {
if (options.method === 'GET') {
delete options.body
}
try {
const response = await fetch(url, options)
el.dispatchEvent(new Event('load'))
Expand All @@ -238,9 +251,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 || !csrf || !state) {
if (!src || (httpMethod === 'POST' && !csrf) || !state) {
if (autoCheckElement.required) {
input.setCustomValidity('')
}
Expand All @@ -255,8 +269,13 @@ async function check(autoCheckElement: AutoCheckElement) {
}

const body = new FormData()
body.append(csrfField, csrf)
body.append('value', input.value)
const url = new URL(src, window.location.origin)
if (httpMethod === 'POST') {
body.append(csrfField, csrf)
body.append('value', input.value)
} else {
url.search = new URLSearchParams({value: input.value}).toString()
}

input.dispatchEvent(new AutoCheckSendEvent(body))

Expand All @@ -269,10 +288,10 @@ 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: 'POST',
method: httpMethod,
body,
})
if (response.ok) {
Expand Down
29 changes: 29 additions & 0 deletions test/auto-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<auto-check src="/success" http-method="GET" required>
<input>
</auto-check>`
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
Expand Down
2 changes: 1 addition & 1 deletion web-test-runner.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading