Skip to content

Commit

Permalink
Merge pull request #1220 from searchspring/autocomplete-bind-settings
Browse files Browse the repository at this point in the history
AutocompleteController Bind settings
  • Loading branch information
korgon authored Dec 9, 2024
2 parents 0edb1ae + 3361def commit a9d884e
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 3 deletions.
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

0 comments on commit a9d884e

Please sign in to comment.