container to simulate the grid container
@@ -86,14 +88,6 @@ describe('AutoCompleteFilter', () => {
}
});
- it('should throw an error when "collectionAsync" Promise does not return a valid array', (done) => {
- mockColumn.filter!.collectionAsync = Promise.resolve({ hello: 'world' });
- filter.init(filterArguments).catch((e) => {
- expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the AutoComplete Filter, the collection is not a valid array.`);
- done();
- });
- });
-
it('should initialize the filter', () => {
mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
filter.init(filterArguments);
@@ -249,55 +243,49 @@ describe('AutoCompleteFilter', () => {
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false });
});
- it('should create the filter with a default search term when using "collectionAsync" as a Promise', (done) => {
+ it('should create the filter with a default search term when using "collectionAsync" as a Promise', async () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');
const mockCollection = ['male', 'female'];
mockColumn.filter!.collectionAsync = Promise.resolve(mockCollection);
filterArguments.searchTerms = ['female'];
- filter.init(filterArguments);
+ await filter.init(filterArguments);
- setTimeout(() => {
- const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement;
- const autocompleteUlElms = document.body.querySelectorAll
('ul.ui-autocomplete');
- filter.setValues('male');
+ const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement;
+ const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete');
+ filter.setValues('male');
- filterElm.focus();
- filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true }));
- const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled');
+ filterElm.focus();
+ filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true }));
+ const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled');
- expect(autocompleteUlElms.length).toBe(1);
- expect(filterFilledElms.length).toBe(1);
- expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true });
- done();
- });
+ expect(autocompleteUlElms.length).toBe(1);
+ expect(filterFilledElms.length).toBe(1);
+ expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true });
});
- it('should create the filter with a default search term when using "collectionAsync" as a Promise with content to simulate http-client', (done) => {
+ it('should create the filter with a default search term when using "collectionAsync" as a Promise with content to simulate http-client', async () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');
const mockCollection = ['male', 'female'];
mockColumn.filter!.collectionAsync = Promise.resolve({ content: mockCollection });
filterArguments.searchTerms = ['female'];
- filter.init(filterArguments);
+ await filter.init(filterArguments);
- setTimeout(() => {
- const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement;
- const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete');
- filter.setValues('male');
+ const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement;
+ const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete');
+ filter.setValues('male');
- filterElm.focus();
- filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true }));
- const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled');
+ filterElm.focus();
+ filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true }));
+ const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled');
- expect(autocompleteUlElms.length).toBe(1);
- expect(filterFilledElms.length).toBe(1);
- expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true });
- done();
- });
+ expect(autocompleteUlElms.length).toBe(1);
+ expect(filterFilledElms.length).toBe(1);
+ expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true });
});
- it('should create the filter with a default search term when using "collectionAsync" is a Fetch Promise', (done) => {
+ it('should create the filter with a default search term when using "collectionAsync" is a Fetch Promise', async () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');
const mockCollection = ['male', 'female'];
@@ -309,22 +297,19 @@ describe('AutoCompleteFilter', () => {
mockColumn.filter!.collectionAsync = http.fetch('/api', { method: 'GET' });
filterArguments.searchTerms = ['female'];
- filter.init(filterArguments);
+ await filter.init(filterArguments);
- setTimeout(() => {
- const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement;
- const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete');
- filter.setValues('male');
+ const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement;
+ const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete');
+ filter.setValues('male');
- filterElm.focus();
- filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true }));
- const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled');
+ filterElm.focus();
+ filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true }));
+ const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled');
- expect(autocompleteUlElms.length).toBe(1);
- expect(filterFilledElms.length).toBe(1);
- expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true });
- done();
- });
+ expect(autocompleteUlElms.length).toBe(1);
+ expect(filterFilledElms.length).toBe(1);
+ expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true });
});
it('should create the filter and filter the string collection when "collectionFilterBy" is set', () => {
@@ -410,25 +395,25 @@ describe('AutoCompleteFilter', () => {
expect(filterCollection[2]).toEqual({ value: 'female', description: 'female' });
});
- it('should create the filter with a value/label pair collectionAsync that is inside an object when "collectionInsideObjectProperty" is defined with a dot notation', (done) => {
- const mockCollection = { deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } };
- mockColumn.filter = {
- collectionAsync: Promise.resolve(mockCollection),
- collectionOptions: { collectionInsideObjectProperty: 'deep.myCollection' },
- customStructure: { value: 'value', label: 'description', },
- };
-
- filter.init(filterArguments);
+ it('should create the filter with a value/label pair collectionAsync that is inside an object when "collectionInsideObjectProperty" is defined with a dot notation', async () => {
+ try {
+ const mockCollection = { deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } };
+ mockColumn.filter = {
+ collectionAsync: Promise.resolve(mockCollection),
+ collectionOptions: { collectionInsideObjectProperty: 'deep.myCollection' },
+ customStructure: { value: 'value', label: 'description', },
+ };
- setTimeout(() => {
+ await filter.init(filterArguments);
const filterCollection = filter.collection as any[];
expect(filterCollection.length).toBe(3);
expect(filterCollection[0]).toEqual({ value: 'other', description: 'other' });
expect(filterCollection[1]).toEqual({ value: 'male', description: 'male' });
expect(filterCollection[2]).toEqual({ value: 'female', description: 'female' });
- done();
- }, 2);
+ } catch (e) {
+ console.log('ERROR', e)
+ }
});
it('should create the filter and sort the string collection when "collectionSortBy" is set', () => {
@@ -534,27 +519,44 @@ describe('AutoCompleteFilter', () => {
expect(spy).toHaveBeenCalledWith(event, { item: 'fem' });
});
- it('should expect the "onSelect" method to be called when the callback method is triggered', () => {
- const spy = jest.spyOn(filter, 'onSelect');
- const event = new CustomEvent('change');
+ it('should trigger a re-render of the DOM element when collection is replaced by new collection', async () => {
+ const renderSpy = jest.spyOn(filter, 'renderDomElement');
+ const newCollection = [{ value: 'val1', label: 'label1' }, { value: 'val2', label: 'label2' }];
+ const mockDataResponse = [{ value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }];
- mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
- filter.init(filterArguments);
- filter.autoCompleteOptions!.select!(event, { item: 'fem' });
+ mockColumn.filter = {
+ collection: [],
+ collectionAsync: Promise.resolve(mockDataResponse),
+ enableCollectionWatch: true,
+ };
- expect(spy).toHaveBeenCalledWith(event, { item: 'fem' });
+ await filter.init(filterArguments);
+ mockColumn.filter!.collection = newCollection;
+ mockColumn.filter!.collection!.push({ value: 'val3', label: 'label3' });
+
+ jest.runAllTimers(); // fast-forward timer]
+
+ expect(renderSpy).toHaveBeenCalledTimes(3);
+ expect(renderSpy).toHaveBeenCalledWith(newCollection);
});
- it('should initialize the filter with filterOptions and expect the "onSelect" method to be called when the callback method is triggered', () => {
- const spy = jest.spyOn(filter, 'onSelect');
- const event = new CustomEvent('change');
+ it('should trigger a re-render of the DOM element when collection changes', async () => {
+ const renderSpy = jest.spyOn(filter, 'renderDomElement');
+ const mockDataResponse = [{ value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }];
- mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
- mockColumn.filter!.filterOptions = { minLength: 3 } as AutocompleteOption;
- filter.init(filterArguments);
- filter.autoCompleteOptions!.select!(event, { item: 'fem' });
+ mockColumn.filter = {
+ collection: [],
+ collectionAsync: new Promise((resolve) => resolve(mockDataResponse)),
+ enableCollectionWatch: true,
+ };
- expect(spy).toHaveBeenCalledWith(event, { item: 'fem' });
+ await filter.init(filterArguments);
+ mockColumn.filter!.collection!.push({ value: 'other', label: 'other' });
+
+ jest.runAllTimers(); // fast-forward timer
+
+ expect(renderSpy).toHaveBeenCalledTimes(2);
+ expect(renderSpy).toHaveBeenCalledWith(mockColumn.filter!.collection);
});
});
@@ -645,5 +647,14 @@ describe('AutoCompleteFilter', () => {
const liElm = ulElm.querySelector('li') as HTMLLIElement;
expect(liElm.innerHTML).toBe(mockTemplateString);
});
+
+ it('should throw an error when "collectionAsync" Promise does not return a valid array', (done) => {
+ const promise = Promise.resolve({ hello: 'world' });
+ mockColumn.filter!.collectionAsync = promise;
+ filter.init(filterArguments).catch((e) => {
+ expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the AutoComplete Filter, the collection is not a valid array.`);
+ done();
+ });
+ });
});
});
diff --git a/packages/common/src/filters/__tests__/selectFilter.spec.ts b/packages/common/src/filters/__tests__/selectFilter.spec.ts
index defe3bd6c..7e4fcd24e 100644
--- a/packages/common/src/filters/__tests__/selectFilter.spec.ts
+++ b/packages/common/src/filters/__tests__/selectFilter.spec.ts
@@ -64,7 +64,6 @@ describe('SelectFilter', () => {
});
afterEach(() => {
- mockColumn.filter = undefined;
filter.destroy();
jest.clearAllMocks();
});
@@ -732,6 +731,62 @@ describe('SelectFilter', () => {
expect(filterListElm[2].textContent).toBe('female');
});
+ it('should trigger a re-render of the DOM element when collection is replaced by new collection', async () => {
+ const renderSpy = jest.spyOn(filter, 'renderDomElement');
+ const newCollection = [{ value: 'val1', label: 'label1' }, { value: 'val2', label: 'label2' }];
+ const mockDataResponse = [{ value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }];
+
+ mockColumn.filter = {
+ collection: [],
+ collectionAsync: Promise.resolve(mockDataResponse),
+ enableCollectionWatch: true,
+ };
+
+ await filter.init(filterArguments);
+ mockColumn.filter!.collection = newCollection;
+ mockColumn.filter!.collection!.push({ value: 'val3', label: 'label3' });
+
+ jest.runAllTimers(); // fast-forward timer
+
+ expect(renderSpy).toHaveBeenCalledTimes(3);
+ expect(renderSpy).toHaveBeenCalledWith(newCollection);
+
+ const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice') as HTMLButtonElement;
+ const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li span`);
+ filterBtnElm.click();
+
+ expect(filterListElm.length).toBe(3);
+ expect(filterListElm[0].textContent).toBe('label1');
+ expect(filterListElm[1].textContent).toBe('label2');
+ expect(filterListElm[2].textContent).toBe('label3');
+ });
+
+ it('should trigger a re-render of the DOM element when collection changes', async () => {
+ const renderSpy = jest.spyOn(filter, 'renderDomElement');
+
+ mockColumn.filter = {
+ collection: [{ value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }],
+ enableCollectionWatch: true,
+ };
+
+ await filter.init(filterArguments);
+ mockColumn.filter!.collection!.push({ value: 'other', label: 'Other' });
+
+ jest.runAllTimers(); // fast-forward timer
+
+ expect(renderSpy).toHaveBeenCalledTimes(2);
+ expect(renderSpy).toHaveBeenCalledWith(mockColumn.filter!.collection);
+
+ const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice') as HTMLButtonElement;
+ const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li span`);
+ filterBtnElm.click();
+
+ expect(filterListElm.length).toBe(3);
+ expect(filterListElm[0].textContent).toBe('Female');
+ expect(filterListElm[1].textContent).toBe('Male');
+ expect(filterListElm[2].textContent).toBe('Other');
+ });
+
it('should throw an error when "collectionAsync" Promise does not return a valid array', async (done) => {
const promise = Promise.resolve({ hello: 'world' });
mockColumn.filter!.collectionAsync = promise;
@@ -743,4 +798,13 @@ describe('SelectFilter', () => {
done();
}
});
+
+ it('should throw an error when "collectionAsync" Promise does not return a valid array', (done) => {
+ const promise = Promise.resolve({ hello: 'world' });
+ mockColumn.filter!.collectionAsync = promise;
+ filter.init(filterArguments).catch((e) => {
+ expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Select Filter, the collection is not a valid array.`);
+ done();
+ });
+ });
});
diff --git a/packages/common/src/filters/autoCompleteFilter.ts b/packages/common/src/filters/autoCompleteFilter.ts
index 4631e273e..2f16cecaa 100644
--- a/packages/common/src/filters/autoCompleteFilter.ts
+++ b/packages/common/src/filters/autoCompleteFilter.ts
@@ -19,6 +19,7 @@ import {
SlickGrid,
} from './../interfaces/index';
import { CollectionService } from '../services/collection.service';
+import { collectionObserver, propertyObserver } from '../services/observers';
import { getDescendantProperty, sanitizeTextByAvailableSanitizer, toKebabCase } from '../services/utilities';
import { TranslaterService } from '../services/translater.service';
@@ -158,14 +159,29 @@ export class AutoCompleteFilter implements Filter {
this._collection = newCollection;
this.renderDomElement(newCollection);
- return new Promise(resolve => {
- const collectionAsync = this.columnFilter.collectionAsync;
- if (collectionAsync && !this.columnFilter.collection) {
- // only read the collectionAsync once (on the 1st load),
- // we do this because Http Fetch will throw an error saying body was already read and is streaming is locked
- resolve(this.renderOptionsAsync(collectionAsync));
- } else {
- resolve(newCollection);
+ return new Promise(async (resolve, reject) => {
+ try {
+ const collectionAsync = this.columnFilter.collectionAsync;
+ let collectionOutput: Promise | any[] | undefined;
+
+ if (collectionAsync && !this.columnFilter.collection) {
+ // only read the collectionAsync once (on the 1st load),
+ // we do this because Http Fetch will throw an error saying body was already read and is streaming is locked
+ collectionOutput = this.renderOptionsAsync(collectionAsync);
+ resolve(collectionOutput);
+ } else {
+ collectionOutput = newCollection;
+ resolve(newCollection);
+ }
+
+ // subscribe to both CollectionObserver and PropertyObserver
+ // any collection changes will trigger a re-render of the DOM element filter
+ if (collectionAsync || this.columnFilter.enableCollectionWatch) {
+ await (collectionOutput ?? collectionAsync);
+ this.watchCollectionChanges();
+ }
+ } catch (e) {
+ reject(e);
}
});
}
@@ -244,6 +260,33 @@ export class AutoCompleteFilter implements Filter {
return outputCollection;
}
+ /**
+ * Subscribe to both CollectionObserver & PropertyObserver with BindingEngine.
+ * They each have their own purpose, the "propertyObserver" will trigger once the collection is replaced entirely
+ * while the "collectionObverser" will trigger on collection changes (`push`, `unshift`, `splice`, ...)
+ */
+ protected watchCollectionChanges() {
+ if (this.columnFilter?.collection) {
+ // subscribe to the "collection" changes (array `push`, `unshift`, `splice`, ...)
+ collectionObserver(this.columnFilter.collection, (updatedArray) => {
+ this.renderDomElement(this.columnFilter.collection || updatedArray || []);
+ });
+
+ // observe for any "collection" changes (array replace)
+ // then simply recreate/re-render the Select (dropdown) DOM Element
+ propertyObserver(this.columnFilter, 'collection', (newValue) => {
+ this.renderDomElement(newValue || []);
+
+ // when new assignment arrives, we need to also reassign observer to the new reference
+ if (this.columnFilter.collection) {
+ collectionObserver(this.columnFilter.collection, (updatedArray) => {
+ this.renderDomElement(this.columnFilter.collection || updatedArray || []);
+ });
+ }
+ });
+ }
+ }
+
renderDomElement(collection: any[]) {
if (!Array.isArray(collection) && this.collectionOptions?.collectionInsideObjectProperty) {
const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty;
diff --git a/packages/common/src/filters/selectFilter.ts b/packages/common/src/filters/selectFilter.ts
index af85f77b0..180cbb7c3 100644
--- a/packages/common/src/filters/selectFilter.ts
+++ b/packages/common/src/filters/selectFilter.ts
@@ -15,6 +15,7 @@ import {
SlickGrid
} from './../interfaces/index';
import { CollectionService } from '../services/collection.service';
+import { collectionObserver, propertyObserver } from '../services/observers';
import { getDescendantProperty, getTranslationPrefix, htmlEncode, sanitizeTextByAvailableSanitizer } from '../services/utilities';
import { TranslaterService } from '../services';
@@ -140,17 +141,29 @@ export class SelectFilter implements Filter {
const newCollection = this.columnFilter.collection || [];
this.renderDomElement(newCollection);
- // return new Promise(resolve => resolve(newCollection));
-
- return new Promise(async resolve => {
- const collectionAsync = this.columnFilter.collectionAsync;
-
- if (collectionAsync && !this.columnFilter.collection) {
- // only read the collectionAsync once (on the 1st load),
- // we do this because Http Fetch will throw an error saying body was already read and its streaming is locked
- resolve(this.renderOptionsAsync(collectionAsync));
- } else {
- resolve(newCollection);
+ return new Promise(async (resolve, reject) => {
+ try {
+ const collectionAsync = this.columnFilter.collectionAsync;
+ let collectionOutput: Promise | any[] | undefined;
+
+ if (collectionAsync && !this.columnFilter.collection) {
+ // only read the collectionAsync once (on the 1st load),
+ // we do this because Http Fetch will throw an error saying body was already read and its streaming is locked
+ collectionOutput = this.renderOptionsAsync(collectionAsync);
+ resolve(collectionOutput);
+ } else {
+ collectionOutput = newCollection;
+ resolve(newCollection);
+ }
+
+ // subscribe to both CollectionObserver and PropertyObserver
+ // any collection changes will trigger a re-render of the DOM element filter
+ if (collectionAsync || this.columnFilter.enableCollectionWatch) {
+ await (collectionOutput ?? collectionAsync);
+ this.watchCollectionChanges();
+ }
+ } catch (e) {
+ reject(e);
}
});
}
@@ -244,6 +257,33 @@ export class SelectFilter implements Filter {
return outputCollection;
}
+ /**
+ * Subscribe to both CollectionObserver & PropertyObserver with BindingEngine.
+ * They each have their own purpose, the "propertyObserver" will trigger once the collection is replaced entirely
+ * while the "collectionObverser" will trigger on collection changes (`push`, `unshift`, `splice`, ...)
+ */
+ protected watchCollectionChanges() {
+ if (this.columnFilter?.collection) {
+ // subscribe to the "collection" changes (array `push`, `unshift`, `splice`, ...)
+ collectionObserver(this.columnFilter.collection, (updatedArray) => {
+ this.renderDomElement(this.columnFilter.collection || updatedArray || []);
+ });
+
+ // observe for any "collection" changes (array replace)
+ // then simply recreate/re-render the Select (dropdown) DOM Element
+ propertyObserver(this.columnFilter, 'collection', (newValue) => {
+ this.renderDomElement(newValue || []);
+
+ // when new assignment arrives, we need to also reassign observer to the new reference
+ if (this.columnFilter.collection) {
+ collectionObserver(this.columnFilter.collection, (updatedArray) => {
+ this.renderDomElement(this.columnFilter.collection || updatedArray || []);
+ });
+ }
+ });
+ }
+ }
+
renderDomElement(inputCollection: any[]) {
if (!Array.isArray(inputCollection) && this.collectionOptions?.collectionInsideObjectProperty) {
const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty;
@@ -302,10 +342,10 @@ export class SelectFilter implements Filter {
protected buildTemplateHtmlString(optionCollection: any[], searchTerms: SearchTerm[]): string {
let options = '';
const columnId = this.columnDef?.id ?? '';
- const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
- const isTranslateEnabled = this.gridOptions && this.gridOptions.enableTranslate;
- const isRenderHtmlEnabled = this.columnFilter && this.columnFilter.enableRenderHtml || false;
- const sanitizedOptions = this.gridOptions && this.gridOptions.sanitizeHtmlOptions || {};
+ const separatorBetweenLabels = this.collectionOptions?.separatorBetweenTextLabels ?? '';
+ const isTranslateEnabled = this.gridOptions?.enableTranslate ?? false;
+ const isRenderHtmlEnabled = this.columnFilter?.enableRenderHtml ?? false;
+ const sanitizedOptions = this.gridOptions?.sanitizeHtmlOptions ?? {};
// collection could be an Array of Strings OR Objects
if (Array.isArray(optionCollection)) {
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 6dfd90b86..20d014e63 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -1,6 +1,7 @@
import 'multiple-select-modified';
import * as BackendUtilities from './services/backend-utilities';
+import * as Observers from './services/observers';
import * as ServiceUtilities from './services/utilities';
import * as SortUtilities from './sortComparers/sortUtilities';
@@ -28,6 +29,6 @@ export * from './sortComparers/sortComparers.index';
export * from './services/index';
export { Enums } from './enums/enums.index';
-const Utilities = { ...BackendUtilities, ...ServiceUtilities, ...SortUtilities };
+const Utilities = { ...BackendUtilities, ...Observers, ...ServiceUtilities, ...SortUtilities };
export { Utilities };
export { SlickgridConfig } from './slickgrid-config';
diff --git a/packages/common/src/services/__tests__/observers.spec.ts b/packages/common/src/services/__tests__/observers.spec.ts
new file mode 100644
index 000000000..b76378d50
--- /dev/null
+++ b/packages/common/src/services/__tests__/observers.spec.ts
@@ -0,0 +1,99 @@
+import {
+ collectionObserver,
+ propertyObserver,
+} from '../observers';
+
+describe('Service/Observers', () => {
+ describe('collectionObserver method', () => {
+ it('should watch for array "pop" change and expect callback to be executed', (done) => {
+ const expectation: any[] = [{ value: true, label: 'True' }, { value: false, label: 'False' }];
+ const inputArray = [{ value: true, label: 'True' }, { value: false, label: 'False' }, { value: '', label: '' }];
+
+ collectionObserver(inputArray, (updatedArray) => {
+ expect(JSON.stringify(updatedArray)).toEqual(JSON.stringify(expectation));
+ done();
+ });
+ inputArray.pop();
+ });
+
+ it('should watch for array "push" change and expect callback to be executed', (done) => {
+ const expectation = [{ value: true, label: 'True' }, { value: false, label: 'False' }, { value: '', label: '' }];
+ const inputArray: any[] = [{ value: true, label: 'True' }, { value: false, label: 'False' }];
+
+ collectionObserver(inputArray, (updatedArray) => {
+ expect(JSON.stringify(updatedArray)).toEqual(JSON.stringify(expectation));
+ done();
+ });
+ inputArray.push({ value: '', label: '' });
+ });
+
+ it('should watch for array "unshift" change and expect callback to be executed', (done) => {
+ const expectation = [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }];
+ const inputArray: any[] = [{ value: true, label: 'True' }, { value: false, label: 'False' }];
+
+ collectionObserver(inputArray, (updatedArray) => {
+ expect(JSON.stringify(updatedArray)).toEqual(JSON.stringify(expectation));
+ done();
+ });
+ inputArray.unshift({ value: '', label: '' });
+ });
+
+ it('should watch for array "unshift" change and expect callback to be executed', (done) => {
+ const expectation = [{ value: true, label: 'True' }, { value: false, label: 'False' }];
+ const inputArray: any[] = [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }];
+
+ collectionObserver(inputArray, (updatedArray) => {
+ expect(JSON.stringify(updatedArray)).toEqual(JSON.stringify(expectation));
+ done();
+ });
+ inputArray.shift();
+ });
+
+ it('should watch for array "unshift" change and expect callback to be executed', (done) => {
+ const expectation = [{ value: '', label: '' }, { value: false, label: 'False' }];
+ const inputArray: any[] = [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }];
+
+ collectionObserver(inputArray, (updatedArray) => {
+ expect(JSON.stringify(updatedArray)).toEqual(JSON.stringify(expectation));
+ done();
+ });
+ inputArray.splice(1, 1);
+ });
+
+ it('should watch for array "reverse" change and expect callback to be executed', (done) => {
+ const expectation = [{ id: 1, value: false, label: 'False' }, { id: 2, value: true, label: 'True' }];
+ const inputArray: any[] = [{ id: 2, value: true, label: 'True' }, { id: 1, value: false, label: 'False' }];
+
+ collectionObserver(inputArray, (updatedArray) => {
+ expect(JSON.stringify(updatedArray)).toEqual(JSON.stringify(expectation));
+ done();
+ });
+ inputArray.reverse();
+ });
+
+ it('should watch for array "sort" change and expect callback to be executed', (done) => {
+ const expectation = [{ id: 1, value: false, label: 'False' }, { id: 2, value: true, label: 'True' }];
+ const inputArray: any[] = [{ id: 2, value: true, label: 'True' }, { id: 1, value: false, label: 'False' }];
+
+ collectionObserver(inputArray, (updatedArray) => {
+ expect(JSON.stringify(updatedArray)).toEqual(JSON.stringify(expectation));
+ done();
+ });
+ inputArray.sort((obj1, obj2) => obj1.id - obj2.id);
+ });
+ });
+
+ describe('propertyObserver method', () => {
+ it('should watch for an object property change and expect the callback to be executed with new value', (done) => {
+ const expectation = { hello: { firstName: 'John' } };
+ const inputObj = { hello: { firstName: '' } };
+
+ propertyObserver(inputObj.hello, 'firstName', (newValue) => {
+ expect(newValue).toEqual('John');
+ expect(inputObj).toEqual(expectation);
+ done();
+ });
+ inputObj.hello.firstName = 'John';
+ });
+ });
+});
diff --git a/packages/common/src/services/observers.ts b/packages/common/src/services/observers.ts
new file mode 100644
index 000000000..880a4e4bc
--- /dev/null
+++ b/packages/common/src/services/observers.ts
@@ -0,0 +1,39 @@
+/**
+ * Collection Observer to watch for any array changes (pop, push, reverse, shift, unshift, splice, sort)
+ * and execute the callback when any of the methods are called
+ * @param {any[]} inputArray - array you want to listen to
+ * @param {Function} callback function that will be called on any change inside array
+ */
+export function collectionObserver(inputArray: any[], callback: (outputArray: any[], newValues: any[]) => void) {
+ // Add more methods here if you want to listen to them
+ const mutationMethods = ['pop', 'push', 'reverse', 'shift', 'unshift', 'splice', 'sort'];
+
+ mutationMethods.forEach((changeMethod) => {
+ inputArray[changeMethod] = (...args: any[]) => {
+ const res = Array.prototype[changeMethod].apply(inputArray, args); // call normal behaviour
+ callback.apply(inputArray, [inputArray, args]); // finally call the callback supplied
+ return res;
+ };
+ });
+}
+
+/**
+ * Object Property Observer and execute the callback whenever any of the object property changes.
+ * @param {*} obj - input object
+ * @param {String} prop - object property name
+ * @param {Function} callback - function that will be called on any change inside array
+ */
+export function propertyObserver(obj: any, prop: string, callback: (newValue: any) => void) {
+ let innerValue = obj[prop];
+
+ Object.defineProperty(obj, prop, {
+ configurable: true,
+ get() {
+ return innerValue;
+ },
+ set(newValue) {
+ innerValue = newValue;
+ callback.apply(obj, [newValue, obj[prop]]);
+ }
+ });
+}
diff --git a/test/cypress/integration/example07.spec.js b/test/cypress/integration/example07.spec.js
index cd3b69826..839212bd9 100644
--- a/test/cypress/integration/example07.spec.js
+++ b/test/cypress/integration/example07.spec.js
@@ -2,7 +2,7 @@
describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
const GRID_ROW_HEIGHT = 45;
- const fullTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed'];
+ const fullTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed', 'Prerequisites'];
it('should display Example title', () => {
cy.visit(`${Cypress.config('baseExampleUrl')}/example07`);
@@ -135,11 +135,11 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
});
it('should dynamically add 2x new "Title" columns', () => {
- const updatedTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed', 'Title', 'Title'];
+ const updatedTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed', 'Prerequisites', 'Title', 'Title'];
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`).should('contain', 'Task 0');
- cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(8)`).should('not.exist');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(9)`).should('not.exist');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(10)`).should('not.exist');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`)
.should('contain', 'Task 0')
@@ -155,12 +155,12 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
.each(($child, index) => expect($child.text()).to.eq(updatedTitles[index]));
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`).should('contain', 'Task 0');
- cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(8)`).should('contain', 'Task 0');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(9)`).should('contain', 'Task 0');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(10)`).should('contain', 'Task 0');
});
it('should dynamically remove 1x of the new "Title" columns', () => {
- const updatedTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed', 'Title'];
+ const updatedTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed', 'Prerequisites', 'Title'];
cy.get('[data-test=remove-title-column-btn]')
.click();
@@ -195,11 +195,11 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
cy.get('.flatpickr-calendar:visible .flatpickr-day').contains('22').click('bottom', { force: true });
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(6)`).should('contain', '2009-01-22');
- cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(8)`).should('contain', 'Task 0000');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(9)`).should('contain', 'Task 0000');
});
it('should move "Duration" column to a different position in the grid', () => {
- const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Duration', 'Completed', 'Title'];
+ const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Duration', 'Completed', 'Prerequisites', 'Title'];
cy.get('.slick-header-columns')
.children('.slick-header-column:nth(3)')
@@ -219,7 +219,7 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
});
it('should be able to hide "Duration" column', () => {
- const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Title'];
+ const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Prerequisites', 'Title'];
cy.get('[data-test="hide-duration-btn"]').click();
@@ -230,7 +230,7 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
});
it('should be able to click disable Filters functionality button and expect no Filters', () => {
- const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Title'];
+ const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Prerequisites', 'Title'];
cy.get('[data-test="disable-filters-btn"]').click().click(); // even clicking twice should have same result
@@ -267,7 +267,7 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
});
it('should be able to toggle Filters functionality', () => {
- const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Title'];
+ const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Prerequisites', 'Title'];
cy.get('[data-test="toggle-filtering-btn"]').click(); // hide it
@@ -280,7 +280,7 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
.each(($child, index) => expect($child.text()).to.eq(expectedTitles[index]));
cy.get('[data-test="toggle-filtering-btn"]').click(); // show it
- cy.get('.slick-headerrow-columns .slick-headerrow-column').should('have.length', 8);
+ cy.get('.slick-headerrow-columns .slick-headerrow-column').should('have.length', 9);
cy.get('.grid7')
.find('.slick-header-columns')
@@ -451,7 +451,7 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
it('should open Column Picker and show the "Duration" column back to visible and expect it to have kept its position after toggling filter/sorting', () => {
// first 2 cols are hidden but they do count as li item
- const expectedFullPickerTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Duration', 'Completed', 'Title'];
+ const expectedFullPickerTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Duration', 'Completed', 'Prerequisites', 'Title'];
cy.get('.grid7')
.find('.slick-header-column')
@@ -491,4 +491,70 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => {
}
});
});
+
+ it('should click Add Item button 2x times and expect "Task 500" and "Task 501" to be created', () => {
+ cy.get('[data-test="add-item-btn"]').click();
+ cy.wait(200);
+ cy.get('[data-test="add-item-btn"]').click();
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`).should('contain', 'Task 501');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(2)`).should('contain', 'Task 500');
+
+ cy.get('[data-test="toggle-filtering-btn"]').click(); // show it back
+ });
+
+ it('should open the "Prerequisites" Filter and expect to have Task 500 & 501 in the Filter', () => {
+ cy.get('div.ms-filter.filter-prerequisites')
+ .trigger('click');
+
+ cy.get('.ms-drop')
+ .find('span:nth(1)')
+ .contains('Task 501');
+
+ cy.get('.ms-drop')
+ .find('span:nth(2)')
+ .contains('Task 500');
+
+ cy.get('div.ms-filter.filter-prerequisites')
+ .trigger('click');
+ });
+
+ it('should open the "Prerequisites" Editor and expect to have Task 500 & 501 in the Editor', () => {
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(8)`)
+ .should('contain', '')
+ .click();
+
+ cy.get('.ms-drop')
+ .find('span:nth(1)')
+ .contains('Task 501');
+
+ cy.get('.ms-drop')
+ .find('span:nth(2)')
+ .contains('Task 500');
+
+ cy.get('[name=editor-prerequisites].ms-drop ul > li:nth(0)')
+ .click();
+
+ cy.get('.ms-ok-button')
+ .last()
+ .click({ force: true });
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(8)`).should('contain', 'Task 501');
+ });
+
+ it('should delete the last item "Task 501" and expect it to be removed from the Filter', () => {
+ cy.get('[data-test="delete-item-btn"]').click();
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`).should('contain', 'Task 500');
+
+ cy.get('div.ms-filter.filter-prerequisites')
+ .trigger('click');
+
+ cy.get('.ms-drop')
+ .find('span:nth(1)')
+ .contains('Task 500');
+
+ cy.get('div.ms-filter.filter-prerequisites')
+ .trigger('click');
+ });
});