Skip to content

Commit 79c1572

Browse files
authored
Merge pull request #35 from github/set-loading-state-before-debouncing
Set input as invalid on input before debounce
2 parents e1d3821 + 91e23c2 commit 79c1572

File tree

3 files changed

+103
-26
lines changed

3 files changed

+103
-26
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,31 @@ check.addEventListener('error', () => container.classList.add('is-error'))
5050

5151
### Auto-check events
5252

53+
**`auto-check-start`** is dispatched on when there has been input in the element. In `event.detail` you can find:
54+
55+
- `setValidity`: A function to provide a custom failure message on the input. By default it is 'Verifying…'.
56+
57+
58+
```js
59+
const input = check.querySelector('input')
60+
61+
input.addEventListener('auto-check-start', function(event) {
62+
const {setValidity} = event.detail
63+
setValidity('Loading validation')
64+
})
65+
```
66+
5367
**`auto-check-send`** is dispatched before the network request begins. In `event.detail` you can find:
5468

5569
- `body`: The FormData request body to modify before the request is sent.
56-
- `setValidity`: A function to provide a custom validation message while the request is in-flight. By default it is 'Verifying…'.
5770

5871

5972
```js
6073
const input = check.querySelector('input')
6174

6275
input.addEventListener('auto-check-send', function(event) {
63-
const {body, setValidity} = event.detail
76+
const {body} = event.detail
6477
body.append('custom_form_data', 'value')
65-
setValidity('Checking with server…')
6678
})
6779
```
6880

src/index.js

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export default class AutoCheckElement extends HTMLElement {
2121
const state = {check: checker, controller: null, previousValue: null}
2222
states.set(this, state)
2323

24+
input.addEventListener('change', setLoadingState)
25+
input.addEventListener('input', setLoadingState)
2426
input.addEventListener('change', checker)
2527
input.addEventListener('input', checker)
2628
input.autocomplete = 'off'
@@ -35,6 +37,8 @@ export default class AutoCheckElement extends HTMLElement {
3537
if (!state) return
3638
states.delete(this)
3739

40+
input.removeEventListener('change', setLoadingState)
41+
input.removeEventListener('input', setLoadingState)
3842
input.removeEventListener('change', state.check)
3943
input.removeEventListener('input', state.check)
4044
input.setCustomValidity('')
@@ -91,6 +95,36 @@ export default class AutoCheckElement extends HTMLElement {
9195
}
9296
}
9397

98+
function setLoadingState(event: Event) {
99+
const input = event.currentTarget
100+
if (!(input instanceof HTMLInputElement)) return
101+
102+
const autoCheckElement = input.closest('auto-check')
103+
if (!(autoCheckElement instanceof AutoCheckElement)) return
104+
105+
const src = autoCheckElement.src
106+
const csrf = autoCheckElement.csrf
107+
const state = states.get(autoCheckElement)
108+
109+
// If some attributes are missing we want to exit early and make sure that the element is valid.
110+
if (!src || !csrf || !state) {
111+
return
112+
}
113+
114+
let message = 'Verifying…'
115+
const setValidity = text => (message = text)
116+
input.dispatchEvent(
117+
new CustomEvent('auto-check-start', {
118+
bubbles: true,
119+
detail: {setValidity}
120+
})
121+
)
122+
123+
if (autoCheckElement.required) {
124+
input.setCustomValidity(message)
125+
}
126+
}
127+
94128
function makeAbortController() {
95129
if ('AbortController' in window) {
96130
return new AbortController()
@@ -114,19 +148,22 @@ async function fetchWithNetworkEvents(el: Element, url: string, options: Request
114148
}
115149

116150
async function check(autoCheckElement: AutoCheckElement) {
117-
const src = autoCheckElement.src
118-
if (!src) {
119-
throw new Error('missing src')
120-
}
121-
const csrf = autoCheckElement.csrf
122-
if (!csrf) {
123-
throw new Error('missing csrf')
124-
}
125151
const input = autoCheckElement.input
126-
if (!input) return
152+
if (!input) {
153+
return
154+
}
127155

156+
const src = autoCheckElement.src
157+
const csrf = autoCheckElement.csrf
128158
const state = states.get(autoCheckElement)
129-
if (!state) return
159+
160+
// If some attributes are missing we want to exit early and make sure that the element is valid.
161+
if (!src || !csrf || !state) {
162+
if (autoCheckElement.required) {
163+
input.setCustomValidity('')
164+
}
165+
return
166+
}
130167

131168
const body = new FormData()
132169
body.append('authenticity_token', csrf)
@@ -136,24 +173,20 @@ async function check(autoCheckElement: AutoCheckElement) {
136173
if (id && id === state.previousValue) return
137174
state.previousValue = id
138175

139-
let message = 'Verifying…'
140-
const setValidity = text => (message = text)
176+
if (!input.value.trim()) {
177+
if (autoCheckElement.required) {
178+
input.setCustomValidity('')
179+
}
180+
return
181+
}
182+
141183
input.dispatchEvent(
142184
new CustomEvent('auto-check-send', {
143185
bubbles: true,
144-
detail: {body, setValidity}
186+
detail: {body}
145187
})
146188
)
147189

148-
if (!input.value.trim()) {
149-
input.dispatchEvent(new CustomEvent('auto-check-complete', {bubbles: true}))
150-
return
151-
}
152-
153-
if (autoCheckElement.required) {
154-
input.setCustomValidity(message)
155-
}
156-
157190
if (state.controller) {
158191
state.controller.abort()
159192
} else {

test/test.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ describe('auto-check element', function() {
4444
assert.isFalse(input.checkValidity())
4545
})
4646

47+
it('invalidates the input element on keypress', async function() {
48+
const inputEvent = once(input, 'change')
49+
triggerChange(input, 'hub')
50+
await inputEvent
51+
assert.isFalse(input.checkValidity())
52+
})
53+
4754
it('invalidates input request is in-flight', async function() {
4855
triggerChange(input, 'hub')
4956
await once(checker, 'loadstart')
@@ -66,7 +73,7 @@ describe('auto-check element', function() {
6673
it('customizes the in-flight message', async function() {
6774
checker.src = '/fail'
6875
const send = new Promise(resolve => {
69-
input.addEventListener('auto-check-send', event => {
76+
input.addEventListener('auto-check-start', event => {
7077
event.detail.setValidity('Checking with server')
7178
resolve()
7279
})
@@ -174,6 +181,23 @@ describe('auto-check element', function() {
174181
triggerChange(input, 'hub')
175182
})
176183

184+
it('emits auto-check-start on input', function(done) {
185+
input.addEventListener('auto-check-start', () => done())
186+
input.value = 'hub'
187+
input.dispatchEvent(new InputEvent('input'))
188+
})
189+
190+
it('emits auto-check-start on change', function(done) {
191+
input.addEventListener('auto-check-start', () => done())
192+
triggerChange(input, 'hub')
193+
})
194+
195+
it('emits auto-check-send 300 milliseconds after keypress', function(done) {
196+
input.addEventListener('auto-check-send', () => done())
197+
input.value = 'hub'
198+
input.dispatchEvent(new InputEvent('input'))
199+
})
200+
177201
it('emits auto-check-success when server responds with 200 OK', async function() {
178202
triggerChange(input, 'hub')
179203
const event = await once(input, 'auto-check-success')
@@ -208,6 +232,14 @@ describe('auto-check element', function() {
208232
input.dispatchEvent(new InputEvent('change'))
209233
input.dispatchEvent(new InputEvent('change'))
210234
})
235+
236+
it('do not emit if essential attributes are missing', async function() {
237+
const events = []
238+
checker.removeAttribute('src')
239+
input.addEventListener('auto-check-start', event => events.push(event.type))
240+
triggerChange(input, 'hub')
241+
assert.deepEqual(events, [])
242+
})
211243
})
212244
})
213245

0 commit comments

Comments
 (0)