Skip to content

Commit cf6593a

Browse files
authored
feat(filters): display operator into input text filter from Grid Presets (#719)
- a use case that we have with the text InputFilter is that if have a filter that includes an operator inside the search text (e.g.: ">=55") and we save the Grid State in Local Storage or anything else and then we reload them as Grid Presets, the operator won't be showing up (it would only show "55" while keeping the Operator hidden but still in play) that is because the FilterService is doing the split (this is an Operator and these are the SearchTerm) and the Grid State only keeps the split, so in that case this PR adds an extra step that if we're using the `setValues()` method, then we're probably using a Grid Preset or an UpdateFilters and we should display the operator as part of the final displayed search string inside the input value
1 parent f123854 commit cf6593a

File tree

3 files changed

+163
-73
lines changed

3 files changed

+163
-73
lines changed

src/app/modules/angular-slickgrid/filters/__tests__/inputFilter.spec.ts

+112-70
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('InputFilter', () => {
2323
let divContainer: HTMLDivElement;
2424
let filter: InputFilter;
2525
let filterArguments: FilterArguments;
26-
let spyGetHeaderRow;
26+
let spyGetHeaderRow: any;
2727
let mockColumn: Column;
2828

2929
beforeEach(() => {
@@ -47,7 +47,7 @@ describe('InputFilter', () => {
4747
});
4848

4949
it('should throw an error when trying to call init without any arguments', () => {
50-
expect(() => filter.init(null)).toThrowError('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.');
50+
expect(() => filter.init(null as any)).toThrowError('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.');
5151
});
5252

5353
it('should initialize the filter', () => {
@@ -61,101 +61,143 @@ describe('InputFilter', () => {
6161

6262
it('should have a placeholder when defined in its column definition', () => {
6363
const testValue = 'test placeholder';
64-
mockColumn.filter.placeholder = testValue;
64+
mockColumn.filter!.placeholder = testValue;
6565

6666
filter.init(filterArguments);
67-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
67+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
6868

6969
expect(filterElm.placeholder).toBe(testValue);
7070
});
7171

72-
it('should call "setValues" and expect that value to be in the callback when triggered', () => {
73-
const spyCallback = jest.spyOn(filterArguments, 'callback');
72+
describe('setValues method', () => {
73+
it('should call "setValues" and expect that value to be in the callback when triggered', () => {
74+
const spyCallback = jest.spyOn(filterArguments, 'callback');
7475

75-
filter.init(filterArguments);
76-
filter.setValues('abc');
77-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
76+
filter.init(filterArguments);
77+
filter.setValues('abc');
78+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
7879

79-
filterElm.focus();
80-
filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true }));
81-
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
80+
filterElm.focus();
81+
filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true }));
82+
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
8283

83-
expect(filterFilledElms.length).toBe(1);
84-
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true });
85-
});
84+
expect(filterFilledElms.length).toBe(1);
85+
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true });
86+
});
8687

87-
it('should call "setValues" and expect that value to be in the callback when triggered by ENTER key', () => {
88-
const spyCallback = jest.spyOn(filterArguments, 'callback');
88+
it('should call "setValues" and expect that value to be in the callback when triggered by ENTER key', () => {
89+
const spyCallback = jest.spyOn(filterArguments, 'callback');
8990

90-
filter.init(filterArguments);
91-
filter.setValues('abc');
92-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
91+
filter.init(filterArguments);
92+
filter.setValues('abc');
93+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
9394

94-
filterElm.focus();
95-
const event = new (window.window as any).Event('keyup', { bubbles: true, cancelable: true });
96-
event.key = 'Enter';
97-
filterElm.dispatchEvent(event);
98-
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
95+
filterElm.focus();
96+
const event = new (window.window as any).Event('keyup', { bubbles: true, cancelable: true });
97+
event.key = 'Enter';
98+
filterElm.dispatchEvent(event);
99+
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
99100

100-
expect(filterFilledElms.length).toBe(1);
101-
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true });
102-
});
101+
expect(filterFilledElms.length).toBe(1);
102+
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true });
103+
});
103104

104-
it('should call "setValues" and expect that value NOT to be in the callback when triggered by a keyup event that is NOT the ENTER key', () => {
105-
const spyCallback = jest.spyOn(filterArguments, 'callback');
105+
it('should call "setValues" and expect that value NOT to be in the callback when triggered by a keyup event that is NOT the ENTER key', () => {
106+
const spyCallback = jest.spyOn(filterArguments, 'callback');
106107

107-
filter.init(filterArguments);
108-
filter.setValues('abc');
109-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
108+
filter.init(filterArguments);
109+
filter.setValues('abc');
110+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
110111

111-
filterElm.focus();
112-
const event = new (window.window as any).Event('keyup', { bubbles: true, cancelable: true });
113-
event.key = 'a';
114-
filterElm.dispatchEvent(event);
115-
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
112+
filterElm.focus();
113+
const event = new (window.window as any).Event('keyup', { bubbles: true, cancelable: true });
114+
event.key = 'a';
115+
filterElm.dispatchEvent(event);
116+
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
116117

117-
expect(filterFilledElms.length).toBe(0);
118-
expect(spyCallback).not.toHaveBeenCalled();
119-
});
118+
expect(filterFilledElms.length).toBe(0);
119+
expect(spyCallback).not.toHaveBeenCalled();
120+
});
120121

121-
it('should call "setValues" an operator and with extra spaces at the beginning of the searchTerms and trim value when "enableFilterTrimWhiteSpace" is enabled in grid options', () => {
122-
gridOptionMock.enableFilterTrimWhiteSpace = true;
123-
const spyCallback = jest.spyOn(filterArguments, 'callback');
122+
it('should call "setValues" an operator and with extra spaces at the beginning of the searchTerms and trim value when "enableFilterTrimWhiteSpace" is enabled in grid options', () => {
123+
gridOptionMock.enableFilterTrimWhiteSpace = true;
124+
const spyCallback = jest.spyOn(filterArguments, 'callback');
124125

125-
filter.init(filterArguments);
126-
filter.setValues(' abc ', 'EQ');
127-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
126+
filter.init(filterArguments);
127+
filter.setValues(' abc ', 'EQ');
128+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
128129

129-
filterElm.focus();
130-
filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true }));
131-
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
130+
filterElm.focus();
131+
filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true }));
132+
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
132133

133-
expect(filterFilledElms.length).toBe(1);
134-
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['abc'], shouldTriggerQuery: true });
135-
});
134+
expect(filterFilledElms.length).toBe(1);
135+
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['abc'], shouldTriggerQuery: true });
136+
});
136137

137-
it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableTrimWhiteSpace" is enabled in the column filter', () => {
138-
gridOptionMock.enableFilterTrimWhiteSpace = false;
139-
mockColumn.filter.enableTrimWhiteSpace = true;
140-
const spyCallback = jest.spyOn(filterArguments, 'callback');
138+
it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableTrimWhiteSpace" is enabled in the column filter', () => {
139+
gridOptionMock.enableFilterTrimWhiteSpace = false;
140+
mockColumn.filter!.enableTrimWhiteSpace = true;
141+
const spyCallback = jest.spyOn(filterArguments, 'callback');
141142

142-
filter.init(filterArguments);
143-
filter.setValues(' abc ');
144-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
143+
filter.init(filterArguments);
144+
filter.setValues(' abc ');
145+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
145146

146-
filterElm.focus();
147-
filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true }));
148-
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
147+
filterElm.focus();
148+
filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true }));
149+
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
150+
151+
expect(filterFilledElms.length).toBe(1);
152+
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true });
153+
});
154+
155+
it('should call "setValues" and include an operator and expect the operator to show up in the output search string shown in the filter input text value', () => {
156+
filter.init(filterArguments);
157+
158+
filter.setValues('abc', '<>');
159+
expect(filter.getValue()).toBe('<>abc');
160+
161+
filter.setValues('abc', '!=');
162+
expect(filter.getValue()).toBe('!=abc');
163+
164+
filter.setValues('abc', '=');
165+
expect(filter.getValue()).toBe('=abc');
166+
167+
filter.setValues('abc', '==');
168+
expect(filter.getValue()).toBe('==abc');
169+
170+
filter.setValues(123, '<');
171+
expect(filter.getValue()).toBe('<123');
172+
173+
filter.setValues(123, '<=');
174+
expect(filter.getValue()).toBe('<=123');
175+
176+
filter.setValues(123, '>');
177+
expect(filter.getValue()).toBe('>123');
178+
179+
filter.setValues(123, '>=');
180+
expect(filter.getValue()).toBe('>=123');
181+
182+
filter.setValues('abc', 'EndsWith');
183+
expect(filter.getValue()).toBe('*abc');
184+
185+
filter.setValues('abc', '*z');
186+
expect(filter.getValue()).toBe('*abc');
187+
188+
filter.setValues('abc', 'StartsWith');
189+
expect(filter.getValue()).toBe('abc*');
149190

150-
expect(filterFilledElms.length).toBe(1);
151-
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['abc'], shouldTriggerQuery: true });
191+
filter.setValues('abc', 'a*');
192+
expect(filter.getValue()).toBe('abc*');
193+
});
152194
});
153195

154196
it('should trigger the callback method when user types something in the input', () => {
155197
const spyCallback = jest.spyOn(filterArguments, 'callback');
156198

157199
filter.init(filterArguments);
158-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
200+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
159201

160202
filterElm.focus();
161203
filterElm.value = 'a';
@@ -168,7 +210,7 @@ describe('InputFilter', () => {
168210
filterArguments.searchTerms = ['xyz'];
169211

170212
filter.init(filterArguments);
171-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
213+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
172214

173215
expect(filterElm.value).toBe('xyz');
174216
});
@@ -177,7 +219,7 @@ describe('InputFilter', () => {
177219
filterArguments.searchTerms = [''];
178220

179221
filter.init(filterArguments);
180-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
222+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
181223
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
182224

183225
expect(filterElm.value).toBe('');
@@ -190,7 +232,7 @@ describe('InputFilter', () => {
190232

191233
filter.init(filterArguments);
192234
filter.clear();
193-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
235+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
194236
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
195237

196238
expect(filterElm.value).toBe('');
@@ -204,7 +246,7 @@ describe('InputFilter', () => {
204246

205247
filter.init(filterArguments);
206248
filter.clear(false);
207-
const filterElm = divContainer.querySelector<HTMLInputElement>('input.filter-duration');
249+
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
208250
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
209251

210252

src/app/modules/angular-slickgrid/filters/inputFilter.ts

+50-2
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,17 @@ export class InputFilter implements Filter {
111111
this.$filterElm = null;
112112
}
113113

114+
getValue() {
115+
return this.$filterElm.val();
116+
}
117+
114118
/** Set value(s) on the DOM element */
115119
setValues(values: SearchTerm | SearchTerm[], operator?: OperatorType | OperatorString) {
116-
if (values) {
117-
this.$filterElm.val(values);
120+
const searchValues = Array.isArray(values) ? values : [values];
121+
let searchValue: SearchTerm = '';
122+
for (const value of searchValues) {
123+
searchValue = operator ? this.addOptionalOperatorIntoSearchString(value, operator) : value;
124+
this.$filterElm.val(searchValue);
118125
}
119126

120127
// set the operator when defined
@@ -125,6 +132,47 @@ export class InputFilter implements Filter {
125132
// protected functions
126133
// ------------------
127134

135+
/**
136+
* When loading the search string from the outside into the input text field, we should also add the prefix/suffix of the operator.
137+
* We do this so that if it was loaded by a Grid Presets then we should also add the operator into the search string
138+
* Let's take these 3 examples:
139+
* 1. (operator: '>=', searchTerms:[55]) should display as ">=55"
140+
* 2. (operator: 'StartsWith', searchTerms:['John']) should display as "John*"
141+
* 3. (operator: 'EndsWith', searchTerms:['John']) should display as "*John"
142+
* @param operator - operator string
143+
*/
144+
protected addOptionalOperatorIntoSearchString(inputValue: SearchTerm, operator: OperatorType | OperatorString): string {
145+
let searchTermPrefix = '';
146+
let searchTermSuffix = '';
147+
let outputValue = inputValue === undefined || inputValue === null ? '' : `${inputValue}`;
148+
149+
if (operator && outputValue) {
150+
switch (operator) {
151+
case '<>':
152+
case '!=':
153+
case '=':
154+
case '==':
155+
case '>':
156+
case '>=':
157+
case '<':
158+
case '<=':
159+
searchTermPrefix = operator;
160+
break;
161+
case 'EndsWith':
162+
case '*z':
163+
searchTermPrefix = '*';
164+
break;
165+
case 'StartsWith':
166+
case 'a*':
167+
searchTermSuffix = '*';
168+
break;
169+
}
170+
outputValue = `${searchTermPrefix}${outputValue}${searchTermSuffix}`;
171+
}
172+
173+
return outputValue;
174+
}
175+
128176
/**
129177
* Create the HTML template as a string
130178
*/

src/app/modules/angular-slickgrid/services/filter.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,7 @@ export class FilterService {
925925
// when hiding/showing (with Column Picker or Grid Menu), it will try to re-create yet again the filters (since SlickGrid does a re-render)
926926
// we need to also set again the values in the DOM elements if the values were set by a searchTerm(s)
927927
if (searchTerms && newFilter.setValues) {
928-
newFilter.setValues(searchTerms);
928+
newFilter.setValues(searchTerms, operator);
929929
}
930930
}
931931
}

0 commit comments

Comments
 (0)