From 5e52dba3682476f4b9e28361c55502b3e43c7982 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Fri, 10 Jul 2026 17:12:21 +0300 Subject: [PATCH 1/3] feat(filter-bar): support custom compareWith for select and multiselect pipes (#DS-5191) --- .../components/filter-bar/filter-bar.en.md | 2 + .../components/filter-bar/filter-bar.ru.md | 2 + .../components/filter-bar/filter-bar.types.ts | 6 ++ .../components/filter-bar/pipes/base-pipe.ts | 13 +++- .../filter-bar/pipes/pipe-multi-select.html | 2 +- .../pipes/pipe-multi-select.spec.ts | 71 ++++++++++++++++++- .../filter-bar/pipes/pipe-select.html | 2 +- .../filter-bar/pipes/pipe-select.spec.ts | 62 ++++++++++++++++ .../filter-bar-pipe-types-example.ts | 4 ++ .../components/filter-bar.api.md | 2 + 10 files changed, 162 insertions(+), 4 deletions(-) diff --git a/packages/components/filter-bar/filter-bar.en.md b/packages/components/filter-bar/filter-bar.en.md index 7c1aef714d..6e746792b6 100644 --- a/packages/components/filter-bar/filter-bar.en.md +++ b/packages/components/filter-bar/filter-bar.en.md @@ -41,6 +41,8 @@ Clicking on a pseudo-link in the list of parameters will add a new value to the +For the `select` and `multiselect` types, the pipe template accepts an optional `compareWith` — a comparator forwarded to the underlying select that controls how the selected value is matched against the option list. Override it when options are compared by a business key, or when the selected value is a distinct object (e.g. restored from a saved filter) rather than the same reference. When omitted, options are matched by their `id`. + ### Search in pipes If the filter has many values, it is useful to enable search in the drop-down menu. diff --git a/packages/components/filter-bar/filter-bar.ru.md b/packages/components/filter-bar/filter-bar.ru.md index 4f1de26fa2..9db52d8a5a 100644 --- a/packages/components/filter-bar/filter-bar.ru.md +++ b/packages/components/filter-bar/filter-bar.ru.md @@ -41,6 +41,8 @@ +Для типов `select` и `multiselect` в шаблоне пайпа доступен необязательный `compareWith` — компаратор, который пробрасывается во внутренний select и определяет, как выбранное значение сопоставляется со списком опций. Переопределите его, когда опции сравниваются по бизнес-ключу или когда выбранное значение — это отдельный объект (например, восстановленный из сохранённого фильтра), а не та же ссылка. Если не задан, опции сопоставляются по их `id`. + ### Поиск значений Если в фильтре много значений, то полезно включить поиск в выпадающем меню. diff --git a/packages/components/filter-bar/filter-bar.types.ts b/packages/components/filter-bar/filter-bar.types.ts index 028a126c61..1b0d4024d5 100644 --- a/packages/components/filter-bar/filter-bar.types.ts +++ b/packages/components/filter-bar/filter-bar.types.ts @@ -183,6 +183,12 @@ export interface KbqPipeData extends KbqPipe { export interface KbqPipeTemplate extends Omit { values?: unknown[]; valueTemplate?: TemplateRef | string; + /** + * Custom equality comparator forwarded to the underlying `kbq-select` of the `select` / + * `multiselect` pipes. When omitted, the pipe's default id-based comparator (`compareByValue`) + * is used. Ignored by other pipe types. + */ + compareWith?: (o1: KbqSelectValue | null, o2: KbqSelectValue | null) => boolean; } export interface KbqSaveFilterError { diff --git a/packages/components/filter-bar/pipes/base-pipe.ts b/packages/components/filter-bar/pipes/base-pipe.ts index 8d20564a47..c8a4c9b48c 100644 --- a/packages/components/filter-bar/pipes/base-pipe.ts +++ b/packages/components/filter-bar/pipes/base-pipe.ts @@ -22,7 +22,8 @@ import { KbqFilterBarConfiguration, KbqPipeData, KbqPipeTemplate, - KbqPipeType + KbqPipeType, + KbqSelectValue } from '../filter-bar.types'; /** Injection Token for providing configuration of filter-bar */ @@ -77,6 +78,12 @@ export abstract class KbqBasePipe implements AfterViewInit { protected values; /** TemplateRef for selecting an option */ protected valueTemplate?: TemplateRef | string; + /** + * Optional equality comparator forwarded from the pipe template to the underlying `kbq-select`. + * Only consumed by the select / multi-select pipe templates; when unset they fall back to their + * own id-based `compareByValue`. + */ + protected optionCompareWith?: (o1: KbqSelectValue | null, o2: KbqSelectValue | null) => boolean; /** * Whether the current platform is a Mac. @@ -167,6 +174,10 @@ export abstract class KbqBasePipe implements AfterViewInit { this.values = template.values; this.valueTemplate = template.valueTemplate; } + + if (template?.compareWith) { + this.optionCompareWith = template.compareWith; + } }; /** removes pipe from filter-bar and triggers changes */ diff --git a/packages/components/filter-bar/pipes/pipe-multi-select.html b/packages/components/filter-bar/pipes/pipe-multi-select.html index 7fef894da7..6465ac9ce9 100644 --- a/packages/components/filter-bar/pipes/pipe-multi-select.html +++ b/packages/components/filter-bar/pipes/pipe-multi-select.html @@ -1,7 +1,7 @@ { }); }); + describe('compareWith forwarding', () => { + const customCompare = (o1: KbqSelectValue | null, o2: KbqSelectValue | null): boolean => + o1?.value === o2?.value; + + // Options without `id`, so the default id-based comparator can't distinguish them and the + // custom `value`-based comparator is what makes matching work. + const idlessValues: KbqSelectValue[] = [ + { name: 'Option 1', value: 'value1' }, + { name: 'Option 2', value: 'value2' }, + { name: 'Option 3', value: 'value3' } + ]; + + const setTemplate = (values: KbqSelectValue[]) => { + fixture.componentInstance.pipeTemplates = [ + { + name: 'MultiSelect', + id: PIPE_TEMPLATE_ID, + type: KbqPipeTypes.MultiSelect, + values, + compareWith: customCompare, + cleanable: false, + removable: false, + disabled: false + } + ]; + }; + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + filterBarDebugElement = fixture.debugElement.query(By.directive(KbqFilterBar)); + }); + + it('should forward a custom compareWith from the pipe template to the inner select', () => { + setTemplate(SELECT_VALUES); + fixture.componentInstance.activeFilter = createFilter([createPipe({ name: 'test', value: [] })]); + fixture.detectChanges(); + + expect(getPipeComponent().select().compareWith).toBe(customCompare); + }); + + it('should use the default id comparator when the template omits compareWith', () => { + fixture.componentInstance.activeFilter = createFilter([createPipe({ name: 'test', value: [] })]); + fixture.detectChanges(); + + const component = getPipeComponent(); + + expect(component.select().compareWith).toBe(component.compareByValue); + }); + + it('should match selected values in the panel using the custom comparator', fakeAsync(() => { + setTemplate(idlessValues); + // A distinct object equal to the second option only by `value`. + fixture.componentInstance.activeFilter = createFilter([ + createPipe({ name: 'test', value: [{ name: 'Option 2', value: 'value2' }] }) + ]); + fixture.detectChanges(); + + openSelect(); + flush(); + fixture.detectChanges(); + + const selectedOptions = document.querySelectorAll('.kbq-option.kbq-selected'); + + expect(selectedOptions.length).toBe(1); + expect(selectedOptions[0].textContent).toContain('Option 2'); + })); + }); + describe('selectAllHandler', () => { beforeEach(() => { fixture = TestBed.createComponent(TestComponent); diff --git a/packages/components/filter-bar/pipes/pipe-select.html b/packages/components/filter-bar/pipes/pipe-select.html index a39c4d87fe..9d4c09ed93 100644 --- a/packages/components/filter-bar/pipes/pipe-select.html +++ b/packages/components/filter-bar/pipes/pipe-select.html @@ -1,6 +1,6 @@ { }); }); + describe('compareWith forwarding', () => { + const customCompare = (o1: KbqSelectValue | null, o2: KbqSelectValue | null): boolean => + o1?.value === o2?.value; + + const setTemplateWithComparator = () => { + fixture.componentInstance.pipeTemplates = [ + { + name: 'Select', + id: PIPE_TEMPLATE_ID, + type: KbqPipeTypes.Select, + values: SELECT_VALUES, + compareWith: customCompare, + cleanable: false, + removable: false, + disabled: false + } + ]; + }; + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + filterBarDebugElement = fixture.debugElement.query(By.directive(KbqFilterBar)); + }); + + it('should forward a custom compareWith from the pipe template to the inner select', () => { + setTemplateWithComparator(); + fixture.componentInstance.activeFilter = createFilter([createPipe({ name: 'test', value: null })]); + fixture.detectChanges(); + + expect(getPipeComponent().select().compareWith).toBe(customCompare); + }); + + it('should use the default id comparator when the template omits compareWith', () => { + fixture.componentInstance.activeFilter = createFilter([createPipe({ name: 'test', value: null })]); + fixture.detectChanges(); + + const component = getPipeComponent(); + + expect(component.select().compareWith).toBe(component.compareByValue); + }); + + it('should match the selected value in the panel using the custom comparator', fakeAsync(() => { + setTemplateWithComparator(); + // A distinct object equal to SELECT_VALUES[1] only by `value` (the options carry no `id`). + // With the default id-based comparator every id-less option would match; the custom + // comparator must single out exactly one option. + fixture.componentInstance.activeFilter = createFilter([ + createPipe({ name: 'test', value: { name: 'Option 2', value: 'value2' } }) + ]); + fixture.detectChanges(); + + openSelect(); + flush(); + fixture.detectChanges(); + + const selectedOptions = document.querySelectorAll('.kbq-option.kbq-selected'); + + expect(selectedOptions.length).toBe(1); + expect(selectedOptions[0].textContent).toContain('Option 2'); + })); + }); + describe('open', () => { beforeEach(() => { fixture = TestBed.createComponent(TestComponent); diff --git a/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts b/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts index a58cb595b6..37568099e9 100644 --- a/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts +++ b/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts @@ -113,6 +113,10 @@ export class FilterBarPipeTypesExample { { name: 'Option 6', id: '6' }, { name: 'Option 7', id: '7' } ], + // `compareWith` controls how the selected value is matched against the option list. + // Override it to compare by a business key (here `id`) — useful when the selected value + // is a distinct object (e.g. restored from a saved filter) rather than the same reference. + compareWith: (o1, o2) => o1?.id === o2?.id, cleanable: false, removable: false, diff --git a/tools/public_api_guard/components/filter-bar.api.md b/tools/public_api_guard/components/filter-bar.api.md index bd2c9c21ae..808f5c768b 100644 --- a/tools/public_api_guard/components/filter-bar.api.md +++ b/tools/public_api_guard/components/filter-bar.api.md @@ -182,6 +182,7 @@ export abstract class KbqBasePipe implements AfterViewInit { onClear(): void; onRemove(): void; abstract open(): void; + protected optionCompareWith?: (o1: KbqSelectValue | null, o2: KbqSelectValue | null) => boolean; protected restoreTriggerFocus(): void; get showRemoveButton(): boolean; readonly stateChanges: Subject; @@ -866,6 +867,7 @@ export class KbqPipeState { // @public (undocumented) export interface KbqPipeTemplate extends Omit { + compareWith?: (o1: KbqSelectValue | null, o2: KbqSelectValue | null) => boolean; // (undocumented) values?: unknown[]; // (undocumented) From b5c6914edbb1194ea69ca7d61d4c3f0a1bc874b4 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Fri, 10 Jul 2026 17:37:06 +0300 Subject: [PATCH 2/3] fix: after review --- .../pipes/pipe-multi-select.spec.ts | 13 +++++++---- .../filter-bar/pipes/pipe-select.spec.ts | 15 +++++++----- .../filter-bar-pipe-types-example.ts | 23 +++++++++---------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/packages/components/filter-bar/pipes/pipe-multi-select.spec.ts b/packages/components/filter-bar/pipes/pipe-multi-select.spec.ts index 9751f4c0a9..2f91c22492 100644 --- a/packages/components/filter-bar/pipes/pipe-multi-select.spec.ts +++ b/packages/components/filter-bar/pipes/pipe-multi-select.spec.ts @@ -658,7 +658,11 @@ describe('KbqPipeMultiSelectComponent', () => { it('should match selected values in the panel using the custom comparator', fakeAsync(() => { setTemplate(idlessValues); - // A distinct object equal to the second option only by `value`. + // The selected value is a distinct object equal to the second option only by `value`; the + // options carry no `id`. `getCorrespondOption` uses `.find`, so exactly one option is + // selected either way — under the default id comparator every id-less option compares equal + // and the first ('Option 1') wins, so asserting the selected option is 'Option 2' is what + // proves the custom `value` comparator is applied. fixture.componentInstance.activeFilter = createFilter([ createPipe({ name: 'test', value: [{ name: 'Option 2', value: 'value2' }] }) ]); @@ -668,10 +672,11 @@ describe('KbqPipeMultiSelectComponent', () => { flush(); fixture.detectChanges(); - const selectedOptions = document.querySelectorAll('.kbq-option.kbq-selected'); + const selectedText = Array.from(document.querySelectorAll('.kbq-option.kbq-selected')).map((el) => + el.textContent?.trim() + ); - expect(selectedOptions.length).toBe(1); - expect(selectedOptions[0].textContent).toContain('Option 2'); + expect(selectedText).toEqual(['Option 2']); })); }); diff --git a/packages/components/filter-bar/pipes/pipe-select.spec.ts b/packages/components/filter-bar/pipes/pipe-select.spec.ts index c275e043c1..da120ebbf5 100644 --- a/packages/components/filter-bar/pipes/pipe-select.spec.ts +++ b/packages/components/filter-bar/pipes/pipe-select.spec.ts @@ -375,9 +375,11 @@ describe('KbqPipeSelectComponent', () => { it('should match the selected value in the panel using the custom comparator', fakeAsync(() => { setTemplateWithComparator(); - // A distinct object equal to SELECT_VALUES[1] only by `value` (the options carry no `id`). - // With the default id-based comparator every id-less option would match; the custom - // comparator must single out exactly one option. + // The selected value is a distinct object equal to SELECT_VALUES[1] only by `value`; the + // options carry no `id`. `getCorrespondOption` uses `.find`, so exactly one option is + // selected either way — under the default id comparator every id-less option compares equal + // and the first ('Option 1') wins, so asserting the selected option is 'Option 2' is what + // proves the custom `value` comparator is applied. fixture.componentInstance.activeFilter = createFilter([ createPipe({ name: 'test', value: { name: 'Option 2', value: 'value2' } }) ]); @@ -387,10 +389,11 @@ describe('KbqPipeSelectComponent', () => { flush(); fixture.detectChanges(); - const selectedOptions = document.querySelectorAll('.kbq-option.kbq-selected'); + const selectedText = Array.from(document.querySelectorAll('.kbq-option.kbq-selected')).map((el) => + el.textContent?.trim() + ); - expect(selectedOptions.length).toBe(1); - expect(selectedOptions[0].textContent).toContain('Option 2'); + expect(selectedText).toEqual(['Option 2']); })); }); diff --git a/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts b/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts index 37568099e9..12ca4b994e 100644 --- a/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts +++ b/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts @@ -105,19 +105,18 @@ export class FilterBarPipeTypesExample { name: 'Select', type: KbqPipeTypes.Select, values: [ - { name: 'Option 1', id: '1' }, - { name: 'Option 2', id: '2' }, - { name: 'Option 3', id: '3' }, - { name: 'Option 4', id: '4' }, - { name: 'Option 5', id: '5' }, - { name: 'Option 6', id: '6' }, - { name: 'Option 7', id: '7' } + { name: 'Option 1', value: 'value1' }, + { name: 'Option 2', value: 'value2' }, + { name: 'Option 3', value: 'value3' }, + { name: 'Option 4', value: 'value4' }, + { name: 'Option 5', value: 'value5' }, + { name: 'Option 6', value: 'value6' }, + { name: 'Option 7', value: 'value7' } ], - // `compareWith` controls how the selected value is matched against the option list. - // Override it to compare by a business key (here `id`) — useful when the selected value - // is a distinct object (e.g. restored from a saved filter) rather than the same reference. - compareWith: (o1, o2) => o1?.id === o2?.id, - + // These options are identified by their business `value` rather than a synthetic `id`, so a + // selected value restored from a saved filter is a distinct object. Override `compareWith` + // to match by `value` instead of the default id-based comparator. + compareWith: (o1, o2) => o1?.value === o2?.value, cleanable: false, removable: false, disabled: false From 0c6c40f8d2e1994bc850ccc9955ab7fbd88e0d80 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Fri, 10 Jul 2026 19:04:07 +0300 Subject: [PATCH 3/3] fix: after review 2 --- .../components/filter-bar/filter-bar.en.md | 2 +- .../components/filter-bar/filter-bar.ru.md | 2 +- .../components/filter-bar/pipes/base-pipe.ts | 5 +- .../pipes/pipe-multi-select.spec.ts | 52 +++++++++++++++---- .../filter-bar/pipes/pipe-select.spec.ts | 27 ++++++++++ .../filter-bar-pipe-types-example.ts | 26 ++++++---- 6 files changed, 88 insertions(+), 26 deletions(-) diff --git a/packages/components/filter-bar/filter-bar.en.md b/packages/components/filter-bar/filter-bar.en.md index 6e746792b6..87c442d6cd 100644 --- a/packages/components/filter-bar/filter-bar.en.md +++ b/packages/components/filter-bar/filter-bar.en.md @@ -41,7 +41,7 @@ Clicking on a pseudo-link in the list of parameters will add a new value to the -For the `select` and `multiselect` types, the pipe template accepts an optional `compareWith` — a comparator forwarded to the underlying select that controls how the selected value is matched against the option list. Override it when options are compared by a business key, or when the selected value is a distinct object (e.g. restored from a saved filter) rather than the same reference. When omitted, options are matched by their `id`. +For the `select` and `multiselect` types, the pipe template accepts an optional `compareWith` — a comparator forwarded to the underlying select that controls how the selected value is matched against the option list. Override it when options are compared by a business key, or when the selected value is a distinct object (e.g. restored from a saved filter) rather than the same reference. When omitted, options are matched by their `id`. A custom comparator is responsible for its own null handling — unlike the default, which never matches a `null`/`undefined` value. ### Search in pipes diff --git a/packages/components/filter-bar/filter-bar.ru.md b/packages/components/filter-bar/filter-bar.ru.md index 9db52d8a5a..ab6a4978a8 100644 --- a/packages/components/filter-bar/filter-bar.ru.md +++ b/packages/components/filter-bar/filter-bar.ru.md @@ -41,7 +41,7 @@ -Для типов `select` и `multiselect` в шаблоне пайпа доступен необязательный `compareWith` — компаратор, который пробрасывается во внутренний select и определяет, как выбранное значение сопоставляется со списком опций. Переопределите его, когда опции сравниваются по бизнес-ключу или когда выбранное значение — это отдельный объект (например, восстановленный из сохранённого фильтра), а не та же ссылка. Если не задан, опции сопоставляются по их `id`. +Для типов `select` и `multiselect` в шаблоне пайпа доступен необязательный `compareWith` — компаратор, который пробрасывается во внутренний select и определяет, как выбранное значение сопоставляется со списком опций. Переопределите его, когда опции сравниваются по бизнес-ключу или когда выбранное значение — это отдельный объект (например, восстановленный из сохранённого фильтра), а не та же ссылка. Если не задан, опции сопоставляются по их `id`. Кастомный компаратор сам отвечает за обработку `null`/`undefined` — в отличие от компаратора по умолчанию, который никогда не считает `null`/`undefined` совпадением. ### Поиск значений diff --git a/packages/components/filter-bar/pipes/base-pipe.ts b/packages/components/filter-bar/pipes/base-pipe.ts index c8a4c9b48c..e9f0b57c86 100644 --- a/packages/components/filter-bar/pipes/base-pipe.ts +++ b/packages/components/filter-bar/pipes/base-pipe.ts @@ -80,7 +80,7 @@ export abstract class KbqBasePipe implements AfterViewInit { protected valueTemplate?: TemplateRef | string; /** * Optional equality comparator forwarded from the pipe template to the underlying `kbq-select`. - * Only consumed by the select / multi-select pipe templates; when unset they fall back to their + * Only consumed by the select / multi-select pipe components; when unset they fall back to their * own id-based `compareByValue`. */ protected optionCompareWith?: (o1: KbqSelectValue | null, o2: KbqSelectValue | null) => boolean; @@ -173,9 +173,6 @@ export abstract class KbqBasePipe implements AfterViewInit { if (template?.values) { this.values = template.values; this.valueTemplate = template.valueTemplate; - } - - if (template?.compareWith) { this.optionCompareWith = template.compareWith; } }; diff --git a/packages/components/filter-bar/pipes/pipe-multi-select.spec.ts b/packages/components/filter-bar/pipes/pipe-multi-select.spec.ts index 2f91c22492..3623d28797 100644 --- a/packages/components/filter-bar/pipes/pipe-multi-select.spec.ts +++ b/packages/components/filter-bar/pipes/pipe-multi-select.spec.ts @@ -611,8 +611,8 @@ describe('KbqPipeMultiSelectComponent', () => { const customCompare = (o1: KbqSelectValue | null, o2: KbqSelectValue | null): boolean => o1?.value === o2?.value; - // Options without `id`, so the default id-based comparator can't distinguish them and the - // custom `value`-based comparator is what makes matching work. + // Options without `id`, used to force reliance on the custom `value`-based comparator (see the + // 'should match selected values' test below for why). const idlessValues: KbqSelectValue[] = [ { name: 'Option 1', value: 'value1' }, { name: 'Option 2', value: 'value2' }, @@ -656,15 +656,49 @@ describe('KbqPipeMultiSelectComponent', () => { expect(component.select().compareWith).toBe(component.compareByValue); }); + it('should clear a previously set compareWith when a later template update omits it', () => { + setTemplate(SELECT_VALUES); + fixture.componentInstance.activeFilter = createFilter([createPipe({ name: 'test', value: [] })]); + fixture.detectChanges(); + + expect(getPipeComponent().select().compareWith).toBe(customCompare); + + // A follow-up pipeTemplates update for the same pipe id that omits compareWith (e.g. new + // id-based values) must fall back to the default comparator, not keep forwarding the stale one. + fixture.componentInstance.pipeTemplates = [ + { + name: 'MultiSelect', + id: PIPE_TEMPLATE_ID, + type: KbqPipeTypes.MultiSelect, + values: SELECT_VALUES, + cleanable: false, + removable: false, + disabled: false + } + ]; + fixture.detectChanges(); + + const component = getPipeComponent(); + + expect(component.select().compareWith).toBe(component.compareByValue); + }); + it('should match selected values in the panel using the custom comparator', fakeAsync(() => { setTemplate(idlessValues); - // The selected value is a distinct object equal to the second option only by `value`; the - // options carry no `id`. `getCorrespondOption` uses `.find`, so exactly one option is - // selected either way — under the default id comparator every id-less option compares equal - // and the first ('Option 1') wins, so asserting the selected option is 'Option 2' is what - // proves the custom `value` comparator is applied. + // The two selected values are distinct objects equal to the second and third options only by + // `value`. Each selected item is resolved independently via `getCorrespondOption`/`.find`, so + // selecting more than one id-less item catches misattribution between items — under the + // default id comparator every id-less option compares equal and only the first ('Option 1') + // would ever be picked, so asserting both 'Option 2' and 'Option 3' are highlighted is what + // proves the custom `value` comparator is applied per item. fixture.componentInstance.activeFilter = createFilter([ - createPipe({ name: 'test', value: [{ name: 'Option 2', value: 'value2' }] }) + createPipe({ + name: 'test', + value: [ + { name: 'Option 2', value: 'value2' }, + { name: 'Option 3', value: 'value3' } + ] + }) ]); fixture.detectChanges(); @@ -676,7 +710,7 @@ describe('KbqPipeMultiSelectComponent', () => { el.textContent?.trim() ); - expect(selectedText).toEqual(['Option 2']); + expect(selectedText).toEqual(['Option 2', 'Option 3']); })); }); diff --git a/packages/components/filter-bar/pipes/pipe-select.spec.ts b/packages/components/filter-bar/pipes/pipe-select.spec.ts index da120ebbf5..7f84a82929 100644 --- a/packages/components/filter-bar/pipes/pipe-select.spec.ts +++ b/packages/components/filter-bar/pipes/pipe-select.spec.ts @@ -373,6 +373,33 @@ describe('KbqPipeSelectComponent', () => { expect(component.select().compareWith).toBe(component.compareByValue); }); + it('should clear a previously set compareWith when a later template update omits it', () => { + setTemplateWithComparator(); + fixture.componentInstance.activeFilter = createFilter([createPipe({ name: 'test', value: null })]); + fixture.detectChanges(); + + expect(getPipeComponent().select().compareWith).toBe(customCompare); + + // A follow-up pipeTemplates update for the same pipe id that omits compareWith (e.g. new + // id-based values) must fall back to the default comparator, not keep forwarding the stale one. + fixture.componentInstance.pipeTemplates = [ + { + name: 'Select', + id: PIPE_TEMPLATE_ID, + type: KbqPipeTypes.Select, + values: SELECT_VALUES, + cleanable: false, + removable: false, + disabled: false + } + ]; + fixture.detectChanges(); + + const component = getPipeComponent(); + + expect(component.select().compareWith).toBe(component.compareByValue); + }); + it('should match the selected value in the panel using the custom comparator', fakeAsync(() => { setTemplateWithComparator(); // The selected value is a distinct object equal to SELECT_VALUES[1] only by `value`; the diff --git a/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts b/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts index 12ca4b994e..3198c91e31 100644 --- a/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts +++ b/packages/docs-examples/components/filter-bar/filter-bar-pipe-types/filter-bar-pipe-types-example.ts @@ -89,14 +89,17 @@ export class FilterBarPipeTypesExample { name: 'MultiSelect', type: KbqPipeTypes.MultiSelect, values: [ - { name: 'Option 1', id: '1' }, - { name: 'Option 2', id: '2' }, - { name: 'Option 3', id: '3' }, - { name: 'Option 4', id: '4' }, - { name: 'Option 5', id: '5' }, - { name: 'Option 6', id: '6' }, - { name: 'Option 7', id: '7' } + { name: 'Option 1', value: 'value1' }, + { name: 'Option 2', value: 'value2' }, + { name: 'Option 3', value: 'value3' }, + { name: 'Option 4', value: 'value4' }, + { name: 'Option 5', value: 'value5' }, + { name: 'Option 6', value: 'value6' }, + { name: 'Option 7', value: 'value7' } ], + // Same rationale as the Select template below: options are identified by `value` rather than + // a synthetic `id`, so a custom `compareWith` is needed instead of the default id-based one. + compareWith: (o1, o2) => !!o1 && !!o2 && o1.value === o2.value, cleanable: false, removable: false, disabled: false @@ -113,10 +116,11 @@ export class FilterBarPipeTypesExample { { name: 'Option 6', value: 'value6' }, { name: 'Option 7', value: 'value7' } ], - // These options are identified by their business `value` rather than a synthetic `id`, so a - // selected value restored from a saved filter is a distinct object. Override `compareWith` - // to match by `value` instead of the default id-based comparator. - compareWith: (o1, o2) => o1?.value === o2?.value, + // Options are identified by their business `value` rather than a synthetic `id`. Override + // `compareWith` whenever a selected value can be a distinct object equal only by that key + // (e.g. one restored from a saved filter) instead of the same reference. A custom comparator + // is responsible for its own null handling — unlike the default, which never matches null. + compareWith: (o1, o2) => !!o1 && !!o2 && o1.value === o2.value, cleanable: false, removable: false, disabled: false