diff --git a/src/Autocomplete/CHANGELOG.md b/src/Autocomplete/CHANGELOG.md new file mode 100644 index 00000000000..5148346a23d --- /dev/null +++ b/src/Autocomplete/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG + +## 2.4.0 + +- Support added for setting the required minimum search query length (defaults to 3) (#492) - @daFish diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js index 73449a1bafa..c8ddad9ffa1 100644 --- a/src/Autocomplete/assets/dist/controller.js +++ b/src/Autocomplete/assets/dist/controller.js @@ -39,7 +39,7 @@ class default_1 extends Controller { } connect() { if (this.urlValue) { - this.tomSelect = __classPrivateFieldGet(this, _instances, "m", _createAutocompleteWithRemoteData).call(this, this.urlValue); + this.tomSelect = __classPrivateFieldGet(this, _instances, "m", _createAutocompleteWithRemoteData).call(this, this.urlValue, this.minCharactersValue); return; } if (this.optionsAsHtmlValue) { @@ -121,7 +121,7 @@ _instances = new WeakSet(), _getCommonConfig = function _getCommonConfig() { }, }); return __classPrivateFieldGet(this, _instances, "m", _createTomSelect).call(this, config); -}, _createAutocompleteWithRemoteData = function _createAutocompleteWithRemoteData(autocompleteEndpointUrl) { +}, _createAutocompleteWithRemoteData = function _createAutocompleteWithRemoteData(autocompleteEndpointUrl, minCharacterLength) { const config = __classPrivateFieldGet(this, _instances, "m", _mergeObjects).call(this, __classPrivateFieldGet(this, _instances, "m", _getCommonConfig).call(this), { firstUrl: (query) => { const separator = autocompleteEndpointUrl.includes('?') ? '&' : '?'; @@ -134,6 +134,10 @@ _instances = new WeakSet(), _getCommonConfig = function _getCommonConfig() { .then(json => { this.setNextUrl(query, json.next_page); callback(json.results); }) .catch(() => callback()); }, + shouldLoad: function (query) { + const minLength = minCharacterLength || 3; + return query.length >= minLength; + }, score: function (search) { return function (item) { return 1; @@ -173,6 +177,7 @@ default_1.values = { optionsAsHtml: Boolean, noResultsFoundText: String, noMoreResultsText: String, + minCharacters: Number, tomSelectOptions: Object, }; diff --git a/src/Autocomplete/assets/src/controller.ts b/src/Autocomplete/assets/src/controller.ts index d6ca0bf1363..3dfa14f8269 100644 --- a/src/Autocomplete/assets/src/controller.ts +++ b/src/Autocomplete/assets/src/controller.ts @@ -8,6 +8,7 @@ export default class extends Controller { optionsAsHtml: Boolean, noResultsFoundText: String, noMoreResultsText: String, + minCharacters: Number, tomSelectOptions: Object, } @@ -15,6 +16,7 @@ export default class extends Controller { readonly optionsAsHtmlValue: boolean; readonly noMoreResultsTextValue: string; readonly noResultsFoundTextValue: string; + readonly minCharactersValue: number; readonly tomSelectOptionsValue: object; tomSelect: TomSelect; @@ -30,7 +32,7 @@ export default class extends Controller { connect() { if (this.urlValue) { - this.tomSelect = this.#createAutocompleteWithRemoteData(this.urlValue); + this.tomSelect = this.#createAutocompleteWithRemoteData(this.urlValue, this.minCharactersValue); return; } @@ -124,7 +126,7 @@ export default class extends Controller { return this.#createTomSelect(config); } - #createAutocompleteWithRemoteData(autocompleteEndpointUrl: string): TomSelect { + #createAutocompleteWithRemoteData(autocompleteEndpointUrl: string, minCharacterLength: number): TomSelect { const config: Partial = this.#mergeObjects(this.#getCommonConfig(), { firstUrl: (query: string) => { const separator = autocompleteEndpointUrl.includes('?') ? '&' : '?'; @@ -142,6 +144,11 @@ export default class extends Controller { .then(json => { this.setNextUrl(query, json.next_page); callback(json.results) }) .catch(() => callback()); }, + shouldLoad: function (query: string) { + const minLength = minCharacterLength || 3; + + return query.length >= minLength; + }, // avoid extra filtering after results are returned score: function(search: string) { return function(item: any) { diff --git a/src/Autocomplete/assets/test/controller.test.ts b/src/Autocomplete/assets/test/controller.test.ts index a0a4ad0643c..719dbf4b733 100644 --- a/src/Autocomplete/assets/test/controller.test.ts +++ b/src/Autocomplete/assets/test/controller.test.ts @@ -138,6 +138,35 @@ describe('AutocompleteController', () => { }); }); + it('limits updates when min-characters', async () => { + const container = mountDOM(` + + + `); + + application = startStimulus(); + + await waitFor(() => { + expect(getByTestId(container, 'main-element')).toHaveClass('connected'); + }); + + const tomSelect = getByTestId(container, 'main-element').tomSelect; + const controlInput = tomSelect.control_input; + + controlInput.value = 'fo'; + controlInput.dispatchEvent(new Event('input')); + + await waitFor(() => { + expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(0); + }); + }); + it('adds live-component support', async () => { const container = mountDOM(`
diff --git a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php index b5a9b5d9994..8e6b6a55d76 100644 --- a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php +++ b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php @@ -71,6 +71,10 @@ public function finishView(FormView $view, FormInterface $form, array $options) $values['max-results'] = $options['max_results']; } + if ($options['min_characters']) { + $values['min-characters'] = $options['min_characters']; + } + $values['no-results-found-text'] = $this->trans($options['no_results_found_text']); $values['no-more-results-text'] = $this->trans($options['no_more_results_text']); @@ -91,6 +95,7 @@ public function configureOptions(OptionsResolver $resolver) 'allow_options_create' => false, 'no_results_found_text' => 'No results found', 'no_more_results_text' => 'No more results', + 'min_characters' => 3, 'max_results' => 10, ]); diff --git a/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php b/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php index dbaeff5456f..b95eeb6323d 100644 --- a/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php +++ b/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php @@ -42,6 +42,7 @@ public function configureOptions(OptionsResolver $resolver) 'data-controller' => 'custom-autocomplete', ], 'max_results' => 5, + 'min_characters' => 2, ]); } diff --git a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php index 7c1acb3eef0..9072a5c17e3 100644 --- a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php +++ b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php @@ -31,6 +31,7 @@ public function testFieldsRenderWithStimulusController() ->get('/test-form') ->assertElementAttributeContains('#product_category_autocomplete', 'data-controller', 'custom-autocomplete symfony--ux-autocomplete--autocomplete') ->assertElementAttributeContains('#product_category_autocomplete', 'data-symfony--ux-autocomplete--autocomplete-url-value', '/test/autocomplete/category_autocomplete_type') + ->assertElementAttributeContains('#product_category_autocomplete', 'data-symfony--ux-autocomplete--autocomplete-min-characters-value', '2') ->assertElementAttributeContains('#product_portionSize', 'data-controller', 'symfony--ux-autocomplete--autocomplete') ->assertElementAttributeContains('#product_tags', 'data-controller', 'symfony--ux-autocomplete--autocomplete')