Skip to content

Commit d2b94ed

Browse files
authored
feat: add onChange event whenever a selection changes, fix #372 (#374)
* feat: add `onChange` event whenever a selection changes, fix #372
1 parent 5cfe7ed commit d2b94ed

File tree

5 files changed

+76
-13
lines changed

5 files changed

+76
-13
lines changed

packages/demo/src/events/events.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export default class Example {
4545
onAfterCreate: () => {
4646
this.log('onAfterCreate event fire!\n');
4747
},
48+
onChange: data => {
49+
this.log(`onChange event fire! data: ${JSON.stringify(data)}\n`);
50+
},
4851
}) as MultipleSelectInstance;
4952
}
5053

packages/multiple-select-vanilla/src/MultipleSelectInstance.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
* @author zhixin wen <[email protected]>
33
*/
44
import Constants from './constants.js';
5-
import type { HtmlStruct, OptGroupRowData, OptionDataObject, OptionRowData } from './models/interfaces.js';
5+
import type { HtmlStruct, OptGroupRowData, OptionRowData, OptionDataObject } from './models/interfaces.js';
66
import type { MultipleSelectLocales } from './models/locale.interface.js';
7-
import type { CloseReason, MultipleSelectOption } from './models/multipleSelectOption.interface.js';
7+
import type { ClickedGroup, ClickedOption, CloseReason, MultipleSelectOption } from './models/multipleSelectOption.interface.js';
88
import { BindingEventService } from './services/binding-event.service.js';
99
import { VirtualScroll } from './services/virtual-scroll.js';
1010
import { compareObjects, deepCopy, findByParam, removeDiacritics, removeUndefined, setDataKeys, stripScripts } from './utils/utils.js';
@@ -787,10 +787,15 @@ export class MultipleSelectInstance {
787787
this.isPartiallyAllSelected = !this.isAllSelected && selectedTotal > 0;
788788

789789
if (!ignoreTrigger) {
790+
let eventCalled: 'onCheckAll' | 'onUncheckAll' | '' = '';
790791
if (this.isAllSelected) {
791-
this.options.onCheckAll();
792+
eventCalled = 'onCheckAll';
792793
} else if (selectedTotal === 0) {
793-
this.options.onUncheckAll();
794+
eventCalled = 'onUncheckAll';
795+
}
796+
if (eventCalled) {
797+
this.options[eventCalled]();
798+
this.handleOnChange(eventCalled);
794799
}
795800
}
796801
}
@@ -974,7 +979,7 @@ export class MultipleSelectInstance {
974979
this.options.onOptgroupClick(
975980
removeUndefined({
976981
label: group.label,
977-
selected: group.selected,
982+
selected: !!group.selected,
978983
data: group._data,
979984
children: group.children.map((child: any) => {
980985
if (child) {
@@ -989,6 +994,11 @@ export class MultipleSelectInstance {
989994
}),
990995
}),
991996
);
997+
this.handleOnChange('onOptgroupClick', {
998+
label: group.label,
999+
selected: !!group.selected,
1000+
type: group.type as 'optgroup',
1001+
});
9921002
}) as EventListener,
9931003
undefined,
9941004
'group-checkbox-list',
@@ -1023,6 +1033,12 @@ export class MultipleSelectInstance {
10231033
data: option._data,
10241034
}),
10251035
);
1036+
this.handleOnChange('onClick', {
1037+
label: option.text,
1038+
value: option.value,
1039+
selected: option.selected,
1040+
type: option.type as 'option',
1041+
});
10261042

10271043
close();
10281044
}) as EventListener,
@@ -1135,6 +1151,17 @@ export class MultipleSelectInstance {
11351151
}
11361152
}
11371153

1154+
protected handleOnChange(eventName: string, item?: ClickedGroup | ClickedOption) {
1155+
this.options.onChange({
1156+
eventName,
1157+
item,
1158+
selection: {
1159+
labels: this.getSelects('text'),
1160+
values: this.getSelects('value'),
1161+
},
1162+
});
1163+
}
1164+
11381165
protected handleEscapeKey() {
11391166
if (!this.options.keepOpen) {
11401167
this.close('key.escape');
@@ -1456,7 +1483,7 @@ export class MultipleSelectInstance {
14561483
let textSelects = this.getSelects('text');
14571484

14581485
if (this.options.displayValues) {
1459-
textSelects = valueSelects;
1486+
textSelects = valueSelects as string[];
14601487
}
14611488

14621489
const spanElm = this.choiceElm?.querySelector<HTMLSpanElement>('span');
@@ -1508,7 +1535,7 @@ export class MultipleSelectInstance {
15081535
// set selects to select
15091536
const selectedValues = this.getSelects();
15101537
if (this.options.single) {
1511-
this.elm.value = selectedValues.length ? selectedValues[0] : '';
1538+
this.elm.value = selectedValues.length ? (selectedValues[0] as string) : '';
15121539
} else {
15131540
// when multiple values could be set, we need to loop through each
15141541
Array.from(this.elm.options).forEach(option => {
@@ -1616,8 +1643,8 @@ export class MultipleSelectInstance {
16161643
}
16171644

16181645
// value html, or text, default: 'value'
1619-
getSelects(type = 'value') {
1620-
const values = [];
1646+
getSelects<T extends 'text' | 'value'>(type: T = 'value' as T) {
1647+
const values: Array<string | number | boolean> = [];
16211648
for (const row of this.data || []) {
16221649
if ((row as OptGroupRowData).type === 'optgroup') {
16231650
const selectedChildren = (row as OptGroupRowData).children.filter(child => child?.selected);
@@ -1640,10 +1667,10 @@ export class MultipleSelectInstance {
16401667
values.push(value.join(''));
16411668
}
16421669
} else if (row.selected) {
1643-
values.push(type === 'value' ? row._value || (row as OptionRowData)[type] : (row as any)[type]);
1670+
values.push(type === 'value' ? row._value || (row as OptionRowData)[type] : (row as OptionRowData)[type]);
16441671
}
16451672
}
1646-
return values;
1673+
return values as T extends 'text' ? string[] : Array<string | number | boolean>;
16471674
}
16481675

16491676
setSelects(values: any[], type = 'value', ignoreTrigger = false) {

packages/multiple-select-vanilla/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const DEFAULTS: Partial<MultipleSelectOption> = {
6767
labelTemplate: (elm: HTMLOptionElement) => elm.label,
6868

6969
onBeforeOpen: noopFalse,
70+
onChange: noopFalse,
7071
onOpen: noopFalse,
7172
onClose: noopFalse,
7273
onCheckAll: noopFalse,

packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ export interface MultipleSelectView {
1010
instance: any;
1111
}
1212

13+
export interface ClickedItem<T extends 'option' | 'optgroup'> {
14+
label: string;
15+
selected: boolean;
16+
type: T;
17+
}
18+
19+
export interface ClickedGroup extends ClickedItem<'optgroup'> {}
20+
21+
export interface ClickedOption extends ClickedItem<'option'> {
22+
value?: any;
23+
}
24+
1325
export type CloseReason =
1426
| 'body.click'
1527
| 'hover.mouseout'
@@ -291,7 +303,17 @@ export interface MultipleSelectOption extends MultipleSelectLocale {
291303
/** Bind an event handler to the “blur” */
292304
onBlur: () => void;
293305

294-
/** Fires when a an optgroup label is clicked on. */
306+
/**
307+
* Fires when any option/group selections changes.
308+
* This event is triggered at the same time as these other events are triggered: (`onCheckAll`, `onUncheckAll`, `onClick`, `onOptgroupClick`)
309+
*/
310+
onChange: (data: {
311+
eventName: string;
312+
item?: ClickedGroup | ClickedOption;
313+
selection: { labels: string[]; values: Array<string | number | boolean> };
314+
}) => void;
315+
316+
/** Fires when an optgroup label is clicked on. */
295317
onOptgroupClick: (view: MultipleSelectView) => void;
296318

297319
/** Fires before a checkbox is clicked. Return `false` to prevent the click event. */

playwright/e2e/events.spec.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect } from '@playwright/test';
1+
import { expect, test } from '@playwright/test';
22

33
test.describe('Events Demo', () => {
44
test('executing multiple actions with ms-select should fire multiple events', async ({ page }) => {
@@ -40,6 +40,7 @@ test.describe('Events Demo', () => {
4040
'onBlur event fire!',
4141
'onOpen event fire!',
4242
'onOptgroupClick event fire! view: {"label":"Group 1","selected":true,"children":[null,{"text":"Option 1","value":"1","selected":true,"disabled":false},null,{"text":"Option 2","value":"2","selected":true,"disabled":false},null,{"text":"Option 3","value":"3","selected":true,"disabled":false},null]}',
43+
'onChange event fire! data: {"eventName":"onOptgroupClick","item":{"label":"Group 1","selected":true,"type":"optgroup"},"selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]"],"values":["1","2","3"]}}',
4344
].join('\n'),
4445
);
4546

@@ -57,7 +58,9 @@ test.describe('Events Demo', () => {
5758
'onBlur event fire!',
5859
'onOpen event fire!',
5960
'onOptgroupClick event fire! view: {"label":"Group 1","selected":true,"children":[null,{"text":"Option 1","value":"1","selected":true,"disabled":false},null,{"text":"Option 2","value":"2","selected":true,"disabled":false},null,{"text":"Option 3","value":"3","selected":true,"disabled":false},null]}',
61+
'onChange event fire! data: {"eventName":"onOptgroupClick","item":{"label":"Group 1","selected":true,"type":"optgroup"},"selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]"],"values":["1","2","3"]}}',
6062
'onCheckAll event fire!',
63+
'onChange event fire! data: {"eventName":"onCheckAll","selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]","[Group 2: Option 4, Option 5, Option 6]","[Group 3: Option 7, Option 8, Option 9]"],"values":["1","2","3","4","5","6","7","8","9"]}}',
6164
].join('\n'),
6265
);
6366

@@ -75,8 +78,11 @@ test.describe('Events Demo', () => {
7578
'onBlur event fire!',
7679
'onOpen event fire!',
7780
'onOptgroupClick event fire! view: {"label":"Group 1","selected":true,"children":[null,{"text":"Option 1","value":"1","selected":true,"disabled":false},null,{"text":"Option 2","value":"2","selected":true,"disabled":false},null,{"text":"Option 3","value":"3","selected":true,"disabled":false},null]}',
81+
'onChange event fire! data: {"eventName":"onOptgroupClick","item":{"label":"Group 1","selected":true,"type":"optgroup"},"selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]"],"values":["1","2","3"]}}',
7882
'onCheckAll event fire!',
83+
'onChange event fire! data: {"eventName":"onCheckAll","selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]","[Group 2: Option 4, Option 5, Option 6]","[Group 3: Option 7, Option 8, Option 9]"],"values":["1","2","3","4","5","6","7","8","9"]}}',
7984
'onCheckAll event fire!',
85+
'onChange event fire! data: {"eventName":"onCheckAll","selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]","[Group 2: Option 4, Option 5, Option 6]","[Group 3: Option 7, Option 8, Option 9]"],"values":["1","2","3","4","5","6","7","8","9"]}}',
8086
'onFilter event fire! text: 1',
8187
].join('\n'),
8288
);
@@ -95,10 +101,14 @@ test.describe('Events Demo', () => {
95101
'onBlur event fire!',
96102
'onOpen event fire!',
97103
'onOptgroupClick event fire! view: {"label":"Group 1","selected":true,"children":[null,{"text":"Option 1","value":"1","selected":true,"disabled":false},null,{"text":"Option 2","value":"2","selected":true,"disabled":false},null,{"text":"Option 3","value":"3","selected":true,"disabled":false},null]}',
104+
'onChange event fire! data: {"eventName":"onOptgroupClick","item":{"label":"Group 1","selected":true,"type":"optgroup"},"selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]"],"values":["1","2","3"]}}',
98105
'onCheckAll event fire!',
106+
'onChange event fire! data: {"eventName":"onCheckAll","selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]","[Group 2: Option 4, Option 5, Option 6]","[Group 3: Option 7, Option 8, Option 9]"],"values":["1","2","3","4","5","6","7","8","9"]}}',
99107
'onCheckAll event fire!',
108+
'onChange event fire! data: {"eventName":"onCheckAll","selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]","[Group 2: Option 4, Option 5, Option 6]","[Group 3: Option 7, Option 8, Option 9]"],"values":["1","2","3","4","5","6","7","8","9"]}}',
100109
'onFilter event fire! text: 1',
101110
'onCheckAll event fire!',
111+
'onChange event fire! data: {"eventName":"onCheckAll","selection":{"labels":["[Group 1: Option 1, Option 2, Option 3]","[Group 2: Option 4, Option 5, Option 6]","[Group 3: Option 7, Option 8, Option 9]"],"values":["1","2","3","4","5","6","7","8","9"]}}',
102112
'onFilter event fire! text: ',
103113
'onFilterClear event fire!',
104114
].join('\n'),

0 commit comments

Comments
 (0)