mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-09 08:09:32 +08:00
feat(radio-group): add compareWith property (#28452)
This commit is contained in:
@ -7,3 +7,5 @@ export interface RadioGroupCustomEvent<T = any> extends CustomEvent {
|
||||
detail: RadioGroupChangeEventDetail<T>;
|
||||
target: HTMLIonRadioGroupElement;
|
||||
}
|
||||
|
||||
export type RadioGroupCompareFn = (currentValue: any, compareValue: any) => boolean;
|
||||
|
||||
@ -4,7 +4,7 @@ import { renderHiddenInput } from '@utils/helpers';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
|
||||
import type { RadioGroupChangeEventDetail } from './radio-group-interface';
|
||||
import type { RadioGroupChangeEventDetail, RadioGroupCompareFn } from './radio-group-interface';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-radio-group',
|
||||
@ -21,6 +21,14 @@ export class RadioGroup implements ComponentInterface {
|
||||
*/
|
||||
@Prop() allowEmptySelection = false;
|
||||
|
||||
/**
|
||||
* This property allows developers to specify a custom function or property
|
||||
* name for comparing objects when determining the selected option in the
|
||||
* ion-radio-group. When not specified, the default behavior will use strict
|
||||
* equality (===) for comparison.
|
||||
*/
|
||||
@Prop() compareWith?: string | RadioGroupCompareFn | null;
|
||||
|
||||
/**
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
|
||||
74
core/src/components/radio-group/test/compare-with/index.html
Normal file
74
core/src/components/radio-group/test/compare-with/index.html
Normal file
@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Radio Group - compareWith</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Radio Group - compareWith</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-list-header>Compare with String</ion-list-header>
|
||||
<ion-radio-group id="compareWithString" compare-with="value"> </ion-radio-group>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>Compare with Function</ion-list-header>
|
||||
<ion-radio-group id="compareWithFn"></ion-radio-group>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const options = [
|
||||
{
|
||||
label: 'Red',
|
||||
value: 'red',
|
||||
},
|
||||
{
|
||||
label: 'Blue',
|
||||
value: 'blue',
|
||||
},
|
||||
{
|
||||
label: 'Green',
|
||||
value: 'green',
|
||||
},
|
||||
];
|
||||
|
||||
const radioGroupWithFn = document.querySelector('#compareWithFn');
|
||||
radioGroupWithFn.compareWith = (a, b) => a.value === b.value;
|
||||
|
||||
document.querySelectorAll('ion-radio-group').forEach((radioGroup) => {
|
||||
radioGroup.value = options[1];
|
||||
|
||||
options.forEach((option) => {
|
||||
const radio = document.createElement('ion-radio');
|
||||
|
||||
radio.value = option;
|
||||
radio.textContent = option.label;
|
||||
|
||||
const item = document.createElement('ion-item');
|
||||
item.appendChild(radio);
|
||||
|
||||
radioGroup.appendChild(item);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
111
core/src/components/radio-group/test/radio-group.spec.tsx
Normal file
111
core/src/components/radio-group/test/radio-group.spec.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { h } from '@stencil/core';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Radio } from '../../radio/radio';
|
||||
import { RadioGroup } from '../radio-group';
|
||||
|
||||
describe('ion-radio-group', () => {
|
||||
it('should correctly set value when using compareWith string', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Radio, RadioGroup],
|
||||
template: () => (
|
||||
<ion-radio-group compareWith="value" value={{ label: 'Blue', value: 'blue' }}>
|
||||
<ion-radio value={{ label: 'Red', value: 'red' }}>Red</ion-radio>
|
||||
<ion-radio value={{ label: 'Blue', value: 'blue' }}>Blue</ion-radio>
|
||||
<ion-radio value={{ label: 'Green', value: 'green' }}>Green</ion-radio>
|
||||
</ion-radio-group>
|
||||
),
|
||||
});
|
||||
|
||||
const radioGroup = page.body.querySelector('ion-radio-group')!;
|
||||
const radios = document.querySelectorAll('ion-radio')!;
|
||||
|
||||
await radios[2].click();
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(radios[2].getAttribute('aria-checked')).toBe('true');
|
||||
expect(radioGroup.value).toEqual({
|
||||
label: 'Green',
|
||||
value: 'green',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly set value when using compareWith function', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Radio, RadioGroup],
|
||||
template: () => (
|
||||
<ion-radio-group value={{ label: 'Blue', value: 'blue' }}>
|
||||
<ion-radio value={{ label: 'Red', value: 'red' }}>Red</ion-radio>
|
||||
<ion-radio value={{ label: 'Blue', value: 'blue' }}>Blue</ion-radio>
|
||||
<ion-radio value={{ label: 'Green', value: 'green' }}>Green</ion-radio>
|
||||
</ion-radio-group>
|
||||
),
|
||||
});
|
||||
|
||||
const radioGroup = page.body.querySelector('ion-radio-group')!;
|
||||
const radios = document.querySelectorAll('ion-radio')!;
|
||||
radioGroup.compareWith = (a, b) => a.value === b.value;
|
||||
|
||||
await radios[2].click();
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(radios[2].getAttribute('aria-checked')).toBe('true');
|
||||
expect(radioGroup.value).toEqual({
|
||||
label: 'Green',
|
||||
value: 'green',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly set value when using compareWith null', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [RadioGroup, Radio],
|
||||
template: () => (
|
||||
<ion-radio-group compareWith={null} value="blue">
|
||||
<ion-radio value="red">Red</ion-radio>
|
||||
<br />
|
||||
<ion-radio value="blue">Blue</ion-radio>
|
||||
<br />
|
||||
<ion-radio value="green">Green</ion-radio>
|
||||
</ion-radio-group>
|
||||
),
|
||||
});
|
||||
|
||||
const radioGroup = page.body.querySelector('ion-radio-group')!;
|
||||
const radios = document.querySelectorAll('ion-radio')!;
|
||||
|
||||
await radios[2].click();
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(radios[2].getAttribute('aria-checked')).toBe('true');
|
||||
expect(radioGroup.value).toEqual('green');
|
||||
});
|
||||
|
||||
it('should work with different parameter types', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Radio, RadioGroup],
|
||||
template: () => (
|
||||
<ion-radio-group value={2}>
|
||||
<ion-radio value={1}>Option #1</ion-radio>
|
||||
<ion-radio value={2}>Option #2</ion-radio>
|
||||
<ion-radio value={3}>Option #3</ion-radio>
|
||||
</ion-radio-group>
|
||||
),
|
||||
});
|
||||
|
||||
const radioGroup = page.body.querySelector('ion-radio-group')!;
|
||||
radioGroup.compareWith = (val1, val2) => {
|
||||
// convert val1 to a number
|
||||
return +val1 === val2;
|
||||
};
|
||||
|
||||
const radios = document.querySelectorAll('ion-radio')!;
|
||||
|
||||
await expect(radios[1].getAttribute('aria-checked')).toBe('true');
|
||||
|
||||
await radios[2].click();
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(radios[2].getAttribute('aria-checked')).toBe('true');
|
||||
expect(radioGroup.value).toEqual(3);
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import type { LegacyFormController } from '@utils/forms';
|
||||
import { createLegacyFormController } from '@utils/forms';
|
||||
import { createLegacyFormController, isOptionSelected } from '@utils/forms';
|
||||
import { addEventListener, getAriaLabel, removeEventListener } from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@ -196,7 +196,9 @@ export class Radio implements ComponentInterface {
|
||||
|
||||
private updateState = () => {
|
||||
if (this.radioGroup) {
|
||||
this.checked = this.radioGroup.value === this.value;
|
||||
const { compareWith, value: radioGroupValue } = this.radioGroup;
|
||||
|
||||
this.checked = isOptionSelected(radioGroupValue, this.value, compareWith);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core';
|
||||
import type { LegacyFormController, NotchController } from '@utils/forms';
|
||||
import { createLegacyFormController, createNotchController } from '@utils/forms';
|
||||
import { compareOptions, createLegacyFormController, createNotchController, isOptionSelected } from '@utils/forms';
|
||||
import { findItemLabel, focusElement, getAriaLabel, renderHiddenInput, inheritAttributes } from '@utils/helpers';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
@ -82,7 +82,10 @@ export class Select implements ComponentInterface {
|
||||
@Prop({ reflect: true }) color?: Color;
|
||||
|
||||
/**
|
||||
* A property name or function used to compare object values
|
||||
* This property allows developers to specify a custom function or property
|
||||
* name for comparing objects when determining the selected option in the
|
||||
* ion-select. When not specified, the default behavior will use strict
|
||||
* equality (===) for comparison.
|
||||
*/
|
||||
@Prop() compareWith?: string | SelectCompareFn | null;
|
||||
|
||||
@ -1076,21 +1079,6 @@ Developers can use the "legacy" property to continue using the legacy form marku
|
||||
}
|
||||
}
|
||||
|
||||
const isOptionSelected = (
|
||||
currentValue: any[] | any,
|
||||
compareValue: any,
|
||||
compareWith?: string | SelectCompareFn | null
|
||||
) => {
|
||||
if (currentValue === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(currentValue)) {
|
||||
return currentValue.some((val) => compareOptions(val, compareValue, compareWith));
|
||||
} else {
|
||||
return compareOptions(currentValue, compareValue, compareWith);
|
||||
}
|
||||
};
|
||||
|
||||
const getOptionValue = (el: HTMLIonSelectOptionElement) => {
|
||||
const value = el.value;
|
||||
return value === undefined ? el.textContent || '' : value;
|
||||
@ -1106,20 +1094,6 @@ const parseValue = (value: any) => {
|
||||
return value.toString();
|
||||
};
|
||||
|
||||
const 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 Array.isArray(compareValue) ? compareValue.includes(currentValue) : currentValue === compareValue;
|
||||
}
|
||||
};
|
||||
|
||||
const generateText = (
|
||||
opts: HTMLIonSelectOptionElement[],
|
||||
value: any | any[],
|
||||
|
||||
Reference in New Issue
Block a user