+ option.label + (option.attrType ? ` (${option.attrType})` : '');
+
// eslint-disable-next-line no-useless-constructor
constructor(private _formBuilder: UntypedFormBuilder) {}
diff --git a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.html b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.html
index b60bb0d7..48a51ffd 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.html
+++ b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.html
@@ -55,7 +55,7 @@
{{
returnObject
- ? value[optionLabel]
+ ? getProp(value, optionLabel)
: (value
| labelByValue: options : optionValue : optionLabel)
}}
@@ -87,7 +87,7 @@
}">
{{
returnObject
- ? val[optionLabel]
+ ? getProp(val, optionLabel)
: (val
| labelByValue
: options
@@ -125,7 +125,7 @@
}"
[label]="
returnObject
- ? val[optionLabel]
+ ? getProp(val, optionLabel)
: (val
| labelByValue: options : optionValue : optionLabel)
">
@@ -371,14 +371,14 @@
data-testid="cps-autocomplete-options"
class="cps-autocomplete-options-option-label"
[class.virtual-row]="virtualScroll"
- >{{ item[optionLabel] }}{{ getProp(item, optionLabel) }}
{{ item[optionInfo] }}{{ getProp(item, optionInfo) }}
diff --git a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.spec.ts b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.spec.ts
index 5aa23e3c..456674d0 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.spec.ts
+++ b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.spec.ts
@@ -441,6 +441,88 @@ describe('CpsAutocompleteComponent', () => {
expect(component.filteredOptions.length).toBe(3);
});
+ describe('function-based option keys', () => {
+ const nestedOptions = [
+ { meta: { title: 'Alpha', id: 1 } },
+ { meta: { title: 'Beta', id: 2 } },
+ { meta: { title: 'Gamma', id: 3 } }
+ ];
+
+ it('getProp should return property value when key is a string', () => {
+ expect(component.getProp({ label: 'hello' }, 'label')).toBe('hello');
+ });
+
+ it('getProp should invoke the function and return its result when key is a function', () => {
+ const fn = (o: any) => o.meta.title;
+ expect(component.getProp({ meta: { title: 'hello' } }, fn)).toBe('hello');
+ });
+
+ it('should display selected label using function optionLabel', () => {
+ fixture.componentRef.setInput('options', nestedOptions);
+ fixture.componentRef.setInput('optionLabel', (o: any) => o.meta.title);
+ component.value = nestedOptions[0];
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ const selectedLabel = fixture.debugElement.query(
+ By.css('.single-item-selection span')
+ );
+ expect(selectedLabel.nativeElement.textContent.trim()).toBe('Alpha');
+ });
+
+ it('should filter options using function optionLabel', fakeAsync(() => {
+ fixture.componentRef.setInput('options', nestedOptions);
+ fixture.componentRef.setInput('optionLabel', (o: any) => o.meta.title);
+ fixture.detectChanges();
+ const inputElement = fixture.debugElement.query(
+ By.css('.cps-autocomplete-box-input')
+ );
+ inputElement.nativeElement.value = 'bet';
+ inputElement.nativeElement.dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+ tick(component.inputChangeDebounceTime);
+ fixture.detectChanges();
+ expect(component.filteredOptions.length).toBe(1);
+ expect(component.filteredOptions[0]).toBe(nestedOptions[1]);
+ }));
+
+ it('should emit the function-derived value when optionValue is a function and returnObject is false', () => {
+ fixture.componentRef.setInput('options', nestedOptions);
+ fixture.componentRef.setInput('optionLabel', (o: any) => o.meta.title);
+ fixture.componentRef.setInput('optionValue', (o: any) => o.meta.id);
+ fixture.componentRef.setInput('returnObject', false);
+ fixture.detectChanges();
+ jest.spyOn(component.valueChanged, 'emit');
+ component.select(nestedOptions[1], false);
+ expect(component.valueChanged.emit).toHaveBeenCalledWith(2);
+ });
+
+ it('should collect all function-derived values when toggleAll is called with returnObject false', () => {
+ fixture.componentRef.setInput('options', nestedOptions);
+ fixture.componentRef.setInput('optionLabel', (o: any) => o.meta.title);
+ fixture.componentRef.setInput('optionValue', (o: any) => o.meta.id);
+ fixture.componentRef.setInput('returnObject', false);
+ fixture.componentRef.setInput('multiple', true);
+ component.value = [];
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ component.toggleAll();
+ expect(component.value).toEqual([1, 2, 3]);
+ });
+
+ it('should display chips with labels from function optionLabel', () => {
+ fixture.componentRef.setInput('options', nestedOptions);
+ fixture.componentRef.setInput('optionLabel', (o: any) => o.meta.title);
+ fixture.componentRef.setInput('multiple', true);
+ component.value = [nestedOptions[0], nestedOptions[2]];
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ const chipElements = fixture.debugElement.queryAll(By.css('cps-chip'));
+ expect(chipElements.length).toBe(2);
+ expect(chipElements[0].nativeElement.textContent.trim()).toBe('Alpha');
+ expect(chipElements[1].nativeElement.textContent.trim()).toBe('Gamma');
+ });
+ });
+
describe('aria-label', () => {
it('should set aria-label from ariaLabel input', () => {
fixture.componentRef.setInput('ariaLabel', 'Search options');
diff --git a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.ts b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.ts
index 8e7414be..ee2d2291 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.ts
+++ b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.ts
@@ -41,6 +41,7 @@ import { LabelByValuePipe } from '../../pipes/internal/label-by-value.pipe';
import { CheckOptionSelectedPipe } from '../../pipes/internal/check-option-selected.pipe';
import { isEqual } from 'lodash-es';
import { CpsTooltipPosition } from '../../directives/cps-tooltip/cps-tooltip.directive';
+import { getOptionProp, OptionKey } from '../../utils/internal/option-utils';
import {
CpsMenuComponent,
CpsMenuHideReason
@@ -194,22 +195,22 @@ export class CpsAutocompleteComponent
@Input() keepInitialOrder = false;
/**
- * Name of the label field of an option.
+ * Name of the label field of an option, or a function that receives the option and returns the label.
* @group Props
*/
- @Input() optionLabel = 'label';
+ @Input() optionLabel: OptionKey = 'label';
/**
- * Name of the value field of an option. Needed only if returnObject prop is false.
+ * Name of the value field of an option, or a function that receives the option and returns the value. Needed only if returnObject prop is false.
* @group Props
*/
- @Input() optionValue = 'value';
+ @Input() optionValue: OptionKey = 'value';
/**
- * Name of the info field of an option, shows the additional information text.
+ * Name of the info field of an option, or a function that receives the option and returns the info text.
* @group Props
*/
- @Input() optionInfo = 'info';
+ @Input() optionInfo: OptionKey = 'info';
/**
* Hides hint and validation errors.
@@ -544,6 +545,10 @@ export class CpsAutocompleteComponent
this._destroy$.complete();
}
+ getProp(option: any, key: OptionKey): any {
+ return getOptionProp(option, key);
+ }
+
select(
option: any,
byValue: boolean,
@@ -559,7 +564,7 @@ export class CpsAutocompleteComponent
? option
: this.returnObject
? option
- : option[this.optionValue];
+ : getOptionProp(option, this.optionValue);
if (this.multiple) {
let res = [];
if (includes(this.value, val)) {
@@ -567,7 +572,9 @@ export class CpsAutocompleteComponent
} else {
if (this.keepInitialOrder) {
this.options.forEach((o) => {
- const ov = this.returnObject ? o : o[this.optionValue];
+ const ov = this.returnObject
+ ? o
+ : getOptionProp(o, this.optionValue);
if (
this.value.some((v: any) => isEqual(v, ov)) ||
isEqual(val, ov)
@@ -577,12 +584,15 @@ export class CpsAutocompleteComponent
});
} else {
const opt = this.options.find((o) => {
- return isEqual(val, this.returnObject ? o : o[this.optionValue]);
+ return isEqual(
+ val,
+ this.returnObject ? o : getOptionProp(o, this.optionValue)
+ );
});
if (opt) {
res = [
...this.value,
- this.returnObject ? opt : opt[this.optionValue]
+ this.returnObject ? opt : getOptionProp(opt, this.optionValue)
];
}
}
@@ -613,7 +623,7 @@ export class CpsAutocompleteComponent
res = this.options;
} else {
this.options.forEach((o) => {
- res.push(o[this.optionValue]);
+ res.push(getOptionProp(o, this.optionValue));
});
}
}
@@ -649,7 +659,9 @@ export class CpsAutocompleteComponent
this.backspaceClickedOnce = false;
let _filteredOptions = this.options.filter((o: any) => {
- let res = o[this.optionLabel].toLowerCase().includes(searchVal);
+ let res = (getOptionProp(o, this.optionLabel) || '')
+ .toLowerCase()
+ .includes(searchVal);
if (
!res &&
this.withOptionsAliases &&
@@ -921,7 +933,7 @@ export class CpsAutocompleteComponent
? undefined
: this.returnObject
? option
- : option[this.optionValue];
+ : getOptionProp(option, this.optionValue);
}
private _toggleOptions(show?: boolean): void {
@@ -1013,7 +1025,7 @@ export class CpsAutocompleteComponent
private _getValueLabel() {
return !this.isEmptyValue()
? this.returnObject
- ? this.value[this.optionLabel]
+ ? getOptionProp(this.value, this.optionLabel)
: this._labelByValue.transform(
this.value,
this.options,
@@ -1162,7 +1174,8 @@ export class CpsAutocompleteComponent
}
const found = this.filteredOptions.find(
- (o: any) => o[this.optionLabel].toLowerCase() === searchVal
+ (o: any) =>
+ (getOptionProp(o, this.optionLabel) || '').toLowerCase() === searchVal
);
if (found) {
this.select(found, false, true, needFocusInput);
diff --git a/projects/cps-ui-kit/src/lib/components/cps-select/cps-select.component.html b/projects/cps-ui-kit/src/lib/components/cps-select/cps-select.component.html
index e2294228..a4f01af9 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-select/cps-select.component.html
+++ b/projects/cps-ui-kit/src/lib/components/cps-select/cps-select.component.html
@@ -54,7 +54,7 @@
{{
returnObject
- ? value[optionLabel]
+ ? getProp(value, optionLabel)
: (value
| labelByValue: options : optionValue : optionLabel)
}}
@@ -212,14 +212,14 @@
{{ item[optionLabel] }}{{ getProp(item, optionLabel) }}
- {{ item[optionInfo] }}
+ {{ getProp(item, optionInfo) }}
@if (item[optionIcon]) {
isEqual(item, val)) || false;
@@ -427,7 +432,7 @@ export class CpsSelectComponent
? option
: this.returnObject
? option
- : option[this.optionValue];
+ : getOptionProp(option, this.optionValue);
if (this.multiple) {
let res = [];
if (includes(this.value, val)) {
@@ -435,7 +440,9 @@ export class CpsSelectComponent
} else {
if (this.keepInitialOrder) {
this.options.forEach((o) => {
- const ov = this.returnObject ? o : o[this.optionValue];
+ const ov = this.returnObject
+ ? o
+ : getOptionProp(o, this.optionValue);
if (
this.value.some((v: any) => isEqual(v, ov)) ||
isEqual(val, ov)
@@ -445,12 +452,15 @@ export class CpsSelectComponent
});
} else {
const opt = this.options.find((o) => {
- return isEqual(val, this.returnObject ? o : o[this.optionValue]);
+ return isEqual(
+ val,
+ this.returnObject ? o : getOptionProp(o, this.optionValue)
+ );
});
if (opt) {
res = [
...this.value,
- this.returnObject ? opt : opt[this.optionValue]
+ this.returnObject ? opt : getOptionProp(opt, this.optionValue)
];
}
}
@@ -578,7 +588,7 @@ export class CpsSelectComponent
res = this.options;
} else {
this.options.forEach((o) => {
- res.push(o[this.optionValue]);
+ res.push(getOptionProp(o, this.optionValue));
});
}
}
diff --git a/projects/cps-ui-kit/src/lib/components/internal/cps-base-tree-dropdown/cps-base-tree-dropdown.component.ts b/projects/cps-ui-kit/src/lib/components/internal/cps-base-tree-dropdown/cps-base-tree-dropdown.component.ts
index 8400fbb4..40f3d713 100644
--- a/projects/cps-ui-kit/src/lib/components/internal/cps-base-tree-dropdown/cps-base-tree-dropdown.component.ts
+++ b/projects/cps-ui-kit/src/lib/components/internal/cps-base-tree-dropdown/cps-base-tree-dropdown.component.ts
@@ -20,6 +20,7 @@ import { Subscription } from 'rxjs';
import { Tree } from 'primeng/tree';
import { isEqual } from 'lodash-es';
import { IconType, iconSizeType } from '../../cps-icon/cps-icon.component';
+import { getOptionProp, OptionKey } from '../../../utils/internal/option-utils';
import { convertSize } from '../../../utils/internal/size-utils';
import { CpsTooltipPosition } from '../../../directives/cps-tooltip/cps-tooltip.directive';
import { CpsMenuComponent } from '../../cps-menu/cps-menu.component';
@@ -90,16 +91,16 @@ export class CpsBaseTreeDropdownComponent
@Input() openOnClear = true;
/**
- * Name of the label field of an option.
+ * Name of the label field of an option, or a function that receives the option and returns the label.
* @group Props
*/
- @Input() optionLabel = 'label';
+ @Input() optionLabel: OptionKey = 'label';
/**
- * Name of the info field of an option, shows the additional information text.
+ * Name of the info field of an option, or a function that receives the option and returns the info text.
* @group Props
*/
- @Input() optionInfo = 'info';
+ @Input() optionInfo: OptionKey = 'info';
/**
* Options for hiding details.
@@ -649,15 +650,15 @@ export class CpsBaseTreeDropdownComponent
private _toInnerOptions(_options: any[]): TreeNode[] {
const mapOption = (
o: any,
- optionLabel: string,
- optionInfo: string,
+ optionLabel: OptionKey,
+ optionInfo: OptionKey,
key: string,
originalOptionsMap: any
) => {
const inner = {
inner: true,
- label: o[optionLabel],
- info: o[optionInfo],
+ label: getOptionProp(o, optionLabel),
+ info: getOptionProp(o, optionInfo),
key,
styleClass: 'key-' + key
} as TreeNode;
diff --git a/projects/cps-ui-kit/src/lib/pipes/internal/check-option-selected.pipe.ts b/projects/cps-ui-kit/src/lib/pipes/internal/check-option-selected.pipe.ts
index 4d2c17d8..c66451e3 100644
--- a/projects/cps-ui-kit/src/lib/pipes/internal/check-option-selected.pipe.ts
+++ b/projects/cps-ui-kit/src/lib/pipes/internal/check-option-selected.pipe.ts
@@ -1,5 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
import { isEqual } from 'lodash-es';
+import { getOptionProp, OptionKey } from '../../utils/internal/option-utils';
@Pipe({ standalone: true, name: 'checkOptionSelected' })
export class CheckOptionSelectedPipe implements PipeTransform {
@@ -8,7 +9,7 @@ export class CheckOptionSelectedPipe implements PipeTransform {
value: any,
multiple: boolean,
returnObject: boolean,
- optionValue: string
+ optionValue: OptionKey
): boolean {
function includes(array: any[], val: any): boolean {
return array?.some((item) => isEqual(item, val)) || false;
@@ -17,9 +18,9 @@ export class CheckOptionSelectedPipe implements PipeTransform {
return multiple
? returnObject
? includes(value, option)
- : includes(value, option[optionValue])
+ : includes(value, getOptionProp(option, optionValue))
: returnObject
? isEqual(option, value)
- : isEqual(option[optionValue], value);
+ : isEqual(getOptionProp(option, optionValue), value);
}
}
diff --git a/projects/cps-ui-kit/src/lib/pipes/internal/combine-labels.pipe.ts b/projects/cps-ui-kit/src/lib/pipes/internal/combine-labels.pipe.ts
index 7f3f66a3..f570ac3a 100644
--- a/projects/cps-ui-kit/src/lib/pipes/internal/combine-labels.pipe.ts
+++ b/projects/cps-ui-kit/src/lib/pipes/internal/combine-labels.pipe.ts
@@ -1,22 +1,25 @@
import { Pipe, PipeTransform } from '@angular/core';
import { isEqual } from 'lodash-es';
+import { getOptionProp, OptionKey } from '../../utils/internal/option-utils';
@Pipe({ standalone: true, name: 'combineLabels' })
export class CombineLabelsPipe implements PipeTransform {
transform(
values: any[],
options: any[],
- valueKey: string,
- labelKey: string,
+ valueKey: OptionKey,
+ labelKey: OptionKey,
returnObject: boolean
): string {
return values
.map((v) => {
if (returnObject) {
- return v[labelKey];
+ return getOptionProp(v, labelKey);
} else {
- const option = options.find((opt) => isEqual(opt[valueKey], v));
- return option ? option[labelKey] : '';
+ const option = options.find((opt) =>
+ isEqual(getOptionProp(opt, valueKey), v)
+ );
+ return option ? getOptionProp(option, labelKey) : '';
}
})
.join(', ');
diff --git a/projects/cps-ui-kit/src/lib/pipes/internal/label-by-value.pipe.ts b/projects/cps-ui-kit/src/lib/pipes/internal/label-by-value.pipe.ts
index a4201005..da1d468a 100644
--- a/projects/cps-ui-kit/src/lib/pipes/internal/label-by-value.pipe.ts
+++ b/projects/cps-ui-kit/src/lib/pipes/internal/label-by-value.pipe.ts
@@ -1,15 +1,18 @@
import { Pipe, PipeTransform } from '@angular/core';
import { isEqual } from 'lodash-es';
+import { getOptionProp, OptionKey } from '../../utils/internal/option-utils';
@Pipe({ standalone: true, name: 'labelByValue' })
export class LabelByValuePipe implements PipeTransform {
transform(
value: any,
options: any[],
- valueKey: string,
- labelKey: string
+ valueKey: OptionKey,
+ labelKey: OptionKey
): string {
- const option = options.find((opt) => isEqual(opt[valueKey], value));
- return option ? option[labelKey] : '';
+ const option = options.find((opt) =>
+ isEqual(getOptionProp(opt, valueKey), value)
+ );
+ return option ? getOptionProp(option, labelKey) : '';
}
}
diff --git a/projects/cps-ui-kit/src/lib/utils/internal/option-utils.ts b/projects/cps-ui-kit/src/lib/utils/internal/option-utils.ts
new file mode 100644
index 00000000..1dea7772
--- /dev/null
+++ b/projects/cps-ui-kit/src/lib/utils/internal/option-utils.ts
@@ -0,0 +1,6 @@
+export type OptionKey = string | ((option: any) => any);
+
+export function getOptionProp(option: any, key: OptionKey): any {
+ if (typeof key === 'function') return key(option);
+ return option?.[key];
+}
diff --git a/projects/cps-ui-kit/src/public-api.ts b/projects/cps-ui-kit/src/public-api.ts
index 797cb1af..6c7b05f3 100644
--- a/projects/cps-ui-kit/src/public-api.ts
+++ b/projects/cps-ui-kit/src/public-api.ts
@@ -65,3 +65,4 @@ export * from './lib/services/cps-theme/cps-theme.service';
export * from './lib/services/cps-cron-validation/cps-cron-validation.service';
export * from './lib/utils/colors-utils';
+export { OptionKey } from './lib/utils/internal/option-utils';