-
Notifications
You must be signed in to change notification settings - Fork 149
/
Copy pathajax-source.html
300 lines (272 loc) · 10.6 KB
/
ajax-source.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accessible Autocomplete AJAX source example</title>
<style>
/* Example page specific styling. */
html {
color: #111;
background: #FFF;
font-family: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, 'helvetica neue', helvetica, ubuntu, roboto, noto, 'segoe ui', arial, sans-serif;
font-size: 16px;
line-height: 1.5;
}
body {
padding-left: 1rem;
padding-right: 1rem;
}
h1, h2, h3, h4, h5, h6 {
line-height: normal;
}
label {
display: block;
margin-bottom: .5rem;
}
code {
padding-left: .5em;
padding-right: .5em;
background: #EFEFEF;
font-weight: normal;
font-family: monospace;
}
main {
max-width: 40em;
margin-left: auto;
margin-right: auto;
}
.autocomplete-wrapper {
max-width: 20em;
margin-bottom: 4rem;
}
.submitted--hidden {
display: none;
}
</style>
<link rel="stylesheet" href="../dist/accessible-autocomplete.min.css">
</head>
<body>
<main>
<h1>Accessible Autocomplete AJAX source example</h1>
<div class="submitted submitted--hidden">
<p>You submitted:</p>
<ul>
<li><code>"last-location": <span class="submitted__last-location"></span></code></li>
</ul>
<hr />
</div>
<form action="form-single.html" method="get">
<label for="last-location">What was the last location you visited?</label>
<div class="autocomplete-wrapper">
</div>
<button type="submit">Submit your answer</button>
</form>
</main>
<script type="text/javascript" src="../dist/accessible-autocomplete.min.js"></script>
<script type="text/javascript">
// Sending requests to a server means that when the autocomplete has no
// result it may not be because there are no results, but because these
// results are being fetched, or because an error happened. We can use the
// function for internationalising the 'No results found' message to
// provide a little more context to users.
//
// It'll rely on a `status` variable updated by the wrappers of the
// function making the request (see thereafter)
let status;
function tNoResults() {
if (status === 'loading') {
return 'Loading suggestions...'
} else if (status === 'error') {
return 'Sorry, an error occurred'
}else {
return 'No results found'
}
}
// The aim being to load suggestions from a server, we'll need a function
// that does just that. This one uses `fetch`, but you could also use
// XMLHttpRequest or whichever library is the most suitable to your
// project
//
// For lack of a actual server able of doing computation our endpoint will
// return the whole list of countries and we'll simulate the work of the
// server client-side
function requestSuggestions(query, fetchArgs = {}) {
return fetch('./suggestions.json', fetchArgs)
.then((response) => response.json())
}
// We'll wrap that function multiple times, each enhancing the previous
// wrapping to handle the the different behaviours necessary to
// appropriately coordinate requests to the server and display feedback to
// users
const makeRequest =
// Wrapping everything is the error handling to make sure it catches
// errors from any of the other wrappers
trackErrors(
// Next up is tracking whether we're loading new results
trackLoading(
// To avoid overloading the server with potentially costly requests
// as well as avoid wasting bandwidth while users are typing we'll
// only send requests a little bit after they stop typing
debounce(
// Finally we want to cancel requests that are already sent, so
// only the results of the last one update the UI This is the role
// of the next two wrappers
abortExisting(
// That last one is for demo only, to simulate server behaviours
// (latency, errors, filtering) on the client
simulateServer(
requestSuggestions
)
),
250
)
)
);
// We can then use the function we built and adapt it to the autocomplete
// API encapsulating the adjustments specific to rendering the 'No result
// found' message
function source(query, populateResults) {
// Start by clearing the results to ensure a loading message
// shows when a the query gets updated after results have loaded
populateResults([])
makeRequest(query)
// Only update the results if an actual array of options get returned
// allowing for `makeRequest` to avoid making updates to results being
// already displayed by resolving to `undefined`, like when we're
// aborting requests
.then(options => options && populateResults(options))
// In case of errors, we need to clear the results so the accessible
// autocomplate show its 'No result found'
.catch(error => populateResults([]))
}
// And finally we can set up our accessible autocomplete
const element = document.querySelector('.autocomplete-wrapper')
const id = 'autocomplete-default'
accessibleAutocomplete({
element: element,
id: id,
source: source,
tNoResults: tNoResults,
menuAttributes: {
"aria-labelledby": id
},
inputClasses: "govuk-input"
})
////
// INTERNAL DETAILS
////
// Technically, it'd be the server doing the filtering but for lack of
// server, we're requesting the whole list and filter client-side.
// Similarly, we'll use a specific query to trigger error for demo
// purpose, which will be easier than going in the devtools and making the
// request error We'll also simulate that the server takes a little time
// to respond to make things more visible in the UI
const SERVER_LATENCY = 2500;
function simulateServer(fn) {
return function(query, ...args) {
return new Promise(resolve => {
setTimeout(() => {
const suggestions = fn(query, ...args)
.then((response) => {
if (query === 'trigger-error') {
throw new Error('Custom error')
}
return response;
})
.then(countries => {
return countries.filter(country => country.toLowerCase().indexOf(query.toLowerCase()) !== -1)
})
resolve(suggestions)
}, SERVER_LATENCY)
})
}
}
// Debouncing limits the number of requests being sent
// but does not guarantee the order in which the responses come in
// Due to network and server latency, a response to an earlier request
// may come back after a response to a later request
// This keeps track of the AbortController of the last request sent
// so it can be cancelled before sending a new one
//
// NOTE: If you're using `XMLHttpRequest`s or a custom library,
// they'll have a different mechanism for aborting. You can either:
// - adapt this function to store whatever object lets you abort in-flight requests
// - or adapt your version of `requestSuggestion` to listen to the `signal`
// that this function passes to the wrapped function and trigger
// whichever API for aborting you have available
// See: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal#events
let abortController;
function abortExisting(fn) {
return function(...args) {
if (abortController) {
abortController.abort();
}
abortController = new AbortController();
return fn(...args, {signal: abortController.signal})
.then(result => {
abortController = null;
return result;
}, error => {
// Aborting requests will lead to `fetch` rejecting with an
// `AbortError` In that situation, that's something we expect, so
// we don't want to show a message to users
if (error.name !== 'AbortError') {
abortController = null;
throw error;
}
})
}
}
// Debounces the given function so it only gets executed after a specific delay
function debounce(fn, wait) {
let timeout
return function (...args) {
return new Promise(resolve => {
clearTimeout(timeout)
const later = function () {
timeout = null
resolve(fn(...args))
}
timeout = setTimeout(later, wait)
})
}
}
// Tracks the loading state so we can adapt the message being displayed to the user
function trackLoading(fn) {
return function(...args) {
status = 'loading';
return fn(...args)
.then(result => {
status = null;
return result
}, error => {
status = null;
throw error
})
}
}
// In a similar fashion, we can track errors happening, which will adjust the message
function trackErrors(fn) {
return function(...args) {
return fn(...args)
.catch(error => {
status = 'error'
throw error
})
}
}
</script>
<script>
var queryStringParameters = window.location.search
var previouslySubmitted = queryStringParameters.length > 0
if (previouslySubmitted) {
var submittedEl = document.querySelector('.submitted')
submittedEl.classList.remove('submitted--hidden')
var params = new URLSearchParams(document.location.search.split('?')[1])
document.querySelector('.submitted__last-location').innerHTML = params.get('last-location')
document.querySelector('.submitted__passport-location').innerHTML = params.get('passport-location')
}
</script>
</body>
</html>