Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AutocompleteController Bind settings #1220

Merged
merged 4 commits into from
Dec 9, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AutocompleteController, INPUT_DELAY as _INPUT_DELAY } from './Autocompl
import { waitFor } from '@testing-library/preact';

import { MockClient } from '@searchspring/snap-shared';
import deepmerge from 'deepmerge';

const KEY_ENTER = 13;
const KEY_ESCAPE = 27;
Expand Down Expand Up @@ -226,6 +227,35 @@ describe('Autocomplete Controller', () => {
expect(controller.urlManager.state.query).toBe(undefined);
});

it('can opt out of binding input event', async () => {
const bindingConfig = deepmerge(acConfig, { settings: { bind: { input: false } } });

const controller = new AutocompleteController(bindingConfig, {
client: new MockClient(globals, {}),
store: new AutocompleteStore(bindingConfig, services),
urlManager,
eventManager: new EventManager(),
profiler: new Profiler(),
logger: new Logger(),
tracker: new Tracker(globals),
});

await controller.bind();

let inputEl: HTMLInputElement | null;

await waitFor(() => {
inputEl = document.querySelector(controller.config.selector);
expect(inputEl).toBeDefined();
});

const query = 'bumpers';
inputEl!.value = query;
inputEl!.focus();
inputEl!.dispatchEvent(new Event('input'));
expect(controller.urlManager.state.query).toBe(undefined);
});

it('can bind to input after input has been focused', async () => {
const controller = new AutocompleteController(acConfig, {
client: new MockClient(globals, {}),
Expand Down Expand Up @@ -672,6 +702,85 @@ describe('Autocomplete Controller', () => {
beforeSubmitfn.mockClear();
});

it('can opt out of submit event', async () => {
document.body.innerHTML = '<div><form action="/search.html"><input type="text" id="search_query"></form></div>';

const bindingConfig = deepmerge(acConfig, { settings: { bind: { submit: false } } });

const controller = new AutocompleteController(bindingConfig, {
client: new MockClient(globals, {}),
store: new AutocompleteStore(bindingConfig, services),
urlManager,
eventManager: new EventManager(),
profiler: new Profiler(),
logger: new Logger(),
tracker: new Tracker(globals),
});

await controller.bind();
(controller.client as MockClient).mockData.updateConfig({ autocomplete: 'autocomplete.query.bumpers' });

const inputEl: HTMLInputElement | null = document.querySelector(controller.config.selector);

const query = 'bumpers';
inputEl!.value = query;

const form = inputEl!.form;
const beforeSubmitfn = jest.spyOn(controller.eventManager, 'fire');
const handlerSubmitfn = jest.spyOn(controller.handlers.input, 'formSubmit');

form?.dispatchEvent(new Event('submit', { bubbles: true }));
//this timeout seems to be needed. Cant replace with waitFor
await new Promise((resolve) => setTimeout(resolve, INPUT_DELAY));

expect(beforeSubmitfn).not.toHaveBeenCalledWith('beforeSubmit', {
controller,
input: inputEl!,
});

expect(handlerSubmitfn).not.toHaveBeenCalled();

beforeSubmitfn.mockClear();
});

it('can opt out of submit event (with no form)', async () => {
const bindingConfig = deepmerge(acConfig, { action: '/search', settings: { bind: { submit: false } } });

const controller = new AutocompleteController(bindingConfig, {
client: new MockClient(globals, {}),
store: new AutocompleteStore(bindingConfig, services),
urlManager,
eventManager: new EventManager(),
profiler: new Profiler(),
logger: new Logger(),
tracker: new Tracker(globals),
});

await controller.bind();
(controller.client as MockClient).mockData.updateConfig({ autocomplete: 'autocomplete.query.bumpers' });

const beforeSubmitfn = jest.spyOn(controller.eventManager, 'fire');
const enterKeyfn = jest.spyOn(controller.handlers.input, 'enterKey');
const inputEl: HTMLInputElement | null = document.querySelector(controller.config.selector);

const query = 'bumpers';
inputEl!.value = query;

inputEl!.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, keyCode: KEY_ENTER }));

// this timeout seems to be needed. Cant replace with waitFor
await new Promise((resolve) => setTimeout(resolve, INPUT_DELAY));

expect(beforeSubmitfn).not.toHaveBeenCalledWith('beforeSubmit', {
controller,
input: inputEl!,
});

expect(enterKeyfn).not.toHaveBeenCalled();

beforeSubmitfn.mockClear();
});

it('adds fallback query when integrated spell correct setting is enabled', async () => {
let acConfig2 = { ...acConfig, settings: { integratedSpellCorrection: true } };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const defaultConfig: AutocompleteControllerConfig = {
merchandising: true,
singleResult: false,
},
bind: {
input: true,
submit: true,
},
},
};

Expand Down Expand Up @@ -445,7 +449,7 @@ export class AutocompleteController extends AbstractController {

input.setAttribute(INPUT_ATTRIBUTE, '');

input.addEventListener('input', this.handlers.input.input);
this.config.settings?.bind?.input && input.addEventListener('input', this.handlers.input.input);

if (this.config?.settings?.initializeFromUrl && !input.value && this.store.state.input) {
input.value = this.store.state.input;
Expand All @@ -458,10 +462,10 @@ export class AutocompleteController extends AbstractController {
let formActionUrl: string | undefined;

if (this.config.action) {
input.addEventListener('keydown', this.handlers.input.enterKey);
this.config.settings?.bind?.submit && input.addEventListener('keydown', this.handlers.input.enterKey);
formActionUrl = this.config.action;
} else if (form) {
form.addEventListener('submit', this.handlers.input.formSubmit as unknown as EventListener);
this.config.settings?.bind?.submit && form.addEventListener('submit', this.handlers.input.formSubmit as unknown as EventListener);
formActionUrl = form.action || '';

// serializeForm will include additional form element in our urlManager as globals
Expand Down
2 changes: 2 additions & 0 deletions packages/snap-controller/src/Autocomplete/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The `AutocompleteController` is used when making queries to the API `autocomplet
| settings.history.showResults | if history limit is set and there is no input, the first term results will be displayed | false | |
| settings.redirects.merchandising | boolean to disable merchandising redirects when ac form is submitted | true | |
| settings.redirects.singleResult | enable redirect to product detail page if search yields 1 result count | false | |
| settings.bind.input | boolean to disable binding of the input element (selector) | true | |
| settings.bind.submit | boolean to disable binding of the submit event (form submission of enter key press) | true | |
| settings.variants.field | used to set the field in which to grab the variant data from || |
| settings.variants.realtime.enabled | enable real time variant updates || |
| settings.variants.realtime.filters | specify which filters to use to determine which results are updated || |
Expand Down
4 changes: 4 additions & 0 deletions packages/snap-store-mobx/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ export type AutocompleteStoreConfigSettings = {
merchandising?: boolean;
singleResult?: boolean;
};
bind?: {
input?: boolean;
submit?: boolean;
};
};

// Autocomplete config
Expand Down
Loading