Skip to content

Commit

Permalink
feat(editors): add onSelect callback to Autocomplete Editor (#286)
Browse files Browse the repository at this point in the history
- since user cannot override the jQueryUI `select` callback (because that would break Slickgrid-Universal code), we can instead provide an extra callback, `onSelect`, to do the same with even more arguments
  • Loading branch information
ghiscoding authored Mar 17, 2021
1 parent 3174c86 commit 2d106d4
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export class Example12 {
editorOptions: {
minLength: 0,
openSearchListOnFocus: false,
// onSelect: (e, ui, row, cell, column, dataContext) => console.log(ui, column, dataContext),
source: (request, response) => {
const countries: any[] = require('./data/countries.json');
const foundCountries = countries.filter((country) => country.name.toLowerCase().includes(request.term.toLowerCase()));
Expand Down
43 changes: 30 additions & 13 deletions packages/common/src/editors/__tests__/autoCompleteEditor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ describe('AutoCompleteEditor', () => {
});
});

describe('onSelect method', () => {
describe('handleSelect method', () => {
beforeEach(() => {
jest.clearAllMocks();
});
Expand All @@ -636,7 +636,7 @@ describe('AutoCompleteEditor', () => {

editor = new AutoCompleteEditor(editorArguments);
const spySetValue = jest.spyOn(editor, 'setValue');
const output = editor.onSelect(null as any, { item: mockItemData.gender });
const output = editor.handleSelect(null as any, { item: mockItemData.gender });

expect(output).toBe(false);
expect(commitEditSpy).not.toHaveBeenCalled();
Expand All @@ -652,10 +652,10 @@ describe('AutoCompleteEditor', () => {

editor = new AutoCompleteEditor(editorArguments);
const spySetValue = jest.spyOn(editor, 'setValue');
const output = editor.onSelect(null as any, { item: mockItemData.gender });
const output = editor.handleSelect(null as any, { item: mockItemData.gender });

// HOW DO WE TRIGGER the jQuery UI autocomplete select event? The following works only on "autocompleteselect"
// but that doesn't trigger the "select" (onSelect) directly
// but that doesn't trigger the "select" (handleSelect) directly
// const editorElm = editor.editorDomElement;
// editorElm.on('autocompleteselect', (event, ui) => console.log(ui));
// editorElm[0].dispatchEvent(new (window.window as any).CustomEvent('autocompleteselect', { detail: { item: 'female' }, bubbles: true, cancelable: true }));
Expand All @@ -675,58 +675,75 @@ describe('AutoCompleteEditor', () => {

editor = new AutoCompleteEditor(editorArguments);
const spySetValue = jest.spyOn(editor, 'setValue');
const output = editor.onSelect(null as any, { item: mockItemData.gender });
const output = editor.handleSelect(null as any, { item: mockItemData.gender });

expect(output).toBe(false);
expect(commitEditSpy).toHaveBeenCalled();
expect(spySetValue).toHaveBeenCalledWith('Female');
expect(editor.isValueTouched()).toBe(true);
});

it('should expect the "onSelect" method to be called when the callback method is triggered when user provide his own filterOptions', () => {
it('should expect the "handleSelect" method to be called when the callback method is triggered when user provide his own filterOptions', () => {
gridOptionMock.autoCommitEdit = true;
(mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { source: [], minLength: 3 } as AutocompleteOption;

const event = new CustomEvent('change');
editor = new AutoCompleteEditor(editorArguments);
const spy = jest.spyOn(editor, 'onSelect');
const spy = jest.spyOn(editor, 'handleSelect');
editor.autoCompleteOptions.select!(event, { item: 'fem' });

expect(spy).toHaveBeenCalledWith(event, { item: 'fem' });
expect(editor.isValueTouched()).toBe(true);
});

it('should expect the "onSelect" method to be called when the callback method is triggered', () => {
it('should expect the "handleSelect" method to be called when the callback method is triggered', () => {
gridOptionMock.autoCommitEdit = true;
(mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }];
mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true };

const event = new CustomEvent('change');
editor = new AutoCompleteEditor(editorArguments);
const spy = jest.spyOn(editor, 'onSelect');
const spy = jest.spyOn(editor, 'handleSelect');
editor.autoCompleteOptions.select!(event, { item: 'fem' });

expect(spy).toHaveBeenCalledWith(event, { item: 'fem' });
expect(editor.isValueTouched()).toBe(true);
});

it('should initialize the editor with editorOptions and expect the "onSelect" method to be called when the callback method is triggered', () => {
it('should initialize the editor with editorOptions and expect the "handleSelect" method to be called when the callback method is triggered', () => {
gridOptionMock.autoCommitEdit = true;
(mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }];
(mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleteOption;
mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true };

const event = new CustomEvent('change');
editor = new AutoCompleteEditor(editorArguments);
const onSelectSpy = jest.spyOn(editor, 'onSelect');
const handleSelectSpy = jest.spyOn(editor, 'handleSelect');
const focusSpy = jest.spyOn(editor, 'focus');
editor.autoCompleteOptions.select!(event, { item: 'fem' });
jest.runAllTimers(); // fast-forward timer

expect(onSelectSpy).toHaveBeenCalledWith(event, { item: 'fem' });
expect(handleSelectSpy).toHaveBeenCalledWith(event, { item: 'fem' });
expect(focusSpy).toHaveBeenCalled();
expect(editor.isValueTouched()).toBe(true);
});

it('should expect the "onSelect" method to be called when defined and the callback method is triggered when user provide his own filterOptions', () => {
gridOptionMock.autoCommitEdit = true;
const mockOnSelect = jest.fn();
const activeCellMock = { row: 1, cell: 0 };
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock);
(mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { source: [], minLength: 3, onSelect: mockOnSelect } as AutocompleteOption;

const event = new CustomEvent('change');
editor = new AutoCompleteEditor(editorArguments);
const handleSelectSpy = jest.spyOn(editor, 'handleSelect');
editor.autoCompleteOptions.select!(event, { item: 'fem' });

expect(handleSelectSpy).toHaveBeenCalledWith(event, { item: 'fem' });
expect(mockOnSelect).toHaveBeenCalledWith(event, { item: 'fem' }, activeCellMock.row, activeCellMock.cell, mockColumn, mockItemData);
expect(editor.isValueTouched()).toBe(true);
});
});

describe('renderItem callback method', () => {
Expand Down Expand Up @@ -935,7 +952,7 @@ describe('AutoCompleteEditor', () => {

editor = new AutoCompleteEditor(editorArguments);
const spySetValue = jest.spyOn(editor, 'setValue');
const output = editor.onSelect(null as any, { item: mockItemData.gender });
const output = editor.handleSelect(null as any, { item: mockItemData.gender });

expect(output).toBe(false);
expect(spySetValue).toHaveBeenCalledWith('female');
Expand Down
14 changes: 11 additions & 3 deletions packages/common/src/editors/autoCompleteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ export class AutoCompleteEditor implements Editor {

// this function should be protected but for unit tests purposes we'll make it public until a better solution is found
// a better solution would be to get the autocomplete DOM element to work with selection but I couldn't find how to do that in Jest
onSelect(event: Event, ui: { item: any; }) {
handleSelect(event: Event, ui: { item: any; }) {
if (ui && ui.item) {
const selectedItem = ui && ui.item;
this._currentValue = selectedItem;
Expand All @@ -466,6 +466,14 @@ export class AutoCompleteEditor implements Editor {
} else {
this.save();
}

// if user wants to hook to the "select", he can do via this "onSelect"
// it purposely has a similar signature as the "select" callback + some extra arguments (row, cell, column, dataContext)
if (this.editorOptions.onSelect) {
const activeCell = this.grid.getActiveCell();
this.editorOptions.onSelect(event, ui, activeCell.row, activeCell.cell, this.args.column, this.args.item);
}

setTimeout(() => this._lastTriggeredByClearInput = false); // reset flag after a cycle
}
return false;
Expand Down Expand Up @@ -575,7 +583,7 @@ export class AutoCompleteEditor implements Editor {
// when user passes it's own autocomplete options
// we still need to provide our own "select" callback implementation
if (autoCompleteOptions?.source) {
autoCompleteOptions.select = (event: Event, ui: { item: any; }) => this.onSelect(event, ui);
autoCompleteOptions.select = (event: Event, ui: { item: any; }) => this.handleSelect(event, ui);
this._autoCompleteOptions = { ...autoCompleteOptions };

// when "renderItem" is defined, we need to add our custom style CSS class
Expand All @@ -595,7 +603,7 @@ export class AutoCompleteEditor implements Editor {
const definedOptions: AutocompleteOption = {
source: finalCollection,
minLength: 0,
select: (event: Event, ui: { item: any; }) => this.onSelect(event, ui),
select: (event: Event, ui: { item: any; }) => this.handleSelect(event, ui),
};
this._autoCompleteOptions = { ...definedOptions, ...(this.columnEditor.editorOptions as AutocompleteOption) };
this._$input.autocomplete(this._autoCompleteOptions);
Expand Down
16 changes: 15 additions & 1 deletion packages/common/src/interfaces/autocompleteOption.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Column } from './column.interface';

export type JQueryAjaxFn = (request: any, response: any) => void;

export interface AutoCompleteRenderItemDefinition {
Expand Down Expand Up @@ -56,9 +58,21 @@ export interface AutocompleteOption {
/** Triggered when the input value becomes in focus */
focus?: (e: Event, ui: { item: any; }) => boolean;

/**
* Triggered when a value is selected from the autocomplete list.
* This is the same as the "select" callback and was created so that user don't overwrite exclusive usage of the "select" callback.
* Also compare to the "select", it has some extra arguments which are: row, cell, column, dataContext
*/
onSelect?: (e: Event, ui: { item: any; }, row: number, cell: number, columnDef: Column, dataContext: any) => boolean;

/** Triggered when user enters a search value */
search?: (e: Event, ui: { item: any; }) => boolean;

/** Triggered when a value is selected from the autocomplete list */
/**
* Triggered when a value is selected from the autocomplete list.
* NOTE: this method should NOT be used since Slickgrid-Universal will use it exclusively
* and if you do try to use it, what will happen is that it will override and break Slickgrid-Universal internal code.
* Please use the "onSelect" which was added specifically to avoid this problem but still provide exact same result
*/
select?: (e: Event, ui: { item: any; }) => boolean;
}
Binary file not shown.

0 comments on commit 2d106d4

Please sign in to comment.