diff --git a/src/Autocomplete/assets/dist/controller.d.ts b/src/Autocomplete/assets/dist/controller.d.ts
index a60f3e94880..587407f8237 100644
--- a/src/Autocomplete/assets/dist/controller.d.ts
+++ b/src/Autocomplete/assets/dist/controller.d.ts
@@ -19,6 +19,7 @@ declare class export_default extends Controller {
minCharacters: NumberConstructor;
tomSelectOptions: ObjectConstructor;
preload: StringConstructor;
+ resetOnFocus: BooleanConstructor;
};
readonly urlValue: string;
readonly optionsAsHtmlValue: boolean;
@@ -31,6 +32,7 @@ declare class export_default extends Controller {
readonly tomSelectOptionsValue: object;
readonly hasPreloadValue: boolean;
readonly preloadValue: string;
+ readonly resetOnFocusValue: boolean;
tomSelect: TomSelect | undefined;
private mutationObserver;
private isObserving;
diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js
index 514e73dcf9f..264b2b3208b 100644
--- a/src/Autocomplete/assets/dist/controller.js
+++ b/src/Autocomplete/assets/dist/controller.js
@@ -290,6 +290,16 @@ function _createAutocompleteWithRemoteData(autocompleteEndpointUrl, minCharacter
return `
${this.createOptionTextValue.replace("%placeholder%", `${escapeData(data.input)}`)}
`;
}
},
+ onFocus: () => {
+ if (this.resetOnFocusValue && this.tomSelect) {
+ if (this.tomSelect.control_input.value.trim() === "") {
+ this.tomSelect.clearOptions();
+ this.tomSelect.loadedSearches = {};
+ if (typeof this.tomSelect.clearPagination === "function") this.tomSelect.clearPagination();
+ this.tomSelect.load("");
+ }
+ }
+ },
preload: this.preload
});
return _assertClassBrand(_Class_brand, this, _createTomSelect).call(this, config);
@@ -333,6 +343,7 @@ _Class.values = {
createOptionText: String,
minCharacters: Number,
tomSelectOptions: Object,
- preload: String
+ preload: String,
+ resetOnFocus: Boolean
};
export { _Class as default };
diff --git a/src/Autocomplete/assets/src/controller.ts b/src/Autocomplete/assets/src/controller.ts
index 646e084bb1e..d0f04d067b1 100644
--- a/src/Autocomplete/assets/src/controller.ts
+++ b/src/Autocomplete/assets/src/controller.ts
@@ -33,6 +33,7 @@ export default class extends Controller {
minCharacters: Number,
tomSelectOptions: Object,
preload: String,
+ resetOnFocus: Boolean,
};
declare readonly urlValue: string;
@@ -46,6 +47,7 @@ export default class extends Controller {
declare readonly tomSelectOptionsValue: object;
declare readonly hasPreloadValue: boolean;
declare readonly preloadValue: string;
+ declare readonly resetOnFocusValue: boolean;
tomSelect: TomSelect | undefined;
private mutationObserver: MutationObserver;
@@ -321,6 +323,19 @@ export default class extends Controller {
return `${this.createOptionTextValue.replace('%placeholder%', `${escapeData(data.input)}`)}
`;
},
},
+ onFocus: () => {
+ if (this.resetOnFocusValue && this.tomSelect) {
+ const query = this.tomSelect.control_input.value.trim();
+ if (query === '') {
+ this.tomSelect.clearOptions();
+ (this.tomSelect as any).loadedSearches = {};
+ if (typeof (this.tomSelect as any).clearPagination === 'function') {
+ (this.tomSelect as any).clearPagination();
+ }
+ this.tomSelect.load('');
+ }
+ }
+ },
preload: this.preload,
});
diff --git a/src/Autocomplete/assets/test/unit/controller.test.ts b/src/Autocomplete/assets/test/unit/controller.test.ts
index ccc4f0fd71c..554c85f42e0 100644
--- a/src/Autocomplete/assets/test/unit/controller.test.ts
+++ b/src/Autocomplete/assets/test/unit/controller.test.ts
@@ -1206,4 +1206,103 @@ describe('AutocompleteController', () => {
// but the absence of "already initialized" error is the key indicator)
expect(newSelect).toBeInTheDocument();
});
+
+ it('reloads options on focus when resetOnFocus is enabled', async () => {
+ const { container, tomSelect } = await startAutocompleteTest(`
+
+
+ `);
+
+ // first focus: initial load
+ fetchMock.mockResponseOnce(
+ JSON.stringify({
+ results: [
+ { value: 1, text: 'pizza' },
+ { value: 2, text: 'popcorn' },
+ ],
+ })
+ );
+
+ const controlInput = tomSelect.control_input;
+
+ userEvent.click(controlInput);
+ await waitFor(() => {
+ expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(2);
+ });
+
+ expect(fetchMock.requests().length).toEqual(1);
+ expect(fetchMock.requests()[0].url).toEqual('/path/to/autocomplete?query=');
+
+ // simulate blur then re-focus: should reload
+ fetchMock.mockResponseOnce(
+ JSON.stringify({
+ results: [
+ { value: 1, text: 'pizza' },
+ { value: 2, text: 'popcorn' },
+ { value: 3, text: 'salad' },
+ ],
+ })
+ );
+
+ // trigger TomSelect's focus event which calls our onFocus callback
+ tomSelect.trigger('focus');
+ await waitFor(() => {
+ expect(fetchMock.requests().length).toEqual(2);
+ });
+
+ expect(fetchMock.requests()[1].url).toEqual('/path/to/autocomplete?query=');
+
+ // open the dropdown to render the new options
+ tomSelect.open();
+ await waitFor(() => {
+ expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(3);
+ });
+ });
+
+ it('does not reload options on focus when resetOnFocus is not set', async () => {
+ const { container, tomSelect } = await startAutocompleteTest(`
+
+
+ `);
+
+ // first focus: initial load (preload: 'focus' is the default)
+ fetchMock.mockResponseOnce(
+ JSON.stringify({
+ results: [
+ { value: 1, text: 'pizza' },
+ { value: 2, text: 'popcorn' },
+ ],
+ })
+ );
+
+ const controlInput = tomSelect.control_input;
+
+ userEvent.click(controlInput);
+ await waitFor(() => {
+ expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(2);
+ });
+
+ expect(fetchMock.requests().length).toEqual(1);
+
+ // blur and re-focus: should NOT make a new request
+ tomSelect.blur();
+ await shortDelay(10);
+
+ userEvent.click(controlInput);
+ await shortDelay(50);
+
+ // still only 1 request — no reload on re-focus
+ expect(fetchMock.requests().length).toEqual(1);
+ });
});
diff --git a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php
index 711ef2e401d..5a97af2059a 100644
--- a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php
+++ b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php
@@ -94,6 +94,10 @@ public function finishView(FormView $view, FormInterface $form, array $options):
$values['create-option-text'] = $this->trans($options['create_option_text']);
$values['preload'] = $options['preload'];
+ if ($options['reset_on_focus']) {
+ $values['reset-on-focus'] = '';
+ }
+
foreach ($values as $name => $value) {
$attr['data-'.$controllerName.'-'.$name.'-value'] = $value;
}
@@ -152,6 +156,7 @@ public function configureOptions(OptionsResolver $resolver): void
'min_characters' => null,
'max_results' => 10,
'preload' => 'focus',
+ 'reset_on_focus' => false,
'extra_options' => [],
]);
diff --git a/src/Autocomplete/tests/Fixtures/Form/ProductType.php b/src/Autocomplete/tests/Fixtures/Form/ProductType.php
index c2c5b35bf18..a34c046481f 100644
--- a/src/Autocomplete/tests/Fixtures/Form/ProductType.php
+++ b/src/Autocomplete/tests/Fixtures/Form/ProductType.php
@@ -54,6 +54,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'createOnBlur' => true,
],
])
+ ->add('portionSizeResetOnFocus', ChoiceType::class, [
+ 'choices' => [
+ 'small' => 's',
+ 'medium' => 'm',
+ 'large' => 'l',
+ ],
+ 'autocomplete' => true,
+ 'reset_on_focus' => true,
+ 'mapped' => false,
+ ])
;
}
diff --git a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php
index 126ac7280b4..0d5294975e5 100644
--- a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php
+++ b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php
@@ -38,6 +38,9 @@ public function testFieldsRenderWithStimulusController()
->assertElementAttributeContains('#product_portionSize', 'data-controller', 'symfony--ux-autocomplete--autocomplete')
->assertElementAttributeContains('#product_tags', 'data-controller', 'symfony--ux-autocomplete--autocomplete')
->assertElementAttributeContains('#product_tags', 'data-symfony--ux-autocomplete--autocomplete-tom-select-options-value', 'createOnBlur')
+
+ ->assertElementAttributeContains('#product_portionSizeResetOnFocus', 'data-controller', 'symfony--ux-autocomplete--autocomplete')
+ ->assertElementAttributeContains('#product_portionSizeResetOnFocus', 'data-symfony--ux-autocomplete--autocomplete-reset-on-focus-value', '')
;
}