Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Autocomplete/assets/dist/controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ declare class export_default extends Controller {
minCharacters: NumberConstructor;
tomSelectOptions: ObjectConstructor;
preload: StringConstructor;
resetOnFocus: BooleanConstructor;
};
readonly urlValue: string;
readonly optionsAsHtmlValue: boolean;
Expand All @@ -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;
Expand Down
13 changes: 12 additions & 1 deletion src/Autocomplete/assets/dist/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,16 @@ function _createAutocompleteWithRemoteData(autocompleteEndpointUrl, minCharacter
return `<div class="create">${this.createOptionTextValue.replace("%placeholder%", `<strong>${escapeData(data.input)}</strong>`)}</div>`;
}
},
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);
Expand Down Expand Up @@ -333,6 +343,7 @@ _Class.values = {
createOptionText: String,
minCharacters: Number,
tomSelectOptions: Object,
preload: String
preload: String,
resetOnFocus: Boolean
};
export { _Class as default };
15 changes: 15 additions & 0 deletions src/Autocomplete/assets/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default class extends Controller {
minCharacters: Number,
tomSelectOptions: Object,
preload: String,
resetOnFocus: Boolean,
};

declare readonly urlValue: string;
Expand All @@ -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;
Expand Down Expand Up @@ -321,6 +323,19 @@ export default class extends Controller {
return `<div class="create">${this.createOptionTextValue.replace('%placeholder%', `<strong>${escapeData(data.input)}</strong>`)}</div>`;
},
},
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();
}
Comment on lines +331 to +334
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the as any, it's often a bad practice, and it does not make sense here AFAIK.

this.tomSelect.load('');
}
}
},
preload: this.preload,
});

Expand Down
99 changes: 99 additions & 0 deletions src/Autocomplete/assets/test/unit/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
<label for="the-select">Items</label>
<select
id="the-select"
data-testid="main-element"
data-controller="autocomplete"
data-autocomplete-url-value="/path/to/autocomplete"
data-autocomplete-reset-on-focus-value="true"
></select>
`);

// 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(`
<label for="the-select">Items</label>
<select
id="the-select"
data-testid="main-element"
data-controller="autocomplete"
data-autocomplete-url-value="/path/to/autocomplete"
></select>
`);

// 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);
});
});
5 changes: 5 additions & 0 deletions src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -152,6 +156,7 @@ public function configureOptions(OptionsResolver $resolver): void
'min_characters' => null,
'max_results' => 10,
'preload' => 'focus',
'reset_on_focus' => false,
'extra_options' => [],
]);

Expand Down
10 changes: 10 additions & 0 deletions src/Autocomplete/tests/Fixtures/Form/ProductType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
])
;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', '')
;
}

Expand Down
Loading