Skip to content

Commit eecb315

Browse files
reinhardtthet
authored andcommitted
feat(pat-autosuggest): Add batching support for AJAX requests.
This PR introduces three new options for that: max-initial-size: Defines the batch size for the initial request (default: 10). ajax-batch-size: Defines the batch size for subsequent requests (default: 10). ajax-timeout: Defines the timeout in milliseconds before a AJAX request is submitted. (default: 400). Ref: scrum-1638
1 parent 70c08a3 commit eecb315

File tree

3 files changed

+214
-10
lines changed

3 files changed

+214
-10
lines changed

src/pat/auto-suggest/auto-suggest.js

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ const log = logging.getLogger("autosuggest");
1111
export const parser = new Parser("autosuggest");
1212
parser.addArgument("ajax-data-type", "JSON");
1313
parser.addArgument("ajax-search-index", "");
14+
parser.addArgument("ajax-timeout", 400);
1415
parser.addArgument("ajax-url", "");
16+
parser.addArgument("max-initial-size", 10); // AJAX search results limit for the first page.
17+
parser.addArgument("ajax-batch-size", 10); // AJAX search results limit for subsequent pages.
1518
parser.addArgument("allow-new-words", true); // Should custom tags be allowed?
1619
parser.addArgument("max-selection-size", 0);
1720
parser.addArgument("minimum-input-length"); // Don't restrict by default so that all results show
@@ -54,10 +57,10 @@ export default Base.extend({
5457
separator: this.options.valueSeparator,
5558
tokenSeparators: [","],
5659
openOnEnter: false,
57-
maximumSelectionSize: this.options.maxSelectionSize,
60+
maximumSelectionSize: this.options.max["selection-size"],
5861
minimumInputLength: this.options.minimumInputLength,
5962
allowClear:
60-
this.options.maxSelectionSize === 1 && !this.el.hasAttribute("required"),
63+
this.options.max["selection-size"] === 1 && !this.el.hasAttribute("required"),
6164
};
6265
if (this.el.hasAttribute("readonly")) {
6366
config.placeholder = "";
@@ -179,7 +182,7 @@ export default Base.extend({
179182
// Even if words was [], we would get a tag stylee select
180183
// That was then properly working with ajax if configured.
181184

182-
if (this.options.maxSelectionSize === 1) {
185+
if (this.options.max["selection-size"] === 1) {
183186
config.data = words;
184187
// We allow exactly one value, use dropdown styles. How do we feed in words?
185188
} else {
@@ -198,8 +201,8 @@ export default Base.extend({
198201
for (const value of values) {
199202
data.push({ id: value, text: value });
200203
}
201-
if (this.options.maxSelectionSize === 1) {
202-
data = data[0];
204+
if (this.options.max["selection-size"] === 1) {
205+
data = data[0]
203206
}
204207
callback(data);
205208
};
@@ -234,7 +237,7 @@ export default Base.extend({
234237
_data.push({ id: d, text: data[d] });
235238
}
236239
}
237-
if (this.options.maxSelectionSize === 1) {
240+
if (this.options.max["selection-size"] === 1) {
238241
_data = _data[0];
239242
}
240243
callback(_data);
@@ -253,19 +256,27 @@ export default Base.extend({
253256
url: this.options.ajax.url,
254257
dataType: this.options.ajax["data-type"],
255258
type: "GET",
256-
quietMillis: 400,
259+
quietMillis: this.options.ajax.timeout,
257260
data: (term, page) => {
258261
return {
259262
index: this.options.ajax["search-index"],
260263
q: term, // search term
261-
page_limit: 10,
264+
page_limit: this.page_limit(page),
262265
page: page,
263266
};
264267
},
265268
results: (data, page) => {
266-
// parse the results into the format expected by Select2.
269+
// Parse the results into the format expected by Select2.
267270
// data must be a list of objects with keys "id" and "text"
268-
return { results: data, page: page };
271+
272+
// Check whether there are more results to come.
273+
// There are maybe more results if the number of
274+
// items is the same as the batch-size.
275+
// We expect the backend to return an empty list if
276+
// a batch page is requested where there are no
277+
// more results.
278+
const load_more = Object.keys(data).length >= this.page_limit(page);
279+
return { results: data, page: page, more: load_more };
269280
},
270281
},
271282
},
@@ -275,6 +286,16 @@ export default Base.extend({
275286
return config;
276287
},
277288

289+
page_limit(page) {
290+
// Page limit for the first page of a batch.
291+
let page_limit = this.options.max["initial-size"];
292+
if (page > 1) {
293+
// Page limit for subsequent pages.
294+
page_limit = this.options.ajax["batch-size"];
295+
}
296+
return page_limit;
297+
},
298+
278299
destroy($el) {
279300
$el.off(".pat-autosuggest");
280301
$el.select2("destroy");

src/pat/auto-suggest/auto-suggest.test.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ import utils from "../../core/utils";
55
import registry from "../../core/registry";
66
import { jest } from "@jest/globals";
77

8+
// Need to import for the ajax mock to work.
9+
import "select2";
10+
11+
12+
const mock_fetch_ajax = (...data) => {
13+
// Data format: [{id: str, text: str}, ... ], ...
14+
// first batch ^ ^ second batch
15+
16+
// NOTE: You need to add a trailing comma if you add only one argument to
17+
// make the multi-argument dereferencing work.
18+
19+
// Mock Select2
20+
$.fn.select2.ajaxDefaults.transport = jest.fn().mockImplementation((opts) => {
21+
// Get the batch page
22+
const page = opts.data.page - 1;
23+
24+
// Return the data for the batch
25+
return opts.success(data[page]);
26+
});
27+
};
28+
29+
830
var testutils = {
931
createInputElement: function (c) {
1032
var cfg = c || {};
@@ -545,4 +567,128 @@ describe("pat-autosuggest", function () {
545567
expect(selected.length).toBe(0);
546568
});
547569
});
570+
571+
describe("6 - AJAX tests", function () {
572+
it("6.1 - AJAX works with a simple data structure.", async function () {
573+
mock_fetch_ajax(
574+
[
575+
{ id: "1", text: "apple" },
576+
{ id: "2", text: "orange" }
577+
], // Note the trailing comma to make the multi-argument dereferencing work.
578+
);
579+
580+
document.body.innerHTML = `
581+
<input
582+
type="text"
583+
class="pat-autosuggest"
584+
data-pat-autosuggest="
585+
ajax-url: http://test.org/test;
586+
ajax-timeout: 1;
587+
" />
588+
`;
589+
590+
const input = document.querySelector("input");
591+
new pattern(input);
592+
await utils.timeout(1); // wait a tick for async to settle.
593+
594+
$(".select2-input").click();
595+
await utils.timeout(1); // wait for ajax to finish.
596+
597+
const results = $(document.querySelectorAll(".select2-results li"));
598+
expect(results.length).toBe(2);
599+
600+
$(results[0]).mouseup();
601+
602+
const selected = document.querySelectorAll(".select2-search-choice");
603+
expect(selected.length).toBe(1);
604+
expect(selected[0].textContent.trim()).toBe("apple");
605+
expect(input.value).toBe("1");
606+
});
607+
608+
it("6.2 - AJAX works with batches.", async function () {
609+
mock_fetch_ajax(
610+
[
611+
{ id: "1", text: "one" },
612+
{ id: "2", text: "two" },
613+
{ id: "3", text: "three" },
614+
{ id: "4", text: "four" },
615+
],
616+
[
617+
{ id: "5", text: "five" },
618+
{ id: "6", text: "six" },
619+
],
620+
[
621+
{ id: "7", text: "three" },
622+
],
623+
);
624+
625+
document.body.innerHTML = `
626+
<input
627+
type="text"
628+
class="pat-autosuggest"
629+
data-pat-autosuggest="
630+
ajax-url: http://test.org/test;
631+
ajax-timeout: 1;
632+
max-initial-size: 4;
633+
ajax-batch-size: 2;
634+
" />
635+
`;
636+
637+
const input = document.querySelector("input");
638+
new pattern(input);
639+
await utils.timeout(1); // wait a tick for async to settle.
640+
641+
// Load batch 1 with batch size 4
642+
$(".select2-input").click();
643+
await utils.timeout(1); // wait for ajax to finish.
644+
645+
const results_1 = $(document.querySelectorAll(".select2-results .select2-result"));
646+
expect(results_1.length).toBe(4);
647+
648+
const load_more_1 = $(document.querySelectorAll(".select2-results .select2-more-results"));
649+
expect(load_more_1.length).toBe(1);
650+
651+
// Load batch 2 with batch size 2
652+
$(load_more_1[0]).mouseup();
653+
// NOTE: Flaky behavior needs multiple timeouts 👌
654+
await utils.timeout(1); // wait for ajax to finish.
655+
await utils.timeout(1); // wait for ajax to finish.
656+
await utils.timeout(1); // wait for ajax to finish.
657+
await utils.timeout(1); // wait for ajax to finish.
658+
await utils.timeout(1); // wait for ajax to finish.
659+
await utils.timeout(1); // wait for ajax to finish.
660+
await utils.timeout(1); // wait for ajax to finish.
661+
await utils.timeout(1); // wait for ajax to finish.
662+
await utils.timeout(1); // wait for ajax to finish.
663+
await utils.timeout(1); // wait for ajax to finish.
664+
665+
const results_2 = $(document.querySelectorAll(".select2-results .select2-result"));
666+
expect(results_2.length).toBe(6);
667+
668+
const load_more_2 = $(document.querySelectorAll(".select2-results .select2-more-results"));
669+
expect(load_more_2.length).toBe(1);
670+
671+
672+
// Load final batch 2
673+
$(load_more_2[0]).mouseup();
674+
// NOTE: Flaky behavior needs multiple timeouts 🤘
675+
await utils.timeout(1); // wait for ajax to finish.
676+
await utils.timeout(1); // wait for ajax to finish.
677+
await utils.timeout(1); // wait for ajax to finish.
678+
await utils.timeout(1); // wait for ajax to finish.
679+
await utils.timeout(1); // wait for ajax to finish.
680+
await utils.timeout(1); // wait for ajax to finish.
681+
await utils.timeout(1); // wait for ajax to finish.
682+
await utils.timeout(1); // wait for ajax to finish.
683+
await utils.timeout(1); // wait for ajax to finish.
684+
await utils.timeout(1); // wait for ajax to finish.
685+
686+
const results_3 = $(document.querySelectorAll(".select2-results .select2-result"));
687+
expect(results_3.length).toBe(7);
688+
689+
const load_more_3 = $(document.querySelectorAll(".select2-results .select2-more-results"));
690+
expect(load_more_3.length).toBe(0);
691+
});
692+
693+
});
548694
});

src/pat/auto-suggest/documentation.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,53 @@ Pre-fill the input element with words in JSON format and don't allow the user to
3434
prefill-json: {"john-snow":"John Snow"};
3535
allow-new-words: false;' type="text"></input>
3636

37+
38+
### Batching support
39+
40+
This pattern can load data in batches via AJAX.
41+
The following example demonstrates how define batch sizes for the initial load (`max-initial-size`) and for subsequent loads (`ajax-batch-size`).
42+
Both values default to 10.
43+
44+
<input
45+
type="text"
46+
class="pat-auto-suggest"
47+
data-pat-auto-suggest="
48+
ajax-url: /path/to/data.json;
49+
ajax-batch-size: 10;
50+
max-initial-size: 10;
51+
"
52+
/>
53+
54+
---
55+
**Note**
56+
57+
The server needs to support batching, otherwise these options do not have any effect.
58+
59+
---
60+
61+
### AJAX parameters submitted to the server
62+
63+
| Parameter | Description |
64+
| --------- | ----------- |
65+
| index | The optional search index to be used on the server, if needed. |
66+
| q | The search term. |
67+
| page_limit | The number of items to be returned per page. Based on the current page it is wether `max-initial-size` (page 1) or `ajax-batch-size` (page 2). |
68+
| page | The current page number. |
69+
70+
3771
### Option reference
3872

3973
You can customise the behaviour of a gallery through options in the `data-pat-auto-suggest` attribute.
4074

4175
| Property | Type | Default Value | Description |
4276
| -------------------- | ------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
77+
| ajax-batch-size | Number | 10 | Batch size for subsequent pages of a bigger batch. For the first page, `max-initial-size` is used. |
4378
| ajax-data-type | String | "json" | In what format will AJAX fetched data be returned in? |
4479
| ajax-search-index | String | | The index or key which must be used to determine the value from the returned data. |
80+
| ajax-timeout | Number | 400 | Timeout before new ajax requests are sent. The default value is set ot `400` milliseconds and prevents querying the server too often while typing. |
4581
| ajax-url | URL | | The URL which must be called via AJAX to fetch remote data. |
4682
| allow-new-words | Boolean | true | Besides the suggested words, also allow custom user-defined words to be entered. |
83+
| max-initial-size | Number | 10 | Initial batch size. Display `max-initial-size` items on the first page of a bigger result set. |
4784
| max-selection-size | Number | 0 | How many values are allowed? Provide a positive number or 0 for unlimited. |
4885
| placeholder | String | Enter text | The placeholder text for the form input. The `placeholder` attribute of the form element can also be used. |
4986
| prefill | List | | A comma separated list of values with which the form element must be filled in with. The `value-separator` option does not have an effect here. |

0 commit comments

Comments
 (0)