diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index b802cbf1b1..29362ba1a2 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -720,7 +720,7 @@ export class IonSegmentButton { proxyInputs(IonSegmentButton, ['mode', 'checked', 'disabled', 'layout', 'value']); export declare interface IonSelect extends StencilComponents<'IonSelect'> {} -@Component({ selector: 'ion-select', changeDetection: 0, template: '', inputs: ['mode', 'disabled', 'cancelText', 'okText', 'placeholder', 'name', 'selectedText', 'multiple', 'interface', 'interfaceOptions', 'value'] }) +@Component({ selector: 'ion-select', changeDetection: 0, template: '', inputs: ['mode', 'disabled', 'cancelText', 'okText', 'placeholder', 'name', 'selectedText', 'multiple', 'interface', 'interfaceOptions', 'compareWith', 'value'] }) export class IonSelect { ionChange!: EventEmitter; ionCancel!: EventEmitter; @@ -734,7 +734,7 @@ export class IonSelect { } } proxyMethods(IonSelect, ['open']); -proxyInputs(IonSelect, ['mode', 'disabled', 'cancelText', 'okText', 'placeholder', 'name', 'selectedText', 'multiple', 'interface', 'interfaceOptions', 'value']); +proxyInputs(IonSelect, ['mode', 'disabled', 'cancelText', 'okText', 'placeholder', 'name', 'selectedText', 'multiple', 'interface', 'interfaceOptions', 'compareWith', 'value']); export declare interface IonSelectOption extends StencilComponents<'IonSelectOption'> {} @Component({ selector: 'ion-select-option', changeDetection: 0, template: '', inputs: ['disabled', 'selected', 'value'] }) diff --git a/core/api.txt b/core/api.txt index ed1379141c..17c1b7f5fa 100644 --- a/core/api.txt +++ b/core/api.txt @@ -954,6 +954,7 @@ ion-select-option,prop,value,any,undefined,false,false ion-select,shadow ion-select,prop,cancelText,string,'Cancel',false,false +ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false ion-select,prop,disabled,boolean,false,false,false ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false ion-select,prop,interfaceOptions,any,{},false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 6da6a69257..8e6b42d13f 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -77,6 +77,9 @@ import { import { EventEmitter, } from '@stencil/core'; +import { + SelectCompareFn, +} from './components/select/select-interface'; export namespace Components { @@ -3993,6 +3996,10 @@ export namespace Components { */ 'cancelText': string; /** + * A property name or function used to compare object values + */ + 'compareWith'?: string | SelectCompareFn | null; + /** * If `true`, the user cannot interact with the select. */ 'disabled': boolean; @@ -4043,6 +4050,10 @@ export namespace Components { */ 'cancelText'?: string; /** + * A property name or function used to compare object values + */ + 'compareWith'?: string | SelectCompareFn | null; + /** * If `true`, the user cannot interact with the select. */ 'disabled'?: boolean; diff --git a/core/src/components/select/readme.md b/core/src/components/select/readme.md index d14f3366c4..f223da326b 100644 --- a/core/src/components/select/readme.md +++ b/core/src/components/select/readme.md @@ -23,6 +23,11 @@ By adding the `multiple` attribute to select, users are able to select multiple Note: the `action-sheet` and `popover` interfaces will not work with multiple selection. +## Object Value References + +When using objects for select values, it is possible for the identities of these objects to change if they are coming from a server or database, while the selected value's identity remains the same. For example, this can occur when an existing record with the desired object value is loaded into the select, but the newly retrieved select options now have different identities. This will result in the select appearing to have no value at all, even though the original selection in still intact. + +By default, the select uses object equality (`===`) to determine if an option is selected. This can be overridden by providing a property name or a function to the `compareWith` property. ## Select Buttons @@ -104,6 +109,56 @@ Since select uses the alert, action sheet and popover interfaces, options can be ``` +## Objects as Values + +```html + + Objects as Values (compareWith) + + + Users + + {{user.first + ' ' + user.last}} + + + +``` + +```typescript +import { Component } from '@angular/core'; + +@Component({ + selector: 'select-example', + templateUrl: 'select-example.html', + styleUrls: ['./select-example.css'], +}) +export class SelectExample { + users: any[] = [ + { + id: 1, + first: 'Alice', + last: 'Smith', + }, + { + id: 2, + first: 'Bob', + last: 'Davis', + }, + { + id: 3, + first: 'Charlie', + last: 'Rosenburg', + } + ]; + + compareWithFn = (o1, o2) => { + return o1 && o2 ? o1.id === o2.id : o1 === o2; + }; + + compareWith = compareWithFn; +} +``` + ## Interface Options ```html @@ -243,6 +298,56 @@ export class SelectExample { ``` +## Objects as Values + +```html + + Objects as Values (compareWith) + + + Users + + + +``` + +```javascript + let objectOptions = [ + { + id: 1, + first: 'Alice', + last: 'Smith', + }, + { + id: 2, + first: 'Bob', + last: 'Davis', + }, + { + id: 3, + first: 'Charlie', + last: 'Rosenburg', + } + ]; + + let compareWithFn = (o1, o2) => { + return o1 && o2 ? o1.id === o2.id : o1 === o2; + }; + + let objectSelectElement = document.getElementById('objectSelectCompareWith'); + objectSelectElement.compareWith = compareWithFn; + + objectOptions.forEach((option, i) => { + let selectOption = document.createElement('ion-select-option'); + selectOption.value = option; + selectOption.textContent = option.first + ' ' + option.last; + selectOption.selected = (i === 0); + + objectSelectElement.appendChild(selectOption) + }); +} +``` + ## Interface Options ```html @@ -341,6 +446,28 @@ const customActionSheetOptions = { subHeader: 'Select your favorite color' }; +const objectOptions = [ + { + id: 1, + first: 'Alice', + last: 'Smith' + }, + { + id: 2, + first: 'Bob', + last: 'Davis' + }, + { + id: 3, + first: 'Charlie', + last: 'Rosenburg' + } +]; + +const compareWith = (o1: any, o2: any) => { + return o1 && o2 ? o1.id === o2.id : o1 === o2; +}; + const Example: React.SFC<{}> = () => ( <> ## Single Selection @@ -401,6 +528,20 @@ const Example: React.SFC<{}> = () => ( + + ## Objects as Values + + + Objects as Values (compareWith) + + Users + + {objectOptions.map((object, i) => { + return {object.first} {object.last} + })} + + + ## Interface Options @@ -597,19 +738,20 @@ export default Example; ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | -------------- | -| `cancelText` | `cancel-text` | The text to display on the cancel button. | `string` | `'Cancel'` | -| `disabled` | `disabled` | If `true`, the user cannot interact with the select. | `boolean` | `false` | -| `interface` | `interface` | The interface the select should use: `action-sheet`, `popover` or `alert`. | `"action-sheet" \| "alert" \| "popover"` | `'alert'` | -| `interfaceOptions` | `interface-options` | Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [AlertController API docs](../../alert/AlertController/#create), the [ActionSheetController API docs](../../action-sheet/ActionSheetController/#create) and the [PopoverController API docs](../../popover/PopoverController/#create) for the create options for each interface. | `any` | `{}` | -| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` | -| `multiple` | `multiple` | If `true`, the select can accept multiple values. | `boolean` | `false` | -| `name` | `name` | The name of the control, which is submitted with the form data. | `string` | `this.inputId` | -| `okText` | `ok-text` | The text to display on the ok button. | `string` | `'OK'` | -| `placeholder` | `placeholder` | The text to display when the select is empty. | `null \| string \| undefined` | `undefined` | -| `selectedText` | `selected-text` | The text to display instead of the selected option's value. | `null \| string \| undefined` | `undefined` | -| `value` | `value` | the value of the select. | `any` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------- | +| `cancelText` | `cancel-text` | The text to display on the cancel button. | `string` | `'Cancel'` | +| `compareWith` | `compare-with` | A property name or function used to compare object values | `((currentValue: any, compareValue: any) => boolean) \| null \| string \| undefined` | `undefined` | +| `disabled` | `disabled` | If `true`, the user cannot interact with the select. | `boolean` | `false` | +| `interface` | `interface` | The interface the select should use: `action-sheet`, `popover` or `alert`. | `"action-sheet" \| "alert" \| "popover"` | `'alert'` | +| `interfaceOptions` | `interface-options` | Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [AlertController API docs](../../alert/AlertController/#create), the [ActionSheetController API docs](../../action-sheet/ActionSheetController/#create) and the [PopoverController API docs](../../popover/PopoverController/#create) for the create options for each interface. | `any` | `{}` | +| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` | +| `multiple` | `multiple` | If `true`, the select can accept multiple values. | `boolean` | `false` | +| `name` | `name` | The name of the control, which is submitted with the form data. | `string` | `this.inputId` | +| `okText` | `ok-text` | The text to display on the ok button. | `string` | `'OK'` | +| `placeholder` | `placeholder` | The text to display when the select is empty. | `null \| string \| undefined` | `undefined` | +| `selectedText` | `selected-text` | The text to display instead of the selected option's value. | `null \| string \| undefined` | `undefined` | +| `value` | `value` | the value of the select. | `any` | `undefined` | ## Events diff --git a/core/src/components/select/select-interface.ts b/core/src/components/select/select-interface.ts index b33d17daa0..242e88f6e4 100644 --- a/core/src/components/select/select-interface.ts +++ b/core/src/components/select/select-interface.ts @@ -1,5 +1,7 @@ export type SelectInterface = 'action-sheet' | 'popover' | 'alert'; +export type SelectCompareFn = (currentValue: any, compareValue: any) => boolean; + export interface SelectChangeEventDetail { value: any | any[] | undefined | null; } diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 22c1432833..dcb614dd5c 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -4,6 +4,8 @@ import { ActionSheetButton, ActionSheetOptions, AlertOptions, CssClassMap, Mode, import { findItemLabel, renderHiddenInput } from '../../utils/helpers'; import { hostContext } from '../../utils/theme'; +import { SelectCompareFn } from './select-interface'; + @Component({ tag: 'ion-select', styleUrls: { @@ -82,6 +84,11 @@ export class Select implements ComponentInterface { */ @Prop() interfaceOptions: any = {}; + /** + * A property name or function used to compare object values + */ + @Prop() compareWith?: string | SelectCompareFn | null; + /** * the value of the select. */ @@ -346,7 +353,7 @@ export class Select implements ComponentInterface { // iterate all options, updating the selected prop let canSelect = true; for (const selectOption of this.childOpts) { - const selected = canSelect && isOptionSelected(this.value, selectOption.value); + const selected = canSelect && isOptionSelected(this.value, selectOption.value, this.compareWith); selectOption.selected = selected; // if current option is selected and select is single-option, we can't select @@ -370,7 +377,7 @@ export class Select implements ComponentInterface { if (selectedText != null && selectedText !== '') { return selectedText; } - return generateText(this.childOpts, this.value); + return generateText(this.childOpts, this.value, this.compareWith); } private setFocus() { @@ -468,33 +475,45 @@ function parseValue(value: any) { return value.toString(); } -function isOptionSelected(currentValue: any[] | any, optionValue: any) { +function isOptionSelected(currentValue: any[] | any, compareValue: any, compareWith?: string | SelectCompareFn | null) { if (currentValue === undefined) { return false; } if (Array.isArray(currentValue)) { - return currentValue.includes(optionValue); + return currentValue.some(val => compareOptions(val, compareValue, compareWith)); } else { - return currentValue === optionValue; + return compareOptions(currentValue, compareValue, compareWith); } } -function generateText(opts: HTMLIonSelectOptionElement[], value: any | any[]) { +function compareOptions(currentValue: any, compareValue: any, compareWith?: string | SelectCompareFn | null): boolean { + if (typeof compareWith === 'function') { + return compareWith(currentValue, compareValue); + } else if (typeof compareWith === 'string') { + return currentValue[compareWith] === compareValue[compareWith]; + } else { + return currentValue === compareValue; + } +} + +function generateText(opts: HTMLIonSelectOptionElement[], value: any | any[], compareWith?: string | SelectCompareFn | null) { if (value === undefined) { return ''; } if (Array.isArray(value)) { return value - .map(v => textForValue(opts, v)) + .map(v => textForValue(opts, v, compareWith)) .filter(opt => opt !== null) .join(', '); } else { - return textForValue(opts, value) || ''; + return textForValue(opts, value, compareWith) || ''; } } -function textForValue(opts: HTMLIonSelectOptionElement[], value: any): string | null { - const selectOpt = opts.find(opt => opt.value === value); +function textForValue(opts: HTMLIonSelectOptionElement[], value: any, compareWith?: string | SelectCompareFn | null): string | null { + const selectOpt = opts.find(opt => { + return compareOptions(opt.value, value, compareWith); + }); return selectOpt ? selectOpt.textContent : null; diff --git a/core/src/components/select/test/basic/index.html b/core/src/components/select/test/basic/index.html index 49f1292cd4..1bd5403f97 100644 --- a/core/src/components/select/test/basic/index.html +++ b/core/src/components/select/test/basic/index.html @@ -55,6 +55,15 @@ + + Object Values with trackBy + + + Users + + + + Select - Custom Interface Options @@ -280,6 +289,48 @@