mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 01:52:19 +08:00
feat(select): add compareWith property (#17358)
* feat(select): add compareWith property * style(select): fix lint errors * test(select): move tests from preview to basic * refactor(select): improve parameter names in compareOptions method * chore(): add react usage docs * chore(): update var names, update examples * rerun build * add doc on compareWith
This commit is contained in:

committed by
Liam DeBeasi

parent
14dd871a85
commit
69ecebb159
@ -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: '<ng-content></ng-content>', inputs: ['mode', 'disabled', 'cancelText', 'okText', 'placeholder', 'name', 'selectedText', 'multiple', 'interface', 'interfaceOptions', 'value'] })
|
||||
@Component({ selector: 'ion-select', changeDetection: 0, template: '<ng-content></ng-content>', inputs: ['mode', 'disabled', 'cancelText', 'okText', 'placeholder', 'name', 'selectedText', 'multiple', 'interface', 'interfaceOptions', 'compareWith', 'value'] })
|
||||
export class IonSelect {
|
||||
ionChange!: EventEmitter<CustomEvent>;
|
||||
ionCancel!: EventEmitter<CustomEvent>;
|
||||
@ -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: '<ng-content></ng-content>', inputs: ['disabled', 'selected', 'value'] })
|
||||
|
@ -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
|
||||
|
11
core/src/components.d.ts
vendored
11
core/src/components.d.ts
vendored
@ -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;
|
||||
|
@ -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
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
## Objects as Values
|
||||
|
||||
```html
|
||||
<ion-list>
|
||||
<ion-list-header>Objects as Values (compareWith)</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select [compareWith]="compareWith">
|
||||
<ion-select-option *ngFor="let user of users">{{user.first + ' ' + user.last}}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
```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 {
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
## Objects as Values
|
||||
|
||||
```html
|
||||
<ion-list>
|
||||
<ion-list-header>Objects as Values (compareWith)</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select id="objectSelectCompareWith"></ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
```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
|
||||
@ -402,6 +529,20 @@ const Example: React.SFC<{}> = () => (
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
## Objects as Values
|
||||
|
||||
<IonList>
|
||||
<IonListHeader>Objects as Values (compareWith)</IonListHeader>
|
||||
<IonItem>
|
||||
<IonLabel>Users</IonLabel>
|
||||
<IonSelect compareWith={compareWith}>
|
||||
{objectOptions.map((object, i) => {
|
||||
return <IonSelectOption key={object.id} value={object.id}>{object.first} {object.last}</IonSelectOption>
|
||||
})}
|
||||
</IonSelect>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
|
||||
## Interface Options
|
||||
|
||||
@ -598,8 +739,9 @@ export default Example;
|
||||
## Properties
|
||||
|
||||
| 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` | `{}` |
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -55,6 +55,15 @@
|
||||
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>Object Values with trackBy</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select id="objectSelectCompareWith"></ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>Select - Custom Interface Options</ion-list-header>
|
||||
|
||||
@ -280,6 +289,48 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
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; // 'id';
|
||||
|
||||
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)
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
objectSelectElement.value = {
|
||||
id: 1,
|
||||
first: 'Alice',
|
||||
last: 'Smith',
|
||||
};
|
||||
}, 3000);
|
||||
|
||||
var pets = document.getElementById('pets');
|
||||
pets.value = ['bird', 'dog'];
|
||||
|
||||
|
@ -59,6 +59,56 @@
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
## Objects as Values
|
||||
|
||||
```html
|
||||
<ion-list>
|
||||
<ion-list-header>Objects as Values (compareWith)</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select [compareWith]="compareWith">
|
||||
<ion-select-option *ngFor="let user of users">{{user.first + ' ' + user.last}}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
```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
|
||||
|
@ -59,6 +59,56 @@
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
## Objects as Values
|
||||
|
||||
```html
|
||||
<ion-list>
|
||||
<ion-list-header>Objects as Values (compareWith)</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select id="objectSelectCompareWith"></ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
```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
|
||||
|
@ -21,6 +21,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
|
||||
@ -82,6 +104,20 @@ const Example: React.SFC<{}> = () => (
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
## Objects as Values
|
||||
|
||||
<IonList>
|
||||
<IonListHeader>Objects as Values (compareWith)</IonListHeader>
|
||||
<IonItem>
|
||||
<IonLabel>Users</IonLabel>
|
||||
<IonSelect compareWith={compareWith}>
|
||||
{objectOptions.map((object, i) => {
|
||||
return <IonSelectOption key={object.id} value={object.id}>{object.first} {object.last}</IonSelectOption>
|
||||
})}
|
||||
</IonSelect>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
|
||||
## Interface Options
|
||||
|
||||
|
Reference in New Issue
Block a user